core-extra/daemon/core/emane/linkmonitor.py

328 lines
11 KiB
Python

import logging
import sched
import threading
import time
from typing import TYPE_CHECKING, Optional
from lxml import etree
from core.emane.nodes import EmaneNet
from core.emulator.data import LinkData
from core.emulator.enumerations import LinkTypes, MessageFlags
from core.nodes.network import CtrlNet
logger = logging.getLogger(__name__)
try:
from emane import shell
except ImportError:
try:
from emanesh import shell
except ImportError:
shell = None
logger.debug("compatible emane python bindings not installed")
if TYPE_CHECKING:
from core.emane.emanemanager import EmaneManager
MAC_COMPONENT_INDEX: int = 1
EMANE_RFPIPE: str = "rfpipemaclayer"
EMANE_80211: str = "ieee80211abgmaclayer"
EMANE_TDMA: str = "tdmaeventschedulerradiomodel"
SINR_TABLE: str = "NeighborStatusTable"
NEM_SELF: int = 65535
class LossTable:
def __init__(self, losses: dict[float, float]) -> None:
self.losses: dict[float, float] = losses
self.sinrs: list[float] = sorted(self.losses.keys())
self.loss_lookup: dict[int, float] = {}
for index, value in enumerate(self.sinrs):
self.loss_lookup[index] = self.losses[value]
self.mac_id: Optional[str] = None
def get_loss(self, sinr: float) -> float:
index = self._get_index(sinr)
loss = 100.0 - self.loss_lookup[index]
return loss
def _get_index(self, current_sinr: float) -> int:
for index, sinr in enumerate(self.sinrs):
if current_sinr <= sinr:
return index
return len(self.sinrs) - 1
class EmaneLink:
def __init__(self, from_nem: int, to_nem: int, sinr: float) -> None:
self.from_nem: int = from_nem
self.to_nem: int = to_nem
self.sinr: float = sinr
self.last_seen: Optional[float] = None
self.updated: bool = False
self.touch()
def update(self, sinr: float) -> None:
self.updated = self.sinr != sinr
self.sinr = sinr
self.touch()
def touch(self) -> None:
self.last_seen = time.monotonic()
def is_dead(self, timeout: int) -> bool:
return (time.monotonic() - self.last_seen) >= timeout
def __repr__(self) -> str:
return f"EmaneLink({self.from_nem}, {self.to_nem}, {self.sinr})"
class EmaneClient:
def __init__(self, address: str, port: int) -> None:
self.address: str = address
self.client: shell.ControlPortClient = shell.ControlPortClient(
self.address, port
)
self.nems: dict[int, LossTable] = {}
self.setup()
def setup(self) -> None:
manifest = self.client.getManifest()
for nem_id, components in manifest.items():
# get mac config
mac_id, _, emane_model = components[MAC_COMPONENT_INDEX]
mac_config = self.client.getConfiguration(mac_id)
logger.debug(
"address(%s) nem(%s) emane(%s)", self.address, nem_id, emane_model
)
# create loss table based on current configuration
if emane_model == EMANE_80211:
loss_table = self.handle_80211(mac_config)
elif emane_model == EMANE_RFPIPE:
loss_table = self.handle_rfpipe(mac_config)
else:
logger.warning("unknown emane link model: %s", emane_model)
continue
logger.info("monitoring links nem(%s) model(%s)", nem_id, emane_model)
loss_table.mac_id = mac_id
self.nems[nem_id] = loss_table
def check_links(
self, links: dict[tuple[int, int], EmaneLink], loss_threshold: int
) -> None:
for from_nem, loss_table in self.nems.items():
tables = self.client.getStatisticTable(loss_table.mac_id, (SINR_TABLE,))
table = tables[SINR_TABLE][1:][0]
for row in table:
row = row
to_nem = row[0][0]
sinr = row[5][0]
age = row[-1][0]
# exclude invalid links
is_self = to_nem == NEM_SELF
has_valid_age = 0 <= age <= 1
if is_self or not has_valid_age:
continue
# check if valid link loss
link_key = (from_nem, to_nem)
loss = loss_table.get_loss(sinr)
if loss < loss_threshold:
link = links.get(link_key)
if link:
link.update(sinr)
else:
link = EmaneLink(from_nem, to_nem, sinr)
links[link_key] = link
def handle_tdma(self, config: dict[str, tuple]):
pcr = config["pcrcurveuri"][0][0]
logger.debug("tdma pcr: %s", pcr)
def handle_80211(self, config: dict[str, tuple]) -> LossTable:
unicastrate = config["unicastrate"][0][0]
pcr = config["pcrcurveuri"][0][0]
logger.debug("80211 pcr: %s", pcr)
tree = etree.parse(pcr)
root = tree.getroot()
table = root.find("table")
losses = {}
for rate in table.iter("datarate"):
index = int(rate.get("index"))
if index == unicastrate:
for row in rate.iter("row"):
sinr = float(row.get("sinr"))
por = float(row.get("por"))
losses[sinr] = por
return LossTable(losses)
def handle_rfpipe(self, config: dict[str, tuple]) -> LossTable:
pcr = config["pcrcurveuri"][0][0]
logger.debug("rfpipe pcr: %s", pcr)
tree = etree.parse(pcr)
root = tree.getroot()
table = root.find("table")
losses = {}
for row in table.iter("row"):
sinr = float(row.get("sinr"))
por = float(row.get("por"))
losses[sinr] = por
return LossTable(losses)
def stop(self) -> None:
self.client.stop()
class EmaneLinkMonitor:
def __init__(self, emane_manager: "EmaneManager") -> None:
self.emane_manager: "EmaneManager" = emane_manager
self.clients: list[EmaneClient] = []
self.links: dict[tuple[int, int], EmaneLink] = {}
self.complete_links: set[tuple[int, int]] = set()
self.loss_threshold: Optional[int] = None
self.link_interval: Optional[int] = None
self.link_timeout: Optional[int] = None
self.scheduler: Optional[sched.scheduler] = None
self.running: bool = False
def start(self) -> None:
options = self.emane_manager.session.options
self.loss_threshold = options.get_int("loss_threshold")
self.link_interval = options.get_int("link_interval")
self.link_timeout = options.get_int("link_timeout")
self.initialize()
if not self.clients:
logger.info("no valid emane models to monitor links")
return
self.scheduler = sched.scheduler()
self.scheduler.enter(0, 0, self.check_links)
self.running = True
thread = threading.Thread(target=self.scheduler.run, daemon=True)
thread.start()
def initialize(self) -> None:
addresses = self.get_addresses()
for address, port in addresses:
client = EmaneClient(address, port)
if client.nems:
self.clients.append(client)
def get_addresses(self) -> list[tuple[str, int]]:
addresses = []
nodes = self.emane_manager.getnodes()
for node in nodes:
control = None
ports = []
for iface in node.get_ifaces():
if isinstance(iface.net, CtrlNet):
ip4 = iface.get_ip4()
if ip4:
control = str(ip4.ip)
if isinstance(iface.net, EmaneNet):
port = self.emane_manager.get_nem_port(iface)
ports.append(port)
if control:
for port in ports:
addresses.append((control, port))
return addresses
def check_links(self) -> None:
# check for new links
previous_links = set(self.links.keys())
for client in self.clients:
try:
client.check_links(self.links, self.loss_threshold)
except shell.ControlPortException:
if self.running:
logger.exception("link monitor error")
# find new links
current_links = set(self.links.keys())
new_links = current_links - previous_links
# find updated and dead links
dead_links = []
for link_id, link in self.links.items():
complete_id = self.get_complete_id(link_id)
if link.is_dead(self.link_timeout):
dead_links.append(link_id)
elif link.updated and complete_id in self.complete_links:
link.updated = False
self.send_link(MessageFlags.NONE, complete_id)
# announce dead links
for link_id in dead_links:
complete_id = self.get_complete_id(link_id)
if complete_id in self.complete_links:
self.complete_links.remove(complete_id)
self.send_link(MessageFlags.DELETE, complete_id)
del self.links[link_id]
# announce new links
for link_id in new_links:
complete_id = self.get_complete_id(link_id)
if complete_id in self.complete_links:
continue
if self.is_complete_link(link_id):
self.complete_links.add(complete_id)
self.send_link(MessageFlags.ADD, complete_id)
if self.running:
self.scheduler.enter(self.link_interval, 0, self.check_links)
def get_complete_id(self, link_id: tuple[int, int]) -> tuple[int, int]:
value1, value2 = link_id
if value1 < value2:
return value1, value2
else:
return value2, value1
def is_complete_link(self, link_id: tuple[int, int]) -> bool:
reverse_id = link_id[1], link_id[0]
return link_id in self.links and reverse_id in self.links
def get_link_label(self, link_id: tuple[int, int]) -> str:
source_id = tuple(sorted(link_id))
source_link = self.links[source_id]
dest_id = link_id[::-1]
dest_link = self.links[dest_id]
return f"{source_link.sinr:.1f} / {dest_link.sinr:.1f}"
def send_link(self, message_type: MessageFlags, link_id: tuple[int, int]) -> None:
nem1, nem2 = link_id
link = self.emane_manager.get_nem_link(nem1, nem2, message_type)
if link:
label = self.get_link_label(link_id)
link.label = label
self.emane_manager.session.broadcast_link(link)
def send_message(
self,
message_type: MessageFlags,
label: str,
node1: int,
node2: int,
emane_id: int,
) -> None:
color = self.emane_manager.session.get_link_color(emane_id)
link_data = LinkData(
message_type=message_type,
type=LinkTypes.WIRELESS,
label=label,
node1_id=node1,
node2_id=node2,
network_id=emane_id,
color=color,
)
self.emane_manager.session.broadcast_link(link_data)
def stop(self) -> None:
self.running = False
for client in self.clients:
client.stop()
self.clients.clear()
self.links.clear()
self.complete_links.clear()