Commit 1e2db468 authored by Mohamad Rahhal's avatar Mohamad Rahhal
Browse files

Mikrotik driver:

Added support for the connectivity service between servers + adjustments on the service component to accept L3
parent a8c399d3
Loading
Loading
Loading
Loading
+24 −0
Original line number Diff line number Diff line
@@ -217,6 +217,17 @@ class MikrotikRouterOSDriver(_Driver):
                                "comment": str(resource_value.get("comment", ""))
                            }

                        # --- IP Route ---
                        elif resource_key.startswith("/ip/route"):
                            endpoint = "/ip/route"
                            payload = {
                                "dst-address": str(resource_value["dst-address"]),
                                "gateway": str(resource_value["gateway"]),
                                "comment": str(resource_value.get("comment", ""))
                            }
                            if "routing-table" in resource_value:
                                payload["routing-table"] = str(resource_value["routing-table"])

                        # --- VRF ---
                        elif resource_key.startswith("/ip/vrf"):
                            endpoint = "/ip/vrf"
@@ -486,6 +497,8 @@ class MikrotikRouterOSDriver(_Driver):
                    return 4
                if resource_key.startswith("/interfaces/ip"):
                    return 5
                if resource_key.startswith("/ip/route"):
                    return 5
                if resource_key.startswith("/routing/filter/rule"):
                    return 6
                if resource_key.startswith("/routing/bgp/template"):
@@ -542,6 +555,17 @@ class MikrotikRouterOSDriver(_Driver):
                                "comment": str(resource_value.get("comment", ""))
                            }

                        # --- IP Route ---
                        elif resource_key.startswith("/ip/route"):
                            endpoint = "/ip/route"
                            payload = {
                                "dst-address": str(resource_value["dst-address"]),
                                "gateway": str(resource_value["gateway"]),
                                "comment": str(resource_value.get("comment", ""))
                            }
                            if "routing-table" in resource_value:
                                payload["routing-table"] = str(resource_value["routing-table"])

                        # --- VRF ---
                        elif resource_key.startswith("/ip/vrf"):
                            endpoint = "/ip/vrf"
