core-extra/daemon/core/nodes/physical.py

542 lines
16 KiB
Python
Raw Normal View History

"""
PhysicalNode class for including real systems in the emulated network.
"""
import logging
import os
import threading
2020-01-13 22:08:49 +00:00
from typing import IO, TYPE_CHECKING, List, Optional
from core import utils
from core.constants import MOUNT_BIN, UMOUNT_BIN
2020-01-13 22:08:49 +00:00
from core.emulator.distributed import DistributedServer
2019-09-10 22:20:51 +01:00
from core.emulator.enumerations import NodeTypes
from core.errors import CoreCommandError, CoreError
2020-01-13 22:08:49 +00:00
from core.nodes.base import CoreNetworkBase, CoreNodeBase
from core.nodes.interface import CoreInterface, Veth
2019-09-10 22:20:51 +01:00
from core.nodes.network import CoreNetwork, GreTap
2020-01-13 22:08:49 +00:00
if TYPE_CHECKING:
from core.emulator.session import Session
class PhysicalNode(CoreNodeBase):
def __init__(
2020-01-13 22:08:49 +00:00
self,
session,
_id: int = None,
name: str = None,
nodedir: str = None,
start: bool = True,
server: DistributedServer = None,
) -> None:
super().__init__(session, _id, name, start, server)
if not self.server:
raise CoreError("physical nodes must be assigned to a remote server")
self.nodedir = nodedir
self.up = start
self.lock = threading.RLock()
self._mounts = []
if start:
self.startup()
2020-01-13 22:08:49 +00:00
def startup(self) -> None:
with self.lock:
self.makenodedir()
2020-01-13 22:08:49 +00:00
def shutdown(self) -> None:
if not self.up:
return
with self.lock:
while self._mounts:
_source, target = self._mounts.pop(-1)
self.umount(target)
for netif in self.netifs():
netif.shutdown()
self.rmnodedir()
2020-01-13 22:08:49 +00:00
def termcmdstring(self, sh: str = "/bin/sh") -> str:
"""
Create a terminal command string.
:param sh: shell to execute command in
:return: str
"""
return sh
2020-01-13 22:08:49 +00:00
def sethwaddr(self, ifindex: int, addr: str) -> None:
"""
Set hardware address for an interface.
:param ifindex: index of interface to set hardware address for
:param addr: hardware address to set
:return: nothing
:raises CoreCommandError: when a non-zero exit status occurs
"""
addr = utils.validate_mac(addr)
interface = self._netif[ifindex]
interface.sethwaddr(addr)
if self.up:
self.net_client.device_mac(interface.name, addr)
2020-01-13 22:08:49 +00:00
def addaddr(self, ifindex: int, addr: str) -> None:
"""
Add an address to an interface.
:param ifindex: index of interface to add address to
:param addr: address to add
:return: nothing
"""
addr = utils.validate_ip(addr)
interface = self._netif[ifindex]
if self.up:
self.net_client.create_address(interface.name, addr)
interface.addaddr(addr)
2020-01-13 22:08:49 +00:00
def deladdr(self, ifindex: int, addr: str) -> None:
"""
Delete an address from an interface.
2020-01-13 22:08:49 +00:00
:param ifindex: index of interface to delete
:param addr: address to delete
2020-01-13 22:08:49 +00:00
:return: nothing
"""
interface = self._netif[ifindex]
try:
interface.deladdr(addr)
except ValueError:
logging.exception("trying to delete unknown address: %s", addr)
if self.up:
self.net_client.delete_address(interface.name, str(addr))
2020-01-13 22:08:49 +00:00
def adoptnetif(
self, netif: CoreInterface, ifindex: int, hwaddr: str, addrlist: List[str]
) -> None:
"""
When a link message is received linking this node to another part of
the emulation, no new interface is created; instead, adopt the
GreTap netif as the node interface.
"""
2019-10-18 02:59:50 +01:00
netif.name = f"gt{ifindex}"
netif.node = self
self.addnetif(netif, ifindex)
# use a more reasonable name, e.g. "gt0" instead of "gt.56286.150"
if self.up:
self.net_client.device_down(netif.localname)
self.net_client.device_name(netif.localname, netif.name)
netif.localname = netif.name
if hwaddr:
self.sethwaddr(ifindex, hwaddr)
for addr in utils.make_tuple(addrlist):
self.addaddr(ifindex, addr)
if self.up:
self.net_client.device_up(netif.localname)
def linkconfig(
self,
2020-01-13 22:08:49 +00:00
netif: CoreInterface,
bw: float = None,
delay: float = None,
loss: float = None,
duplicate: float = None,
jitter: float = None,
netif2: CoreInterface = None,
) -> None:
"""
Apply tc queing disciplines using linkconfig.
"""
linux_bridge = CoreNetwork(session=self.session, start=False)
linux_bridge.up = True
linux_bridge.linkconfig(
netif,
bw=bw,
delay=delay,
loss=loss,
duplicate=duplicate,
jitter=jitter,
netif2=netif2,
)
del linux_bridge
2020-01-13 22:08:49 +00:00
def newifindex(self) -> int:
with self.lock:
while self.ifindex in self._netif:
self.ifindex += 1
ifindex = self.ifindex
self.ifindex += 1
return ifindex
2020-01-13 22:08:49 +00:00
def newnetif(
self,
net: Veth = None,
addrlist: List[str] = None,
hwaddr: str = None,
ifindex: int = None,
ifname: str = None,
) -> int:
logging.info("creating interface")
if not addrlist:
addrlist = []
if self.up and net is None:
raise NotImplementedError
if ifindex is None:
ifindex = self.newifindex()
if ifname is None:
2019-10-18 02:59:50 +01:00
ifname = f"gt{ifindex}"
if self.up:
# this is reached when this node is linked to a network node
# tunnel to net not built yet, so build it now and adopt it
_, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server)
self.adoptnetif(remote_tap, ifindex, hwaddr, addrlist)
return ifindex
else:
# this is reached when configuring services (self.up=False)
netif = GreTap(node=self, name=ifname, session=self.session, start=False)
self.adoptnetif(netif, ifindex, hwaddr, addrlist)
return ifindex
2020-01-13 22:08:49 +00:00
def privatedir(self, path: str) -> None:
if path[0] != "/":
2019-10-18 02:59:50 +01:00
raise ValueError(f"path not fully qualified: {path}")
hostpath = os.path.join(
self.nodedir, os.path.normpath(path).strip("/").replace("/", ".")
)
os.mkdir(hostpath)
self.mount(hostpath, path)
2020-01-13 22:08:49 +00:00
def mount(self, source: str, target: str) -> None:
source = os.path.abspath(source)
logging.info("mounting %s at %s", source, target)
os.makedirs(target)
self.host_cmd(f"{MOUNT_BIN} --bind {source} {target}", cwd=self.nodedir)
self._mounts.append((source, target))
2020-01-13 22:08:49 +00:00
def umount(self, target: str) -> None:
2019-10-18 02:59:50 +01:00
logging.info("unmounting '%s'", target)
try:
self.host_cmd(f"{UMOUNT_BIN} -l {target}", cwd=self.nodedir)
except CoreCommandError:
logging.exception("unmounting failed for %s", target)
2020-01-13 22:08:49 +00:00
def opennodefile(self, filename: str, mode: str = "w") -> IO:
dirname, basename = os.path.split(filename)
if not basename:
raise ValueError("no basename for filename: " + filename)
if dirname and dirname[0] == "/":
dirname = dirname[1:]
dirname = dirname.replace("/", ".")
dirname = os.path.join(self.nodedir, dirname)
if not os.path.isdir(dirname):
os.makedirs(dirname, mode=0o755)
hostfilename = os.path.join(dirname, basename)
return open(hostfilename, mode)
2020-01-13 22:08:49 +00:00
def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None:
with self.opennodefile(filename, "w") as node_file:
node_file.write(contents)
os.chmod(node_file.name, mode)
logging.info("created nodefile: '%s'; mode: 0%o", node_file.name, mode)
2020-01-13 22:08:49 +00:00
def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str:
return self.host_cmd(args, wait=wait)
class Rj45Node(CoreNodeBase, CoreInterface):
"""
RJ45Node is a physical interface on the host linked to the emulated
network.
"""
apitype = NodeTypes.RJ45.value
type = "rj45"
2020-01-13 22:08:49 +00:00
def __init__(
self,
session: "Session",
_id: int = None,
name: str = None,
mtu: int = 1500,
start: bool = True,
server: DistributedServer = None,
) -> None:
"""
Create an RJ45Node instance.
:param session: core session instance
:param _id: node id
:param name: node name
:param mtu: rj45 mtu
:param start: start flag
:param server: remote server node
will run on, default is None for localhost
"""
CoreNodeBase.__init__(self, session, _id, name, start, server)
CoreInterface.__init__(self, session, self, name, mtu, server)
self.up = False
self.lock = threading.RLock()
self.ifindex = None
# the following are PyCoreNetIf attributes
self.transport_type = "raw"
self.localname = name
self.old_up = False
self.old_addrs = []
if start:
self.startup()
2020-01-13 22:08:49 +00:00
def startup(self) -> None:
"""
Set the interface in the up state.
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
# interface will also be marked up during net.attach()
self.savestate()
self.net_client.device_up(self.localname)
self.up = True
2020-01-13 22:08:49 +00:00
def shutdown(self) -> None:
"""
Bring the interface down. Remove any addresses and queuing
disciplines.
:return: nothing
"""
if not self.up:
return
try:
self.net_client.device_down(self.localname)
self.net_client.device_flush(self.localname)
self.net_client.delete_tc(self.localname)
except CoreCommandError:
logging.exception("error shutting down")
self.up = False
self.restorestate()
# TODO: issue in that both classes inherited from provide the same method with
# different signatures
2020-01-13 22:08:49 +00:00
def attachnet(self, net: CoreNetworkBase) -> None:
"""
Attach a network.
:param net: network to attach
:return: nothing
"""
CoreInterface.attachnet(self, net)
# TODO: issue in that both classes inherited from provide the same method with
# different signatures
2020-01-13 22:08:49 +00:00
def detachnet(self) -> None:
"""
Detach a network.
:return: nothing
"""
CoreInterface.detachnet(self)
2020-01-13 22:08:49 +00:00
def newnetif(
self,
net: CoreNetworkBase = None,
addrlist: List[str] = None,
hwaddr: str = None,
ifindex: int = None,
ifname: str = None,
) -> int:
"""
This is called when linking with another node. Since this node
represents an interface, we do not create another object here,
but attach ourselves to the given network.
:param net: new network instance
:param addrlist: address list
:param hwaddr: hardware address
:param ifindex: interface index
:param ifname: interface name
:return: interface index
:raises ValueError: when an interface has already been created, one max
"""
with self.lock:
if ifindex is None:
ifindex = 0
if self.net is not None:
raise ValueError("RJ45 nodes support at most 1 network interface")
self._netif[ifindex] = self
# PyCoreNetIf.node is self
self.node = self
self.ifindex = ifindex
if net is not None:
self.attachnet(net)
if addrlist:
for addr in utils.make_tuple(addrlist):
self.addaddr(addr)
return ifindex
2020-01-13 22:08:49 +00:00
def delnetif(self, ifindex: int) -> None:
"""
Delete a network interface.
:param ifindex: interface index to delete
:return: nothing
"""
if ifindex is None:
ifindex = 0
self._netif.pop(ifindex)
if ifindex == self.ifindex:
self.shutdown()
else:
2019-10-18 02:59:50 +01:00
raise ValueError(f"ifindex {ifindex} does not exist")
2020-01-13 22:08:49 +00:00
def netif(
self, ifindex: int, net: CoreNetworkBase = None
) -> Optional[CoreInterface]:
"""
This object is considered the network interface, so we only
return self here. This keeps the RJ45Node compatible with
real nodes.
:param ifindex: interface index to retrieve
:param net: network to retrieve
:return: a network interface
"""
if net is not None and net == self.net:
return self
if ifindex is None:
ifindex = 0
if ifindex == self.ifindex:
return self
return None
2020-01-13 22:08:49 +00:00
def getifindex(self, netif: CoreInterface) -> Optional[int]:
"""
Retrieve network interface index.
:param netif: network interface to retrieve
2020-01-13 22:08:49 +00:00
index for
:return: interface index, None otherwise
"""
if netif != self:
return None
return self.ifindex
2020-01-13 22:08:49 +00:00
def addaddr(self, addr: str) -> None:
"""
Add address to to network interface.
:param addr: address to add
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
addr = utils.validate_ip(addr)
if self.up:
self.net_client.create_address(self.name, addr)
CoreInterface.addaddr(self, addr)
2020-01-13 22:08:49 +00:00
def deladdr(self, addr: str) -> None:
"""
Delete address from network interface.
:param addr: address to delete
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
if self.up:
self.net_client.delete_address(self.name, str(addr))
CoreInterface.deladdr(self, addr)
2020-01-13 22:08:49 +00:00
def savestate(self) -> None:
"""
Save the addresses and other interface state before using the
interface for emulation purposes. TODO: save/restore the PROMISC flag
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
self.old_up = False
self.old_addrs = []
output = self.net_client.device_show(self.localname)
for line in output.split("\n"):
items = line.split()
if len(items) < 2:
continue
2019-10-18 02:59:50 +01:00
if items[1] == f"{self.localname}:":
flags = items[2][1:-1].split(",")
if "UP" in flags:
self.old_up = True
elif items[0] == "inet":
self.old_addrs.append((items[1], items[3]))
elif items[0] == "inet6":
if items[1][:4] == "fe80":
continue
self.old_addrs.append((items[1], None))
2020-01-13 22:08:49 +00:00
def restorestate(self) -> None:
"""
Restore the addresses and other interface state after using it.
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
for addr in self.old_addrs:
if addr[1] is None:
self.net_client.create_address(self.localname, addr[0])
else:
self.net_client.create_address(
self.localname, addr[0], broadcast=addr[1]
)
if self.old_up:
self.net_client.device_up(self.localname)
2020-01-13 22:08:49 +00:00
def setposition(self, x: float = None, y: float = None, z: float = None) -> bool:
"""
Uses setposition from both parent classes.
:param x: x position
:param y: y position
:param z: z position
:return: True if position changed, False otherwise
"""
result = CoreNodeBase.setposition(self, x, y, z)
CoreInterface.setposition(self, x, y, z)
return result
2020-01-13 22:08:49 +00:00
def termcmdstring(self, sh: str) -> str:
"""
Create a terminal command string.
:param sh: shell to execute command in
:return: str
"""
raise NotImplementedError