diff --git a/daemon/core/api/grpc/clientw.py b/daemon/core/api/grpc/clientw.py new file mode 100644 index 00000000..10de850b --- /dev/null +++ b/daemon/core/api/grpc/clientw.py @@ -0,0 +1,1411 @@ +""" +gRpc client for interfacing with CORE. +""" + +import logging +import threading +from contextlib import contextmanager +from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Tuple + +import grpc + +from core.api.grpc import ( + configservices_pb2, + core_pb2, + core_pb2_grpc, + emane_pb2, + mobility_pb2, + services_pb2, + wlan_pb2, + wrappers, +) +from core.api.grpc.configservices_pb2 import ( + GetConfigServiceDefaultsRequest, + GetConfigServicesRequest, + GetNodeConfigServiceConfigsRequest, + GetNodeConfigServiceRequest, + GetNodeConfigServicesRequest, + SetNodeConfigServiceRequest, +) +from core.api.grpc.core_pb2 import ExecuteScriptRequest +from core.api.grpc.emane_pb2 import ( + EmaneLinkRequest, + EmanePathlossesRequest, + GetEmaneConfigRequest, + GetEmaneEventChannelRequest, + GetEmaneModelConfigRequest, + GetEmaneModelConfigsRequest, + GetEmaneModelsRequest, + SetEmaneConfigRequest, + SetEmaneModelConfigRequest, +) +from core.api.grpc.mobility_pb2 import ( + GetMobilityConfigRequest, + GetMobilityConfigsRequest, + MobilityActionRequest, + MobilityConfig, + SetMobilityConfigRequest, +) +from core.api.grpc.services_pb2 import ( + GetNodeServiceConfigsRequest, + GetNodeServiceFileRequest, + GetNodeServiceRequest, + GetServiceDefaultsRequest, + GetServicesRequest, + ServiceActionRequest, + ServiceDefaults, + ServiceFileConfig, + SetNodeServiceFileRequest, + SetNodeServiceRequest, + SetServiceDefaultsRequest, +) +from core.api.grpc.wlan_pb2 import ( + GetWlanConfigRequest, + GetWlanConfigsRequest, + SetWlanConfigRequest, + WlanConfig, + WlanLinkRequest, +) +from core.emulator.data import IpPrefixes + + +class InterfaceHelper: + """ + Convenience class to help generate IP4 and IP6 addresses for gRPC clients. + """ + + def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None: + """ + Creates an InterfaceHelper object. + + :param ip4_prefix: ip4 prefix to use for generation + :param ip6_prefix: ip6 prefix to use for generation + :raises ValueError: when both ip4 and ip6 prefixes have not been provided + """ + self.prefixes: IpPrefixes = IpPrefixes(ip4_prefix, ip6_prefix) + + def create_iface( + self, node_id: int, iface_id: int, name: str = None, mac: str = None + ) -> wrappers.Interface: + """ + Create an interface protobuf object. + + :param node_id: node id to create interface for + :param iface_id: interface id + :param name: name of interface + :param mac: mac address for interface + :return: interface protobuf + """ + iface_data = self.prefixes.gen_iface(node_id, name, mac) + return wrappers.Interface( + id=iface_id, + name=iface_data.name, + ip4=iface_data.ip4, + ip4_mask=iface_data.ip4_mask, + ip6=iface_data.ip6, + ip6_mask=iface_data.ip6_mask, + mac=iface_data.mac, + ) + + +def stream_listener(stream: Any, handler: Callable[[core_pb2.Event], None]) -> None: + """ + Listen for stream events and provide them to the handler. + + :param stream: grpc stream that will provide events + :param handler: function that handles an event + :return: nothing + """ + try: + for event in stream: + handler(event) + except grpc.RpcError as e: + if e.code() == grpc.StatusCode.CANCELLED: + logging.debug("stream closed") + else: + logging.exception("stream error") + + +def start_streamer(stream: Any, handler: Callable[[core_pb2.Event], None]) -> None: + """ + Convenience method for starting a grpc stream thread for handling streamed events. + + :param stream: grpc stream that will provide events + :param handler: function that handles an event + :return: nothing + """ + thread = threading.Thread( + target=stream_listener, args=(stream, handler), daemon=True + ) + thread.start() + + +class CoreGrpcClient: + """ + Provides convenience methods for interfacing with the CORE grpc server. + """ + + def __init__(self, address: str = "localhost:50051", proxy: bool = False) -> None: + """ + Creates a CoreGrpcClient instance. + + :param address: grpc server address to connect to + """ + self.address: str = address + self.stub: Optional[core_pb2_grpc.CoreApiStub] = None + self.channel: Optional[grpc.Channel] = None + self.proxy: bool = proxy + + def start_session( + self, session: wrappers.Session, asymmetric_links: List[wrappers.Link] = None + ) -> Tuple[bool, List[str]]: + """ + Start a session. + + :param session: session to start + :param asymmetric_links: link configuration for asymmetric links + :return: tuple of result and exception strings + """ + nodes = [x.to_proto() for x in session.nodes.values()] + links = [x.to_proto() for x in session.links] + 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 = [] + service_configs = [] + service_file_configs = [] + config_service_configs = [] + for node in session.nodes.values(): + for key, config in node.emane_model_configs.items(): + model, iface_id = key + config = wrappers.ConfigOption.to_dict(config) + if iface_id is None: + iface_id = -1 + emane_model_config = emane_pb2.EmaneModelConfig( + node_id=node.id, iface_id=iface_id, model=model, config=config + ) + emane_model_configs.append(emane_model_config) + if node.wlan_config: + config = wrappers.ConfigOption.to_dict(node.wlan_config) + wlan_config = wlan_pb2.WlanConfig(node_id=node.id, config=config) + wlan_configs.append(wlan_config) + if node.mobility_config: + config = wrappers.ConfigOption.to_dict(node.mobility_config) + mobility_config = mobility_pb2.MobilityConfig( + node_id=node.id, config=config + ) + mobility_configs.append(mobility_config) + for name, config in node.service_configs.items(): + service_config = services_pb2.ServiceConfig( + node_id=node.id, + service=name, + directories=config.dirs, + files=config.configs, + startup=config.startup, + validate=config.validate, + shutdown=config.shutdown, + ) + service_configs.append(service_config) + for service, file_configs in node.service_file_configs.items(): + for file, data in file_configs.items(): + service_file_config = services_pb2.ServiceFileConfig( + node_id=node.id, service=service, file=file, data=data + ) + service_file_configs.append(service_file_config) + for name, service_config in node.config_service_configs.items(): + config_service_config = configservices_pb2.ConfigServiceConfig( + node_id=node.id, + name=name, + templates=service_config.templates, + config=service_config.config, + ) + config_service_configs.append(config_service_config) + request = core_pb2.StartSessionRequest( + session_id=session.id, + nodes=nodes, + 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, + service_configs=service_configs, + service_file_configs=service_file_configs, + asymmetric_links=asymmetric_links, + config_service_configs=config_service_configs, + ) + response = self.stub.StartSession(request) + return response.result, list(response.exceptions) + + def stop_session(self, session_id: int) -> bool: + """ + Stop a running session. + + :param session_id: id of session + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.StopSessionRequest(session_id=session_id) + response = self.stub.StopSession(request) + return response.result + + def create_session(self, session_id: int = None) -> int: + """ + Create a session. + + :param session_id: id for session, default is None and one will be created + for you + :return: session id + """ + request = core_pb2.CreateSessionRequest(session_id=session_id) + response = self.stub.CreateSession(request) + return response.session_id + + def delete_session(self, session_id: int) -> bool: + """ + Delete a session. + + :param session_id: id of session + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.DeleteSessionRequest(session_id=session_id) + response = self.stub.DeleteSession(request) + return response.result + + def get_sessions(self) -> List[wrappers.SessionSummary]: + """ + Retrieves all currently known sessions. + + :return: response with a list of currently known session, their state and + number of nodes + """ + response = self.stub.GetSessions(core_pb2.GetSessionsRequest()) + sessions = [] + for session_proto in response.sessions: + session = wrappers.SessionSummary.from_proto(session_proto) + sessions.append(session) + return sessions + + def check_session(self, session_id: int) -> bool: + """ + Check if a session exists. + + :param session_id: id of session to check for + :return: True if exists, False otherwise + """ + request = core_pb2.CheckSessionRequest(session_id=session_id) + response = self.stub.CheckSession(request) + return response.result + + def get_session(self, session_id: int) -> wrappers.Session: + """ + Retrieve a session. + + :param session_id: id of session + :return: session + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetSessionRequest(session_id=session_id) + response = self.stub.GetSession(request) + return wrappers.Session.from_proto(response.session) + + def get_session_options(self, session_id: int) -> Dict[str, wrappers.ConfigOption]: + """ + Retrieve session options as a dict with id mapping. + + :param session_id: id of session + :return: session configuration options + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetSessionOptionsRequest(session_id=session_id) + response = self.stub.GetSessionOptions(request) + return wrappers.ConfigOption.from_dict(response.config) + + def set_session_options(self, session_id: int, config: Dict[str, str]) -> bool: + """ + Set options for a session. + + :param session_id: id of session + :param config: configuration values to set + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.SetSessionOptionsRequest( + session_id=session_id, config=config + ) + response = self.stub.SetSessionOptions(request) + return response.result + + def get_session_metadata(self, session_id: int) -> Dict[str, str]: + """ + Retrieve session metadata as a dict with id mapping. + + :param session_id: id of session + :return: response with metadata dict + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetSessionMetadataRequest(session_id=session_id) + response = self.stub.GetSessionMetadata(request) + return dict(response.config) + + def set_session_metadata(self, session_id: int, config: Dict[str, str]) -> bool: + """ + Set metadata for a session. + + :param session_id: id of session + :param config: configuration values to set + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.SetSessionMetadataRequest( + session_id=session_id, config=config + ) + response = self.stub.SetSessionMetadata(request) + return response.result + + def get_session_location(self, session_id: int) -> wrappers.SessionLocation: + """ + Get session location. + + :param session_id: id of session + :return: response with session position reference and scale + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetSessionLocationRequest(session_id=session_id) + response = self.stub.GetSessionLocation(request) + return wrappers.SessionLocation.from_proto(response.location) + + def set_session_location( + self, session_id: int, location: wrappers.SessionLocation + ) -> bool: + """ + Set session location. + + :param session_id: id of session + :param location: session location + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.SetSessionLocationRequest( + session_id=session_id, location=location.to_proto() + ) + response = self.stub.SetSessionLocation(request) + return response.result + + def set_session_state(self, session_id: int, state: wrappers.SessionState) -> bool: + """ + Set session state. + + :param session_id: id of session + :param state: session state to transition to + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.SetSessionStateRequest( + session_id=session_id, state=state.value + ) + response = self.stub.SetSessionState(request) + return response.result + + def set_session_user(self, session_id: int, user: str) -> bool: + """ + Set session user, used for helping to find files without full paths. + + :param session_id: id of session + :param user: user to set for session + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.SetSessionUserRequest(session_id=session_id, user=user) + response = self.stub.SetSessionUser(request) + return response.result + + def add_session_server(self, session_id: int, name: str, host: str) -> bool: + """ + Add distributed session server. + + :param session_id: id of session + :param name: name of server to add + :param host: host address to connect to + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.AddSessionServerRequest( + session_id=session_id, name=name, host=host + ) + response = self.stub.AddSessionServer(request) + return response.result + + def alert( + self, + session_id: int, + level: wrappers.ExceptionLevel, + source: str, + text: str, + node_id: int = None, + ) -> bool: + """ + Initiate an alert to be broadcast out to all listeners. + + :param session_id: id of session + :param level: alert level + :param source: source of alert + :param text: alert text + :param node_id: node associated with alert + :return: True for success, False otherwise + """ + request = core_pb2.SessionAlertRequest( + session_id=session_id, + level=level.value, + source=source, + text=text, + node_id=node_id, + ) + response = self.stub.SessionAlert(request) + return response.result + + # TODO: determine best path for handling non proto events + def events( + self, + session_id: int, + handler: Callable[[core_pb2.Event], None], + events: List[core_pb2.Event] = None, + ) -> grpc.Future: + """ + Listen for session events. + + :param session_id: id of session + :param handler: handler for received events + :param events: events to listen to, defaults to all + :return: stream processing events, can be used to cancel stream + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.EventsRequest(session_id=session_id, events=events) + stream = self.stub.Events(request) + start_streamer(stream, handler) + return stream + + # TODO: determine best path for handling non proto events + def throughputs( + self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None] + ) -> grpc.Future: + """ + Listen for throughput events with information for interfaces and bridges. + + :param session_id: session id + :param handler: handler for every event + :return: stream processing events, can be used to cancel stream + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.ThroughputsRequest(session_id=session_id) + stream = self.stub.Throughputs(request) + start_streamer(stream, handler) + return stream + + # TODO: determine best path for handling non proto events + def cpu_usage( + self, delay: int, handler: Callable[[core_pb2.CpuUsageEvent], None] + ) -> grpc.Future: + """ + Listen for cpu usage events with the given repeat delay. + + :param delay: delay between receiving events + :param handler: handler for every event + :return: stream processing events, can be used to cancel stream + """ + request = core_pb2.CpuUsageRequest(delay=delay) + stream = self.stub.CpuUsage(request) + start_streamer(stream, handler) + return stream + + def add_node(self, session_id: int, node: wrappers.Node, source: str = None) -> int: + """ + Add node to session. + + :param session_id: session id + :param node: node to add + :param source: source application + :return: id of added node + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.AddNodeRequest( + session_id=session_id, node=node.to_proto(), source=source + ) + response = self.stub.AddNode(request) + return response.node_id + + def get_node( + self, session_id: int, node_id: int + ) -> Tuple[wrappers.Node, List[wrappers.Interface]]: + """ + Get node details. + + :param session_id: session id + :param node_id: node id + :return: tuple of node and its interfaces + :raises grpc.RpcError: when session or node doesn't exist + """ + request = core_pb2.GetNodeRequest(session_id=session_id, node_id=node_id) + response = self.stub.GetNode(request) + node = wrappers.Node.from_proto(response.node) + ifaces = [] + for iface_proto in response.ifaces: + iface = wrappers.Interface.from_proto(iface_proto) + ifaces.append(iface) + return node, ifaces + + def edit_node( + self, + session_id: int, + node_id: int, + position: wrappers.Position = None, + icon: str = None, + geo: wrappers.Geo = None, + source: str = None, + ) -> bool: + """ + Edit a node's icon and/or location, can only use position(x,y) or + geo(lon, lat, alt), not both. + + :param session_id: session id + :param node_id: node id + :param position: x,y location for node + :param icon: path to icon for gui to use for node + :param geo: lon,lat,alt location for node + :param source: application source + :return: True for success, False otherwise + :raises grpc.RpcError: when session or node doesn't exist + """ + request = core_pb2.EditNodeRequest( + session_id=session_id, + node_id=node_id, + position=position.to_proto(), + icon=icon, + source=source, + geo=geo.to_proto(), + ) + response = self.stub.EditNode(request) + return response.result + + # TODO: determine path to stream non proto requests + def move_nodes(self, move_iterator: Iterable[core_pb2.MoveNodesRequest]) -> None: + """ + Stream node movements using the provided iterator. + + :param move_iterator: iterator for generating node movements + :return: nothing + :raises grpc.RpcError: when session or nodes do not exist + """ + self.stub.MoveNodes(move_iterator) + + def delete_node(self, session_id: int, node_id: int, source: str = None) -> bool: + """ + Delete node from session. + + :param session_id: session id + :param node_id: node id + :param source: application source + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.DeleteNodeRequest( + session_id=session_id, node_id=node_id, source=source + ) + response = self.stub.DeleteNode(request) + return response.result + + def node_command( + self, + session_id: int, + node_id: int, + command: str, + wait: bool = True, + shell: bool = False, + ) -> Tuple[int, str]: + """ + Send command to a node and get the output. + + :param session_id: session id + :param node_id: node id + :param command: command to run on node + :param wait: wait for command to complete + :param shell: send shell command + :return: returns tuple of return code and output + :raises grpc.RpcError: when session or node doesn't exist + """ + request = core_pb2.NodeCommandRequest( + session_id=session_id, + node_id=node_id, + command=command, + wait=wait, + shell=shell, + ) + response = self.stub.NodeCommand(request) + return response.return_code, response.output + + def get_node_terminal(self, session_id: int, node_id: int) -> str: + """ + Retrieve terminal command string for launching a local terminal. + + :param session_id: session id + :param node_id: node id + :return: node terminal + :raises grpc.RpcError: when session or node doesn't exist + """ + request = core_pb2.GetNodeTerminalRequest( + session_id=session_id, node_id=node_id + ) + response = self.stub.GetNodeTerminal(request) + return response.terminal + + def get_node_links(self, session_id: int, node_id: int) -> List[wrappers.Link]: + """ + Get current links for a node. + + :param session_id: session id + :param node_id: node id + :return: list of links + :raises grpc.RpcError: when session or node doesn't exist + """ + request = core_pb2.GetNodeLinksRequest(session_id=session_id, node_id=node_id) + response = self.stub.GetNodeLinks(request) + links = [] + for link_proto in response.links: + link = wrappers.Link.from_proto(link_proto) + links.append(link) + return links + + def add_link( + self, session_id: int, link: wrappers.Link, source: str = None + ) -> Tuple[bool, wrappers.Interface, wrappers.Interface]: + """ + Add a link between nodes. + + :param session_id: session id + :param link: link to add + :param source: application source + :return: tuple of result and finalized interface values + :raises grpc.RpcError: when session or one of the nodes don't exist + """ + request = core_pb2.AddLinkRequest( + session_id=session_id, link=link.to_proto(), source=source + ) + response = self.stub.AddLink(request) + iface1 = wrappers.Interface.from_proto(response.iface1) + iface2 = wrappers.Interface.from_proto(response.iface2) + return response.result, iface1, iface2 + + def edit_link( + self, session_id: int, link: wrappers.Link, source: str = None + ) -> bool: + """ + Edit a link between nodes. + + :param session_id: session id + :param link: link to edit + :param source: application source + :return: response with result of success or failure + :raises grpc.RpcError: when session or one of the nodes don't exist + """ + iface1_id = link.iface1.id if link.iface1 else None + iface2_id = link.iface2.id if link.iface2 else None + request = core_pb2.EditLinkRequest( + session_id=session_id, + node1_id=link.node1_id, + node2_id=link.node2_id, + options=link.options.to_proto(), + iface1_id=iface1_id, + iface2_id=iface2_id, + source=source, + ) + response = self.stub.EditLink(request) + return response.result + + def delete_link( + self, session_id: int, link: wrappers.Link, source: str = None + ) -> bool: + """ + Delete a link between nodes. + + :param session_id: session id + :param link: link to delete + :param source: application source + :return: response with result of success or failure + :raises grpc.RpcError: when session doesn't exist + """ + iface1_id = link.iface1.id if link.iface1 else None + iface2_id = link.iface2.id if link.iface2 else None + request = core_pb2.DeleteLinkRequest( + session_id=session_id, + node1_id=link.node1_id, + node2_id=link.node2_id, + iface1_id=iface1_id, + iface2_id=iface2_id, + source=source, + ) + response = self.stub.DeleteLink(request) + return response.result + + def get_hooks(self, session_id: int) -> List[wrappers.Hook]: + """ + Get all hook scripts. + + :param session_id: session id + :return: list of hooks + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.GetHooksRequest(session_id=session_id) + response = self.stub.GetHooks(request) + hooks = [] + for hook_proto in response.hooks: + hook = wrappers.Hook.from_proto(hook_proto) + hooks.append(hook) + return hooks + + def add_hook( + self, + session_id: int, + state: wrappers.SessionState, + file_name: str, + file_data: str, + ) -> bool: + """ + Add hook scripts. + + :param session_id: session id + :param state: state to trigger hook + :param file_name: name of file for hook script + :param file_data: hook script contents + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + hook = core_pb2.Hook(state=state.value, file=file_name, data=file_data) + request = core_pb2.AddHookRequest(session_id=session_id, hook=hook) + response = self.stub.AddHook(request) + return response.result + + def get_mobility_configs( + self, session_id: int + ) -> Dict[int, Dict[str, wrappers.ConfigOption]]: + """ + Get all mobility configurations. + + :param session_id: session id + :return: dict of node id to mobility configuration dict + :raises grpc.RpcError: when session doesn't exist + """ + request = GetMobilityConfigsRequest(session_id=session_id) + response = self.stub.GetMobilityConfigs(request) + configs = {} + for node_id, mapped_config in response.configs.items(): + configs[node_id] = wrappers.ConfigOption.from_dict(mapped_config.config) + return configs + + def get_mobility_config( + self, session_id: int, node_id: int + ) -> Dict[str, wrappers.ConfigOption]: + """ + Get mobility configuration for a node. + + :param session_id: session id + :param node_id: node id + :return: dict of config name to options + :raises grpc.RpcError: when session or node doesn't exist + """ + request = GetMobilityConfigRequest(session_id=session_id, node_id=node_id) + response = self.stub.GetMobilityConfig(request) + return wrappers.ConfigOption.from_dict(response.config) + + def set_mobility_config( + self, session_id: int, node_id: int, config: Dict[str, str] + ) -> bool: + """ + Set mobility configuration for a node. + + :param session_id: session id + :param node_id: node id + :param config: mobility configuration + :return: True for success, False otherwise + :raises grpc.RpcError: when session or node doesn't exist + """ + mobility_config = MobilityConfig(node_id=node_id, config=config) + request = SetMobilityConfigRequest( + session_id=session_id, mobility_config=mobility_config + ) + response = self.stub.SetMobilityConfig(request) + return response.result + + def mobility_action( + self, session_id: int, node_id: int, action: wrappers.MobilityAction + ) -> bool: + """ + Send a mobility action for a node. + + :param session_id: session id + :param node_id: node id + :param action: action to take + :return: True for success, False otherwise + :raises grpc.RpcError: when session or node doesn't exist + """ + request = MobilityActionRequest( + session_id=session_id, node_id=node_id, action=action.value + ) + response = self.stub.MobilityAction(request) + return response.result + + def get_services(self) -> List[wrappers.Service]: + """ + Get all currently loaded services. + + :return: list of services, name and groups only + """ + request = GetServicesRequest() + response = self.stub.GetServices(request) + services = [] + for service_proto in response.services: + service = wrappers.Service.from_proto(service_proto) + services.append(service) + return services + + def get_service_defaults(self, session_id: int) -> List[wrappers.ServiceDefault]: + """ + Get default services for different default node models. + + :param session_id: session id + :return: list of service defaults + :raises grpc.RpcError: when session doesn't exist + """ + request = GetServiceDefaultsRequest(session_id=session_id) + response = self.stub.GetServiceDefaults(request) + defaults = [] + for default_proto in response.defaults: + default = wrappers.ServiceDefault.from_proto(default_proto) + defaults.append(default) + return defaults + + def set_service_defaults( + self, session_id: int, service_defaults: Dict[str, List[str]] + ) -> bool: + """ + Set default services for node models. + + :param session_id: session id + :param service_defaults: node models to lists of services + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + defaults = [] + for node_type in service_defaults: + services = service_defaults[node_type] + default = ServiceDefaults(node_type=node_type, services=services) + defaults.append(default) + request = SetServiceDefaultsRequest(session_id=session_id, defaults=defaults) + response = self.stub.SetServiceDefaults(request) + return response.result + + def get_node_service_configs( + self, session_id: int + ) -> List[wrappers.NodeServiceData]: + """ + Get service data for a node. + + :param session_id: session id + :return: list of node service data + :raises grpc.RpcError: when session doesn't exist + """ + request = GetNodeServiceConfigsRequest(session_id=session_id) + response = self.stub.GetNodeServiceConfigs(request) + node_services = [] + for service_proto in response.configs: + node_service = wrappers.NodeServiceData.from_proto(service_proto) + node_services.append(node_service) + return node_services + + def get_node_service( + self, session_id: int, node_id: int, service: str + ) -> wrappers.NodeServiceData: + """ + Get service data for a node. + + :param session_id: session id + :param node_id: node id + :param service: service name + :return: node service data + :raises grpc.RpcError: when session or node doesn't exist + """ + request = GetNodeServiceRequest( + session_id=session_id, node_id=node_id, service=service + ) + response = self.stub.GetNodeService(request) + return wrappers.NodeServiceData.from_proto(response.service) + + def get_node_service_file( + self, session_id: int, node_id: int, service: str, file_name: str + ) -> str: + """ + Get a service file for a node. + + :param session_id: session id + :param node_id: node id + :param service: service name + :param file_name: file name to get data for + :return: file data + :raises grpc.RpcError: when session or node doesn't exist + """ + request = GetNodeServiceFileRequest( + session_id=session_id, node_id=node_id, service=service, file=file_name + ) + response = self.stub.GetNodeServiceFile(request) + return response.data + + def set_node_service( + self, session_id: int, service_config: wrappers.ServiceConfig + ) -> bool: + """ + Set service data for a node. + + :param session_id: session id + :param service_config: service configuration for a node + :return: True for success, False otherwise + :raises grpc.RpcError: when session or node doesn't exist + """ + request = SetNodeServiceRequest( + session_id=session_id, config=service_config.to_proto() + ) + response = self.stub.SetNodeService(request) + return response.result + + def set_node_service_file( + self, session_id: int, node_id: int, service: str, file_name: str, data: str + ) -> bool: + """ + Set a service file for a node. + + :param session_id: session id + :param node_id: node id + :param service: service name + :param file_name: file name to save + :param data: data to save for file + :return: True for success, False otherwise + :raises grpc.RpcError: when session or node doesn't exist + """ + config = ServiceFileConfig( + node_id=node_id, service=service, file=file_name, data=data + ) + request = SetNodeServiceFileRequest(session_id=session_id, config=config) + response = self.stub.SetNodeServiceFile(request) + return response.result + + def service_action( + self, + session_id: int, + node_id: int, + service: str, + action: wrappers.ServiceAction, + ) -> bool: + """ + Send an action to a service for a node. + + :param session_id: session id + :param node_id: node id + :param service: service name + :param action: action for service (start, stop, restart, + validate) + :return: True for success, False otherwise + :raises grpc.RpcError: when session or node doesn't exist + """ + request = ServiceActionRequest( + session_id=session_id, node_id=node_id, service=service, action=action.value + ) + response = self.stub.ServiceAction(request) + return response.result + + def get_wlan_configs( + self, session_id: int + ) -> Dict[int, Dict[str, wrappers.ConfigOption]]: + """ + Get all wlan configurations. + + :param session_id: session id + :return: dict of node ids to dict of names to options + :raises grpc.RpcError: when session doesn't exist + """ + request = GetWlanConfigsRequest(session_id=session_id) + response = self.stub.GetWlanConfigs(request) + configs = {} + for node_id, mapped_config in response.configs.items(): + configs[node_id] = wrappers.ConfigOption.from_dict(mapped_config.config) + return configs + + def get_wlan_config( + self, session_id: int, node_id: int + ) -> Dict[str, wrappers.ConfigOption]: + """ + Get wlan configuration for a node. + + :param session_id: session id + :param node_id: node id + :return: dict of names to options + :raises grpc.RpcError: when session doesn't exist + """ + request = GetWlanConfigRequest(session_id=session_id, node_id=node_id) + response = self.stub.GetWlanConfig(request) + return wrappers.ConfigOption.from_dict(response.config) + + def set_wlan_config( + self, session_id: int, node_id: int, config: Dict[str, str] + ) -> bool: + """ + Set wlan configuration for a node. + + :param session_id: session id + :param node_id: node id + :param config: wlan configuration + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + wlan_config = WlanConfig(node_id=node_id, config=config) + request = SetWlanConfigRequest(session_id=session_id, wlan_config=wlan_config) + 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_models(self, session_id: int) -> List[str]: + """ + Get session emane models. + + :param session_id: session id + :return: list of emane models + :raises grpc.RpcError: when session doesn't exist + """ + request = GetEmaneModelsRequest(session_id=session_id) + response = self.stub.GetEmaneModels(request) + return list(response.models) + + def get_emane_model_config( + self, session_id: int, node_id: int, model: str, iface_id: int = -1 + ) -> Dict[str, wrappers.ConfigOption]: + """ + Get emane model configuration for a node or a node's interface. + + :param session_id: session id + :param node_id: node id + :param model: emane model name + :param iface_id: node interface id + :return: dict of names to options + :raises grpc.RpcError: when session doesn't exist + """ + request = GetEmaneModelConfigRequest( + session_id=session_id, node_id=node_id, model=model, iface_id=iface_id + ) + response = self.stub.GetEmaneModelConfig(request) + return wrappers.ConfigOption.from_dict(response.config) + + def set_emane_model_config( + self, session_id: int, emane_model_config: wrappers.EmaneModelConfig + ) -> bool: + """ + Set emane model configuration for a node or a node's interface. + + :param session_id: session id + :param emane_model_config: emane model config to set + :return: True for success, False otherwise + :raises grpc.RpcError: when session doesn't exist + """ + request = SetEmaneModelConfigRequest( + session_id=session_id, emane_model_config=emane_model_config.to_proto() + ) + response = self.stub.SetEmaneModelConfig(request) + return response.result + + def get_emane_model_configs( + self, session_id: int + ) -> List[wrappers.EmaneModelConfig]: + """ + Get all EMANE model configurations for a session. + + :param session_id: session to get emane model configs + :return: list of emane model configs + :raises grpc.RpcError: when session doesn't exist + """ + request = GetEmaneModelConfigsRequest(session_id=session_id) + response = self.stub.GetEmaneModelConfigs(request) + configs = [] + for config_proto in response.configs: + config = wrappers.EmaneModelConfig.from_proto(config_proto) + configs.append(config) + return configs + + def save_xml(self, session_id: int, file_path: str) -> None: + """ + Save the current scenario to an XML file. + + :param session_id: session to save xml file for + :param file_path: local path to save scenario XML file to + :return: nothing + :raises grpc.RpcError: when session doesn't exist + """ + request = core_pb2.SaveXmlRequest(session_id=session_id) + response = self.stub.SaveXml(request) + with open(file_path, "w") as xml_file: + xml_file.write(response.data) + + def open_xml(self, file_path: str, start: bool = False) -> Tuple[bool, int]: + """ + Load a local scenario XML file to open as a new session. + + :param file_path: path of scenario XML file + :param start: tuple of result and session id when successful + :return: response with opened session id + """ + with open(file_path, "r") as xml_file: + data = xml_file.read() + request = core_pb2.OpenXmlRequest(data=data, start=start, file=file_path) + response = self.stub.OpenXml(request) + return response.result, response.session_id + + def emane_link(self, session_id: int, nem1: int, nem2: int, linked: bool) -> bool: + """ + Helps broadcast wireless link/unlink between EMANE nodes. + + :param session_id: session to emane link + :param nem1: first nem for emane link + :param nem2: second nem for emane link + :param linked: True to link, False to unlink + :return: True for success, False otherwise + :raises grpc.RpcError: when session or nodes related to nems do not exist + """ + request = EmaneLinkRequest( + session_id=session_id, nem1=nem1, nem2=nem2, linked=linked + ) + response = self.stub.EmaneLink(request) + return response.result + + def get_ifaces(self) -> List[str]: + """ + Retrieves a list of interfaces available on the host machine that are not + a part of a CORE session. + + :return: list of interfaces + """ + request = core_pb2.GetInterfacesRequest() + response = self.stub.GetInterfaces(request) + return list(response.ifaces) + + def get_config_services(self) -> List[wrappers.ConfigService]: + """ + Retrieve all known config services. + + :return: list of config services + """ + request = GetConfigServicesRequest() + response = self.stub.GetConfigServices(request) + services = [] + for service_proto in response.services: + service = wrappers.ConfigService.from_proto(service_proto) + services.append(service) + return services + + def get_config_service_defaults(self, name: str) -> wrappers.ConfigServiceDefaults: + """ + Retrieves config service default values. + + :param name: name of service to get defaults for + :return: config service defaults + """ + request = GetConfigServiceDefaultsRequest(name=name) + response = self.stub.GetConfigServiceDefaults(request) + return wrappers.ConfigServiceDefaults.from_proto(response) + + def get_node_config_service_configs( + self, session_id: int + ) -> List[wrappers.ConfigServiceConfig]: + """ + Retrieves all node config service configurations for a session. + + :param session_id: session to get config service configurations for + :return: list of node config service configs + :raises grpc.RpcError: when session doesn't exist + """ + request = GetNodeConfigServiceConfigsRequest(session_id=session_id) + response = self.stub.GetNodeConfigServiceConfigs(request) + configs = [] + for config_proto in response.configs: + config = wrappers.ConfigServiceConfig.from_proto(config_proto) + configs.append(config) + return configs + + def get_node_config_service( + self, session_id: int, node_id: int, name: str + ) -> Dict[str, str]: + """ + Retrieves information for a specific config service on a node. + + :param session_id: session node belongs to + :param node_id: id of node to get service information from + :param name: name of service + :return: config dict of names to values + :raises grpc.RpcError: when session or node doesn't exist + """ + request = GetNodeConfigServiceRequest( + session_id=session_id, node_id=node_id, name=name + ) + response = self.stub.GetNodeConfigService(request) + return dict(response.config) + + def get_node_config_services(self, session_id: int, node_id: int) -> List[str]: + """ + Retrieves the config services currently assigned to a node. + + :param session_id: session node belongs to + :param node_id: id of node to get config services for + :return: list of config services + :raises grpc.RpcError: when session or node doesn't exist + """ + request = GetNodeConfigServicesRequest(session_id=session_id, node_id=node_id) + response = self.stub.GetNodeConfigServices(request) + return list(response.services) + + def set_node_config_service( + self, session_id: int, node_id: int, name: str, config: Dict[str, str] + ) -> bool: + """ + Assigns a config service to a node with the provided configuration. + + :param session_id: session node belongs to + :param node_id: id of node to assign config service to + :param name: name of service + :param config: service configuration + :return: True for success, False otherwise + :raises grpc.RpcError: when session or node doesn't exist + """ + request = SetNodeConfigServiceRequest( + session_id=session_id, node_id=node_id, name=name, config=config + ) + response = self.stub.SetNodeConfigService(request) + return response.result + + def get_emane_event_channel(self, session_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 + :return: emane event channel + :raises grpc.RpcError: when session doesn't exist + """ + request = GetEmaneEventChannelRequest(session_id=session_id) + response = self.stub.GetEmaneEventChannel(request) + return wrappers.EmaneEventChannel.from_proto(response) + + def execute_script(self, script: str) -> Optional[int]: + """ + Executes a python script given context of the current CoreEmu object. + + :param script: script to execute + :return: create session id for script executed + """ + request = ExecuteScriptRequest(script=script) + response = self.stub.ExecuteScript(request) + return response.session_id if response.session_id else None + + def wlan_link( + self, session_id: int, wlan_id: int, node1_id: int, node2_id: int, linked: bool + ) -> bool: + """ + Links/unlinks nodes on the same WLAN. + + :param session_id: session id containing wlan and nodes + :param wlan_id: wlan nodes must belong to + :param node1_id: first node of pair to link/unlink + :param node2_id: second node of pair to link/unlin + :param linked: True to link, False to unlink + :return: True for success, False otherwise + :raises grpc.RpcError: when session or one of the nodes do not exist + """ + request = WlanLinkRequest( + session_id=session_id, + wlan=wlan_id, + node1_id=node1_id, + node2_id=node2_id, + linked=linked, + ) + response = self.stub.WlanLink(request) + return response.result + + # TODO: determine path to stream non proto requests + def emane_pathlosses( + self, pathloss_iterator: Iterable[EmanePathlossesRequest] + ) -> None: + """ + Stream EMANE pathloss events. + + :param pathloss_iterator: iterator for sending emane pathloss events + :return: nothing + :raises grpc.RpcError: when a pathloss event session or one of the nodes do not + exist + """ + self.stub.EmanePathlosses(pathloss_iterator) + + def connect(self) -> None: + """ + Open connection to server, must be closed manually. + + :return: nothing + """ + self.channel = grpc.insecure_channel( + self.address, options=[("grpc.enable_http_proxy", self.proxy)] + ) + self.stub = core_pb2_grpc.CoreApiStub(self.channel) + + def close(self) -> None: + """ + Close currently opened server channel connection. + + :return: nothing + """ + if self.channel: + self.channel.close() + self.channel = None + + @contextmanager + def context_connect(self) -> Generator: + """ + Makes a context manager based connection to the server, will close after + context ends. + + :return: nothing + """ + try: + self.connect() + yield + finally: + self.close() diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index aabf2177..2b0bc2a8 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -1438,7 +1438,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logging.debug("get emane model config: %s", request) session = self.get_session(request.session_id, context) - model = session.emane.models[request.model] + model = session.emane.models.get(request.model) + if not model: + raise CoreError(f"invalid emane model: {request.model}") _id = get_emane_model_id(request.node_id, request.iface_id) current_config = session.emane.get_model_config(_id, request.model) config = get_config_options(current_config, model) @@ -1483,7 +1485,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): self, request: core_pb2.SaveXmlRequest, context: ServicerContext ) -> core_pb2.SaveXmlResponse: """ - Export the session nto the EmulationScript XML format + Export the session into the EmulationScript XML format :param request: save xml request :param context: context object diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py index 52384fe2..285bdfce 100644 --- a/daemon/core/api/grpc/wrappers.py +++ b/daemon/core/api/grpc/wrappers.py @@ -3,7 +3,13 @@ from enum import Enum from pathlib import Path from typing import Dict, List, Optional, Set, Tuple -from core.api.grpc import common_pb2, configservices_pb2, core_pb2, services_pb2 +from core.api.grpc import ( + common_pb2, + configservices_pb2, + core_pb2, + emane_pb2, + services_pb2, +) class ConfigServiceValidationMode(Enum): @@ -87,6 +93,13 @@ class MessageType(Enum): TTY = 64 +class ServiceAction(Enum): + START = 0 + STOP = 1 + RESTART = 2 + VALIDATE = 3 + + @dataclass class ConfigService: group: str @@ -120,12 +133,67 @@ class ConfigService: ) +@dataclass +class ConfigServiceConfig: + node_id: int + name: str + templates: Dict[str, str] + config: Dict[str, str] + + @classmethod + def from_proto( + cls, proto: configservices_pb2.ConfigServiceConfig + ) -> "ConfigServiceConfig": + return ConfigServiceConfig( + node_id=proto.node_id, + name=proto.name, + templates=dict(proto.templates), + config=dict(proto.config), + ) + + @dataclass class ConfigServiceData: templates: Dict[str, str] = field(default_factory=dict) config: Dict[str, str] = field(default_factory=dict) +@dataclass +class ConfigServiceDefaults: + templates: Dict[str, str] + config: Dict[str, "ConfigOption"] + modes: List[str] + + @classmethod + def from_proto( + cls, proto: configservices_pb2.GetConfigServicesResponse + ) -> "ConfigServiceDefaults": + config = ConfigOption.from_dict(proto.config) + return ConfigServiceDefaults( + templates=dict(proto.templates), config=config, modes=list(proto.modes) + ) + + +@dataclass +class Service: + group: str + name: str + + @classmethod + def from_proto(cls, proto: services_pb2.Service) -> "Service": + return Service(group=proto.group, name=proto.name) + + +@dataclass +class ServiceDefault: + node_type: str + services: List[str] + + @classmethod + def from_proto(cls, proto: services_pb2.ServiceDefaults) -> "ServiceDefault": + return ServiceDefault(node_type=proto.node_type, services=list(proto.services)) + + @dataclass class NodeServiceData: executables: List[str] @@ -155,6 +223,28 @@ class NodeServiceData: ) +@dataclass +class ServiceConfig: + node_id: int + service: str + files: List[str] = None + directories: List[str] = None + startup: List[str] = None + validate: List[str] = None + shutdown: List[str] = None + + def to_proto(self) -> services_pb2.ServiceConfig: + return services_pb2.ServiceConfig( + node_id=self.node_id, + service=self.service, + files=self.files, + directories=self.directories, + startup=self.startup, + validate=self.validate, + shutdown=self.shutdown, + ) + + @dataclass class BridgeThroughput: node_id: int @@ -471,6 +561,30 @@ class Hook: return core_pb2.Hook(state=self.state.value, file=self.file, data=self.data) +@dataclass +class EmaneModelConfig: + node_id: int + model: str + iface_id: int = -1 + config: Dict[str, ConfigOption] = None + + @classmethod + def from_proto(cls, proto: emane_pb2.GetEmaneModelConfig) -> "EmaneModelConfig": + iface_id = proto.iface_id if proto.iface_id != -1 else None + config = ConfigOption.from_dict(proto.config) + return EmaneModelConfig( + node_id=proto.node_id, iface_id=iface_id, model=proto.model, config=config + ) + + def to_proto(self) -> emane_pb2.EmaneModelConfig: + return emane_pb2.EmaneModelConfig( + node_id=self.node_id, + model=self.model, + iface_id=self.iface_id, + config=self.config, + ) + + @dataclass class Position: x: float @@ -660,3 +774,18 @@ class NodeEvent: message_type=MessageType(proto.message_type), node=Node.from_proto(proto.node), ) + + +@dataclass +class EmaneEventChannel: + group: str + port: int + device: str + + @classmethod + def from_proto( + cls, proto: emane_pb2.GetEmaneEventChannelResponse + ) -> "EmaneEventChannel": + return EmaneEventChannel( + group=proto.group, port=proto.port, device=proto.device + ) diff --git a/daemon/core/config.py b/daemon/core/config.py index 618e1273..222abf01 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -212,7 +212,7 @@ class ConfigurableManager: def get_configs( self, node_id: int = _default_node, config_type: str = _default_type - ) -> Dict[str, str]: + ) -> Optional[Dict[str, str]]: """ Retrieve configurations for a node and configuration type. diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index ec39137d..7c1e5b83 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -145,14 +145,17 @@ class EmaneManager(ModelManager): key += iface.node_id # try retrieve interface specific configuration, avoid getting defaults config = self.get_configs(node_id=key, config_type=model_name) - # otherwise retrieve the interfaces node configuration, avoid using defaults + # attempt to retrieve node specific conifg, when iface config is not present if not config: config = self.get_configs(node_id=iface.node.id, config_type=model_name) - # get non interface config, when none found + # attempt to get emane net specific config, when node config is not present if not config: # with EMANE 0.9.2+, we need an extra NEM XML from # model.buildnemxmlfiles(), so defaults are returned here config = self.get_configs(node_id=emane_net.id, config_type=model_name) + # return default config values, when a config is not present + if not config: + config = emane_net.model.default_values() return config def config_reset(self, node_id: int = None) -> None: diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 309f0ac6..7622127a 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -545,7 +545,12 @@ class Session: # ensure default emane configuration if isinstance(node, EmaneNet) and options.emane: - self.emane.set_model_config(_id, options.emane) + model = self.emane.models.get(options.emane) + if not model: + raise CoreError( + f"node({node.name}) emane model({options.emane}) does not exist" + ) + node.setmodel(model, {}) if self.state == EventTypes.RUNTIME_STATE: self.emane.add_node(node) # set default wlan config if needed diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 116b1617..f4f8b79a 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -937,8 +937,6 @@ class CoreClient: def get_emane_model_configs_proto(self) -> List[emane_pb2.EmaneModelConfig]: configs = [] for node in self.session.nodes.values(): - if node.type != NodeType.EMANE: - continue for key, config in node.emane_model_configs.items(): model, iface_id = key config = ConfigOption.to_dict(config) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 6327206f..9ceafa35 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -282,9 +282,7 @@ class NodeConfigDialog(Dialog): button.grid(row=0, column=1, sticky=tk.EW) def click_emane_config(self, emane_model: str, iface_id: int) -> None: - dialog = EmaneModelDialog( - self, self.app, self.canvas_node, emane_model, iface_id - ) + dialog = EmaneModelDialog(self, self.app, self.node, emane_model, iface_id) dialog.show() def click_icon(self) -> None: diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 7e3b35a2..20a30296 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -91,10 +91,14 @@ def create_emane_config(session: "Session") -> etree.Element: def create_emane_model_config( - node_id: int, model: "EmaneModelType", config: Dict[str, str] + node_id: int, + model: "EmaneModelType", + config: Dict[str, str], + iface_id: Optional[int], ) -> etree.Element: emane_element = etree.Element("emane_configuration") add_attribute(emane_element, "node", node_id) + add_attribute(emane_element, "iface", iface_id) add_attribute(emane_element, "model", model.name) mac_element = etree.SubElement(emane_element, "mac") @@ -378,13 +382,19 @@ class CoreXmlWriter: all_configs = self.session.emane.get_all_configs(node_id) if not all_configs: continue + iface_id = None + if node_id >= 1000: + iface_id = node_id % 1000 + node_id = node_id // 1000 for model_name in all_configs: config = all_configs[model_name] logging.debug( "writing emane config node(%s) model(%s)", node_id, model_name ) model = self.session.emane.models[model_name] - emane_configuration = create_emane_model_config(node_id, model, config) + emane_configuration = create_emane_model_config( + node_id, model, config, iface_id + ) emane_configurations.append(emane_configuration) if emane_configurations.getchildren(): self.scenario.append(emane_configurations) @@ -588,9 +598,9 @@ class CoreXmlReader: self.read_mobility_configs() self.read_emane_global_config() self.read_nodes() + self.read_links() self.read_emane_configs() self.read_configservice_configs() - self.read_links() def read_default_services(self) -> None: default_services = self.scenario.find("default_services") @@ -748,6 +758,7 @@ class CoreXmlReader: for emane_configuration in emane_configurations.iterchildren(): node_id = get_int(emane_configuration, "node") + iface_id = get_int(emane_configuration, "iface") model_name = emane_configuration.get("model") configs = {} @@ -755,12 +766,13 @@ class CoreXmlReader: node = self.session.nodes.get(node_id) if not node: raise CoreXmlError(f"node for emane config doesn't exist: {node_id}") - if not isinstance(node, EmaneNet): - raise CoreXmlError(f"invalid node for emane config: {node.name}") model = self.session.emane.models.get(model_name) if not model: raise CoreXmlError(f"invalid emane model: {model_name}") - node.setmodel(model, {}) + if iface_id is not None and iface_id not in node.ifaces: + raise CoreXmlError( + f"invalid interface id({iface_id}) for node({node.name})" + ) # read and set emane model configuration mac_configuration = emane_configuration.find("mac") @@ -784,7 +796,10 @@ class CoreXmlReader: logging.info( "reading emane configuration node(%s) model(%s)", node_id, model_name ) - self.session.emane.set_model_config(node_id, model_name, configs) + key = node_id + if iface_id is not None: + key = node_id * 1000 + iface_id + self.session.emane.set_model_config(key, model_name, configs) def read_mobility_configs(self) -> None: mobility_configurations = self.scenario.find("mobility_configurations") @@ -869,6 +884,9 @@ class CoreXmlReader: icon = network_element.get("icon") server = network_element.get("server") options = NodeOptions(name=name, icon=icon, server=server) + if node_type == NodeTypes.EMANE: + model = network_element.get("model") + options.emane = model position_element = network_element.find("position") if position_element is not None: