diff options
Diffstat (limited to 'rumba/irm_backend.py')
| -rw-r--r-- | rumba/irm_backend.py | 343 |
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) |