+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-server1-server8"
        }
      ]
    },
    "sites": {
      "site": [
        {
          "site-id": "site_MikroTik-5-Leaf1",
          "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.1"
                      }
                    ]
                  }
                }
              }
            ]
          },
          "site-network-accesses": {
            "site-network-access": [
              {
                "site-network-access-id": "sfp-sfpplus7",
                "site-network-access-type": "ietf-l3vpn-svc:multipoint",
                "device-reference": "6ac9614b-6893-549d-b39c-d4af4320678a",
                "vpn-attachment": {
                  "vpn-id": "svc-mikrotik-leaf1-leaf4-server1-server8",
                  "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.1",
                      "prefix-length": 24
                    }
                  }
                },
                "service": {
                  "svc-mtu": 1500,
                  "svc-input-bandwidth": 1000000000,
                  "svc-output-bandwidth": 1000000000,
                  "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",
          "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.2"
                      }
                    ]
                  }
                }
              }
            ]
          },
          "site-network-accesses": {
            "site-network-access": [
              {
                "site-network-access-id": "sfp-sfpplus8",
                "site-network-access-type": "ietf-l3vpn-svc:multipoint",
                "device-reference": "03d3c087-da28-539e-aae0-60da7b90bad6",
                "vpn-attachment": {
                  "vpn-id": "svc-mikrotik-leaf1-leaf4-server1-server8",
                  "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.2",
                      "prefix-length": 24
                    }
                  }
                },
                "service": {
                  "svc-mtu": 1500,
                  "svc-input-bandwidth": 1000000000,
                  "svc-output-bandwidth": 1000000000,
                  "qos": {
                    "qos-profile": {
                      "classes": {
                        "class": [
                          {
                            "class-id": "qos-realtime",
                            "direction": "ietf-l3vpn-svc:both",
                            "latency": {
                              "latency-boundary": 10
                            },
                            "bandwidth": {
                              "guaranteed-bw-percent": 100
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            ]
          }
        }
      ]
    }
  }
}
+7 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ from .l3nm_gnmi_openconfig.L3NMGnmiOpenConfigServiceHandler import L3NMGnmiOpenC
from .l3nm_ietfactn.L3NM_IETFACTN_ServiceHandler import L3NM_IETFACTN_ServiceHandler
from .l3nm_ietfl3vpn.L3NM_IETFL3VPN_ServiceHandler import L3NM_IETFL3VPN_ServiceHandler
from .l3nm_ietfslice.L3NM_IETFSlice_ServiceHandler import L3NM_IETFSlice_ServiceHandler
from .l3nm_mikrotik_routeros.L3NMMikrotikRouterOSServiceHandler import L3NMMikrotikRouterOSServiceHandler
from .l3nm_ncefan.L3NM_NCEFAN_ServiceHandler import L3NM_NCEFAN_ServiceHandler
from .l3nm_openconfig.L3NMOpenConfigServiceHandler import L3NMOpenConfigServiceHandler
from .microwave.MicrowaveServiceHandler import MicrowaveServiceHandler
@@ -91,6 +92,12 @@ SERVICE_HANDLERS = [
            FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_IETF_L3VPN,
        }
    ]),
    (L3NMMikrotikRouterOSServiceHandler, [
        {
            FilterFieldEnum.SERVICE_TYPE  : ServiceTypeEnum.SERVICETYPE_L3NM,
            FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_MIKROTIK_ROUTEROS,
        }
    ]),
    (L3NM_NCEFAN_ServiceHandler, [
        {
            FilterFieldEnum.SERVICE_TYPE  : ServiceTypeEnum.SERVICETYPE_L3NM,
+123 −0
Original line number Diff line number Diff line
# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# 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 ipaddress
import os
from typing import Any, Dict, List

from common.tools.object_factory.ConfigRule import (
    json_config_rule_delete,
    json_config_rule_set,
)


DEFAULT_BRIDGE = os.environ.get("MIKROTIK_L3VPN_BRIDGE", "br-vni1010")
DEFAULT_TRANSIT_CIDR = os.environ.get("MIKROTIK_L3VPN_TRANSIT_CIDR", "169.254.101.0/30")


def _network(address: str, prefix_length: int) -> str:
    return str(ipaddress.ip_network(f"{address}/{prefix_length}", strict=False))


def _transit_hosts() -> List[str]:
    hosts = list(ipaddress.ip_network(DEFAULT_TRANSIT_CIDR, strict=False).hosts())
    if len(hosts) < 2:
        raise ValueError(f"MIKROTIK_L3VPN_TRANSIT_CIDR needs at least two usable addresses: {DEFAULT_TRANSIT_CIDR}")
    return [str(hosts[0]), str(hosts[1])]


def _ip_address_rule(service_uuid: str, suffix: str, address: str, prefix_length: int, interface: str) -> Dict[str, Any]:
    return {
        "address": f"{address}/{prefix_length}",
        "interface": interface,
        "comment": f"tfs-l3vpn:{service_uuid}:{suffix}",
    }


def _route_rule(service_uuid: str, dst_address: str, gateway: str) -> Dict[str, Any]:
    return {
        "dst-address": dst_address,
        "gateway": gateway,
        "comment": f"tfs-l3vpn:{service_uuid}:remote-route",
    }


def setup_config_rules(
    service_uuid: str,
    local_settings: Dict[str, Any],
    remote_settings: Dict[str, Any],
    local_index: int,
    bridge: str = DEFAULT_BRIDGE,
) -> List[Dict[str, Any]]:
    transit_a, transit_b = _transit_hosts()
    local_transit = transit_a if local_index == 0 else transit_b
    remote_transit = transit_b if local_index == 0 else transit_a

    local_provider = local_settings["neighbor_address"]
    remote_customer = remote_settings["ip_address"]
    local_prefix = int(local_settings["prefix_length"])
    remote_prefix = int(remote_settings["prefix_length"])

    transit_prefix = ipaddress.ip_network(DEFAULT_TRANSIT_CIDR, strict=False).prefixlen
    remote_lan = _network(remote_customer, remote_prefix)

    return [
        json_config_rule_set(
            f"/interfaces/ip/l3vpn[{service_uuid}]/gateway",
            _ip_address_rule(service_uuid, "gateway", local_provider, local_prefix, bridge),
        ),
        json_config_rule_set(
            f"/interfaces/ip/l3vpn[{service_uuid}]/transit",
            _ip_address_rule(service_uuid, "transit", local_transit, transit_prefix, bridge),
        ),
        json_config_rule_set(
            f"/ip/route/l3vpn[{service_uuid}]/remote",
            _route_rule(service_uuid, remote_lan, remote_transit),
        ),
    ]


def teardown_config_rules(
    service_uuid: str,
    local_settings: Dict[str, Any],
    remote_settings: Dict[str, Any],
    local_index: int,
    bridge: str = DEFAULT_BRIDGE,
) -> List[Dict[str, Any]]:
    transit_a, transit_b = _transit_hosts()
    local_transit = transit_a if local_index == 0 else transit_b
    remote_transit = transit_b if local_index == 0 else transit_a

    local_provider = local_settings["neighbor_address"]
    remote_customer = remote_settings["ip_address"]
    local_prefix = int(local_settings["prefix_length"])
    remote_prefix = int(remote_settings["prefix_length"])

    transit_prefix = ipaddress.ip_network(DEFAULT_TRANSIT_CIDR, strict=False).prefixlen
    remote_lan = _network(remote_customer, remote_prefix)

    return [
        json_config_rule_delete(
            f"/ip/route/l3vpn[{service_uuid}]/remote",
            _route_rule(service_uuid, remote_lan, remote_transit),
        ),
        json_config_rule_delete(
            f"/interfaces/ip/l3vpn[{service_uuid}]/transit",
            _ip_address_rule(service_uuid, "transit", local_transit, transit_prefix, bridge),
        ),
        json_config_rule_delete(
            f"/interfaces/ip/l3vpn[{service_uuid}]/gateway",
            _ip_address_rule(service_uuid, "gateway", local_provider, local_prefix, bridge),
        ),
    ]
+147 −0
Original line number Diff line number Diff line
# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# 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 json
import logging
from typing import Any, List, Optional, Tuple, Union

from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method
from common.proto.context_pb2 import ConfigRule, Device, DeviceId, Service
from common.tools.object_factory.Device import json_device_id
from common.type_checkers.Checkers import chk_type
from service.service.service_handler_api._ServiceHandler import _ServiceHandler
from service.service.service_handler_api.Tools import get_device_endpoint_uuids, get_endpoint_matching
from service.service.task_scheduler.TaskExecutor import TaskExecutor

from .ConfigRules import setup_config_rules, teardown_config_rules


LOGGER = logging.getLogger(__name__)
METRICS_POOL = MetricsPool("Service", "Handler", labels={"handler": "l3nm_mikrotik_routeros"})


class L3NMMikrotikRouterOSServiceHandler(_ServiceHandler):
    def __init__(self, service: Service, task_executor: TaskExecutor, **settings) -> None:
        self.__service = service
        self.__task_executor = task_executor

    def __get_endpoint_settings(self, device_uuid: str, endpoint_uuid_or_name: str) -> dict:
        resource_key = f"/device[{device_uuid}]/endpoint[{endpoint_uuid_or_name}]/settings"
        for rule in self.__service.service_config.config_rules:
            if rule.WhichOneof("config_rule") != "custom":
                continue
            if rule.custom.resource_key != resource_key:
                continue
            return json.loads(rule.custom.resource_value)
        raise ValueError(f"Missing L3NM settings for endpoint {device_uuid}/{endpoint_uuid_or_name}")

    def __get_entry(
        self, endpoint: Tuple[str, str, Optional[str]]
    ) -> Tuple[str, str, Device, dict]:
        device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint)
        device = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid)))
        endpoint_obj = get_endpoint_matching(device, endpoint_uuid)
        try:
            settings = self.__get_endpoint_settings(device_uuid, endpoint_uuid)
        except ValueError:
            settings = self.__get_endpoint_settings(device_uuid, endpoint_obj.name)
        return device_uuid, endpoint_obj.name, device, settings

    def __get_entries(
        self, endpoints: List[Tuple[str, str, Optional[str]]]
    ) -> List[Tuple[int, str, str, Device, dict]]:
        entries = []
        for index, endpoint in enumerate(endpoints):
            try:
                entries.append((index, *self.__get_entry(endpoint)))
            except ValueError:
                LOGGER.debug("Skipping non-site endpoint without L3NM settings: %s", endpoint)
        return entries

    def __apply(self, endpoints: List[Tuple[str, str, Optional[str]]], delete: bool = False) -> List[Union[bool, Exception]]:
        results: List[Union[bool, Exception]] = [True for _ in endpoints]

        service_uuid = self.__service.service_id.service_uuid.uuid
        entries = self.__get_entries(endpoints)
        if len(entries) != 2:
            LOGGER.info(
                "MikroTik RouterOS L3NM service %s expected 2 site endpoints; got %d from %d path endpoints",
                service_uuid,
                len(entries),
                len(endpoints),
            )
            return results

        for local_index, (result_index, device_uuid, _endpoint_uuid, device, settings) in enumerate(entries):
            remote_settings = entries[1 - local_index][4]
            try:
                rules = (
                    teardown_config_rules(service_uuid, settings, remote_settings, local_index)
                    if delete
                    else setup_config_rules(service_uuid, settings, remote_settings, local_index)
                )
                del device.device_config.config_rules[:]
                for rule in rules:
                    device.device_config.config_rules.append(ConfigRule(**rule))
                self.__task_executor.configure_device(device)
            except Exception as e:  # pylint: disable=broad-except
                LOGGER.exception(
                    "Unable to %s MikroTik RouterOS L3NM service %s on %s",
                    "delete" if delete else "set",
                    service_uuid,
                    device_uuid,
                )
                results[result_index] = e

        return results

    @metered_subclass_method(METRICS_POOL)
    def SetEndpoint(
        self,
        endpoints: List[Tuple[str, str, Optional[str]]],
        connection_uuid: Optional[str] = None,
    ) -> List[Union[bool, Exception]]:
        LOGGER.debug("[SetEndpoint] endpoints=%s connection_uuid=%s", endpoints, connection_uuid)
        chk_type("endpoints", endpoints, list)
        return self.__apply(endpoints, delete=False)

    @metered_subclass_method(METRICS_POOL)
    def DeleteEndpoint(
        self,
        endpoints: List[Tuple[str, str, Optional[str]]],
        connection_uuid: Optional[str] = None,
    ) -> List[Union[bool, Exception]]:
        LOGGER.debug("[DeleteEndpoint] endpoints=%s connection_uuid=%s", endpoints, connection_uuid)
        chk_type("endpoints", endpoints, list)
        return self.__apply(endpoints, delete=True)

    @metered_subclass_method(METRICS_POOL)
    def SetConstraint(self, constraints: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]:
        chk_type("constraints", constraints, list)
        return [True for _ in constraints]

    @metered_subclass_method(METRICS_POOL)
    def DeleteConstraint(self, constraints: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]:
        chk_type("constraints", constraints, list)
        return [True for _ in constraints]

    @metered_subclass_method(METRICS_POOL)
    def SetConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]:
        chk_type("resources", resources, list)
        return [True for _ in resources]

    @metered_subclass_method(METRICS_POOL)
    def DeleteConfig(self, resources: List[Tuple[str, Any]]) -> List[Union[bool, Exception]]:
        chk_type("resources", resources, list)
        return [True for _ in resources]
Loading