aboutsummaryrefslogtreecommitdiff
path: root/rumba/irm_backend.py
diff options
context:
space:
mode:
Diffstat (limited to 'rumba/irm_backend.py')
-rw-r--r--rumba/irm_backend.py343
1 files changed, 343 insertions, 0 deletions
diff --git a/rumba/irm_backend.py b/rumba/irm_backend.py
new file mode 100644
index 0000000..e9cc5c6
--- /dev/null
+++ b/rumba/irm_backend.py
@@ -0,0 +1,343 @@
+#
+# Rumba - IRM Backend Abstraction
+#
+# Copyright (C) 2017-2026 imec
+#
+# Dimitri Staessens <dimitri.staessens@ugent.be>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., http://www.fsf.org/about/contact/.
+#
+
+"""
+IRM backend abstraction for Ouroboros.
+
+Provides two implementations:
+- IrmPython: uses pyouroboros CFFI bindings (in-process, local testbed)
+- IrmCLI: generates irm CLI commands for SSH execution (remote testbeds)
+"""
+
+import abc
+import rumba.log as log
+from rumba.elements.topology import LayerType
+
+logger = log.get_logger(__name__)
+
+
+class IrmBackend(abc.ABC):
+ """
+ Abstract interface for IRM operations.
+
+ Maps to the pyouroboros irm API. Implementations execute the
+ operations either locally (via CFFI) or remotely (via CLI commands).
+ """
+
+ @abc.abstractmethod
+ def create_ipcp(self, node, name, layer_type):
+ """
+ Create an IPCP process.
+
+ :param node: The node to create the IPCP on.
+ :param name: Name for the IPCP.
+ :param layer_type: LayerType enum value.
+ :return: IPCP identifier (PID for local, name for remote).
+ """
+ pass
+
+ @abc.abstractmethod
+ def bootstrap_ipcp(self, node, name, layer_type, layer_name,
+ policies=None, eth_dev=None, ip_addr=None,
+ autobind=True):
+ """
+ Bootstrap an IPCP into a layer.
+
+ :param node: The node.
+ :param name: IPCP name.
+ :param layer_type: LayerType enum value.
+ :param layer_name: Name of the layer to bootstrap.
+ :param policies: Dict of component->policy mappings.
+ :param eth_dev: Ethernet device name (for eth-dix/eth-llc).
+ :param ip_addr: IP address (for UDP shim).
+ :param autobind: Automatically bind the IPCP name.
+ """
+ pass
+
+ @abc.abstractmethod
+ def enroll_ipcp(self, node, name, dst_name, autobind=True):
+ """
+ Enroll an IPCP into an existing layer.
+
+ :param node: The node.
+ :param name: IPCP name.
+ :param dst_name: Name of the destination IPCP to enroll with.
+ :param autobind: Automatically bind the IPCP name.
+ """
+ pass
+
+ @abc.abstractmethod
+ def connect_ipcp(self, node, name, dst_name):
+ """
+ Create a data transfer connection between IPCPs.
+
+ :param node: The node.
+ :param name: IPCP name.
+ :param dst_name: Destination IPCP name.
+ """
+ pass
+
+ @abc.abstractmethod
+ def reg_name(self, node, name, ipcp_names):
+ """
+ Register a name in N-1 layers.
+
+ :param node: The node.
+ :param name: The name to register.
+ :param ipcp_names: List of IPCP names to register with.
+ """
+ pass
+
+ @abc.abstractmethod
+ def bind_process(self, node, pid, name):
+ """
+ Bind a process to a name.
+
+ :param node: The node.
+ :param pid: Process ID.
+ :param name: Name to bind to.
+ """
+ pass
+
+ @abc.abstractmethod
+ def destroy_ipcp(self, node, name):
+ """
+ Destroy an IPCP.
+
+ :param node: The node.
+ :param name: IPCP name to destroy.
+ """
+ pass
+
+
+# -----------------------------------------------------------------------
+# Remote IRM: generates CLI commands for SSH execution
+# -----------------------------------------------------------------------
+
+# Map LayerType to Ouroboros irm CLI type strings
+_LAYER_TYPE_TO_CLI = {
+ LayerType.LOCAL: 'local',
+ LayerType.UNICAST: 'unicast',
+ LayerType.BROADCAST: 'broadcast',
+ LayerType.ETH_LLC: 'eth-llc',
+ LayerType.ETH_DIX: 'eth-dix',
+ LayerType.UDP4: 'udp4',
+ LayerType.UDP6: 'udp6',
+}
+
+
+class IrmCLI(IrmBackend):
+ """
+ IRM backend that generates ``irm`` CLI commands and executes them
+ on the node via its executor (SSH, Docker exec, etc.).
+ """
+
+ def create_ipcp(self, node, name, layer_type):
+ type_str = _LAYER_TYPE_TO_CLI[layer_type]
+ cmd = "irm i c n %s type %s" % (name, type_str)
+ node.execute_command(cmd, time_out=None)
+ return name # remote: we track by name
+
+ def bootstrap_ipcp(self, node, name, layer_type, layer_name,
+ policies=None, eth_dev=None, ip_addr=None,
+ autobind=True):
+ type_str = _LAYER_TYPE_TO_CLI[layer_type]
+ cmd = "irm i b n %s type %s" % (name, type_str)
+
+ if layer_type in (LayerType.ETH_DIX, LayerType.ETH_LLC):
+ if eth_dev:
+ cmd += " dev %s" % eth_dev
+
+ if layer_type in (LayerType.UDP4, LayerType.UDP6):
+ if ip_addr:
+ cmd += " ip %s" % ip_addr
+
+ if policies:
+ for comp, pol in policies.items():
+ cmd += " %s %s" % (comp, pol)
+
+ cmd += " layer %s" % layer_name
+
+ if autobind:
+ cmd += " autobind"
+
+ node.execute_command(cmd, time_out=None)
+
+ def enroll_ipcp(self, node, name, dst_name, autobind=True):
+ cmd = "irm i e n %s dst %s" % (name, dst_name)
+ if autobind:
+ cmd += " autobind"
+ node.execute_command(cmd, time_out=None)
+
+ def connect_ipcp(self, node, name, dst_name):
+ cmd = "irm i conn n %s dst %s" % (name, dst_name)
+ node.execute_command(cmd, time_out=None)
+
+ def reg_name(self, node, name, ipcp_names):
+ cmd = "irm n r %s" % name
+ for ipcp_name in ipcp_names:
+ cmd += " ipcp %s" % ipcp_name
+ node.execute_command(cmd, time_out=None)
+
+ def bind_process(self, node, pid, name):
+ cmd = "irm b process %s name %s" % (pid, name)
+ node.execute_command(cmd, time_out=None)
+
+ def destroy_ipcp(self, node, name):
+ cmd = "irm i d n %s" % name
+ node.execute_command(cmd, time_out=None)
+
+
+# -----------------------------------------------------------------------
+# Local IRM: uses pyouroboros CFFI bindings
+# -----------------------------------------------------------------------
+
+class IrmPython(IrmBackend):
+ """
+ IRM backend that uses pyouroboros CFFI bindings for direct
+ in-process IRM calls. Only works on the local machine where
+ the Ouroboros IRMd is running.
+ """
+
+ def __init__(self):
+ try:
+ from ouroboros import irm as _irm
+ from ouroboros import cli as _cli
+ self._irm = _irm
+ self._cli = _cli
+ logger.info("pyouroboros IRM backend loaded successfully")
+ except ImportError:
+ raise ImportError(
+ "pyouroboros is required for Python backend. "
+ "Install it from the pyouroboros repository.")
+
+ def _get_ipcp_type(self, layer_type):
+ """Convert LayerType to pyouroboros IpcpType."""
+ type_map = {
+ LayerType.LOCAL: self._irm.IpcpType.LOCAL,
+ LayerType.UNICAST: self._irm.IpcpType.UNICAST,
+ LayerType.BROADCAST: self._irm.IpcpType.BROADCAST,
+ LayerType.ETH_LLC: self._irm.IpcpType.ETH_LLC,
+ LayerType.ETH_DIX: self._irm.IpcpType.ETH_DIX,
+ LayerType.UDP4: self._irm.IpcpType.UDP4,
+ LayerType.UDP6: self._irm.IpcpType.UDP6,
+ }
+ return type_map[layer_type]
+
+ def create_ipcp(self, node, name, layer_type):
+ ipcp_type = self._get_ipcp_type(layer_type)
+ pid = self._cli.create_ipcp(name, ipcp_type)
+ logger.debug("Created IPCP %s (pid=%d, type=%s)",
+ name, pid, layer_type.value)
+ return pid
+
+ def bootstrap_ipcp(self, node, name, layer_type, layer_name,
+ policies=None, eth_dev=None, ip_addr=None,
+ autobind=True):
+ pid = self._cli.pid_of(name)
+ ipcp_type = self._get_ipcp_type(layer_type)
+ irm = self._irm
+
+ # Build config based on type
+ if layer_type == LayerType.UNICAST:
+ uc_kwargs = {}
+ dt_kwargs = {}
+ routing_kwargs = {}
+ ls_kwargs = {}
+
+ # Parse policies dict into pyouroboros config
+ if policies:
+ for comp, pol in policies.items():
+ if comp == 'rmt.pff':
+ pol_map = {
+ 'lfa': irm.LinkStatePolicy.LFA,
+ 'ecmp': irm.LinkStatePolicy.ECMP,
+ 'simple': irm.LinkStatePolicy.SIMPLE,
+ }
+ if pol in pol_map:
+ ls_kwargs['pol'] = pol_map[pol]
+
+ if ls_kwargs:
+ routing_kwargs['ls'] = irm.LinkStateConfig(**ls_kwargs)
+ if routing_kwargs:
+ dt_kwargs['routing'] = irm.RoutingConfig(**routing_kwargs)
+ if dt_kwargs:
+ uc_kwargs['dt'] = irm.DtConfig(**dt_kwargs)
+
+ conf = irm.IpcpConfig(
+ ipcp_type=ipcp_type,
+ layer_name=layer_name,
+ unicast=irm.UnicastConfig(**uc_kwargs))
+
+ elif layer_type in (LayerType.ETH_DIX, LayerType.ETH_LLC):
+ eth_conf = irm.EthConfig(dev=eth_dev or "")
+ conf = irm.IpcpConfig(
+ ipcp_type=ipcp_type,
+ layer_name=layer_name,
+ eth=eth_conf)
+
+ elif layer_type == LayerType.UDP4:
+ udp4_kwargs = {}
+ if ip_addr:
+ udp4_kwargs['ip_addr'] = ip_addr
+ conf = irm.IpcpConfig(
+ ipcp_type=ipcp_type,
+ layer_name=layer_name,
+ udp4=irm.Udp4Config(**udp4_kwargs))
+
+ elif layer_type == LayerType.UDP6:
+ udp6_kwargs = {}
+ if ip_addr:
+ udp6_kwargs['ip_addr'] = ip_addr
+ conf = irm.IpcpConfig(
+ ipcp_type=ipcp_type,
+ layer_name=layer_name,
+ udp6=irm.Udp6Config(**udp6_kwargs))
+
+ else:
+ conf = irm.IpcpConfig(
+ ipcp_type=ipcp_type,
+ layer_name=layer_name)
+
+ self._cli.bootstrap_ipcp(pid, conf, autobind=autobind)
+
+ logger.debug("Bootstrapped IPCP %s in layer %s", name, layer_name)
+
+ def enroll_ipcp(self, node, name, dst_name, autobind=True):
+ pid = self._cli.pid_of(name)
+ self._cli.enroll_ipcp(pid, dst_name, autobind=autobind)
+ logger.debug("Enrolled IPCP %s via %s", name, dst_name)
+
+ def connect_ipcp(self, node, name, dst_name):
+ pid = self._cli.pid_of(name)
+ self._cli.connect_ipcp(pid, dst_name)
+ logger.debug("Connected IPCP %s to %s", name, dst_name)
+
+ def reg_name(self, node, name, ipcp_names):
+ self._cli.reg_name(name, ipcps=ipcp_names)
+
+ def bind_process(self, node, pid, name):
+ self._cli.bind_process(int(pid), name)
+
+ def destroy_ipcp(self, node, name):
+ self._cli.destroy_ipcp(name)
+ logger.debug("Destroyed IPCP %s", name)