From e2a9f6b1f4cca9d365796edbaba81866dc4f9487 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 18 May 2021 09:48:38 -0700 Subject: [PATCH 01/15] daemon: initial changes to support one emane process per nem --- .../configservices/quaggaservices/services.py | 6 -- daemon/core/emane/emanemanager.py | 86 ++++++++--------- daemon/core/emane/linkmonitor.py | 24 +++-- daemon/core/xml/emanexml.py | 92 ++++++++++--------- 4 files changed, 109 insertions(+), 99 deletions(-) diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index 922117cb..fba892b4 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -227,12 +227,6 @@ class Ospfv3mdr(Ospfv3): name: str = "OSPFv3MDR" - def data(self) -> Dict[str, Any]: - for iface in self.node.get_ifaces(): - is_wireless = isinstance(iface.net, (WlanNode, EmaneNet)) - logger.info("MDR wireless: %s", is_wireless) - return dict() - def quagga_iface_config(self, iface: CoreInterface) -> str: config = super().quagga_iface_config(iface) if isinstance(iface.net, (WlanNode, EmaneNet)): diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 11eda990..337cd0de 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -114,10 +114,13 @@ class EmaneManager: self.eventchannel: Optional[Tuple[str, int, str]] = None self.event_device: Optional[str] = None - def next_nem_id(self) -> int: + def next_nem_id(self, iface: CoreInterface) -> int: nem_id = int(self.config["nem_id_start"]) while nem_id in self.nems_to_ifaces: nem_id += 1 + self.nems_to_ifaces[nem_id] = iface + self.ifaces_to_nems[iface] = nem_id + self.write_nem(iface, nem_id) return nem_id def get_config( @@ -385,26 +388,54 @@ class EmaneManager: return start_nodes def start_node(self, data: StartData) -> None: + node = data.node control_net = self.session.add_remove_control_net( 0, remove=False, conf_required=False ) - emanexml.build_platform_xml(self, control_net, data) - self.start_daemon(data.node) + if isinstance(node, CoreNode): + # setup ota device + otagroup, _otaport = self.config["otamanagergroup"].split(":") + otadev = self.config["otamanagerdevice"] + otanetidx = self.session.get_control_net_index(otadev) + eventgroup, _eventport = self.config["eventservicegroup"].split(":") + eventdev = self.config["eventservicedevice"] + eventservicenetidx = self.session.get_control_net_index(eventdev) + # control network not yet started here + self.session.add_remove_control_iface( + node, 0, remove=False, conf_required=False + ) + if otanetidx > 0: + logger.info("adding ota device ctrl%d", otanetidx) + self.session.add_remove_control_iface( + node, otanetidx, remove=False, conf_required=False + ) + if eventservicenetidx >= 0: + logger.info("adding event service device ctrl%d", eventservicenetidx) + self.session.add_remove_control_iface( + node, eventservicenetidx, remove=False, conf_required=False + ) + # multicast route is needed for OTA data + logger.info("OTA GROUP(%s) OTA DEV(%s)", otagroup, otadev) + node.node_net_client.create_route(otagroup, otadev) + # multicast route is also needed for event data if on control network + if eventservicenetidx >= 0 and eventgroup != otagroup: + node.node_net_client.create_route(eventgroup, eventdev) + # builds xmls and start emane daemons for iface in data.ifaces: + emanexml.build_platform_xml(self, control_net, node, iface) + self.start_daemon(node, iface) self.install_iface(iface) - def set_nem(self, nem_id: int, iface: CoreInterface) -> None: - if nem_id in self.nems_to_ifaces: - raise CoreError(f"adding duplicate nem: {nem_id}") - self.nems_to_ifaces[nem_id] = iface - self.ifaces_to_nems[iface] = nem_id - def get_iface(self, nem_id: int) -> Optional[CoreInterface]: return self.nems_to_ifaces.get(nem_id) def get_nem_id(self, iface: CoreInterface) -> Optional[int]: return self.ifaces_to_nems.get(iface) + def get_nem_port(self, iface: CoreInterface) -> int: + nem_id = self.get_nem_id(iface) + return int(f"47{nem_id:03}") + def write_nem(self, iface: CoreInterface, nem_id: int) -> None: path = self.session.directory / "emane_nems" try: @@ -549,7 +580,7 @@ class EmaneManager: ) ) - def start_daemon(self, node: CoreNodeBase) -> None: + def start_daemon(self, node: CoreNodeBase, iface: CoreInterface) -> None: """ Start one EMANE daemon per node having a radio. Add a control network even if the user has not configured one. @@ -565,42 +596,15 @@ class EmaneManager: if realtime: emanecmd += " -r" if isinstance(node, CoreNode): - otagroup, _otaport = self.config["otamanagergroup"].split(":") - otadev = self.config["otamanagerdevice"] - otanetidx = self.session.get_control_net_index(otadev) - eventgroup, _eventport = self.config["eventservicegroup"].split(":") - eventdev = self.config["eventservicedevice"] - eventservicenetidx = self.session.get_control_net_index(eventdev) - - # control network not yet started here - self.session.add_remove_control_iface( - node, 0, remove=False, conf_required=False - ) - if otanetidx > 0: - logger.info("adding ota device ctrl%d", otanetidx) - self.session.add_remove_control_iface( - node, otanetidx, remove=False, conf_required=False - ) - if eventservicenetidx >= 0: - logger.info("adding event service device ctrl%d", eventservicenetidx) - self.session.add_remove_control_iface( - node, eventservicenetidx, remove=False, conf_required=False - ) - # multicast route is needed for OTA data - logger.info("OTA GROUP(%s) OTA DEV(%s)", otagroup, otadev) - node.node_net_client.create_route(otagroup, otadev) - # multicast route is also needed for event data if on control network - if eventservicenetidx >= 0 and eventgroup != otagroup: - node.node_net_client.create_route(eventgroup, eventdev) # start emane - log_file = node.directory / f"{node.name}-emane.log" - platform_xml = node.directory / f"{node.name}-platform.xml" + log_file = node.directory / f"{iface.name}-emane.log" + platform_xml = node.directory / emanexml.platform_file_name(iface) args = f"{emanecmd} -f {log_file} {platform_xml}" node.cmd(args) logger.info("node(%s) emane daemon running: %s", node.name, args) else: - log_file = self.session.directory / f"{node.name}-emane.log" - platform_xml = self.session.directory / f"{node.name}-platform.xml" + log_file = self.session.directory / f"{iface.name}-emane.log" + platform_xml = self.session.directory / emanexml.platform_file_name(iface) args = f"{emanecmd} -f {log_file} {platform_xml}" node.host_cmd(args, cwd=self.session.directory) logger.info("node(%s) host emane daemon running: %s", node.name, args) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 0c29b7a8..fd27c51d 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple 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 @@ -24,7 +25,6 @@ except ImportError: if TYPE_CHECKING: from core.emane.emanemanager import EmaneManager -DEFAULT_PORT: int = 47_000 MAC_COMPONENT_INDEX: int = 1 EMANE_RFPIPE: str = "rfpipemaclayer" EMANE_80211: str = "ieee80211abgmaclayer" @@ -79,10 +79,10 @@ class EmaneLink: class EmaneClient: - def __init__(self, address: str) -> None: + def __init__(self, address: str, port: int) -> None: self.address: str = address self.client: shell.ControlPortClient = shell.ControlPortClient( - self.address, DEFAULT_PORT + self.address, port ) self.nems: Dict[int, LossTable] = {} self.setup() @@ -204,22 +204,28 @@ class EmaneLinkMonitor: def initialize(self) -> None: addresses = self.get_addresses() - for address in addresses: - client = EmaneClient(address) + for address, port in addresses: + client = EmaneClient(address, port) if client.nems: self.clients.append(client) - def get_addresses(self) -> List[str]: + 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: - address = str(ip4.ip) - addresses.append(address) - break + 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: diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index ab7c2039..88c5aa38 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -18,7 +18,7 @@ from core.xml import corexml logger = logging.getLogger(__name__) if TYPE_CHECKING: - from core.emane.emanemanager import EmaneManager, StartData + from core.emane.emanemanager import EmaneManager from core.emane.emanemodel import EmaneModel _MAC_PREFIX = "02:02" @@ -146,7 +146,10 @@ def add_configurations( def build_platform_xml( - emane_manager: "EmaneManager", control_net: CtrlNet, data: "StartData" + emane_manager: "EmaneManager", + control_net: CtrlNet, + node: CoreNodeBase, + iface: CoreInterface, ) -> None: """ Create platform xml for a specific node. @@ -155,65 +158,64 @@ def build_platform_xml( configurations :param control_net: control net node for this emane network - :param data: start data for a node connected to emane and associated interfaces + :param node: node to create a platform xml for + :param iface: node interface to create platform xml for :return: the next nem id that can be used for creating platform xml files """ + # create nem xml entries for all interfaces + emane_net = iface.net + if not isinstance(emane_net, EmaneNet): + raise CoreError(f"emane interface not connected to emane net: {emane_net.name}") + nem_id = emane_manager.next_nem_id(iface) + config = emane_manager.get_iface_config(emane_net, iface) + emane_net.model.build_xml_files(config, iface) + # create top level platform element transport_configs = {"otamanagerdevice", "eventservicedevice"} platform_element = etree.Element("platform") for configuration in emane_manager.emane_config.emulator_config: name = configuration.id - if not isinstance(data.node, CoreNode) and name in transport_configs: + if not isinstance(node, CoreNode) and name in transport_configs: value = control_net.brname else: value = emane_manager.config[name] + if name == "controlportendpoint": + port = emane_manager.get_nem_port(iface) + value = f"0.0.0.0:{port}" add_param(platform_element, name, value) - # create nem xml entries for all interfaces - for iface in data.ifaces: - emane_net = iface.net - if not isinstance(emane_net, EmaneNet): - raise CoreError( - f"emane interface not connected to emane net: {emane_net.name}" - ) - nem_id = emane_manager.next_nem_id() - emane_manager.set_nem(nem_id, iface) - emane_manager.write_nem(iface, nem_id) - config = emane_manager.get_iface_config(emane_net, iface) - emane_net.model.build_xml_files(config, iface) + # build nem xml + nem_definition = nem_file_name(iface) + nem_element = etree.Element( + "nem", id=str(nem_id), name=iface.localname, definition=nem_definition + ) - # build nem xml - nem_definition = nem_file_name(iface) - nem_element = etree.Element( - "nem", id=str(nem_id), name=iface.localname, definition=nem_definition - ) + # check if this is an external transport + if is_external(config): + nem_element.set("transport", "external") + platform_endpoint = "platformendpoint" + add_param(nem_element, platform_endpoint, config[platform_endpoint]) + transport_endpoint = "transportendpoint" + add_param(nem_element, transport_endpoint, config[transport_endpoint]) - # check if this is an external transport - if is_external(config): - nem_element.set("transport", "external") - platform_endpoint = "platformendpoint" - add_param(nem_element, platform_endpoint, config[platform_endpoint]) - transport_endpoint = "transportendpoint" - add_param(nem_element, transport_endpoint, config[transport_endpoint]) + # define transport element + transport_name = transport_file_name(iface) + transport_element = etree.SubElement( + nem_element, "transport", definition=transport_name + ) + add_param(transport_element, "device", iface.name) - # define transport element - transport_name = transport_file_name(iface) - transport_element = etree.SubElement( - nem_element, "transport", definition=transport_name - ) - add_param(transport_element, "device", iface.name) + # add nem element to platform element + platform_element.append(nem_element) - # add nem element to platform element - platform_element.append(nem_element) - - # generate and assign interface mac address based on nem id - mac = _MAC_PREFIX + ":00:00:" - mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" - iface.set_mac(mac) + # generate and assign interface mac address based on nem id + mac = _MAC_PREFIX + ":00:00:" + mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}" + iface.set_mac(mac) doc_name = "platform" - file_name = f"{data.node.name}-platform.xml" - create_node_file(data.node, platform_element, doc_name, file_name) + file_name = platform_file_name(iface) + create_node_file(node, platform_element, doc_name, file_name) def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None: @@ -396,3 +398,7 @@ def phy_file_name(iface: CoreInterface) -> str: :return: phy xml file name """ return f"{iface.name}-phy.xml" + + +def platform_file_name(iface: CoreInterface) -> str: + return f"{iface.name}-platform.xml" From 071023b1d9ea13abee4c19ebdccd86f2ac61edd8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 18 May 2021 21:29:38 -0700 Subject: [PATCH 02/15] added platform config to emane models, to replace global config, moved core specific emane global configs to session options --- daemon/core/emane/emanemodel.py | 39 ++++++++++++++++++++------ daemon/core/emane/models/bypass.py | 6 ++-- daemon/core/emane/models/commeffect.py | 13 +++++++-- daemon/core/emulator/sessionconfig.py | 36 ++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index b6c037e0..54f8c72d 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -16,6 +16,8 @@ from core.nodes.interface import CoreInterface from core.xml import emanexml logger = logging.getLogger(__name__) +DEFAULT_DEV: str = "ctrl0" +MANIFEST_PATH: str = "share/emane/manifest" class EmaneModel(WirelessModel): @@ -25,6 +27,16 @@ class EmaneModel(WirelessModel): configurable parameters. Helper functions also live here. """ + # default platform configuration settings + platform_xml: str = "nemmanager.xml" + platform_defaults: Dict[str, str] = { + "eventservicedevice": DEFAULT_DEV, + "eventservicegroup": "224.1.2.8:45703", + "otamanagerdevice": DEFAULT_DEV, + "otamanagergroup": "224.1.2.8:45702", + } + platform_config: List[Configuration] = [] + # default mac configuration settings mac_library: Optional[str] = None mac_xml: Optional[str] = None @@ -57,20 +69,27 @@ class EmaneModel(WirelessModel): @classmethod def load(cls, emane_prefix: Path) -> None: """ - Called after being loaded within the EmaneManager. Provides configured emane_prefix for - parsing xml files. + Called after being loaded within the EmaneManager. Provides configured + emane_prefix for parsing xml files. :param emane_prefix: configured emane prefix path :return: nothing """ - manifest_path = "share/emane/manifest" + cls._load_platform_config(emane_prefix) # load mac configuration - mac_xml_path = emane_prefix / manifest_path / cls.mac_xml + mac_xml_path = emane_prefix / MANIFEST_PATH / cls.mac_xml cls.mac_config = emanemanifest.parse(mac_xml_path, cls.mac_defaults) # load phy configuration - phy_xml_path = emane_prefix / manifest_path / cls.phy_xml + phy_xml_path = emane_prefix / MANIFEST_PATH / cls.phy_xml cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults) + @classmethod + def _load_platform_config(cls, emane_prefix: Path) -> None: + platform_xml_path = emane_prefix / MANIFEST_PATH / cls.platform_xml + cls.platform_config = emanemanifest.parse( + platform_xml_path, cls.platform_defaults + ) + @classmethod def configurations(cls) -> List[Configuration]: """ @@ -78,7 +97,9 @@ class EmaneModel(WirelessModel): :return: all configurations """ - return cls.mac_config + cls.phy_config + cls.external_config + return ( + cls.platform_config + cls.mac_config + cls.phy_config + cls.external_config + ) @classmethod def config_groups(cls) -> List[ConfigGroup]: @@ -87,11 +108,13 @@ class EmaneModel(WirelessModel): :return: list of configuration groups. """ - mac_len = len(cls.mac_config) + platform_len = len(cls.platform_config) + mac_len = len(cls.mac_config) + platform_len phy_len = len(cls.phy_config) + mac_len config_len = len(cls.configurations()) return [ - ConfigGroup("MAC Parameters", 1, mac_len), + ConfigGroup("Platform Parameters", 1, platform_len), + ConfigGroup("MAC Parameters", platform_len + 1, mac_len), ConfigGroup("PHY Parameters", mac_len + 1, phy_len), ConfigGroup("External Parameters", phy_len + 1, config_len), ] diff --git a/daemon/core/emane/models/bypass.py b/daemon/core/emane/models/bypass.py index aebdde21..67b7707d 100644 --- a/daemon/core/emane/models/bypass.py +++ b/daemon/core/emane/models/bypass.py @@ -1,6 +1,7 @@ """ EMANE Bypass model for CORE """ +from pathlib import Path from typing import List, Set from core.config import Configuration @@ -30,6 +31,5 @@ class EmaneBypassModel(emanemodel.EmaneModel): phy_config: List[Configuration] = [] @classmethod - def load(cls, emane_prefix: str) -> None: - # ignore default logic - pass + def load(cls, emane_prefix: Path) -> None: + cls._load_platform_config(emane_prefix) diff --git a/daemon/core/emane/models/commeffect.py b/daemon/core/emane/models/commeffect.py index 2ce1715f..b73dc837 100644 --- a/daemon/core/emane/models/commeffect.py +++ b/daemon/core/emane/models/commeffect.py @@ -51,16 +51,25 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): @classmethod def load(cls, emane_prefix: Path) -> None: + cls._load_platform_config(emane_prefix) shim_xml_path = emane_prefix / "share/emane/manifest" / cls.shim_xml cls.config_shim = emanemanifest.parse(shim_xml_path, cls.shim_defaults) @classmethod def configurations(cls) -> List[Configuration]: - return cls.config_shim + return cls.platform_config + cls.config_shim @classmethod def config_groups(cls) -> List[ConfigGroup]: - return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))] + platform_len = len(cls.platform_config) + return [ + ConfigGroup("Platform Parameters", 1, platform_len), + ConfigGroup( + "CommEffect SHIM Parameters", + platform_len + 1, + len(cls.configurations()), + ), + ] def build_xml_files(self, config: Dict[str, str], iface: CoreInterface) -> None: """ diff --git a/daemon/core/emulator/sessionconfig.py b/daemon/core/emulator/sessionconfig.py index f76f4638..f40161cb 100644 --- a/daemon/core/emulator/sessionconfig.py +++ b/daemon/core/emulator/sessionconfig.py @@ -59,6 +59,42 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): Configuration( id="ovs", type=ConfigDataTypes.BOOL, default="0", label="Enable OVS" ), + Configuration( + id="platform_id_start", + type=ConfigDataTypes.INT32, + default="1", + label="EMANE Platform ID Start", + ), + Configuration( + id="nem_id_start", + type=ConfigDataTypes.INT32, + default="1", + label="EMANE NEM ID Start", + ), + Configuration( + id="link_enabled", + type=ConfigDataTypes.BOOL, + default="1", + label="EMANE Links?", + ), + Configuration( + id="loss_threshold", + type=ConfigDataTypes.INT32, + default="30", + label="EMANE Link Loss Threshold (%)", + ), + Configuration( + id="link_interval", + type=ConfigDataTypes.INT32, + default="1", + label="EMANE Link Check Interval (sec)", + ), + Configuration( + id="link_timeout", + type=ConfigDataTypes.INT32, + default="4", + label="EMANE Link Timeout (sec)", + ), ] config_type: RegisterTlvs = RegisterTlvs.UTILITY From 5bc3345d37af6770ea15e8d7e7a5a631ca3e926e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 19 May 2021 20:44:00 -0700 Subject: [PATCH 03/15] adjustments to remove global emane configuration, platform configurations can now be configured per nem, retrieve emane specific core settings from session options --- daemon/core/api/grpc/client.py | 29 --- daemon/core/api/grpc/grpcutils.py | 6 - daemon/core/api/grpc/server.py | 35 ---- daemon/core/api/grpc/wrappers.py | 7 - daemon/core/api/tlv/corehandlers.py | 40 ---- daemon/core/emane/emanemanager.py | 273 ++++++------------------- daemon/core/emane/linkmonitor.py | 7 +- daemon/core/nodes/netclient.py | 2 +- daemon/core/xml/corexml.py | 50 +---- daemon/core/xml/emanexml.py | 6 +- daemon/examples/grpc/emane80211.py | 5 +- daemon/proto/core/api/grpc/core.proto | 29 ++- daemon/proto/core/api/grpc/emane.proto | 17 -- daemon/scripts/core-cleanup | 1 + daemon/tests/test_grpc.py | 35 ---- daemon/tests/test_gui.py | 32 --- docs/grpc.md | 7 +- 17 files changed, 98 insertions(+), 483 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index b07d2c04..5ebed44e 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -28,10 +28,8 @@ from core.api.grpc.configservices_pb2 import ( from core.api.grpc.core_pb2 import ExecuteScriptRequest, GetConfigRequest from core.api.grpc.emane_pb2 import ( EmaneLinkRequest, - GetEmaneConfigRequest, GetEmaneEventChannelRequest, GetEmaneModelConfigRequest, - SetEmaneConfigRequest, SetEmaneModelConfigRequest, ) from core.api.grpc.mobility_pb2 import ( @@ -253,7 +251,6 @@ class CoreGrpcClient: if asymmetric_links: asymmetric_links = [x.to_proto() for x in asymmetric_links] hooks = [x.to_proto() for x in session.hooks.values()] - emane_config = {k: v.value for k, v in session.emane_config.items()} emane_model_configs = [] mobility_configs = [] wlan_configs = [] @@ -313,7 +310,6 @@ class CoreGrpcClient: links=links, location=session.location.to_proto(), hooks=hooks, - emane_config=emane_config, emane_model_configs=emane_model_configs, wlan_configs=wlan_configs, mobility_configs=mobility_configs, @@ -917,31 +913,6 @@ class CoreGrpcClient: response = self.stub.SetWlanConfig(request) return response.result - def get_emane_config(self, session_id: int) -> Dict[str, wrappers.ConfigOption]: - """ - Get session emane configuration. - - :param session_id: session id - :return: response with a list of configuration groups - :raises grpc.RpcError: when session doesn't exist - """ - request = GetEmaneConfigRequest(session_id=session_id) - response = self.stub.GetEmaneConfig(request) - return wrappers.ConfigOption.from_dict(response.config) - - def set_emane_config(self, session_id: int, config: Dict[str, str]) -> bool: - """ - Set session emane configuration. - - :param session_id: session id - :param config: emane configuration - :return: True for success, False otherwise - :raises grpc.RpcError: when session doesn't exist - """ - request = SetEmaneConfigRequest(session_id=session_id, config=config) - response = self.stub.SetEmaneConfig(request) - return response.result - def get_emane_model_config( self, session_id: int, node_id: int, model: str, iface_id: int = -1 ) -> Dict[str, wrappers.ConfigOption]: diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 67e11c5e..169819ba 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -630,10 +630,6 @@ def get_node_config_service_configs(session: Session) -> List[ConfigServiceConfi return configs -def get_emane_config(session: Session) -> Dict[str, common_pb2.ConfigOption]: - return get_config_options(session.emane.config, session.emane.emane_config) - - def get_mobility_node( session: Session, node_id: int, context: ServicerContext ) -> Union[WlanNode, EmaneNet]: @@ -663,7 +659,6 @@ def convert_session(session: Session) -> wrappers.Session: x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=session.location.refscale ) hooks = get_hooks(session) - emane_config = get_emane_config(session) emane_model_configs = get_emane_model_configs(session) wlan_configs = get_wlan_configs(session) mobility_configs = get_mobility_configs(session) @@ -685,7 +680,6 @@ def convert_session(session: Session) -> wrappers.Session: default_services=default_services, location=location, hooks=hooks, - emane_config=emane_config, emane_model_configs=emane_model_configs, wlan_configs=wlan_configs, service_configs=service_configs, diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 8b5c5c1f..e2851964 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -33,14 +33,10 @@ from core.api.grpc.emane_pb2 import ( EmaneLinkResponse, EmanePathlossesRequest, EmanePathlossesResponse, - GetEmaneConfigRequest, - GetEmaneConfigResponse, GetEmaneEventChannelRequest, GetEmaneEventChannelResponse, GetEmaneModelConfigRequest, GetEmaneModelConfigResponse, - SetEmaneConfigRequest, - SetEmaneConfigResponse, SetEmaneModelConfigRequest, SetEmaneModelConfigResponse, ) @@ -266,7 +262,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) # emane configs - session.emane.config.update(request.emane_config) for config in request.emane_model_configs: _id = utils.iface_config_id(config.node_id, config.iface_id) session.emane.set_config(_id, config.model, config.config) @@ -1045,36 +1040,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node.updatemodel(config) return SetWlanConfigResponse(result=True) - def GetEmaneConfig( - self, request: GetEmaneConfigRequest, context: ServicerContext - ) -> GetEmaneConfigResponse: - """ - Retrieve EMANE configuration of a session - - :param request: get-EMANE-configuration request - :param context: context object - :return: get-EMANE-configuration response - """ - logger.debug("get emane config: %s", request) - session = self.get_session(request.session_id, context) - config = grpcutils.get_emane_config(session) - return GetEmaneConfigResponse(config=config) - - def SetEmaneConfig( - self, request: SetEmaneConfigRequest, context: ServicerContext - ) -> SetEmaneConfigResponse: - """ - Set EMANE configuration of a session - - :param request: set-EMANE-configuration request - :param context: context object - :return: set-EMANE-configuration response - """ - logger.debug("set emane config: %s", request) - session = self.get_session(request.session_id, context) - session.emane.config.update(request.config) - return SetEmaneConfigResponse(result=True) - def GetEmaneModelConfig( self, request: GetEmaneModelConfigRequest, context: ServicerContext ) -> GetEmaneModelConfigResponse: diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py index a0277edd..802af3c3 100644 --- a/daemon/core/api/grpc/wrappers.py +++ b/daemon/core/api/grpc/wrappers.py @@ -783,7 +783,6 @@ class Session: x=0.0, y=0.0, z=0.0, lat=47.57917, lon=-122.13232, alt=2.0, scale=150.0 ) hooks: Dict[str, Hook] = field(default_factory=dict) - emane_config: Dict[str, ConfigOption] = field(default_factory=dict) metadata: Dict[str, str] = field(default_factory=dict) file: Path = None options: Dict[str, ConfigOption] = field(default_factory=dict) @@ -836,7 +835,6 @@ class Session: default_services=default_services, location=SessionLocation.from_proto(proto.location), hooks=hooks, - emane_config=ConfigOption.from_dict(proto.emane_config), metadata=dict(proto.metadata), file=file_path, options=options, @@ -889,11 +887,6 @@ class Session: self.links.append(link) return link - def set_emane(self, config: Dict[str, str]) -> None: - for key, value in config.items(): - option = ConfigOption(name=key, value=value) - self.emane_config[key] = option - def set_options(self, config: Dict[str, str]) -> None: for key, value in config.items(): option = ConfigOption(name=key, value=value) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 527924c1..a1c1d34a 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -1046,8 +1046,6 @@ class CoreHandler(socketserver.BaseRequestHandler): self.handle_config_mobility(message_type, config_data) elif config_data.object in self.session.mobility.models: replies = self.handle_config_mobility_models(message_type, config_data) - elif config_data.object == self.session.emane.name: - replies = self.handle_config_emane(message_type, config_data) elif config_data.object in EmaneModelManager.models: replies = self.handle_config_emane_models(message_type, config_data) else: @@ -1379,36 +1377,6 @@ class CoreHandler(socketserver.BaseRequestHandler): return replies - def handle_config_emane(self, message_type, config_data): - replies = [] - node_id = config_data.node - object_name = config_data.object - iface_id = config_data.iface_id - values_str = config_data.data_values - - node_id = utils.iface_config_id(node_id, iface_id) - logger.debug( - "received configure message for %s nodenum: %s", object_name, node_id - ) - if message_type == ConfigFlags.REQUEST: - logger.info("replying to configure request for %s model", object_name) - typeflags = ConfigFlags.NONE.value - config = self.session.emane.config - config_response = ConfigShim.config_data( - 0, node_id, typeflags, self.session.emane.emane_config, config - ) - replies.append(config_response) - elif message_type != ConfigFlags.RESET: - if not object_name: - logger.info("no configuration object for node %s", node_id) - return [] - - if values_str: - config = ConfigShim.str_to_dict(values_str) - self.session.emane.config = config - - return replies - def handle_config_emane_models(self, message_type, config_data): replies = [] node_id = config_data.node @@ -1851,14 +1819,6 @@ class CoreHandler(socketserver.BaseRequestHandler): ) self.session.broadcast_config(config_data) - # send global emane config - config = self.session.emane.config - logger.debug("global emane config: values(%s)", config) - config_data = ConfigShim.config_data( - 0, None, ConfigFlags.UPDATE.value, self.session.emane.emane_config, config - ) - self.session.broadcast_config(config_data) - # send emane model configs for node_id, model_configs in self.session.emane.node_configs.items(): for model_name, config in model_configs.items(): diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 337cd0de..b952d942 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -1,30 +1,21 @@ """ -emane.py: definition of an Emane class for implementing configuration control of an EMANE emulation. +Implements configuration and control of an EMANE emulation. """ import logging import os import threading -from collections import OrderedDict from dataclasses import dataclass, field from enum import Enum -from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type from core import utils -from core.config import ConfigGroup, Configuration -from core.emane import emanemanifest from core.emane.emanemodel import EmaneModel from core.emane.linkmonitor import EmaneLinkMonitor from core.emane.modelmanager import EmaneModelManager from core.emane.nodes import EmaneNet from core.emulator.data import LinkData -from core.emulator.enumerations import ( - ConfigDataTypes, - LinkTypes, - MessageFlags, - RegisterTlvs, -) +from core.emulator.enumerations import LinkTypes, MessageFlags, RegisterTlvs from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase from core.nodes.interface import CoreInterface, TunTap @@ -102,8 +93,6 @@ class EmaneManager: self.eventmonthread: Optional[threading.Thread] = None # model for global EMANE configuration options - self.emane_config: EmaneGlobalModel = EmaneGlobalModel(session) - self.config: Dict[str, str] = self.emane_config.default_values() self.node_configs: Dict[int, Dict[str, Dict[str, str]]] = {} self.node_models: Dict[int, str] = {} @@ -115,7 +104,7 @@ class EmaneManager: self.event_device: Optional[str] = None def next_nem_id(self, iface: CoreInterface) -> int: - nem_id = int(self.config["nem_id_start"]) + nem_id = self.session.options.get_config_int("nem_id_start") while nem_id in self.nems_to_ifaces: nem_id += 1 self.nems_to_ifaces[nem_id] = iface @@ -206,7 +195,6 @@ class EmaneManager: def config_reset(self, node_id: int = None) -> None: if node_id is None: - self.config = self.emane_config.default_values() self.node_configs.clear() self.node_models.clear() else: @@ -224,25 +212,20 @@ class EmaneManager: self.service = None self.event_device = None - def initeventservice(self, filename: str = None, shutdown: bool = False) -> None: + def initeventservice(self) -> None: """ Re-initialize the EMANE Event service. The multicast group and/or port may be configured. """ - self.deleteeventservice() - if shutdown: - return - # Get the control network to be used for events - group, port = self.config["eventservicegroup"].split(":") - self.event_device = self.config["eventservicedevice"] + group, port = "224.1.2.8:45703".split(":") + self.event_device = DEFAULT_DEV eventnetidx = self.session.get_control_net_index(self.event_device) if eventnetidx < 0: logger.error( "invalid emane event service device provided: %s", self.event_device ) return - # make sure the event control network is in place eventnet = self.session.add_remove_control_net( net_index=eventnetidx, remove=False, conf_required=False @@ -251,14 +234,13 @@ class EmaneManager: # direct EMANE events towards control net bridge self.event_device = eventnet.brname self.eventchannel = (group, int(port), self.event_device) - # disabled otachannel for event service # only needed for e.g. antennaprofile events xmit by models - logger.info("using %s for event service traffic", self.event_device) + logger.info("using %s for emane event service", self.event_device) try: self.service = EventService(eventchannel=self.eventchannel, otachannel=None) except EventServiceException: - logger.exception("error instantiating emane EventService") + logger.exception("error starting emane event service") def add_node(self, emane_net: EmaneNet) -> None: """ @@ -309,9 +291,8 @@ class EmaneManager: if EventService is None: raise CoreError("EMANE python bindings are not installed") - # control network bridge required for EMANE 0.9.2 - # - needs to exist when eventservice binds to it (initeventservice) - otadev = self.config["otamanagerdevice"] + # control network bridge required for emane + otadev = DEFAULT_DEV netidx = self.session.get_control_net_index(otadev) logger.debug("emane ota manager device: index(%s) otadev(%s)", netidx, otadev) if netidx < 0: @@ -320,25 +301,9 @@ class EmaneManager: otadev, ) return EmaneState.NOT_READY - self.session.add_remove_control_net( net_index=netidx, remove=False, conf_required=False ) - eventdev = self.config["eventservicedevice"] - logger.debug("emane event service device: eventdev(%s)", eventdev) - if eventdev != otadev: - netidx = self.session.get_control_net_index(eventdev) - logger.debug("emane event service device index: %s", netidx) - if netidx < 0: - logger.error( - "emane cannot start due to invalid event service device: %s", - eventdev, - ) - return EmaneState.NOT_READY - - self.session.add_remove_control_net( - net_index=netidx, remove=False, conf_required=False - ) self.check_node_models() return EmaneState.SUCCESS @@ -354,8 +319,9 @@ class EmaneManager: status = self.setup() if status != EmaneState.SUCCESS: return status - self.starteventmonitor() - self.buildeventservicexml() + self.initeventservice() + if self.service and self.doeventmonitor(): + self.starteventmonitor() with self._emane_node_lock: logger.info("emane building xmls...") start_data = self.get_start_data() @@ -392,40 +358,54 @@ class EmaneManager: control_net = self.session.add_remove_control_net( 0, remove=False, conf_required=False ) - if isinstance(node, CoreNode): - # setup ota device - otagroup, _otaport = self.config["otamanagergroup"].split(":") - otadev = self.config["otamanagerdevice"] - otanetidx = self.session.get_control_net_index(otadev) - eventgroup, _eventport = self.config["eventservicegroup"].split(":") - eventdev = self.config["eventservicedevice"] - eventservicenetidx = self.session.get_control_net_index(eventdev) - # control network not yet started here - self.session.add_remove_control_iface( - node, 0, remove=False, conf_required=False - ) - if otanetidx > 0: - logger.info("adding ota device ctrl%d", otanetidx) - self.session.add_remove_control_iface( - node, otanetidx, remove=False, conf_required=False - ) - if eventservicenetidx >= 0: - logger.info("adding event service device ctrl%d", eventservicenetidx) - self.session.add_remove_control_iface( - node, eventservicenetidx, remove=False, conf_required=False - ) - # multicast route is needed for OTA data - logger.info("OTA GROUP(%s) OTA DEV(%s)", otagroup, otadev) - node.node_net_client.create_route(otagroup, otadev) - # multicast route is also needed for event data if on control network - if eventservicenetidx >= 0 and eventgroup != otagroup: - node.node_net_client.create_route(eventgroup, eventdev) # builds xmls and start emane daemons for iface in data.ifaces: + if isinstance(node, CoreNode): + self.setup_ota(node, iface) emanexml.build_platform_xml(self, control_net, node, iface) self.start_daemon(node, iface) self.install_iface(iface) + def setup_ota(self, node: CoreNode, iface: CoreInterface) -> None: + if not isinstance(iface.net, EmaneNet): + raise CoreError( + f"emane interface not connected to emane net: {iface.net.name}" + ) + config = self.get_iface_config(iface.net, iface) + # setup ota device + otagroup, _otaport = config["otamanagergroup"].split(":") + otadev = config["otamanagerdevice"] + otanetidx = self.session.get_control_net_index(otadev) + eventgroup, _eventport = config["eventservicegroup"].split(":") + eventdev = config["eventservicedevice"] + eventservicenetidx = self.session.get_control_net_index(eventdev) + # control network not yet started here + self.session.add_remove_control_iface( + node, 0, remove=False, conf_required=False + ) + if otanetidx > 0: + logger.info("adding ota device ctrl%d", otanetidx) + self.session.add_remove_control_iface( + node, otanetidx, remove=False, conf_required=False + ) + if eventservicenetidx >= 0: + logger.info("adding event service device ctrl%d", eventservicenetidx) + self.session.add_remove_control_iface( + node, eventservicenetidx, remove=False, conf_required=False + ) + # multicast route is needed for OTA data + logger.info( + "node(%s) interface(%s) ota group(%s) dev(%s)", + node.name, + iface.name, + otagroup, + otadev, + ) + node.node_net_client.create_route(otagroup, otadev) + # multicast route is also needed for event data if on control network + if eventservicenetidx >= 0 and eventgroup != otagroup: + node.node_net_client.create_route(eventgroup, eventdev) + def get_iface(self, nem_id: int) -> Optional[CoreInterface]: return self.nems_to_ifaces.get(nem_id) @@ -445,7 +425,7 @@ class EmaneManager: logger.exception("error writing to emane nem file") def links_enabled(self) -> bool: - return self.config["link_enabled"] == "1" + return self.session.options.get_config_int("link_enabled") == 1 def poststartup(self) -> None: """ @@ -551,35 +531,6 @@ class EmaneManager: color=color, ) - def buildeventservicexml(self) -> None: - """ - Build the libemaneeventservice.xml file if event service options - were changed in the global config. - """ - need_xml = False - default_values = self.emane_config.default_values() - for name in ["eventservicegroup", "eventservicedevice"]: - a = default_values[name] - b = self.config[name] - if a != b: - need_xml = True - if not need_xml: - # reset to using default config - self.initeventservice() - return - try: - group, port = self.config["eventservicegroup"].split(":") - except ValueError: - logger.exception("invalid eventservicegroup in EMANE config") - return - dev = self.config["eventservicedevice"] - emanexml.create_event_service_xml(group, port, dev, self.session.directory) - self.session.distributed.execute( - lambda x: emanexml.create_event_service_xml( - group, port, dev, self.session.directory, x - ) - ) - def start_daemon(self, node: CoreNodeBase, iface: CoreInterface) -> None: """ Start one EMANE daemon per node having a radio. @@ -648,17 +599,7 @@ class EmaneManager: """ Start monitoring EMANE location events if configured to do so. """ - logger.info("emane start event monitor") - if not self.doeventmonitor(): - return - if self.service is None: - logger.error( - "Warning: EMANE events will not be generated " - "because the emaneeventservice\n binding was " - "unable to load " - "(install the python-emaneeventservice bindings)" - ) - return + logger.info("starting emane event monitor") self.doeventloop = True self.eventmonthread = threading.Thread( target=self.eventmonitorloop, daemon=True @@ -673,8 +614,7 @@ class EmaneManager: if self.service is not None: self.service.breakloop() # reset the service, otherwise nextEvent won"t work - self.initeventservice(shutdown=True) - + self.deleteeventservice() if self.eventmonthread is not None: self.eventmonthread.join() self.eventmonthread = None @@ -685,26 +625,17 @@ class EmaneManager: """ if self.service is None: return - logger.info( - "subscribing to EMANE location events. (%s)", - threading.currentThread().getName(), - ) - while self.doeventloop is True: + logger.info("subscribing to EMANE location events") + while self.doeventloop: _uuid, _seq, events = self.service.nextEvent() - # this occurs with 0.9.1 event service if not self.doeventloop: break - for event in events: nem, eid, data = event if eid == LocationEvent.IDENTIFIER: self.handlelocationevent(nem, eid, data) - - logger.info( - "unsubscribing from EMANE location events. (%s)", - threading.currentThread().getName(), - ) + logger.info("unsubscribing from EMANE location events") def handlelocationevent(self, rxnemid: int, eid: int, data: str) -> None: """ @@ -818,85 +749,3 @@ class EmaneManager: event.append(nem2, forward=rx2) self.service.publish(nem1, event) self.service.publish(nem2, event) - - -class EmaneGlobalModel: - """ - Global EMANE configuration options. - """ - - name: str = "emane" - bitmap: Optional[str] = None - - def __init__(self, session: "Session") -> None: - self.session: "Session" = session - self.core_config: List[Configuration] = [ - Configuration( - id="platform_id_start", - type=ConfigDataTypes.INT32, - default="1", - label="Starting Platform ID", - ), - Configuration( - id="nem_id_start", - type=ConfigDataTypes.INT32, - default="1", - label="Starting NEM ID", - ), - Configuration( - id="link_enabled", - type=ConfigDataTypes.BOOL, - default="1", - label="Enable Links?", - ), - Configuration( - id="loss_threshold", - type=ConfigDataTypes.INT32, - default="30", - label="Link Loss Threshold (%)", - ), - Configuration( - id="link_interval", - type=ConfigDataTypes.INT32, - default="1", - label="Link Check Interval (sec)", - ), - Configuration( - id="link_timeout", - type=ConfigDataTypes.INT32, - default="4", - label="Link Timeout (sec)", - ), - ] - self.emulator_config = None - self.parse_config() - - def parse_config(self) -> None: - emane_prefix = self.session.options.get_config( - "emane_prefix", default=DEFAULT_EMANE_PREFIX - ) - emane_prefix = Path(emane_prefix) - emulator_xml = emane_prefix / "share/emane/manifest/nemmanager.xml" - emulator_defaults = { - "eventservicedevice": DEFAULT_DEV, - "eventservicegroup": "224.1.2.8:45703", - "otamanagerdevice": DEFAULT_DEV, - "otamanagergroup": "224.1.2.8:45702", - } - self.emulator_config = emanemanifest.parse(emulator_xml, emulator_defaults) - - def configurations(self) -> List[Configuration]: - return self.emulator_config + self.core_config - - def config_groups(self) -> List[ConfigGroup]: - emulator_len = len(self.emulator_config) - config_len = len(self.configurations()) - return [ - ConfigGroup("Platform Attributes", 1, emulator_len), - ConfigGroup("CORE Configuration", emulator_len + 1, config_len), - ] - - def default_values(self) -> Dict[str, str]: - return OrderedDict( - [(config.id, config.default) for config in self.configurations()] - ) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index fd27c51d..9b18bae2 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -189,9 +189,10 @@ class EmaneLinkMonitor: self.running: bool = False def start(self) -> None: - self.loss_threshold = int(self.emane_manager.config["loss_threshold"]) - self.link_interval = int(self.emane_manager.config["link_interval"]) - self.link_timeout = int(self.emane_manager.config["link_timeout"]) + options = self.emane_manager.session.options + self.loss_threshold = options.get_config_int("loss_threshold") + self.link_interval = options.get_config_int("link_interval") + self.link_timeout = options.get_config_int("link_timeout") self.initialize() if not self.clients: logger.info("no valid emane models to monitor links") diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 729550b6..c066910b 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -38,7 +38,7 @@ class LinuxNetClient: :param device: device to add route to :return: nothing """ - self.run(f"{IP} route add {route} dev {device}") + self.run(f"{IP} route replace {route} dev {device}") def device_up(self, device: str) -> None: """ diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 629750bf..647300fc 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -80,20 +80,6 @@ def create_iface_data(iface_element: etree.Element) -> InterfaceData: ) -def create_emane_config(session: "Session") -> etree.Element: - emane_configuration = etree.Element("emane_global_configuration") - config = session.emane.config - emulator_element = etree.SubElement(emane_configuration, "emulator") - for emulator_config in session.emane.emane_config.emulator_config: - value = config[emulator_config.id] - add_configuration(emulator_element, emulator_config.id, value) - core_element = etree.SubElement(emane_configuration, "core") - for core_config in session.emane.emane_config.core_config: - value = config[core_config.id] - add_configuration(core_element, core_config.id, value) - return emane_configuration - - def create_emane_model_config( node_id: int, model: "EmaneModelType", @@ -104,22 +90,22 @@ def create_emane_model_config( add_attribute(emane_element, "node", node_id) add_attribute(emane_element, "iface", iface_id) add_attribute(emane_element, "model", model.name) - + platform_element = etree.SubElement(emane_element, "platform") + for platform_config in model.platform_config: + value = config[platform_config.id] + add_configuration(platform_element, platform_config.id, value) mac_element = etree.SubElement(emane_element, "mac") for mac_config in model.mac_config: value = config[mac_config.id] add_configuration(mac_element, mac_config.id, value) - phy_element = etree.SubElement(emane_element, "phy") for phy_config in model.phy_config: value = config[phy_config.id] add_configuration(phy_element, phy_config.id, value) - external_element = etree.SubElement(emane_element, "external") for external_config in model.external_config: value = config[external_config.id] add_configuration(external_element, external_config.id, value) - return emane_element @@ -376,8 +362,6 @@ class CoreXmlWriter: self.scenario.append(metadata_elements) def write_emane_configs(self) -> None: - emane_global_configuration = create_emane_config(self.session) - self.scenario.append(emane_global_configuration) emane_configurations = etree.Element("emane_configurations") for node_id, model_configs in self.session.emane.node_configs.items(): node_id, iface_id = utils.parse_iface_config_id(node_id) @@ -591,7 +575,6 @@ class CoreXmlReader: self.read_session_origin() self.read_service_configs() self.read_mobility_configs() - self.read_emane_global_config() self.read_nodes() self.read_links() self.read_emane_configs() @@ -729,28 +712,10 @@ class CoreXmlReader: files.add(name) service.configs = tuple(files) - def read_emane_global_config(self) -> None: - emane_global_configuration = self.scenario.find("emane_global_configuration") - if emane_global_configuration is None: - return - emulator_configuration = emane_global_configuration.find("emulator") - configs = {} - for config in emulator_configuration.iterchildren(): - name = config.get("name") - value = config.get("value") - configs[name] = value - core_configuration = emane_global_configuration.find("core") - for config in core_configuration.iterchildren(): - name = config.get("name") - value = config.get("value") - configs[name] = value - self.session.emane.config = configs - def read_emane_configs(self) -> None: emane_configurations = self.scenario.find("emane_configurations") if emane_configurations is None: return - for emane_configuration in emane_configurations.iterchildren(): node_id = get_int(emane_configuration, "node") iface_id = get_int(emane_configuration, "iface") @@ -768,18 +733,21 @@ class CoreXmlReader: ) # read and set emane model configuration + platform_configuration = emane_configuration.find("platform") + for config in platform_configuration.iterchildren(): + name = config.get("name") + value = config.get("value") + configs[name] = value mac_configuration = emane_configuration.find("mac") for config in mac_configuration.iterchildren(): name = config.get("name") value = config.get("value") configs[name] = value - phy_configuration = emane_configuration.find("phy") for config in phy_configuration.iterchildren(): name = config.get("name") value = config.get("value") configs[name] = value - external_configuration = emane_configuration.find("external") for config in external_configuration.iterchildren(): name = config.get("name") diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 88c5aa38..97646699 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -162,7 +162,7 @@ def build_platform_xml( :param iface: node interface to create platform xml for :return: the next nem id that can be used for creating platform xml files """ - # create nem xml entries for all interfaces + # create model based xml files emane_net = iface.net if not isinstance(emane_net, EmaneNet): raise CoreError(f"emane interface not connected to emane net: {emane_net.name}") @@ -173,12 +173,12 @@ def build_platform_xml( # create top level platform element transport_configs = {"otamanagerdevice", "eventservicedevice"} platform_element = etree.Element("platform") - for configuration in emane_manager.emane_config.emulator_config: + for configuration in emane_net.model.platform_config: name = configuration.id if not isinstance(node, CoreNode) and name in transport_configs: value = control_net.brname else: - value = emane_manager.config[name] + value = config[configuration.id] if name == "controlportendpoint": port = emane_manager.get_nem_port(iface) value = f"0.0.0.0:{port}" diff --git a/daemon/examples/grpc/emane80211.py b/daemon/examples/grpc/emane80211.py index fbafbe07..00c5458f 100644 --- a/daemon/examples/grpc/emane80211.py +++ b/daemon/examples/grpc/emane80211.py @@ -30,8 +30,9 @@ iface1 = iface_helper.create_iface(node2.id, 0) session.add_link(node1=node2, node2=emane, iface1=iface1) # setup emane configurations using a dict mapping currently support values as strings -session.set_emane({"eventservicettl": "2"}) -emane.set_emane_model(EmaneIeee80211abgModel.name, {"unicastrate": "3"}) +emane.set_emane_model( + EmaneIeee80211abgModel.name, {"eventservicettl": "2", "unicastrate": "3"} +) # start session core.start_session(session) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index ce66b038..3c703866 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -95,10 +95,6 @@ service CoreApi { } // emane rpc - rpc GetEmaneConfig (emane.GetEmaneConfigRequest) returns (emane.GetEmaneConfigResponse) { - } - rpc SetEmaneConfig (emane.SetEmaneConfigRequest) returns (emane.SetEmaneConfigResponse) { - } rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) { } rpc SetEmaneModelConfig (emane.SetEmaneModelConfigRequest) returns (emane.SetEmaneModelConfigResponse) { @@ -144,19 +140,18 @@ message StartSessionRequest { repeated Link links = 3; repeated Hook hooks = 4; SessionLocation location = 5; - map emane_config = 6; - repeated wlan.WlanConfig wlan_configs = 7; - repeated emane.EmaneModelConfig emane_model_configs = 8; - repeated mobility.MobilityConfig mobility_configs = 9; - repeated services.ServiceConfig service_configs = 10; - repeated services.ServiceFileConfig service_file_configs = 11; - repeated Link asymmetric_links = 12; - repeated configservices.ConfigServiceConfig config_service_configs = 13; - map options = 14; - string user = 15; - bool definition = 16; - map metadata = 17; - repeated Server servers = 18; + repeated wlan.WlanConfig wlan_configs = 6; + repeated emane.EmaneModelConfig emane_model_configs = 7; + repeated mobility.MobilityConfig mobility_configs = 8; + repeated services.ServiceConfig service_configs = 9; + repeated services.ServiceFileConfig service_file_configs = 10; + repeated Link asymmetric_links = 11; + repeated configservices.ConfigServiceConfig config_service_configs = 12; + map options = 13; + string user = 14; + bool definition = 15; + map metadata = 16; + repeated Server servers = 17; } message StartSessionResponse { diff --git a/daemon/proto/core/api/grpc/emane.proto b/daemon/proto/core/api/grpc/emane.proto index de739891..1e56a0aa 100644 --- a/daemon/proto/core/api/grpc/emane.proto +++ b/daemon/proto/core/api/grpc/emane.proto @@ -4,23 +4,6 @@ package emane; import "core/api/grpc/common.proto"; -message GetEmaneConfigRequest { - int32 session_id = 1; -} - -message GetEmaneConfigResponse { - map config = 1; -} - -message SetEmaneConfigRequest { - int32 session_id = 1; - map config = 2; -} - -message SetEmaneConfigResponse { - bool result = 1; -} - message GetEmaneModelConfigRequest { int32 session_id = 1; int32 node_id = 2; diff --git a/daemon/scripts/core-cleanup b/daemon/scripts/core-cleanup index c97d6843..ced76634 100755 --- a/daemon/scripts/core-cleanup +++ b/daemon/scripts/core-cleanup @@ -61,6 +61,7 @@ eval "$ifcommand" | awk ' /tmp\./ {print "removing interface " $1; system("ip link del " $1);} /gt\./ {print "removing interface " $1; system("ip link del " $1);} /b\./ {print "removing bridge " $1; system("ip link set " $1 " down; ip link del " $1);} + /ctrl[0-9]+\./ {print "removing bridge " $1; system("ip link set " $1 " down; ip link del " $1);} ' nft list ruleset | awk ' diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index e836251e..ebfe29eb 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -83,11 +83,6 @@ class TestGrpc: scale=location_scale, ) - # setup global emane config - emane_config_key = "platform_id_start" - emane_config_value = "2" - session.set_emane({emane_config_key: emane_config_value}) - # setup wlan config wlan_config_key = "range" wlan_config_value = "333" @@ -151,7 +146,6 @@ class TestGrpc: location_alt, ) assert real_session.location.refscale == location_scale - assert real_session.emane.config[emane_config_key] == emane_config_value set_wlan_config = real_session.mobility.get_model_config( wlan_node.id, BasicRangeModel.name ) @@ -518,35 +512,6 @@ class TestGrpc: assert config[range_key] == range_value assert wlan.model.range == int(range_value) - def test_get_emane_config(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - - # then - with client.context_connect(): - config = client.get_emane_config(session.id) - - # then - assert len(config) > 0 - - def test_set_emane_config(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - config_key = "platform_id_start" - config_value = "2" - - # then - with client.context_connect(): - result = client.set_emane_config(session.id, {config_key: config_value}) - - # then - assert result is True - config = session.emane.config - assert len(config) > 1 - assert config[config_key] == config_value - def test_set_emane_model_config(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index 7b8a987d..5f4ab487 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -941,35 +941,3 @@ class TestGui: config = coretlv.session.emane.get_config(wlan.id, EmaneIeee80211abgModel.name) assert config[config_key] == config_value - - def test_config_emane_request(self, coretlv: CoreHandler): - message = coreapi.CoreConfMessage.create( - 0, - [ - (ConfigTlvs.OBJECT, "emane"), - (ConfigTlvs.TYPE, ConfigFlags.REQUEST.value), - ], - ) - coretlv.handle_broadcast_config = mock.MagicMock() - - coretlv.handle_message(message) - - coretlv.handle_broadcast_config.assert_called_once() - - def test_config_emane_update(self, coretlv: CoreHandler): - config_key = "eventservicedevice" - config_value = "eth4" - values = {config_key: config_value} - message = coreapi.CoreConfMessage.create( - 0, - [ - (ConfigTlvs.OBJECT, "emane"), - (ConfigTlvs.TYPE, ConfigFlags.UPDATE.value), - (ConfigTlvs.VALUES, dict_to_str(values)), - ], - ) - - coretlv.handle_message(message) - - config = coretlv.session.emane.config - assert config[config_key] == config_value diff --git a/docs/grpc.md b/docs/grpc.md index 5cc0e7ae..aef79308 100644 --- a/docs/grpc.md +++ b/docs/grpc.md @@ -328,10 +328,11 @@ session.add_link(node1=node1, node2=emane, iface1=iface1) iface1 = iface_helper.create_iface(node2.id, 0) session.add_link(node1=node2, node2=emane, iface1=iface1) -# setting global emane configuration -session.set_emane({"eventservicettl": "2"}) # setting emane specific emane model configuration -emane.set_emane_model(EmaneIeee80211abgModel.name, {"unicastrate": "3"}) +emane.set_emane_model(EmaneIeee80211abgModel.name, { + "eventservicettl": "2", + "unicastrate": "3", +}) # start session core.start_session(session) From 6b5148566cf8ee1d81ffd4ae7fd653127ebbb64e Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 19 May 2021 20:49:18 -0700 Subject: [PATCH 04/15] daemon: adjustment for emane monitor shutdown to avoid locking --- daemon/core/emane/emanemanager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index b952d942..eb8c743b 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -613,11 +613,11 @@ class EmaneManager: self.doeventloop = False if self.service is not None: self.service.breakloop() + if self.eventmonthread is not None: + self.eventmonthread.join() + self.eventmonthread = None # reset the service, otherwise nextEvent won"t work self.deleteeventservice() - if self.eventmonthread is not None: - self.eventmonthread.join() - self.eventmonthread = None def eventmonitorloop(self) -> None: """ From 4ff650af675979b6f1db3d11ecea91662749a332 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 20 May 2021 12:24:54 -0700 Subject: [PATCH 05/15] pygui: removed global emane configuration dialog --- daemon/core/gui/dialogs/emaneconfig.py | 54 +------------------------- 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 9d9090b6..24ddf36c 100644 --- a/daemon/core/gui/dialogs/emaneconfig.py +++ b/daemon/core/gui/dialogs/emaneconfig.py @@ -19,40 +19,6 @@ if TYPE_CHECKING: from core.gui.app import Application -class GlobalEmaneDialog(Dialog): - def __init__(self, master: tk.BaseWidget, app: "Application") -> None: - super().__init__(app, "EMANE Configuration", master=master) - self.config_frame: Optional[ConfigFrame] = None - self.enabled: bool = not self.app.core.is_runtime() - self.draw() - - def draw(self) -> None: - self.top.columnconfigure(0, weight=1) - self.top.rowconfigure(0, weight=1) - session = self.app.core.session - self.config_frame = ConfigFrame( - self.top, self.app, session.emane_config, self.enabled - ) - self.config_frame.draw_config() - self.config_frame.grid(sticky=tk.NSEW, pady=PADY) - self.draw_buttons() - - def draw_buttons(self) -> None: - frame = ttk.Frame(self.top) - frame.grid(sticky=tk.EW) - for i in range(2): - frame.columnconfigure(i, weight=1) - state = tk.NORMAL if self.enabled else tk.DISABLED - button = ttk.Button(frame, text="Apply", command=self.click_apply, state=state) - button.grid(row=0, column=0, sticky=tk.EW, padx=PADX) - button = ttk.Button(frame, text="Cancel", command=self.destroy) - button.grid(row=0, column=1, sticky=tk.EW) - - def click_apply(self) -> None: - self.config_frame.parse_config() - self.destroy() - - class EmaneModelDialog(Dialog): def __init__( self, @@ -180,8 +146,7 @@ class EmaneConfigDialog(Dialog): def draw_emane_buttons(self) -> None: frame = ttk.Frame(self.top) frame.grid(sticky=tk.EW, pady=PADY) - for i in range(2): - frame.columnconfigure(i, weight=1) + frame.columnconfigure(0, weight=1) image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE) self.emane_model_button = ttk.Button( @@ -192,18 +157,7 @@ class EmaneConfigDialog(Dialog): command=self.click_model_config, ) self.emane_model_button.image = image - self.emane_model_button.grid(row=0, column=0, padx=PADX, sticky=tk.EW) - - image = images.from_enum(ImageEnum.EDITNODE, width=images.BUTTON_SIZE) - button = ttk.Button( - frame, - text="EMANE options", - image=image, - compound=tk.RIGHT, - command=self.click_emane_config, - ) - button.image = image - button.grid(row=0, column=1, sticky=tk.EW) + self.emane_model_button.grid(padx=PADX, sticky=tk.EW) def draw_apply_and_cancel(self) -> None: frame = ttk.Frame(self.top) @@ -216,10 +170,6 @@ class EmaneConfigDialog(Dialog): button = ttk.Button(frame, text="Cancel", command=self.destroy) button.grid(row=0, column=1, sticky=tk.EW) - def click_emane_config(self) -> None: - dialog = GlobalEmaneDialog(self, self.app) - dialog.show() - def click_model_config(self) -> None: """ draw emane model configuration From aea727ba42a0f4a7741eef3e7eaafea9eace1fb5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 20 May 2021 12:48:42 -0700 Subject: [PATCH 06/15] gui: adjustments to remove emane global options from legacy gui, since it is no longer applicable --- gui/wlan.tcl | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/gui/wlan.tcl b/gui/wlan.tcl index bea770a7..55f5319c 100644 --- a/gui/wlan.tcl +++ b/gui/wlan.tcl @@ -541,13 +541,7 @@ proc wlanConfigDialogHelper { wi target apply } { ttk::button $opts.model -text "model options" \ -image $plugin_img_edit -compound right -command "" -state disabled \ -command "configCap $target \[set g_selected_model\]" - # global EMANE model uses no node in config request message, although any - # config will be stored with the EMANE node having the lowest ID - ttk::button $opts.gen -text "EMANE options" \ - -image $plugin_img_edit -compound right \ - -command "configCap -1 emane" - #-command "popupPluginsCapConfigHelper $wi config $target" - pack $opts.model $opts.gen -side left -padx 4 -pady 4 + pack $opts.model -side left -padx 4 -pady 4 pack $opts -side top -anchor c -padx 4 -pady 4 # show correct tab basic/emane based on selection From ef0fa8c1a7d277ea091c837e58ca02f5654ff5c0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 21 May 2021 22:57:27 -0700 Subject: [PATCH 07/15] daemon: updates to emane manager to setup ota/event control networks on nodes and host based on individual nem configurations --- daemon/core/emane/emanemanager.py | 93 ++++++++++++------------------- daemon/core/xml/emanexml.py | 14 +---- 2 files changed, 37 insertions(+), 70 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index eb8c743b..f61bf452 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -274,6 +274,9 @@ class EmaneManager: :return: SUCCESS, NOT_NEEDED, NOT_READY in order to delay session instantiation """ + # check if bindings were installed + if EventService is None: + raise CoreError("EMANE python bindings are not installed") logger.debug("emane setup") with self.session.nodes_lock: for node_id in self.session.nodes: @@ -286,24 +289,6 @@ class EmaneManager: if not self._emane_nets: logger.debug("no emane nodes in session") return EmaneState.NOT_NEEDED - - # check if bindings were installed - if EventService is None: - raise CoreError("EMANE python bindings are not installed") - - # control network bridge required for emane - otadev = DEFAULT_DEV - netidx = self.session.get_control_net_index(otadev) - logger.debug("emane ota manager device: index(%s) otadev(%s)", netidx, otadev) - if netidx < 0: - logger.error( - "EMANE cannot start, check core config. invalid OTA device provided: %s", - otadev, - ) - return EmaneState.NOT_READY - self.session.add_remove_control_net( - net_index=netidx, remove=False, conf_required=False - ) self.check_node_models() return EmaneState.SUCCESS @@ -322,14 +307,23 @@ class EmaneManager: self.initeventservice() if self.service and self.doeventmonitor(): self.starteventmonitor() + self.startup_nodes() + if self.links_enabled(): + self.link_monitor.start() + return EmaneState.SUCCESS + + def startup_nodes(self) -> None: with self._emane_node_lock: logger.info("emane building xmls...") start_data = self.get_start_data() for data in start_data: - self.start_node(data) - if self.links_enabled(): - self.link_monitor.start() - return EmaneState.SUCCESS + node = data.node + for iface in data.ifaces: + if isinstance(node, CoreNode): + self.setup_ota(node, iface) + emanexml.build_platform_xml(self, node, iface) + self.start_daemon(node, iface) + self.install_iface(iface) def get_start_data(self) -> List[StartData]: node_map = {} @@ -353,19 +347,6 @@ class EmaneManager: start_node.ifaces = sorted(start_node.ifaces, key=lambda x: x.node_id) return start_nodes - def start_node(self, data: StartData) -> None: - node = data.node - control_net = self.session.add_remove_control_net( - 0, remove=False, conf_required=False - ) - # builds xmls and start emane daemons - for iface in data.ifaces: - if isinstance(node, CoreNode): - self.setup_ota(node, iface) - emanexml.build_platform_xml(self, control_net, node, iface) - self.start_daemon(node, iface) - self.install_iface(iface) - def setup_ota(self, node: CoreNode, iface: CoreInterface) -> None: if not isinstance(iface.net, EmaneNet): raise CoreError( @@ -375,35 +356,27 @@ class EmaneManager: # setup ota device otagroup, _otaport = config["otamanagergroup"].split(":") otadev = config["otamanagerdevice"] - otanetidx = self.session.get_control_net_index(otadev) + ota_index = self.session.get_control_net_index(otadev) + self.session.add_remove_control_net(ota_index, conf_required=False) + self.session.add_remove_control_iface(node, ota_index, conf_required=False) + # setup event device eventgroup, _eventport = config["eventservicegroup"].split(":") eventdev = config["eventservicedevice"] - eventservicenetidx = self.session.get_control_net_index(eventdev) - # control network not yet started here - self.session.add_remove_control_iface( - node, 0, remove=False, conf_required=False - ) - if otanetidx > 0: - logger.info("adding ota device ctrl%d", otanetidx) - self.session.add_remove_control_iface( - node, otanetidx, remove=False, conf_required=False - ) - if eventservicenetidx >= 0: - logger.info("adding event service device ctrl%d", eventservicenetidx) - self.session.add_remove_control_iface( - node, eventservicenetidx, remove=False, conf_required=False - ) - # multicast route is needed for OTA data + event_index = self.session.get_control_net_index(eventdev) + self.session.add_remove_control_net(event_index, conf_required=False) + self.session.add_remove_control_iface(node, event_index, conf_required=False) + # setup multicast routes as needed logger.info( - "node(%s) interface(%s) ota group(%s) dev(%s)", + "node(%s) interface(%s) ota(%s:%s) event(%s:%s)", node.name, iface.name, otagroup, otadev, + eventgroup, + eventdev, ) node.node_net_client.create_route(otagroup, otadev) - # multicast route is also needed for event data if on control network - if eventservicenetidx >= 0 and eventgroup != otagroup: + if eventgroup != otagroup: node.node_net_client.create_route(eventgroup, eventdev) def get_iface(self, nem_id: int) -> Optional[CoreInterface]: @@ -536,7 +509,13 @@ class EmaneManager: Start one EMANE daemon per node having a radio. Add a control network even if the user has not configured one. """ - logger.info("starting emane daemons...") + nem = self.get_nem_id(iface) + logger.info( + "starting emane daemon node(%s) iface(%s) nem(%s)", + node.name, + iface.name, + nem, + ) loglevel = str(DEFAULT_LOG_LEVEL) cfgloglevel = self.session.options.get_config_int("emane_log_level") realtime = self.session.options.get_config_bool("emane_realtime", default=True) @@ -552,13 +531,11 @@ class EmaneManager: platform_xml = node.directory / emanexml.platform_file_name(iface) args = f"{emanecmd} -f {log_file} {platform_xml}" node.cmd(args) - logger.info("node(%s) emane daemon running: %s", node.name, args) else: log_file = self.session.directory / f"{iface.name}-emane.log" platform_xml = self.session.directory / emanexml.platform_file_name(iface) args = f"{emanecmd} -f {log_file} {platform_xml}" node.host_cmd(args, cwd=self.session.directory) - logger.info("node(%s) host emane daemon running: %s", node.name, args) def install_iface(self, iface: CoreInterface) -> None: emane_net = iface.net diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 97646699..e26d0488 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -12,7 +12,6 @@ from core.emulator.distributed import DistributedServer from core.errors import CoreError from core.nodes.base import CoreNode, CoreNodeBase from core.nodes.interface import CoreInterface -from core.nodes.network import CtrlNet from core.xml import corexml logger = logging.getLogger(__name__) @@ -146,18 +145,13 @@ def add_configurations( def build_platform_xml( - emane_manager: "EmaneManager", - control_net: CtrlNet, - node: CoreNodeBase, - iface: CoreInterface, + emane_manager: "EmaneManager", node: CoreNodeBase, iface: CoreInterface ) -> None: """ Create platform xml for a specific node. :param emane_manager: emane manager with emane configurations - :param control_net: control net node for this emane - network :param node: node to create a platform xml for :param iface: node interface to create platform xml for :return: the next nem id that can be used for creating platform xml files @@ -171,14 +165,10 @@ def build_platform_xml( emane_net.model.build_xml_files(config, iface) # create top level platform element - transport_configs = {"otamanagerdevice", "eventservicedevice"} platform_element = etree.Element("platform") for configuration in emane_net.model.platform_config: name = configuration.id - if not isinstance(node, CoreNode) and name in transport_configs: - value = control_net.brname - else: - value = config[configuration.id] + value = config[configuration.id] if name == "controlportendpoint": port = emane_manager.get_nem_port(iface) value = f"0.0.0.0:{port}" From bcd9cc7ac20c1a21e89e298bc5885523e7834a57 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 24 May 2021 21:41:05 -0700 Subject: [PATCH 08/15] daemon: updates to provide new logic for emane event services, creating one per unique control channel, added mapping for nems to associated service for generated events --- daemon/core/emane/emanemanager.py | 237 +++++++++++++------------ daemon/core/emane/models/commeffect.py | 8 +- daemon/core/emane/models/tdma.py | 11 +- daemon/core/emane/nodes.py | 28 ++- daemon/core/xml/emanexml.py | 4 +- 5 files changed, 146 insertions(+), 142 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index f61bf452..47279592 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -7,7 +7,7 @@ import os import threading from dataclasses import dataclass, field from enum import Enum -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union from core import utils from core.emane.emanemodel import EmaneModel @@ -27,15 +27,19 @@ if TYPE_CHECKING: from core.emulator.session import Session try: - from emane.events import EventService, PathlossEvent - from emane.events import LocationEvent + from emane.events import EventService, PathlossEvent, CommEffectEvent, LocationEvent from emane.events.eventserviceexception import EventServiceException except ImportError: try: - from emanesh.events import EventService - from emanesh.events import LocationEvent + from emanesh.events import ( + EventService, + PathlossEvent, + CommEffectEvent, + LocationEvent, + ) from emanesh.events.eventserviceexception import EventServiceException except ImportError: + CommEffectEvent = None EventService = None LocationEvent = None PathlossEvent = None @@ -59,6 +63,59 @@ class StartData: ifaces: List[CoreInterface] = field(default_factory=list) +class EmaneEventService: + def __init__( + self, manager: "EmaneManager", device: str, group: str, port: int + ) -> None: + self.manager: "EmaneManager" = manager + self.device: str = device + self.group: str = group + self.port: int = port + self.running: bool = False + self.thread: Optional[threading.Thread] = None + logger.info("starting emane event service %s %s:%s", device, group, port) + self.events: EventService = EventService( + eventchannel=(group, port, device), otachannel=None + ) + + def start(self) -> None: + self.running = True + self.thread = threading.Thread(target=self.run, daemon=True) + self.thread.start() + + def run(self) -> None: + """ + Run and monitor events. + """ + logger.info("subscribing to emane location events") + while self.running: + _uuid, _seq, events = self.events.nextEvent() + # this occurs with 0.9.1 event service + if not self.running: + break + for event in events: + nem, eid, data = event + if eid == LocationEvent.IDENTIFIER: + self.manager.handlelocationevent(nem, eid, data) + logger.info("unsubscribing from emane location events") + + def stop(self) -> None: + """ + Stop service and monitoring events. + """ + self.events.breakloop() + self.running = False + if self.thread: + self.thread.join() + self.thread = None + for fd in self.events._readFd, self.events._writeFd: + if fd >= 0: + os.close(fd) + for f in self.events._socket, self.events._socketOTA: + if f: + f.close() + + class EmaneManager: """ EMANE controller object. Lives in a Session instance and is used for @@ -98,10 +155,10 @@ class EmaneManager: # link monitor self.link_monitor: EmaneLinkMonitor = EmaneLinkMonitor(self) - - self.service: Optional[EventService] = None + # emane event monitoring + self.services: Dict[str, EmaneEventService] = {} + self.nem_service: Dict[int, EmaneEventService] = {} self.eventchannel: Optional[Tuple[str, int, str]] = None - self.event_device: Optional[str] = None def next_nem_id(self, iface: CoreInterface) -> int: nem_id = self.session.options.get_config_int("nem_id_start") @@ -201,47 +258,6 @@ class EmaneManager: self.node_configs.get(node_id, {}).clear() self.node_models.pop(node_id, None) - def deleteeventservice(self) -> None: - if self.service: - for fd in self.service._readFd, self.service._writeFd: - if fd >= 0: - os.close(fd) - for f in self.service._socket, self.service._socketOTA: - if f: - f.close() - self.service = None - self.event_device = None - - def initeventservice(self) -> None: - """ - Re-initialize the EMANE Event service. - The multicast group and/or port may be configured. - """ - # Get the control network to be used for events - group, port = "224.1.2.8:45703".split(":") - self.event_device = DEFAULT_DEV - eventnetidx = self.session.get_control_net_index(self.event_device) - if eventnetidx < 0: - logger.error( - "invalid emane event service device provided: %s", self.event_device - ) - return - # make sure the event control network is in place - eventnet = self.session.add_remove_control_net( - net_index=eventnetidx, remove=False, conf_required=False - ) - if eventnet is not None: - # direct EMANE events towards control net bridge - self.event_device = eventnet.brname - self.eventchannel = (group, int(port), self.event_device) - # disabled otachannel for event service - # only needed for e.g. antennaprofile events xmit by models - logger.info("using %s for emane event service", self.event_device) - try: - self.service = EventService(eventchannel=self.eventchannel, otachannel=None) - except EventServiceException: - logger.exception("error starting emane event service") - def add_node(self, emane_net: EmaneNet) -> None: """ Add EMANE network object to this manager. @@ -304,9 +320,6 @@ class EmaneManager: status = self.setup() if status != EmaneState.SUCCESS: return status - self.initeventservice() - if self.service and self.doeventmonitor(): - self.starteventmonitor() self.startup_nodes() if self.links_enabled(): self.link_monitor.start() @@ -319,9 +332,15 @@ class EmaneManager: for data in start_data: node = data.node for iface in data.ifaces: - if isinstance(node, CoreNode): - self.setup_ota(node, iface) - emanexml.build_platform_xml(self, node, iface) + nem_id = self.next_nem_id(iface) + logger.info( + "starting emane for node(%s) iface(%s) nem(%s)", + node.name, + iface.name, + nem_id, + ) + self.setup_control_channels(nem_id, node, iface) + emanexml.build_platform_xml(self, nem_id, node, iface) self.start_daemon(node, iface) self.install_iface(iface) @@ -347,7 +366,9 @@ class EmaneManager: start_node.ifaces = sorted(start_node.ifaces, key=lambda x: x.node_id) return start_nodes - def setup_ota(self, node: CoreNode, iface: CoreInterface) -> None: + def setup_control_channels( + self, nem_id: int, node: CoreNodeBase, iface: CoreInterface + ) -> None: if not isinstance(iface.net, EmaneNet): raise CoreError( f"emane interface not connected to emane net: {iface.net.name}" @@ -358,13 +379,35 @@ class EmaneManager: otadev = config["otamanagerdevice"] ota_index = self.session.get_control_net_index(otadev) self.session.add_remove_control_net(ota_index, conf_required=False) - self.session.add_remove_control_iface(node, ota_index, conf_required=False) + if isinstance(node, CoreNode): + self.session.add_remove_control_iface(node, ota_index, conf_required=False) # setup event device - eventgroup, _eventport = config["eventservicegroup"].split(":") + eventgroup, eventport = config["eventservicegroup"].split(":") eventdev = config["eventservicedevice"] event_index = self.session.get_control_net_index(eventdev) - self.session.add_remove_control_net(event_index, conf_required=False) - self.session.add_remove_control_iface(node, event_index, conf_required=False) + event_net = self.session.add_remove_control_net( + event_index, conf_required=False + ) + if isinstance(node, CoreNode): + self.session.add_remove_control_iface( + node, event_index, conf_required=False + ) + # initialize emane event services + service = self.services.get(event_net.brname) + if not service: + try: + service = EmaneEventService( + self, event_net.brname, eventgroup, int(eventport) + ) + self.services[event_net.brname] = service + self.nem_service[nem_id] = service + except EventServiceException: + raise CoreError( + "failed to start emane event services " + f"{event_net.brname} {eventgroup}:{eventport}" + ) + else: + self.nem_service[nem_id] = service # setup multicast routes as needed logger.info( "node(%s) interface(%s) ota(%s:%s) event(%s:%s)", @@ -451,7 +494,11 @@ class EmaneManager: node.cmd(kill_emaned, wait=False) else: node.host_cmd(kill_emaned, wait=False) - self.stopeventmonitor() + # stop emane event services + while self.services: + _, service = self.services.popitem() + service.stop() + self.nem_service.clear() def check_node_models(self) -> None: """ @@ -509,13 +556,6 @@ class EmaneManager: Start one EMANE daemon per node having a radio. Add a control network even if the user has not configured one. """ - nem = self.get_nem_id(iface) - logger.info( - "starting emane daemon node(%s) iface(%s) nem(%s)", - node.name, - iface.name, - nem, - ) loglevel = str(DEFAULT_LOG_LEVEL) cfgloglevel = self.session.options.get_config_int("emane_log_level") realtime = self.session.options.get_config_bool("emane_realtime", default=True) @@ -572,48 +612,6 @@ class EmaneManager: tmp = not self.doeventmonitor() return tmp - def starteventmonitor(self) -> None: - """ - Start monitoring EMANE location events if configured to do so. - """ - logger.info("starting emane event monitor") - self.doeventloop = True - self.eventmonthread = threading.Thread( - target=self.eventmonitorloop, daemon=True - ) - self.eventmonthread.start() - - def stopeventmonitor(self) -> None: - """ - Stop monitoring EMANE location events. - """ - self.doeventloop = False - if self.service is not None: - self.service.breakloop() - if self.eventmonthread is not None: - self.eventmonthread.join() - self.eventmonthread = None - # reset the service, otherwise nextEvent won"t work - self.deleteeventservice() - - def eventmonitorloop(self) -> None: - """ - Thread target that monitors EMANE location events. - """ - if self.service is None: - return - logger.info("subscribing to EMANE location events") - while self.doeventloop: - _uuid, _seq, events = self.service.nextEvent() - # this occurs with 0.9.1 event service - if not self.doeventloop: - break - for event in events: - nem, eid, data = event - if eid == LocationEvent.IDENTIFIER: - self.handlelocationevent(nem, eid, data) - logger.info("unsubscribing from EMANE location events") - def handlelocationevent(self, rxnemid: int, eid: int, data: str) -> None: """ Handle an EMANE location event. @@ -629,7 +627,6 @@ class EmaneManager: ): logger.warning("dropped invalid location event") continue - # yaw,pitch,roll,azimuth,elevation,velocity are unhandled lat = attrs["latitude"] lon = attrs["longitude"] @@ -724,5 +721,19 @@ class EmaneManager: event = PathlossEvent() event.append(nem1, forward=rx1) event.append(nem2, forward=rx2) - self.service.publish(nem1, event) - self.service.publish(nem2, event) + self.publish_event(nem1, event) + self.publish_event(nem2, event) + + def publish_event( + self, + nem_id: int, + event: Union[PathlossEvent, CommEffectEvent, LocationEvent], + send_all: bool = False, + ) -> None: + service = self.nem_service.get(nem_id) + if not service: + logger.error("no service to publish event nem(%s)", nem_id) + return + if send_all: + nem_id = 0 + service.events.publish(nem_id, event) diff --git a/daemon/core/emane/models/commeffect.py b/daemon/core/emane/models/commeffect.py index b73dc837..c3f0b07b 100644 --- a/daemon/core/emane/models/commeffect.py +++ b/daemon/core/emane/models/commeffect.py @@ -122,15 +122,9 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): Generate CommEffect events when a Link Message is received having link parameters. """ - service = self.session.emane.service - if service is None: - logger.warning("%s: EMANE event service unavailable", self.name) - return - if iface is None or iface2 is None: logger.warning("%s: missing NEM information", self.name) return - # TODO: batch these into multiple events per transmission # TODO: may want to split out seconds portion of delay and jitter event = CommEffectEvent() @@ -146,4 +140,4 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): unicast=int(convert_none(options.bandwidth)), broadcast=int(convert_none(options.bandwidth)), ) - service.publish(nem2, event) + self.session.emane.publish_event(nem2, event) diff --git a/daemon/core/emane/models/tdma.py b/daemon/core/emane/models/tdma.py index 0ba756e4..62843ec1 100644 --- a/daemon/core/emane/models/tdma.py +++ b/daemon/core/emane/models/tdma.py @@ -59,8 +59,9 @@ class EmaneTdmaModel(emanemodel.EmaneModel): logger.warning("ignoring invalid tdma schedule: %s", schedule) return # initiate tdma schedule - event_device = self.session.emane.event_device - logger.info( - "setting up tdma schedule: schedule(%s) device(%s)", schedule, event_device - ) - utils.cmd(f"emaneevent-tdmaschedule -i {event_device} {schedule}") + for service in self.session.emane.services.values(): + device = service.device + logger.info( + "setting up tdma schedule: schedule(%s) device(%s)", schedule, device + ) + utils.cmd(f"emaneevent-tdmaschedule -i {device} {schedule}") diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 1e43723b..12caf408 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -140,15 +140,12 @@ class EmaneNet(CoreNetworkBase): :param iface: interface to set nem position for """ - if self.session.emane.service is None: - logger.info("position service not available") - return position = self._nem_position(iface) if position: nemid, lon, lat, alt = position event = LocationEvent() event.append(nemid, latitude=lat, longitude=lon, altitude=alt) - self.session.emane.service.publish(0, event) + self.session.emane.publish_event(nemid, event, send_all=True) def setnempositions(self, moved_ifaces: List[CoreInterface]) -> None: """ @@ -156,20 +153,21 @@ class EmaneNet(CoreNetworkBase): calculation. Generate an EMANE Location Event having several entries for each interface that has moved. """ - if len(moved_ifaces) == 0: + if not moved_ifaces: return - - if self.session.emane.service is None: - logger.info("position service not available") - return - - event = LocationEvent() + services = {} for iface in moved_ifaces: position = self._nem_position(iface) - if position: - nemid, lon, lat, alt = position - event.append(nemid, latitude=lat, longitude=lon, altitude=alt) - self.session.emane.service.publish(0, event) + if not position: + continue + nem_id, lon, lat, alt = position + service = self.session.emane.nem_service.get(nem_id) + if not service: + continue + event = services.setdefault(service, LocationEvent()) + event.append(nem_id, latitude=lat, longitude=lon, altitude=alt) + for service, event in services.items(): + service.events.publish(0, event) def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: links = super().links(flags) diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index e26d0488..9d0753cb 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -145,13 +145,14 @@ def add_configurations( def build_platform_xml( - emane_manager: "EmaneManager", node: CoreNodeBase, iface: CoreInterface + emane_manager: "EmaneManager", nem_id: int, node: CoreNodeBase, iface: CoreInterface ) -> None: """ Create platform xml for a specific node. :param emane_manager: emane manager with emane configurations + :param nem_id: nem id for current node/interface :param node: node to create a platform xml for :param iface: node interface to create platform xml for :return: the next nem id that can be used for creating platform xml files @@ -160,7 +161,6 @@ def build_platform_xml( emane_net = iface.net if not isinstance(emane_net, EmaneNet): raise CoreError(f"emane interface not connected to emane net: {emane_net.name}") - nem_id = emane_manager.next_nem_id(iface) config = emane_manager.get_iface_config(emane_net, iface) emane_net.model.build_xml_files(config, iface) From 8d5c3bd2126590759cc7f2dd5ad1afe295f4b61d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 25 May 2021 10:52:50 -0700 Subject: [PATCH 09/15] grpc: update grpc call for get emane event channel to take in a nem id, since channels may now be unique per nem --- daemon/core/api/grpc/client.py | 7 +++++-- daemon/core/api/grpc/server.py | 12 ++++++------ daemon/core/emane/emanemanager.py | 3 +-- daemon/proto/core/api/grpc/emane.proto | 1 + 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index 5ebed44e..2785a037 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -1034,15 +1034,18 @@ class CoreGrpcClient: response = self.stub.GetNodeConfigService(request) return dict(response.config) - def get_emane_event_channel(self, session_id: int) -> wrappers.EmaneEventChannel: + def get_emane_event_channel( + self, session_id: int, nem_id: int + ) -> wrappers.EmaneEventChannel: """ Retrieves the current emane event channel being used for a session. :param session_id: session to get emane event channel for + :param nem_id: nem id for the desired event channel :return: emane event channel :raises grpc.RpcError: when session doesn't exist """ - request = GetEmaneEventChannelRequest(session_id=session_id) + request = GetEmaneEventChannelRequest(session_id=session_id, nem_id=nem_id) response = self.stub.GetEmaneEventChannel(request) return wrappers.EmaneEventChannel.from_proto(response) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index e2851964..8b0b903a 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1241,12 +1241,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): self, request: GetEmaneEventChannelRequest, context: ServicerContext ) -> GetEmaneEventChannelResponse: session = self.get_session(request.session_id, context) - group = None - port = None - device = None - if session.emane.eventchannel: - group, port, device = session.emane.eventchannel - return GetEmaneEventChannelResponse(group=group, port=port, device=device) + service = session.emane.nem_service.get(request.nem_id) + if not service: + context.abort(grpc.StatusCode.NOT_FOUND, f"unknown nem id {request.nem_id}") + return GetEmaneEventChannelResponse( + group=service.group, port=service.port, device=service.device + ) def ExecuteScript(self, request, context): existing_sessions = set(self.coreemu.sessions.keys()) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 47279592..5a403c20 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -7,7 +7,7 @@ import os import threading from dataclasses import dataclass, field from enum import Enum -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Type, Union from core import utils from core.emane.emanemodel import EmaneModel @@ -158,7 +158,6 @@ class EmaneManager: # emane event monitoring self.services: Dict[str, EmaneEventService] = {} self.nem_service: Dict[int, EmaneEventService] = {} - self.eventchannel: Optional[Tuple[str, int, str]] = None def next_nem_id(self, iface: CoreInterface) -> int: nem_id = self.session.options.get_config_int("nem_id_start") diff --git a/daemon/proto/core/api/grpc/emane.proto b/daemon/proto/core/api/grpc/emane.proto index 1e56a0aa..5aa0c952 100644 --- a/daemon/proto/core/api/grpc/emane.proto +++ b/daemon/proto/core/api/grpc/emane.proto @@ -33,6 +33,7 @@ message GetEmaneModelConfig { message GetEmaneEventChannelRequest { int32 session_id = 1; + int32 nem_id = 2; } message GetEmaneEventChannelResponse { From 820539191d987453d986bac2bdf37edba42df5fe Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Tue, 25 May 2021 12:14:28 -0700 Subject: [PATCH 10/15] daemon: adjustments to emane model post startup to accept an interface, since settings may be unique per interface, updated tdma to use this information for trying to initialize its tdma schedule properly --- daemon/core/emane/emanemanager.py | 8 ++++---- daemon/core/emane/emanemodel.py | 3 ++- daemon/core/emane/models/tdma.py | 23 ++++++++++++----------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 5a403c20..60f25023 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -446,17 +446,17 @@ class EmaneManager: """ Retransmit location events now that all NEMs are active. """ - if not self.genlocationevents(): - return + events_enabled = self.genlocationevents() with self._emane_node_lock: for node_id in sorted(self._emane_nets): emane_net = self._emane_nets[node_id] logger.debug( "post startup for emane node: %s - %s", emane_net.id, emane_net.name ) - emane_net.model.post_startup() for iface in emane_net.get_ifaces(): - iface.setposition() + emane_net.model.post_startup(iface) + if events_enabled: + iface.setposition() def reset(self) -> None: """ diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 54f8c72d..62e9fb0d 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -134,10 +134,11 @@ class EmaneModel(WirelessModel): emanexml.create_phy_xml(self, iface, config) emanexml.create_transport_xml(iface, config) - def post_startup(self) -> None: + def post_startup(self, iface: CoreInterface) -> None: """ Logic to execute after the emane manager is finished with startup. + :param iface: interface for post startup :return: nothing """ logger.debug("emane model(%s) has no post setup tasks", self.name) diff --git a/daemon/core/emane/models/tdma.py b/daemon/core/emane/models/tdma.py index 62843ec1..c23e3d73 100644 --- a/daemon/core/emane/models/tdma.py +++ b/daemon/core/emane/models/tdma.py @@ -9,7 +9,9 @@ from typing import Set from core import constants, utils from core.config import Configuration from core.emane import emanemodel +from core.emane.nodes import EmaneNet from core.emulator.enumerations import ConfigDataTypes +from core.nodes.interface import CoreInterface logger = logging.getLogger(__name__) @@ -44,22 +46,21 @@ class EmaneTdmaModel(emanemodel.EmaneModel): ) cls.mac_config.insert(0, config_item) - def post_startup(self) -> None: - """ - Logic to execute after the emane manager is finished with startup. - - :return: nothing - """ + def post_startup(self, iface: CoreInterface) -> None: # get configured schedule - config = self.session.emane.get_config(self.id, self.name) - if not config: - return + emane_net = self.session.get_node(self.id, EmaneNet) + config = self.session.emane.get_iface_config(emane_net, iface) schedule = Path(config[self.schedule_name]) if not schedule.is_file(): - logger.warning("ignoring invalid tdma schedule: %s", schedule) + logger.error("ignoring invalid tdma schedule: %s", schedule) return # initiate tdma schedule - for service in self.session.emane.services.values(): + nem_id = self.session.emane.get_nem_id(iface) + if not nem_id: + logger.error("could not find nem for interface") + return + service = self.session.emane.nem_service.get(nem_id) + if service: device = service.device logger.info( "setting up tdma schedule: schedule(%s) device(%s)", schedule, device From 795a5f5865542c47bd6e19ab8eaf66538ba3a6d6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 26 May 2021 09:54:32 -0700 Subject: [PATCH 11/15] daemon: refactoring for starting up and shutting down emane daemon per interface --- daemon/core/emane/emanemanager.py | 104 +++++++++++++----------------- daemon/core/xml/emanexml.py | 32 +++++---- 2 files changed, 60 insertions(+), 76 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 60f25023..01b9758c 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -5,9 +5,8 @@ Implements configuration and control of an EMANE emulation. import logging import os import threading -from dataclasses import dataclass, field from enum import Enum -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Type, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union from core import utils from core.emane.emanemodel import EmaneModel @@ -17,7 +16,7 @@ from core.emane.nodes import EmaneNet from core.emulator.data import LinkData from core.emulator.enumerations import LinkTypes, MessageFlags, RegisterTlvs from core.errors import CoreCommandError, CoreError -from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase +from core.nodes.base import CoreNetworkBase, CoreNode, NodeBase from core.nodes.interface import CoreInterface, TunTap from core.xml import emanexml @@ -57,12 +56,6 @@ class EmaneState(Enum): NOT_READY = 2 -@dataclass -class StartData: - node: CoreNodeBase - ifaces: List[CoreInterface] = field(default_factory=list) - - class EmaneEventService: def __init__( self, manager: "EmaneManager", device: str, group: str, port: int @@ -327,26 +320,24 @@ class EmaneManager: def startup_nodes(self) -> None: with self._emane_node_lock: logger.info("emane building xmls...") - start_data = self.get_start_data() - for data in start_data: - node = data.node - for iface in data.ifaces: - nem_id = self.next_nem_id(iface) - logger.info( - "starting emane for node(%s) iface(%s) nem(%s)", - node.name, - iface.name, - nem_id, - ) - self.setup_control_channels(nem_id, node, iface) - emanexml.build_platform_xml(self, nem_id, node, iface) - self.start_daemon(node, iface) - self.install_iface(iface) + for emane_net, iface in self.get_ifaces(): + nem_id = self.next_nem_id(iface) + nem_port = self.get_nem_port(iface) + logger.info( + "starting emane for node(%s) iface(%s) nem(%s)", + iface.node.name, + iface.name, + nem_id, + ) + config = self.get_iface_config(emane_net, iface) + self.setup_control_channels(nem_id, iface, config) + emanexml.build_platform_xml(nem_id, nem_port, emane_net, iface, config) + self.start_daemon(iface) + self.install_iface(emane_net, iface, config) - def get_start_data(self) -> List[StartData]: - node_map = {} - for node_id in sorted(self._emane_nets): - emane_net = self._emane_nets[node_id] + def get_ifaces(self) -> List[Tuple[EmaneNet, CoreInterface]]: + ifaces = [] + for emane_net in self._emane_nets.values(): if not emane_net.model: logger.error("emane net(%s) has no model", emane_net.name) continue @@ -358,21 +349,13 @@ class EmaneManager: iface.name, ) continue - start_node = node_map.setdefault(iface.node, StartData(iface.node)) - start_node.ifaces.append(iface) - start_nodes = sorted(node_map.values(), key=lambda x: x.node.id) - for start_node in start_nodes: - start_node.ifaces = sorted(start_node.ifaces, key=lambda x: x.node_id) - return start_nodes + ifaces.append((emane_net, iface)) + return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].node_id)) def setup_control_channels( - self, nem_id: int, node: CoreNodeBase, iface: CoreInterface + self, nem_id: int, iface: CoreInterface, config: Dict[str, str] ) -> None: - if not isinstance(iface.net, EmaneNet): - raise CoreError( - f"emane interface not connected to emane net: {iface.net.name}" - ) - config = self.get_iface_config(iface.net, iface) + node = iface.node # setup ota device otagroup, _otaport = config["otamanagergroup"].split(":") otadev = config["otamanagerdevice"] @@ -467,6 +450,8 @@ class EmaneManager: self._emane_nets.clear() self.nems_to_ifaces.clear() self.ifaces_to_nems.clear() + self.nems_to_ifaces.clear() + self.services.clear() def shutdown(self) -> None: """ @@ -478,17 +463,19 @@ class EmaneManager: logger.info("stopping EMANE daemons") if self.links_enabled(): self.link_monitor.stop() - # shutdown interfaces and stop daemons - kill_emaned = "killall -q emane" - start_data = self.get_start_data() - for data in start_data: - node = data.node + # shutdown interfaces + nodes = set() + for _, iface in self.get_ifaces(): + node = iface.node if not node.up: continue - for iface in data.ifaces: - if isinstance(node, CoreNode): - iface.shutdown() - iface.poshook = None + nodes.add(node) + if isinstance(node, CoreNode): + iface.shutdown() + iface.poshook = None + kill_emaned = "killall -q emane" + # stop all emane daemons on associated nodes + for node in nodes: if isinstance(node, CoreNode): node.cmd(kill_emaned, wait=False) else: @@ -550,11 +537,14 @@ class EmaneManager: color=color, ) - def start_daemon(self, node: CoreNodeBase, iface: CoreInterface) -> None: + def start_daemon(self, iface: CoreInterface) -> None: """ - Start one EMANE daemon per node having a radio. - Add a control network even if the user has not configured one. + Start emane daemon for a given nem/interface. + + :param iface: interface to start emane daemon for + :return: nothing """ + node = iface.node loglevel = str(DEFAULT_LOG_LEVEL) cfgloglevel = self.session.options.get_config_int("emane_log_level") realtime = self.session.options.get_config_bool("emane_realtime", default=True) @@ -576,13 +566,9 @@ class EmaneManager: args = f"{emanecmd} -f {log_file} {platform_xml}" node.host_cmd(args, cwd=self.session.directory) - def install_iface(self, iface: CoreInterface) -> None: - emane_net = iface.net - if not isinstance(emane_net, EmaneNet): - raise CoreError( - f"emane interface not connected to emane net: {emane_net.name}" - ) - config = self.get_iface_config(emane_net, iface) + def install_iface( + self, emane_net: EmaneNet, iface: CoreInterface, config: Dict[str, str] + ) -> None: external = config.get("external", "0") if isinstance(iface, TunTap) and external == "0": iface.set_ips() diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 9d0753cb..f8489b5b 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -17,7 +17,6 @@ from core.xml import corexml logger = logging.getLogger(__name__) if TYPE_CHECKING: - from core.emane.emanemanager import EmaneManager from core.emane.emanemodel import EmaneModel _MAC_PREFIX = "02:02" @@ -145,33 +144,29 @@ def add_configurations( def build_platform_xml( - emane_manager: "EmaneManager", nem_id: int, node: CoreNodeBase, iface: CoreInterface + nem_id: int, + nem_port: int, + emane_net: EmaneNet, + iface: CoreInterface, + config: Dict[str, str], ) -> None: """ - Create platform xml for a specific node. + Create platform xml for a nem/interface. - :param emane_manager: emane manager with emane - configurations :param nem_id: nem id for current node/interface - :param node: node to create a platform xml for + :param nem_port: control port to configure for emane + :param emane_net: emane network associate with node and interface :param iface: node interface to create platform xml for - :return: the next nem id that can be used for creating platform xml files + :param config: emane configuration for interface + :return: nothing """ - # create model based xml files - emane_net = iface.net - if not isinstance(emane_net, EmaneNet): - raise CoreError(f"emane interface not connected to emane net: {emane_net.name}") - config = emane_manager.get_iface_config(emane_net, iface) - emane_net.model.build_xml_files(config, iface) - # create top level platform element platform_element = etree.Element("platform") for configuration in emane_net.model.platform_config: name = configuration.id value = config[configuration.id] if name == "controlportendpoint": - port = emane_manager.get_nem_port(iface) - value = f"0.0.0.0:{port}" + value = f"0.0.0.0:{nem_port}" add_param(platform_element, name, value) # build nem xml @@ -180,6 +175,9 @@ def build_platform_xml( "nem", id=str(nem_id), name=iface.localname, definition=nem_definition ) + # create model based xml files + emane_net.model.build_xml_files(config, iface) + # check if this is an external transport if is_external(config): nem_element.set("transport", "external") @@ -205,7 +203,7 @@ def build_platform_xml( doc_name = "platform" file_name = platform_file_name(iface) - create_node_file(node, platform_element, doc_name, file_name) + create_node_file(iface.node, platform_element, doc_name, file_name) def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None: From 777097c85e91991e0070c79b145524bbf7eb639c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 26 May 2021 12:22:36 -0700 Subject: [PATCH 12/15] daemon: updated emane position hooks and updating nem position logic to live in emane manager --- daemon/core/emane/emanemanager.py | 93 +++++++++++++++++++++++++------ daemon/core/emane/emanemodel.py | 4 +- daemon/core/emane/nodes.py | 61 +------------------- 3 files changed, 78 insertions(+), 80 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 01b9758c..46f10a1b 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -321,19 +321,22 @@ class EmaneManager: with self._emane_node_lock: logger.info("emane building xmls...") for emane_net, iface in self.get_ifaces(): - nem_id = self.next_nem_id(iface) - nem_port = self.get_nem_port(iface) - logger.info( - "starting emane for node(%s) iface(%s) nem(%s)", - iface.node.name, - iface.name, - nem_id, - ) - config = self.get_iface_config(emane_net, iface) - self.setup_control_channels(nem_id, iface, config) - emanexml.build_platform_xml(nem_id, nem_port, emane_net, iface, config) - self.start_daemon(iface) - self.install_iface(emane_net, iface, config) + self.start_iface(emane_net, iface) + + def start_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None: + nem_id = self.next_nem_id(iface) + nem_port = self.get_nem_port(iface) + logger.info( + "starting emane for node(%s) iface(%s) nem(%s)", + iface.node.name, + iface.name, + nem_id, + ) + config = self.get_iface_config(emane_net, iface) + self.setup_control_channels(nem_id, iface, config) + emanexml.build_platform_xml(nem_id, nem_port, emane_net, iface, config) + self.start_daemon(iface) + self.install_iface(iface, config) def get_ifaces(self) -> List[Tuple[EmaneNet, CoreInterface]]: ifaces = [] @@ -414,6 +417,64 @@ class EmaneManager: nem_id = self.get_nem_id(iface) return int(f"47{nem_id:03}") + def get_nem_position( + self, iface: CoreInterface + ) -> Optional[Tuple[int, float, float, int]]: + """ + Retrieves nem position for a given interface. + + :param iface: interface to get nem emane position for + :return: nem position tuple, None otherwise + """ + nem_id = self.get_nem_id(iface) + if nem_id is None: + logger.info("nem for %s is unknown", iface.localname) + return + node = iface.node + x, y, z = node.getposition() + lat, lon, alt = self.session.location.getgeo(x, y, z) + if node.position.alt is not None: + alt = node.position.alt + node.position.set_geo(lon, lat, alt) + # altitude must be an integer or warning is printed + alt = int(round(alt)) + return nem_id, lon, lat, alt + + def set_nem_position(self, iface: CoreInterface) -> None: + """ + Publish a NEM location change event using the EMANE event service. + + :param iface: interface to set nem position for + """ + position = self.get_nem_position(iface) + if position: + nemid, lon, lat, alt = position + event = LocationEvent() + event.append(nemid, latitude=lat, longitude=lon, altitude=alt) + self.publish_event(nemid, event, send_all=True) + + def set_nem_positions(self, moved_ifaces: List[CoreInterface]) -> None: + """ + Several NEMs have moved, from e.g. a WaypointMobilityModel + calculation. Generate an EMANE Location Event having several + entries for each interface that has moved. + """ + if not moved_ifaces: + return + services = {} + for iface in moved_ifaces: + position = self.get_nem_position(iface) + if not position: + continue + nem_id, lon, lat, alt = position + service = self.nem_service.get(nem_id) + if not service: + continue + event = services.setdefault(service, LocationEvent()) + event.append(nem_id, latitude=lat, longitude=lon, altitude=alt) + for service, event in services.items(): + service.events.publish(0, event) + def write_nem(self, iface: CoreInterface, nem_id: int) -> None: path = self.session.directory / "emane_nems" try: @@ -566,16 +627,14 @@ class EmaneManager: args = f"{emanecmd} -f {log_file} {platform_xml}" node.host_cmd(args, cwd=self.session.directory) - def install_iface( - self, emane_net: EmaneNet, iface: CoreInterface, config: Dict[str, str] - ) -> None: + def install_iface(self, iface: CoreInterface, config: Dict[str, str]) -> None: external = config.get("external", "0") if isinstance(iface, TunTap) and external == "0": iface.set_ips() # at this point we register location handlers for generating # EMANE location events if self.genlocationevents(): - iface.poshook = emane_net.setnemposition + iface.poshook = self.set_nem_position iface.setposition() def doeventmonitor(self) -> bool: diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 62e9fb0d..92346676 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -7,7 +7,6 @@ from typing import Dict, List, Optional, Set from core.config import ConfigGroup, Configuration from core.emane import emanemanifest -from core.emane.nodes import EmaneNet from core.emulator.data import LinkOptions from core.emulator.enumerations import ConfigDataTypes from core.errors import CoreError @@ -153,8 +152,7 @@ class EmaneModel(WirelessModel): :return: nothing """ try: - emane_net = self.session.get_node(self.id, EmaneNet) - emane_net.setnempositions(moved_ifaces) + self.session.emane.set_nem_positions(moved_ifaces) except CoreError: logger.exception("error during update") diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 12caf408..76a93767 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -4,7 +4,7 @@ share the same MAC+PHY model. """ import logging -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type +from typing import TYPE_CHECKING, Dict, List, Optional, Type from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.distributed import DistributedServer @@ -110,65 +110,6 @@ class EmaneNet(CoreNetworkBase): self.mobility = model(session=self.session, _id=self.id) self.mobility.update_config(config) - def _nem_position( - self, iface: CoreInterface - ) -> Optional[Tuple[int, float, float, float]]: - """ - Creates nem position for emane event for a given interface. - - :param iface: interface to get nem emane position for - :return: nem position tuple, None otherwise - """ - nem_id = self.session.emane.get_nem_id(iface) - ifname = iface.localname - if nem_id is None: - logger.info("nemid for %s is unknown", ifname) - return - node = iface.node - x, y, z = node.getposition() - lat, lon, alt = self.session.location.getgeo(x, y, z) - if node.position.alt is not None: - alt = node.position.alt - node.position.set_geo(lon, lat, alt) - # altitude must be an integer or warning is printed - alt = int(round(alt)) - return nem_id, lon, lat, alt - - def setnemposition(self, iface: CoreInterface) -> None: - """ - Publish a NEM location change event using the EMANE event service. - - :param iface: interface to set nem position for - """ - position = self._nem_position(iface) - if position: - nemid, lon, lat, alt = position - event = LocationEvent() - event.append(nemid, latitude=lat, longitude=lon, altitude=alt) - self.session.emane.publish_event(nemid, event, send_all=True) - - def setnempositions(self, moved_ifaces: List[CoreInterface]) -> None: - """ - Several NEMs have moved, from e.g. a WaypointMobilityModel - calculation. Generate an EMANE Location Event having several - entries for each interface that has moved. - """ - if not moved_ifaces: - return - services = {} - for iface in moved_ifaces: - position = self._nem_position(iface) - if not position: - continue - nem_id, lon, lat, alt = position - service = self.session.emane.nem_service.get(nem_id) - if not service: - continue - event = services.setdefault(service, LocationEvent()) - event.append(nem_id, latitude=lat, longitude=lon, altitude=alt) - for service, event in services.items(): - service.events.publish(0, event) - def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: links = super().links(flags) emane_manager = self.session.emane From 3fcefc4d79fb74aa7a84f33c38da5ee5dcbdac0f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 26 May 2021 12:29:46 -0700 Subject: [PATCH 13/15] daemon: updated emane shutdown to kill emane daemon for a given interface, instead of killall --- daemon/core/emane/emanemanager.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 46f10a1b..5f35f4d9 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -525,22 +525,17 @@ class EmaneManager: if self.links_enabled(): self.link_monitor.stop() # shutdown interfaces - nodes = set() for _, iface in self.get_ifaces(): node = iface.node if not node.up: continue - nodes.add(node) + kill_cmd = f'pkill -f "emane.+{iface.name}"' if isinstance(node, CoreNode): iface.shutdown() - iface.poshook = None - kill_emaned = "killall -q emane" - # stop all emane daemons on associated nodes - for node in nodes: - if isinstance(node, CoreNode): - node.cmd(kill_emaned, wait=False) + node.cmd(kill_cmd, wait=False) else: - node.host_cmd(kill_emaned, wait=False) + node.host_cmd(kill_cmd, wait=False) + iface.poshook = None # stop emane event services while self.services: _, service = self.services.popitem() From b51200e39783eef14eb30860c676a231e5c3fb92 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 26 May 2021 12:57:35 -0700 Subject: [PATCH 14/15] daemon: updated emane model platform configuration to remove controlportendpoint option, as this will be something core itself will define --- daemon/core/emane/emanemodel.py | 9 +++++++++ daemon/core/xml/emanexml.py | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 92346676..21fcccb3 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -27,6 +27,7 @@ class EmaneModel(WirelessModel): """ # default platform configuration settings + platform_controlport: str = "controlportendpoint" platform_xml: str = "nemmanager.xml" platform_defaults: Dict[str, str] = { "eventservicedevice": DEFAULT_DEV, @@ -88,6 +89,14 @@ class EmaneModel(WirelessModel): cls.platform_config = emanemanifest.parse( platform_xml_path, cls.platform_defaults ) + # remove controlport configuration, since core will set this directly + controlport_index = None + for index, configuration in enumerate(cls.platform_config): + if configuration.id == cls.platform_controlport: + controlport_index = index + break + if controlport_index is not None: + cls.platform_config.pop(controlport_index) @classmethod def configurations(cls) -> List[Configuration]: diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index f8489b5b..c45259f7 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -165,9 +165,10 @@ def build_platform_xml( for configuration in emane_net.model.platform_config: name = configuration.id value = config[configuration.id] - if name == "controlportendpoint": - value = f"0.0.0.0:{nem_port}" add_param(platform_element, name, value) + add_param( + platform_element, emane_net.model.platform_controlport, f"0.0.0.0:{nem_port}" + ) # build nem xml nem_definition = nem_file_name(iface) From f928284fb779664f10c576c2ff699be0c33a9605 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Wed, 26 May 2021 14:48:28 -0700 Subject: [PATCH 15/15] daemon: fixed emane setup to move binding check to after validating if emane is needed --- daemon/core/emane/emanemanager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 5f35f4d9..4ffed725 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -282,9 +282,6 @@ class EmaneManager: :return: SUCCESS, NOT_NEEDED, NOT_READY in order to delay session instantiation """ - # check if bindings were installed - if EventService is None: - raise CoreError("EMANE python bindings are not installed") logger.debug("emane setup") with self.session.nodes_lock: for node_id in self.session.nodes: @@ -297,6 +294,9 @@ class EmaneManager: if not self._emane_nets: logger.debug("no emane nodes in session") return EmaneState.NOT_NEEDED + # check if bindings were installed + if EventService is None: + raise CoreError("EMANE python bindings are not installed") self.check_node_models() return EmaneState.SUCCESS