diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index b07d2c04..2785a037 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]: @@ -1063,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/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..8b0b903a 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: @@ -1276,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/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/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..4ffed725 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -1,32 +1,22 @@ """ -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 typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union 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.base import CoreNetworkBase, CoreNode, NodeBase from core.nodes.interface import CoreInterface, TunTap from core.xml import emanexml @@ -36,15 +26,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 @@ -62,10 +56,57 @@ 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 + ) -> 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: @@ -102,22 +143,22 @@ 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] = {} # link monitor self.link_monitor: EmaneLinkMonitor = EmaneLinkMonitor(self) + # emane event monitoring + self.services: Dict[str, EmaneEventService] = {} + self.nem_service: Dict[int, EmaneEventService] = {} - self.service: Optional[EventService] = None - self.eventchannel: Optional[Tuple[str, int, str]] = None - self.event_device: Optional[str] = None - - def next_nem_id(self) -> int: - nem_id = int(self.config["nem_id_start"]) + def next_nem_id(self, iface: CoreInterface) -> int: + 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 + self.ifaces_to_nems[iface] = nem_id + self.write_nem(iface, nem_id) return nem_id def get_config( @@ -203,60 +244,12 @@ 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: 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, filename: str = None, shutdown: bool = False) -> 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"] - 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 event service traffic", self.event_device) - try: - self.service = EventService(eventchannel=self.eventchannel, otachannel=None) - except EventServiceException: - logger.exception("error instantiating emane EventService") - def add_node(self, emane_net: EmaneNet) -> None: """ Add EMANE network object to this manager. @@ -301,41 +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") - - # control network bridge required for EMANE 0.9.2 - # - needs to exist when eventservice binds to it (initeventservice) - otadev = self.config["otamanagerdevice"] - 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 - ) - 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 @@ -351,21 +312,35 @@ class EmaneManager: status = self.setup() if status != EmaneState.SUCCESS: return status - self.starteventmonitor() - self.buildeventservicexml() - 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) + self.startup_nodes() if self.links_enabled(): self.link_monitor.start() return EmaneState.SUCCESS - 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 startup_nodes(self) -> None: + with self._emane_node_lock: + logger.info("emane building xmls...") + for emane_net, iface in self.get_ifaces(): + 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 = [] + 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 @@ -377,27 +352,60 @@ 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 start_node(self, data: StartData) -> None: - control_net = self.session.add_remove_control_net( - 0, remove=False, conf_required=False + def setup_control_channels( + self, nem_id: int, iface: CoreInterface, config: Dict[str, str] + ) -> None: + node = iface.node + # setup ota device + otagroup, _otaport = config["otamanagergroup"].split(":") + otadev = config["otamanagerdevice"] + ota_index = self.session.get_control_net_index(otadev) + self.session.add_remove_control_net(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(":") + eventdev = config["eventservicedevice"] + event_index = self.session.get_control_net_index(eventdev) + event_net = self.session.add_remove_control_net( + event_index, conf_required=False ) - emanexml.build_platform_xml(self, control_net, data) - self.start_daemon(data.node) - for iface in data.ifaces: - 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 + 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)", + node.name, + iface.name, + otagroup, + otadev, + eventgroup, + eventdev, + ) + node.node_net_client.create_route(otagroup, otadev) + if 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) @@ -405,6 +413,68 @@ class EmaneManager: 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 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: @@ -414,23 +484,23 @@ 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: """ 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: """ @@ -441,6 +511,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: """ @@ -452,22 +524,23 @@ 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 + 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 + kill_cmd = f'pkill -f "emane.+{iface.name}"' if isinstance(node, CoreNode): - node.cmd(kill_emaned, wait=False) + iface.shutdown() + node.cmd(kill_cmd, wait=False) else: - node.host_cmd(kill_emaned, wait=False) - self.stopeventmonitor() + node.host_cmd(kill_cmd, wait=False) + iface.poshook = None + # stop emane event services + while self.services: + _, service = self.services.popitem() + service.stop() + self.nem_service.clear() def check_node_models(self) -> None: """ @@ -520,41 +593,14 @@ class EmaneManager: color=color, ) - def buildeventservicexml(self) -> None: + def start_daemon(self, iface: CoreInterface) -> 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 - ) - ) + Start emane daemon for a given nem/interface. - def start_daemon(self, node: CoreNodeBase) -> None: + :param iface: interface to start emane daemon for + :return: nothing """ - 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...") + 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) @@ -565,60 +611,25 @@ 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) - 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, 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: @@ -640,68 +651,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("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 - 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() - # reset the service, otherwise nextEvent won"t work - self.initeventservice(shutdown=True) - - if self.eventmonthread is not None: - self.eventmonthread.join() - self.eventmonthread = None - - def eventmonitorloop(self) -> None: - """ - Thread target that monitors EMANE location events. - """ - if self.service is None: - return - logger.info( - "subscribing to EMANE location events. (%s)", - threading.currentThread().getName(), - ) - while self.doeventloop is True: - _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(), - ) - def handlelocationevent(self, rxnemid: int, eid: int, data: str) -> None: """ Handle an EMANE location event. @@ -717,7 +666,6 @@ class EmaneManager: ): logger.warning("dropped invalid location event") continue - # yaw,pitch,roll,azimuth,elevation,velocity are unhandled lat = attrs["latitude"] lon = attrs["longitude"] @@ -812,87 +760,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) - -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()] - ) + 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/emanemodel.py b/daemon/core/emane/emanemodel.py index b6c037e0..21fcccb3 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 @@ -16,6 +15,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 +26,17 @@ class EmaneModel(WirelessModel): configurable parameters. Helper functions also live here. """ + # default platform configuration settings + platform_controlport: str = "controlportendpoint" + 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,35 @@ 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 + ) + # 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]: """ @@ -78,7 +105,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 +116,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), ] @@ -111,10 +142,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) @@ -129,8 +161,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/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 0c29b7a8..9b18bae2 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() @@ -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") @@ -204,22 +205,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/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..c3f0b07b 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: """ @@ -113,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() @@ -137,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..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,23 +46,23 @@ 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 - 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}") + 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 + ) + utils.cmd(f"emaneevent-tdmaschedule -i {device} {schedule}") diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 1e43723b..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,67 +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 - """ - 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) - - 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 len(moved_ifaces) == 0: - return - - if self.session.emane.service is None: - logger.info("position service not available") - return - - event = LocationEvent() - 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) - def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: links = super().links(flags) emane_manager = self.session.emane 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 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 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 ab7c2039..c45259f7 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -12,13 +12,11 @@ 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__) if TYPE_CHECKING: - from core.emane.emanemanager import EmaneManager, StartData from core.emane.emanemodel import EmaneModel _MAC_PREFIX = "02:02" @@ -146,74 +144,67 @@ def add_configurations( def build_platform_xml( - emane_manager: "EmaneManager", control_net: CtrlNet, data: "StartData" + 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 control_net: control net node for this emane - network - :param data: start data for a node connected to emane and associated interfaces - :return: the next nem id that can be used for creating platform xml files + :param nem_id: nem id for current node/interface + :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 + :param config: emane configuration for interface + :return: nothing """ # 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(data.node, CoreNode) and name in transport_configs: - value = control_net.brname - else: - value = emane_manager.config[name] + value = config[configuration.id] add_param(platform_element, name, value) + add_param( + platform_element, emane_net.model.platform_controlport, f"0.0.0.0:{nem_port}" + ) - # 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 - ) + # 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") - 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(iface.node, platform_element, doc_name, file_name) def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None: @@ -396,3 +387,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" 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..5aa0c952 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; @@ -50,6 +33,7 @@ message GetEmaneModelConfig { message GetEmaneEventChannelRequest { int32 session_id = 1; + int32 nem_id = 2; } message GetEmaneEventChannelResponse { 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) 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