diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..13332ecf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,101 @@ +# syntax=docker/dockerfile:1 +FROM ubuntu:20.04 +LABEL Description="CORE Docker Image" + +# define variables +ARG DEBIAN_FRONTEND=noninteractive +ARG PREFIX=/usr/local +ARG BRANCH=develop +ARG CORE_TARBALL=core.tar.gz +ARG OSPF_TARBALL=ospf.tar.gz + +# install system dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + automake \ + bash \ + ca-certificates \ + ethtool \ + gawk \ + gcc \ + g++ \ + iproute2 \ + iputils-ping \ + libc-dev \ + libev-dev \ + libreadline-dev \ + libtool \ + libtk-img \ + make \ + nftables \ + python3 \ + python3-pip \ + python3-tk \ + pkg-config \ + systemctl \ + tk \ + wget \ + xauth \ + xterm \ + && apt-get clean +# install python dependencies +RUN python3 -m pip install \ + grpcio==1.27.2 \ + grpcio-tools==1.27.2 \ + poetry==1.1.7 +# retrieve, build, and install core +RUN wget -q -O ${CORE_TARBALL} https://api.github.com/repos/coreemu/core/tarball/${BRANCH} && \ + tar xf ${CORE_TARBALL} && \ + cd coreemu-core* && \ + ./bootstrap.sh && \ + ./configure && \ + make -j $(nproc) && \ + make install && \ + cd daemon && \ + python3 -m poetry build -f wheel && \ + python3 -m pip install dist/* && \ + cp scripts/* ${PREFIX}/bin && \ + mkdir /etc/core && \ + cp -n data/core.conf /etc/core && \ + cp -n data/logging.conf /etc/core && \ + mkdir -p ${PREFIX}/share/core && \ + cp -r examples ${PREFIX}/share/core && \ + echo '\ +[Unit]\n\ +Description=Common Open Research Emulator Service\n\ +After=network.target\n\ +\n\ +[Service]\n\ +Type=simple\n\ +ExecStart=/usr/local/bin/core-daemon\n\ +TasksMax=infinity\n\ +\n\ +[Install]\n\ +WantedBy=multi-user.target\ +' > /lib/systemd/system/core-daemon.service && \ + cd ../.. && \ + rm ${CORE_TARBALL} && \ + rm -rf coreemu-core* +# retrieve, build, and install ospf mdr +RUN wget -q -O ${OSPF_TARBALL} https://github.com/USNavalResearchLaboratory/ospf-mdr/tarball/master && \ + tar xf ${OSPF_TARBALL} && \ + cd USNavalResearchLaboratory-ospf-mdr* && \ + ./bootstrap.sh && \ + ./configure --disable-doc --enable-user=root --enable-group=root \ + --with-cflags=-ggdb --sysconfdir=/usr/local/etc/quagga --enable-vtysh \ + --localstatedir=/var/run/quagga && \ + make -j $(nproc) && \ + make install && \ + cd .. && \ + rm ${OSPF_TARBALL} && \ + rm -rf USNavalResearchLaboratory-ospf-mdr* +# retrieve and install emane packages +RUN wget -q https://adjacentlink.com/downloads/emane/emane-1.2.7-release-1.ubuntu-20_04.amd64.tar.gz && \ + tar xf emane*.tar.gz && \ + cd emane-1.2.7-release-1/debs/ubuntu-20_04/amd64 && \ + apt-get install -y ./emane*.deb ./python3-emane_*.deb && \ + cd ../../../.. && \ + rm emane-1.2.7-release-1.ubuntu-20_04.amd64.tar.gz && \ + rm -rf emane-1.2.7-release-1 +CMD ["systemctl", "start", "core-daemon"] +# sudo docker run -itd --name core -e DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix:rw --privileged core diff --git a/Makefile.am b/Makefile.am index 7a3799fc..bd15cf09 100644 --- a/Makefile.am +++ b/Makefile.am @@ -57,7 +57,7 @@ fpm -s dir -t deb -n core-distributed \ -d "procps" \ -d "libc6 >= 2.14" \ -d "bash >= 3.0" \ - -d "ebtables" \ + -d "nftables" \ -d "iproute2" \ -d "libev4" \ -d "openssh-server" \ @@ -77,7 +77,7 @@ fpm -s dir -t rpm -n core-distributed \ -d "ethtool" \ -d "procps-ng" \ -d "bash >= 3.0" \ - -d "ebtables" \ + -d "nftables" \ -d "iproute" \ -d "libev" \ -d "net-tools" \ @@ -123,7 +123,7 @@ all: change-files .PHONY: change-files change-files: - $(call change-files,gui/core-gui) + $(call change-files,gui/core-gui-legacy) $(call change-files,daemon/core/constants.py) $(call change-files,netns/setup.py) diff --git a/README.md b/README.md index 62f21628..dc991f14 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ CORE: Common Open Research Emulator -Copyright (c)2005-2020 the Boeing Company. +Copyright (c)2005-2022 the Boeing Company. See the LICENSE file included in this distribution. @@ -14,6 +14,24 @@ networks to live networks. CORE consists of a GUI for drawing topologies of lightweight virtual machines, and Python modules for scripting network emulation. +## Quick Start + +The following should get you up and running on Ubuntu 18+ and CentOS 7+ +from a clean install, it will prompt you for sudo password. This would +install CORE into a python3 virtual environment and install +[OSPF MDR](https://github.com/USNavalResearchLaboratory/ospf-mdr) from source. +For more detailed installation see [here](https://coreemu.github.io/core/install.html). + +```shell +git clone https://github.com/coreemu/core.git +cd core +./setup.sh +# Ubuntu +inv install +# CentOS +./install.sh -p /usr +``` + ## Documentation & Support We are leveraging GitHub hosted documentation and Discord for persistent diff --git a/configure.ac b/configure.ac index dcfddccc..6b06966a 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. # this defines the CORE version number, must be static for AC_INIT -AC_INIT(core, 7.5.2) +AC_INIT(core, 8.0.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) @@ -123,9 +123,9 @@ if test "x$enable_daemon" = "xyes"; then AC_MSG_ERROR([Could not locate sysctl (from procps package).]) fi - AC_CHECK_PROG(ebtables_path, ebtables, $as_dir, no, $SEARCHPATH) - if test "x$ebtables_path" = "xno" ; then - AC_MSG_ERROR([Could not locate ebtables (from ebtables package).]) + AC_CHECK_PROG(nftables_path, nft, $as_dir, no, $SEARCHPATH) + if test "x$nftables_path" = "xno" ; then + AC_MSG_ERROR([Could not locate nftables (from nftables package).]) fi AC_CHECK_PROG(ip_path, ip, $as_dir, no, $SEARCHPATH) diff --git a/daemon/core/__init__.py b/daemon/core/__init__.py index 40ca3604..c847c8dc 100644 --- a/daemon/core/__init__.py +++ b/daemon/core/__init__.py @@ -2,6 +2,3 @@ import logging.config # setup default null handler logging.getLogger(__name__).addHandler(logging.NullHandler()) - -# disable paramiko logging -logging.getLogger("paramiko").setLevel(logging.WARNING) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index e28233fc..783e80c0 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -5,94 +5,106 @@ gRpc client for interfacing with CORE. import logging import threading from contextlib import contextmanager -from typing import Any, Callable, Dict, Generator, Iterable, List, Optional +from pathlib import Path +from queue import Queue +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 +from core.api.grpc import core_pb2, core_pb2_grpc, emane_pb2, wrappers from core.api.grpc.configservices_pb2 import ( GetConfigServiceDefaultsRequest, - GetConfigServiceDefaultsResponse, - GetConfigServicesRequest, - GetConfigServicesResponse, - GetNodeConfigServiceConfigsRequest, - GetNodeConfigServiceConfigsResponse, GetNodeConfigServiceRequest, - GetNodeConfigServiceResponse, - GetNodeConfigServicesRequest, - GetNodeConfigServicesResponse, - SetNodeConfigServiceRequest, - SetNodeConfigServiceResponse, ) -from core.api.grpc.core_pb2 import ExecuteScriptRequest, ExecuteScriptResponse +from core.api.grpc.core_pb2 import ExecuteScriptRequest, GetConfigRequest from core.api.grpc.emane_pb2 import ( EmaneLinkRequest, - EmaneLinkResponse, - EmaneModelConfig, - EmanePathlossesRequest, - EmanePathlossesResponse, - GetEmaneConfigRequest, - GetEmaneConfigResponse, GetEmaneEventChannelRequest, - GetEmaneEventChannelResponse, GetEmaneModelConfigRequest, - GetEmaneModelConfigResponse, - GetEmaneModelConfigsRequest, - GetEmaneModelConfigsResponse, - GetEmaneModelsRequest, - GetEmaneModelsResponse, - SetEmaneConfigRequest, - SetEmaneConfigResponse, SetEmaneModelConfigRequest, - SetEmaneModelConfigResponse, ) from core.api.grpc.mobility_pb2 import ( GetMobilityConfigRequest, - GetMobilityConfigResponse, - GetMobilityConfigsRequest, - GetMobilityConfigsResponse, MobilityActionRequest, - MobilityActionResponse, MobilityConfig, SetMobilityConfigRequest, - SetMobilityConfigResponse, ) from core.api.grpc.services_pb2 import ( - GetNodeServiceConfigsRequest, - GetNodeServiceConfigsResponse, GetNodeServiceFileRequest, - GetNodeServiceFileResponse, GetNodeServiceRequest, - GetNodeServiceResponse, GetServiceDefaultsRequest, - GetServiceDefaultsResponse, - GetServicesRequest, - GetServicesResponse, - ServiceAction, ServiceActionRequest, - ServiceActionResponse, - ServiceConfig, ServiceDefaults, - ServiceFileConfig, - SetNodeServiceFileRequest, - SetNodeServiceFileResponse, - SetNodeServiceRequest, - SetNodeServiceResponse, SetServiceDefaultsRequest, - SetServiceDefaultsResponse, ) from core.api.grpc.wlan_pb2 import ( GetWlanConfigRequest, - GetWlanConfigResponse, - GetWlanConfigsRequest, - GetWlanConfigsResponse, SetWlanConfigRequest, - SetWlanConfigResponse, WlanConfig, WlanLinkRequest, - WlanLinkResponse, ) from core.emulator.data import IpPrefixes +from core.errors import CoreError + +logger = logging.getLogger(__name__) + + +class MoveNodesStreamer: + def __init__(self, session_id: int = None, source: str = None) -> None: + self.session_id = session_id + self.source = source + self.queue: Queue = Queue() + + def send_position(self, node_id: int, x: float, y: float) -> None: + position = wrappers.Position(x=x, y=y) + request = wrappers.MoveNodesRequest( + session_id=self.session_id, + node_id=node_id, + source=self.source, + position=position, + ) + self.send(request) + + def send_geo(self, node_id: int, lon: float, lat: float, alt: float) -> None: + geo = wrappers.Geo(lon=lon, lat=lat, alt=alt) + request = wrappers.MoveNodesRequest( + session_id=self.session_id, node_id=node_id, source=self.source, geo=geo + ) + self.send(request) + + def send(self, request: wrappers.MoveNodesRequest) -> None: + self.queue.put(request) + + def stop(self) -> None: + self.queue.put(None) + + def next(self) -> Optional[core_pb2.MoveNodesRequest]: + request: Optional[wrappers.MoveNodesRequest] = self.queue.get() + if request: + return request.to_proto() + else: + return request + + def iter(self) -> Iterable: + return iter(self.next, None) + + +class EmanePathlossesStreamer: + def __init__(self) -> None: + self.queue: Queue = Queue() + + def send(self, request: Optional[wrappers.EmanePathlossesRequest]) -> None: + self.queue.put(request) + + def next(self) -> Optional[emane_pb2.EmanePathlossesRequest]: + request: Optional[wrappers.EmanePathlossesRequest] = self.queue.get() + if request: + return request.to_proto() + else: + return request + + def iter(self): + return iter(self.next, None) class InterfaceHelper: @@ -112,7 +124,7 @@ class InterfaceHelper: def create_iface( self, node_id: int, iface_id: int, name: str = None, mac: str = None - ) -> core_pb2.Interface: + ) -> wrappers.Interface: """ Create an interface protobuf object. @@ -123,7 +135,7 @@ class InterfaceHelper: :return: interface protobuf """ iface_data = self.prefixes.gen_iface(node_id, name, mac) - return core_pb2.Interface( + return wrappers.Interface( id=iface_id, name=iface_data.name, ip4=iface_data.ip4, @@ -134,36 +146,65 @@ class InterfaceHelper: ) -def stream_listener(stream: Any, handler: Callable[[core_pb2.Event], None]) -> None: +def throughput_listener( + stream: Any, handler: Callable[[wrappers.ThroughputsEvent], None] +) -> None: """ - Listen for stream events and provide them to the handler. + Listen for throughput 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_proto in stream: + event = wrappers.ThroughputsEvent.from_proto(event_proto) + handler(event) + except grpc.RpcError as e: + if e.code() == grpc.StatusCode.CANCELLED: + logger.debug("throughput stream closed") + else: + logger.exception("throughput stream error") + + +def cpu_listener( + stream: Any, handler: Callable[[wrappers.CpuUsageEvent], None] +) -> None: + """ + Listen for cpu 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: + for event_proto in stream: + event = wrappers.CpuUsageEvent.from_proto(event_proto) handler(event) except grpc.RpcError as e: if e.code() == grpc.StatusCode.CANCELLED: - logging.debug("stream closed") + logger.debug("cpu stream closed") else: - logging.exception("stream error") + logger.exception("cpu stream error") -def start_streamer(stream: Any, handler: Callable[[core_pb2.Event], None]) -> None: +def event_listener(stream: Any, handler: Callable[[wrappers.Event], None]) -> None: """ - Convenience method for starting a grpc stream thread for handling streamed events. + Listen for session events and provide them to the handler. :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() + try: + for event_proto in stream: + event = wrappers.Event.from_proto(event_proto) + handler(event) + except grpc.RpcError as e: + if e.code() == grpc.StatusCode.CANCELLED: + logger.debug("session stream closed") + else: + logger.exception("session stream error") class CoreGrpcClient: @@ -183,290 +224,127 @@ class CoreGrpcClient: self.proxy: bool = proxy def start_session( - self, - session_id: int, - nodes: List[core_pb2.Node], - links: List[core_pb2.Link], - location: core_pb2.SessionLocation = None, - hooks: List[core_pb2.Hook] = None, - emane_config: Dict[str, str] = None, - emane_model_configs: List[EmaneModelConfig] = None, - wlan_configs: List[WlanConfig] = None, - mobility_configs: List[MobilityConfig] = None, - service_configs: List[ServiceConfig] = None, - service_file_configs: List[ServiceFileConfig] = None, - asymmetric_links: List[core_pb2.Link] = None, - config_service_configs: List[configservices_pb2.ConfigServiceConfig] = None, - ) -> core_pb2.StartSessionResponse: + self, session: wrappers.Session, definition: bool = False + ) -> Tuple[bool, List[str]]: """ Start a session. - :param session_id: id of session - :param nodes: list of nodes to create - :param links: list of links to create - :param location: location to set - :param hooks: session hooks to set - :param emane_config: emane configuration to set - :param emane_model_configs: node emane model configurations - :param wlan_configs: node wlan configurations - :param mobility_configs: node mobility configurations - :param service_configs: node service configurations - :param service_file_configs: node service file configurations - :param asymmetric_links: asymmetric links to edit - :param config_service_configs: config service configurations - :return: start session response + :param session: session to start + :param definition: True to only define session data, False to start session + :return: tuple of result and exception strings """ request = core_pb2.StartSessionRequest( - session_id=session_id, - nodes=nodes, - links=links, - location=location, - 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, + session=session.to_proto(), definition=definition ) - return self.stub.StartSession(request) + response = self.stub.StartSession(request) + return response.result, list(response.exceptions) - def stop_session(self, session_id: int) -> core_pb2.StopSessionResponse: + def stop_session(self, session_id: int) -> bool: """ Stop a running session. :param session_id: id of session - :return: stop session response + :return: True for success, False otherwise :raises grpc.RpcError: when session doesn't exist """ request = core_pb2.StopSessionRequest(session_id=session_id) - return self.stub.StopSession(request) + response = self.stub.StopSession(request) + return response.result - def create_session(self, session_id: int = None) -> core_pb2.CreateSessionResponse: + def create_session(self, session_id: int = None) -> wrappers.Session: """ Create a session. :param session_id: id for session, default is None and one will be created for you - :return: response with created session id + :return: session id """ request = core_pb2.CreateSessionRequest(session_id=session_id) - return self.stub.CreateSession(request) + response = self.stub.CreateSession(request) + return wrappers.Session.from_proto(response.session) - def delete_session(self, session_id: int) -> core_pb2.DeleteSessionResponse: + def delete_session(self, session_id: int) -> bool: """ Delete a session. :param session_id: id of session - :return: response with result of deletion success or failure + :return: True for success, False otherwise :raises grpc.RpcError: when session doesn't exist """ request = core_pb2.DeleteSessionRequest(session_id=session_id) - return self.stub.DeleteSession(request) + response = self.stub.DeleteSession(request) + return response.result - def get_sessions(self) -> core_pb2.GetSessionsResponse: + 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 """ - return self.stub.GetSessions(core_pb2.GetSessionsRequest()) + 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) -> core_pb2.CheckSessionResponse: + def check_session(self, session_id: int) -> bool: """ Check if a session exists. :param session_id: id of session to check for - :return: response with result if session was found + :return: True if exists, False otherwise """ request = core_pb2.CheckSessionRequest(session_id=session_id) - return self.stub.CheckSession(request) + response = self.stub.CheckSession(request) + return response.result - def get_session(self, session_id: int) -> core_pb2.GetSessionResponse: + def get_session(self, session_id: int) -> wrappers.Session: """ Retrieve a session. :param session_id: id of session - :return: response with sessions state, nodes, and links + :return: session :raises grpc.RpcError: when session doesn't exist """ request = core_pb2.GetSessionRequest(session_id=session_id) - return self.stub.GetSession(request) - - def get_session_options( - self, session_id: int - ) -> core_pb2.GetSessionOptionsResponse: - """ - Retrieve session options as a dict with id mapping. - - :param session_id: id of session - :return: response with a list of configuration groups - :raises grpc.RpcError: when session doesn't exist - """ - request = core_pb2.GetSessionOptionsRequest(session_id=session_id) - return self.stub.GetSessionOptions(request) - - def set_session_options( - self, session_id: int, config: Dict[str, str] - ) -> core_pb2.SetSessionOptionsResponse: - """ - Set options for a session. - - :param session_id: id of session - :param config: configuration values to set - :return: response with result of success or failure - :raises grpc.RpcError: when session doesn't exist - """ - request = core_pb2.SetSessionOptionsRequest( - session_id=session_id, config=config - ) - return self.stub.SetSessionOptions(request) - - def get_session_metadata( - self, session_id: int - ) -> core_pb2.GetSessionMetadataResponse: - """ - 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) - return self.stub.GetSessionMetadata(request) - - def set_session_metadata( - self, session_id: int, config: Dict[str, str] - ) -> core_pb2.SetSessionMetadataResponse: - """ - Set metadata for a session. - - :param session_id: id of session - :param config: configuration values to set - :return: response with result of success or failure - :raises grpc.RpcError: when session doesn't exist - """ - request = core_pb2.SetSessionMetadataRequest( - session_id=session_id, config=config - ) - return self.stub.SetSessionMetadata(request) - - def get_session_location( - self, session_id: int - ) -> core_pb2.GetSessionLocationResponse: - """ - 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) - return self.stub.GetSessionLocation(request) - - def set_session_location( - self, - session_id: int, - x: float = None, - y: float = None, - z: float = None, - lat: float = None, - lon: float = None, - alt: float = None, - scale: float = None, - ) -> core_pb2.SetSessionLocationResponse: - """ - Set session location. - - :param session_id: id of session - :param x: x position - :param y: y position - :param z: z position - :param lat: latitude position - :param lon: longitude position - :param alt: altitude position - :param scale: geo scale - :return: response with result of success or failure - :raises grpc.RpcError: when session doesn't exist - """ - location = core_pb2.SessionLocation( - x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=scale - ) - request = core_pb2.SetSessionLocationRequest( - session_id=session_id, location=location - ) - return self.stub.SetSessionLocation(request) - - def set_session_state( - self, session_id: int, state: core_pb2.SessionState - ) -> core_pb2.SetSessionStateResponse: - """ - Set session state. - - :param session_id: id of session - :param state: session state to transition to - :return: response with result of success or failure - :raises grpc.RpcError: when session doesn't exist - """ - request = core_pb2.SetSessionStateRequest(session_id=session_id, state=state) - return self.stub.SetSessionState(request) - - def set_session_user( - self, session_id: int, user: str - ) -> core_pb2.SetSessionUserResponse: - """ - 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: response with result of success or failure - :raises grpc.RpcError: when session doesn't exist - """ - request = core_pb2.SetSessionUserRequest(session_id=session_id, user=user) - return self.stub.SetSessionUser(request) - - def add_session_server( - self, session_id: int, name: str, host: str - ) -> core_pb2.AddSessionServerResponse: - """ - 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: response with result of success or failure - :raises grpc.RpcError: when session doesn't exist - """ - request = core_pb2.AddSessionServerRequest( - session_id=session_id, name=name, host=host - ) - return self.stub.AddSessionServer(request) + response = self.stub.GetSession(request) + return wrappers.Session.from_proto(response.session) def alert( self, session_id: int, - level: core_pb2.ExceptionLevel, + level: wrappers.ExceptionLevel, source: str, text: str, node_id: int = None, - ) -> core_pb2.SessionAlertResponse: + ) -> 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, + level=level.value, source=source, text=text, node_id=node_id, ) - return self.stub.SessionAlert(request) + response = self.stub.SessionAlert(request) + return response.result def events( self, session_id: int, - handler: Callable[[core_pb2.Event], None], - events: List[core_pb2.Event] = None, + handler: Callable[[wrappers.Event], None], + events: List[wrappers.EventType] = None, ) -> grpc.Future: """ Listen for session events. @@ -479,11 +357,14 @@ class CoreGrpcClient: """ request = core_pb2.EventsRequest(session_id=session_id, events=events) stream = self.stub.Events(request) - start_streamer(stream, handler) + thread = threading.Thread( + target=event_listener, args=(stream, handler), daemon=True + ) + thread.start() return stream def throughputs( - self, session_id: int, handler: Callable[[core_pb2.ThroughputsEvent], None] + self, session_id: int, handler: Callable[[wrappers.ThroughputsEvent], None] ) -> grpc.Future: """ Listen for throughput events with information for interfaces and bridges. @@ -495,11 +376,14 @@ class CoreGrpcClient: """ request = core_pb2.ThroughputsRequest(session_id=session_id) stream = self.stub.Throughputs(request) - start_streamer(stream, handler) + thread = threading.Thread( + target=throughput_listener, args=(stream, handler), daemon=True + ) + thread.start() return stream def cpu_usage( - self, delay: int, handler: Callable[[core_pb2.CpuUsageEvent], None] + self, delay: int, handler: Callable[[wrappers.CpuUsageEvent], None] ) -> grpc.Future: """ Listen for cpu usage events with the given repeat delay. @@ -510,98 +394,130 @@ class CoreGrpcClient: """ request = core_pb2.CpuUsageRequest(delay=delay) stream = self.stub.CpuUsage(request) - start_streamer(stream, handler) + thread = threading.Thread( + target=cpu_listener, args=(stream, handler), daemon=True + ) + thread.start() return stream - def add_node( - self, session_id: int, node: core_pb2.Node, source: str = None - ) -> core_pb2.AddNodeResponse: + 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: response with node id + :return: id of added node :raises grpc.RpcError: when session doesn't exist """ request = core_pb2.AddNodeRequest( - session_id=session_id, node=node, source=source + session_id=session_id, node=node.to_proto(), source=source ) - return self.stub.AddNode(request) + response = self.stub.AddNode(request) + return response.node_id - def get_node(self, session_id: int, node_id: int) -> core_pb2.GetNodeResponse: + def get_node( + self, session_id: int, node_id: int + ) -> Tuple[wrappers.Node, List[wrappers.Interface], List[wrappers.Link]]: """ Get node details. :param session_id: session id :param node_id: node id - :return: response with node details + :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) - return self.stub.GetNode(request) + 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) + links = [] + for link_proto in response.links: + link = wrappers.Link.from_proto(link_proto) + links.append(link) + return node, ifaces, links def edit_node( - self, - session_id: int, - node_id: int, - position: core_pb2.Position = None, - icon: str = None, - geo: core_pb2.Geo = None, - source: str = None, - ) -> core_pb2.EditNodeResponse: + self, session_id: int, node_id: int, icon: str = 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: response with result of success or failure + :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, icon=icon, source=source + ) + response = self.stub.EditNode(request) + return response.result + + def move_node( + self, + session_id: int, + node_id: int, + position: wrappers.Position = None, + geo: wrappers.Geo = None, + source: str = None, + ) -> bool: + """ + Move node using provided position or geo location. + + :param session_id: session id + :param node_id: node id + :param position: x,y position to move to + :param geo: geospatial position to move to + :param source: source generating motion + :return: nothing + :raises grpc.RpcError: when session or nodes do not exist + """ + if not position and not geo: + raise CoreError("must provide position or geo to move node") + position = position.to_proto() if position else None + geo = geo.to_proto() if geo else None + request = core_pb2.MoveNodeRequest( session_id=session_id, node_id=node_id, position=position, - icon=icon, - source=source, geo=geo, + source=source, ) - return self.stub.EditNode(request) + response = self.stub.MoveNode(request) + return response.result - def move_nodes( - self, move_iterator: Iterable[core_pb2.MoveNodesRequest] - ) -> core_pb2.MoveNodesResponse: + def move_nodes(self, streamer: MoveNodesStreamer) -> None: """ Stream node movements using the provided iterator. - :param move_iterator: iterator for generating node movements - :return: move nodes response + :param streamer: move nodes streamer + :return: nothing :raises grpc.RpcError: when session or nodes do not exist """ - return self.stub.MoveNodes(move_iterator) + self.stub.MoveNodes(streamer.iter()) - def delete_node( - self, session_id: int, node_id: int, source: str = None - ) -> core_pb2.DeleteNodeResponse: + 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: response with result of success or failure + :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 ) - return self.stub.DeleteNode(request) + response = self.stub.DeleteNode(request) + return response.result def node_command( self, @@ -610,7 +526,7 @@ class CoreGrpcClient: command: str, wait: bool = True, shell: bool = False, - ) -> core_pb2.NodeCommandResponse: + ) -> Tuple[int, str]: """ Send command to a node and get the output. @@ -619,7 +535,7 @@ class CoreGrpcClient: :param command: command to run on node :param wait: wait for command to complete :param shell: send shell command - :return: response with command combined stdout/stderr + :return: returns tuple of return code and output :raises grpc.RpcError: when session or node doesn't exist """ request = core_pb2.NodeCommandRequest( @@ -629,260 +545,199 @@ class CoreGrpcClient: wait=wait, shell=shell, ) - return self.stub.NodeCommand(request) + response = self.stub.NodeCommand(request) + return response.return_code, response.output - def get_node_terminal( - self, session_id: int, node_id: int - ) -> core_pb2.GetNodeTerminalResponse: + 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: response with a node terminal command + :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 ) - return self.stub.GetNodeTerminal(request) + response = self.stub.GetNodeTerminal(request) + return response.terminal - def get_node_links( - self, session_id: int, node_id: int - ) -> core_pb2.GetNodeLinksResponse: + 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: response with a list of links + :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) - return self.stub.GetNodeLinks(request) + 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, - node1_id: int, - node2_id: int, - iface1: core_pb2.Interface = None, - iface2: core_pb2.Interface = None, - options: core_pb2.LinkOptions = None, - source: str = None, - ) -> core_pb2.AddLinkResponse: + 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 node1_id: node one id - :param node2_id: node two id - :param iface1: node one interface data - :param iface2: node two interface data - :param options: options for link (jitter, bandwidth, etc) + :param link: link to add :param source: application source - :return: response with result of success or failure + :return: tuple of result and finalized interface values :raises grpc.RpcError: when session or one of the nodes don't exist """ - link = core_pb2.Link( - node1_id=node1_id, - node2_id=node2_id, - type=core_pb2.LinkType.WIRED, - iface1=iface1, - iface2=iface2, - options=options, - ) request = core_pb2.AddLinkRequest( - session_id=session_id, link=link, source=source + session_id=session_id, link=link.to_proto(), source=source ) - return self.stub.AddLink(request) + 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, - node1_id: int, - node2_id: int, - options: core_pb2.LinkOptions, - iface1_id: int = None, - iface2_id: int = None, - source: str = None, - ) -> core_pb2.EditLinkResponse: + self, session_id: int, link: wrappers.Link, source: str = None + ) -> bool: """ Edit a link between nodes. :param session_id: session id - :param node1_id: node one id - :param node2_id: node two id - :param options: options for link (jitter, bandwidth, etc) - :param iface1_id: node one interface id - :param iface2_id: node two interface 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=node1_id, - node2_id=node2_id, - options=options, + 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, ) - return self.stub.EditLink(request) + response = self.stub.EditLink(request) + return response.result def delete_link( - self, - session_id: int, - node1_id: int, - node2_id: int, - iface1_id: int = None, - iface2_id: int = None, - source: str = None, - ) -> core_pb2.DeleteLinkResponse: + self, session_id: int, link: wrappers.Link, source: str = None + ) -> bool: """ Delete a link between nodes. :param session_id: session id - :param node1_id: node one id - :param node2_id: node two id - :param iface1_id: node one interface id - :param iface2_id: node two interface 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=node1_id, - node2_id=node2_id, + node1_id=link.node1_id, + node2_id=link.node2_id, iface1_id=iface1_id, iface2_id=iface2_id, source=source, ) - return self.stub.DeleteLink(request) - - def get_hooks(self, session_id: int) -> core_pb2.GetHooksResponse: - """ - Get all hook scripts. - - :param session_id: session id - :return: response with a list of hooks - :raises grpc.RpcError: when session doesn't exist - """ - request = core_pb2.GetHooksRequest(session_id=session_id) - return self.stub.GetHooks(request) - - def add_hook( - self, - session_id: int, - state: core_pb2.SessionState, - file_name: str, - file_data: str, - ) -> core_pb2.AddHookResponse: - """ - 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: response with result of success or failure - :raises grpc.RpcError: when session doesn't exist - """ - hook = core_pb2.Hook(state=state, file=file_name, data=file_data) - request = core_pb2.AddHookRequest(session_id=session_id, hook=hook) - return self.stub.AddHook(request) - - def get_mobility_configs(self, session_id: int) -> GetMobilityConfigsResponse: - """ - Get all mobility configurations. - - :param session_id: session id - :return: response with a dict of node ids to mobility configurations - :raises grpc.RpcError: when session doesn't exist - """ - request = GetMobilityConfigsRequest(session_id=session_id) - return self.stub.GetMobilityConfigs(request) + response = self.stub.DeleteLink(request) + return response.result def get_mobility_config( self, session_id: int, node_id: int - ) -> GetMobilityConfigResponse: + ) -> Dict[str, wrappers.ConfigOption]: """ Get mobility configuration for a node. :param session_id: session id :param node_id: node id - :return: response with a list of configuration groups + :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) - return self.stub.GetMobilityConfig(request) + 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] - ) -> SetMobilityConfigResponse: + ) -> bool: """ Set mobility configuration for a node. :param session_id: session id :param node_id: node id :param config: mobility configuration - :return: response with result of success or failure + :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 ) - return self.stub.SetMobilityConfig(request) + response = self.stub.SetMobilityConfig(request) + return response.result def mobility_action( - self, session_id: int, node_id: int, action: ServiceAction - ) -> MobilityActionResponse: + 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: response with result of success or failure + :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 + session_id=session_id, node_id=node_id, action=action.value ) - return self.stub.MobilityAction(request) + response = self.stub.MobilityAction(request) + return response.result - def get_services(self) -> GetServicesResponse: + def get_config(self) -> wrappers.CoreConfig: """ - Get all currently loaded services. + Retrieve the current core configuration values. - :return: response with a list of services + :return: core configuration """ - request = GetServicesRequest() - return self.stub.GetServices(request) + request = GetConfigRequest() + response = self.stub.GetConfig(request) + return wrappers.CoreConfig.from_proto(response) - def get_service_defaults(self, session_id: int) -> GetServiceDefaultsResponse: + 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: response with a dict of node model to a list of services + :return: list of service defaults :raises grpc.RpcError: when session doesn't exist """ request = GetServiceDefaultsRequest(session_id=session_id) - return self.stub.GetServiceDefaults(request) + 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]] - ) -> SetServiceDefaultsResponse: + ) -> bool: """ Set default services for node models. :param session_id: session id :param service_defaults: node models to lists of services - :return: response with result of success or failure + :return: True for success, False otherwise :raises grpc.RpcError: when session doesn't exist """ defaults = [] @@ -891,41 +746,30 @@ class CoreGrpcClient: default = ServiceDefaults(node_type=node_type, services=services) defaults.append(default) request = SetServiceDefaultsRequest(session_id=session_id, defaults=defaults) - return self.stub.SetServiceDefaults(request) - - def get_node_service_configs( - self, session_id: int - ) -> GetNodeServiceConfigsResponse: - """ - Get service data for a node. - - :param session_id: session id - :return: response with all node service configs - :raises grpc.RpcError: when session doesn't exist - """ - request = GetNodeServiceConfigsRequest(session_id=session_id) - return self.stub.GetNodeServiceConfigs(request) + response = self.stub.SetServiceDefaults(request) + return response.result def get_node_service( self, session_id: int, node_id: int, service: str - ) -> GetNodeServiceResponse: + ) -> wrappers.NodeServiceData: """ Get service data for a node. :param session_id: session id :param node_id: node id :param service: service name - :return: response with node service data + :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 ) - return self.stub.GetNodeService(request) + 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 - ) -> GetNodeServiceFileResponse: + ) -> str: """ Get a service file for a node. @@ -933,74 +777,22 @@ class CoreGrpcClient: :param node_id: node id :param service: service name :param file_name: file name to get data for - :return: response with file data + :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 ) - return self.stub.GetNodeServiceFile(request) + response = self.stub.GetNodeServiceFile(request) + return response.data - def set_node_service( + def service_action( self, session_id: int, 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, - ) -> SetNodeServiceResponse: - """ - Set service data for a node. - - :param session_id: session id - :param node_id: node id - :param service: service name - :param files: service files - :param directories: service directories - :param startup: startup commands - :param validate: validation commands - :param shutdown: shutdown commands - :return: response with result of success or failure - :raises grpc.RpcError: when session or node doesn't exist - """ - config = ServiceConfig( - node_id=node_id, - service=service, - files=files, - directories=directories, - startup=startup, - validate=validate, - shutdown=shutdown, - ) - request = SetNodeServiceRequest(session_id=session_id, config=config) - return self.stub.SetNodeService(request) - - def set_node_service_file( - self, session_id: int, node_id: int, service: str, file_name: str, data: str - ) -> SetNodeServiceFileResponse: - """ - 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: response with result of success or failure - :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) - return self.stub.SetNodeServiceFile(request) - - def service_action( - self, session_id: int, node_id: int, service: str, action: ServiceAction - ) -> ServiceActionResponse: + action: wrappers.ServiceAction, + ) -> bool: """ Send an action to a service for a node. @@ -1009,92 +801,50 @@ class CoreGrpcClient: :param service: service name :param action: action for service (start, stop, restart, validate) - :return: response with result of success or failure + :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 + session_id=session_id, node_id=node_id, service=service, action=action.value ) - return self.stub.ServiceAction(request) + response = self.stub.ServiceAction(request) + return response.result - def get_wlan_configs(self, session_id: int) -> GetWlanConfigsResponse: - """ - Get all wlan configurations. - - :param session_id: session id - :return: response with a dict of node ids to wlan configurations - :raises grpc.RpcError: when session doesn't exist - """ - request = GetWlanConfigsRequest(session_id=session_id) - return self.stub.GetWlanConfigs(request) - - def get_wlan_config(self, session_id: int, node_id: int) -> GetWlanConfigResponse: + 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: response with a list of configuration groups + :return: dict of names to options :raises grpc.RpcError: when session doesn't exist """ request = GetWlanConfigRequest(session_id=session_id, node_id=node_id) - return self.stub.GetWlanConfig(request) + 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] - ) -> SetWlanConfigResponse: + ) -> bool: """ Set wlan configuration for a node. :param session_id: session id :param node_id: node id :param config: wlan configuration - :return: response with result of success or failure + :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) - return self.stub.SetWlanConfig(request) - - def get_emane_config(self, session_id: int) -> GetEmaneConfigResponse: - """ - 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) - return self.stub.GetEmaneConfig(request) - - def set_emane_config( - self, session_id: int, config: Dict[str, str] - ) -> SetEmaneConfigResponse: - """ - Set session emane configuration. - - :param session_id: session id - :param config: emane configuration - :return: response with result of success or failure - :raises grpc.RpcError: when session doesn't exist - """ - request = SetEmaneConfigRequest(session_id=session_id, config=config) - return self.stub.SetEmaneConfig(request) - - def get_emane_models(self, session_id: int) -> GetEmaneModelsResponse: - """ - Get session emane models. - - :param session_id: session id - :return: response with a list of emane models - :raises grpc.RpcError: when session doesn't exist - """ - request = GetEmaneModelsRequest(session_id=session_id) - return self.stub.GetEmaneModels(request) + response = self.stub.SetWlanConfig(request) + return response.result def get_emane_model_config( self, session_id: int, node_id: int, model: str, iface_id: int = -1 - ) -> GetEmaneModelConfigResponse: + ) -> Dict[str, wrappers.ConfigOption]: """ Get emane model configuration for a node or a node's interface. @@ -1102,53 +852,33 @@ class CoreGrpcClient: :param node_id: node id :param model: emane model name :param iface_id: node interface id - :return: response with a list of configuration groups + :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 ) - return self.stub.GetEmaneModelConfig(request) + response = self.stub.GetEmaneModelConfig(request) + return wrappers.ConfigOption.from_dict(response.config) def set_emane_model_config( - self, - session_id: int, - node_id: int, - model: str, - config: Dict[str, str] = None, - iface_id: int = -1, - ) -> SetEmaneModelConfigResponse: + 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 node_id: node id - :param model: emane model name - :param config: emane model configuration - :param iface_id: node interface id - :return: response with result of success or failure + :param emane_model_config: emane model config to set + :return: True for success, False otherwise :raises grpc.RpcError: when session doesn't exist """ - model_config = EmaneModelConfig( - node_id=node_id, model=model, config=config, iface_id=iface_id - ) request = SetEmaneModelConfigRequest( - session_id=session_id, emane_model_config=model_config + session_id=session_id, emane_model_config=emane_model_config.to_proto() ) - return self.stub.SetEmaneModelConfig(request) + response = self.stub.SetEmaneModelConfig(request) + return response.result - def get_emane_model_configs(self, session_id: int) -> GetEmaneModelConfigsResponse: - """ - Get all EMANE model configurations for a session. - - :param session_id: session to get emane model configs - :return: response with a dictionary of node/interface ids to configurations - :raises grpc.RpcError: when session doesn't exist - """ - request = GetEmaneModelConfigsRequest(session_id=session_id) - return self.stub.GetEmaneModelConfigs(request) - - def save_xml(self, session_id: int, file_path: str) -> core_pb2.SaveXmlResponse: + def save_xml(self, session_id: int, file_path: str) -> None: """ Save the current scenario to an XML file. @@ -1162,22 +892,21 @@ class CoreGrpcClient: with open(file_path, "w") as xml_file: xml_file.write(response.data) - def open_xml(self, file_path: str, start: bool = False) -> core_pb2.OpenXmlResponse: + def open_xml(self, file_path: Path, 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: True to start session, False otherwise - :return: response with opened session id + :param start: tuple of result and session id when successful + :return: tuple of result and 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) - return self.stub.OpenXml(request) + with file_path.open("r") as f: + data = f.read() + request = core_pb2.OpenXmlRequest(data=data, start=start, file=str(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 - ) -> EmaneLinkResponse: + def emane_link(self, session_id: int, nem1: int, nem2: int, linked: bool) -> bool: """ Helps broadcast wireless link/unlink between EMANE nodes. @@ -1185,131 +914,85 @@ class CoreGrpcClient: :param nem1: first nem for emane link :param nem2: second nem for emane link :param linked: True to link, False to unlink - :return: get emane link response + :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 ) - return self.stub.EmaneLink(request) + response = self.stub.EmaneLink(request) + return response.result - def get_ifaces(self) -> core_pb2.GetInterfacesResponse: + 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: get interfaces response + :return: list of interfaces """ request = core_pb2.GetInterfacesRequest() - return self.stub.GetInterfaces(request) + response = self.stub.GetInterfaces(request) + return list(response.ifaces) - def get_config_services(self) -> GetConfigServicesResponse: - """ - Retrieve all known config services. - - :return: get config services response - """ - request = GetConfigServicesRequest() - return self.stub.GetConfigServices(request) - - def get_config_service_defaults( - self, name: str - ) -> GetConfigServiceDefaultsResponse: + 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: get config service defaults + :return: config service defaults """ request = GetConfigServiceDefaultsRequest(name=name) - return self.stub.GetConfigServiceDefaults(request) - - def get_node_config_service_configs( - self, session_id: int - ) -> GetNodeConfigServiceConfigsResponse: - """ - Retrieves all node config service configurations for a session. - - :param session_id: session to get config service configurations for - :return: get node config service configs response - :raises grpc.RpcError: when session doesn't exist - """ - request = GetNodeConfigServiceConfigsRequest(session_id=session_id) - return self.stub.GetNodeConfigServiceConfigs(request) + response = self.stub.GetConfigServiceDefaults(request) + return wrappers.ConfigServiceDefaults.from_proto(response) def get_node_config_service( self, session_id: int, node_id: int, name: str - ) -> GetNodeConfigServiceResponse: + ) -> 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: get node config service response + :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 ) - return self.stub.GetNodeConfigService(request) + response = self.stub.GetNodeConfigService(request) + return dict(response.config) - def get_node_config_services( - self, session_id: int, node_id: int - ) -> GetNodeConfigServicesResponse: - """ - 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: get node config services response - :raises grpc.RpcError: when session or node doesn't exist - """ - request = GetNodeConfigServicesRequest(session_id=session_id, node_id=node_id) - return self.stub.GetNodeConfigServices(request) - - def set_node_config_service( - self, session_id: int, node_id: int, name: str, config: Dict[str, str] - ) -> SetNodeConfigServiceResponse: - """ - 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: set node config service response - :raises grpc.RpcError: when session or node doesn't exist - """ - request = SetNodeConfigServiceRequest( - session_id=session_id, node_id=node_id, name=name, config=config - ) - return self.stub.SetNodeConfigService(request) - - def get_emane_event_channel(self, session_id: int) -> GetEmaneEventChannelResponse: + 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 - :return: emane event channel response + :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) - return self.stub.GetEmaneEventChannel(request) + request = GetEmaneEventChannelRequest(session_id=session_id, nem_id=nem_id) + response = self.stub.GetEmaneEventChannel(request) + return wrappers.EmaneEventChannel.from_proto(response) - def execute_script(self, script: str) -> ExecuteScriptResponse: + def execute_script(self, script: str, args: str) -> Optional[int]: """ Executes a python script given context of the current CoreEmu object. :param script: script to execute - :return: execute script response + :param args: arguments to provide to script + :return: create session id for script executed """ - request = ExecuteScriptRequest(script=script) - return self.stub.ExecuteScript(request) + request = ExecuteScriptRequest(script=script, args=args) + 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 - ) -> WlanLinkResponse: + ) -> bool: """ Links/unlinks nodes on the same WLAN. @@ -1318,7 +1001,7 @@ class CoreGrpcClient: :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: wlan link response + :return: True for success, False otherwise :raises grpc.RpcError: when session or one of the nodes do not exist """ request = WlanLinkRequest( @@ -1328,20 +1011,19 @@ class CoreGrpcClient: node2_id=node2_id, linked=linked, ) - return self.stub.WlanLink(request) + response = self.stub.WlanLink(request) + return response.result - def emane_pathlosses( - self, pathloss_iterator: Iterable[EmanePathlossesRequest] - ) -> EmanePathlossesResponse: + def emane_pathlosses(self, streamer: EmanePathlossesStreamer) -> None: """ Stream EMANE pathloss events. - :param pathloss_iterator: iterator for sending emane pathloss events - :return: emane pathloss response + :param streamer: emane pathlosses streamer + :return: nothing :raises grpc.RpcError: when a pathloss event session or one of the nodes do not exist """ - return self.stub.EmanePathlosses(pathloss_iterator) + self.stub.EmanePathlosses(streamer.iter()) def connect(self) -> None: """ diff --git a/daemon/core/api/grpc/clientw.py b/daemon/core/api/grpc/clientw.py deleted file mode 100644 index 36ec69ad..00000000 --- a/daemon/core/api/grpc/clientw.py +++ /dev/null @@ -1,1500 +0,0 @@ -""" -gRpc client for interfacing with CORE. -""" - -import logging -import threading -from contextlib import contextmanager -from queue import Queue -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, - 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 MoveNodesStreamer: - def __init__(self, session_id: int = None, source: str = None) -> None: - self.session_id = session_id - self.source = source - self.queue: Queue = Queue() - - def send_position(self, node_id: int, x: float, y: float) -> None: - position = wrappers.Position(x=x, y=y) - request = wrappers.MoveNodesRequest( - session_id=self.session_id, - node_id=node_id, - source=self.source, - position=position, - ) - self.send(request) - - def send_geo(self, node_id: int, lon: float, lat: float, alt: float) -> None: - geo = wrappers.Geo(lon=lon, lat=lat, alt=alt) - request = wrappers.MoveNodesRequest( - session_id=self.session_id, node_id=node_id, source=self.source, geo=geo - ) - self.send(request) - - def send(self, request: wrappers.MoveNodesRequest) -> None: - self.queue.put(request) - - def stop(self) -> None: - self.queue.put(None) - - def next(self) -> Optional[core_pb2.MoveNodesRequest]: - request: Optional[wrappers.MoveNodesRequest] = self.queue.get() - if request: - return request.to_proto() - else: - return request - - def iter(self) -> Iterable: - return iter(self.next, None) - - -class EmanePathlossesStreamer: - def __init__(self) -> None: - self.queue: Queue = Queue() - - def send(self, request: Optional[wrappers.EmanePathlossesRequest]) -> None: - self.queue.put(request) - - def next(self) -> Optional[emane_pb2.EmanePathlossesRequest]: - request: Optional[wrappers.EmanePathlossesRequest] = self.queue.get() - if request: - return request.to_proto() - else: - return request - - def iter(self): - return iter(self.next, None) - - -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 throughput_listener( - stream: Any, handler: Callable[[wrappers.ThroughputsEvent], None] -) -> None: - """ - Listen for throughput 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_proto in stream: - event = wrappers.ThroughputsEvent.from_proto(event_proto) - handler(event) - except grpc.RpcError as e: - if e.code() == grpc.StatusCode.CANCELLED: - logging.debug("throughput stream closed") - else: - logging.exception("throughput stream error") - - -def cpu_listener( - stream: Any, handler: Callable[[wrappers.CpuUsageEvent], None] -) -> None: - """ - Listen for cpu 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_proto in stream: - event = wrappers.CpuUsageEvent.from_proto(event_proto) - handler(event) - except grpc.RpcError as e: - if e.code() == grpc.StatusCode.CANCELLED: - logging.debug("cpu stream closed") - else: - logging.exception("cpu stream error") - - -def event_listener(stream: Any, handler: Callable[[wrappers.Event], None]) -> None: - """ - Listen for session 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_proto in stream: - event = wrappers.Event.from_proto(event_proto) - handler(event) - except grpc.RpcError as e: - if e.code() == grpc.StatusCode.CANCELLED: - logging.debug("session stream closed") - else: - logging.exception("session stream error") - - -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 - - def events( - self, - session_id: int, - handler: Callable[[wrappers.Event], None], - events: List[wrappers.EventType] = 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) - thread = threading.Thread( - target=event_listener, args=(stream, handler), daemon=True - ) - thread.start() - return stream - - def throughputs( - self, session_id: int, handler: Callable[[wrappers.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) - thread = threading.Thread( - target=throughput_listener, args=(stream, handler), daemon=True - ) - thread.start() - return stream - - def cpu_usage( - self, delay: int, handler: Callable[[wrappers.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) - thread = threading.Thread( - target=cpu_listener, args=(stream, handler), daemon=True - ) - thread.start() - 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 - - def move_nodes(self, streamer: MoveNodesStreamer) -> None: - """ - Stream node movements using the provided iterator. - - :param streamer: move nodes streamer - :return: nothing - :raises grpc.RpcError: when session or nodes do not exist - """ - self.stub.MoveNodes(streamer.iter()) - - 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 - - def emane_pathlosses(self, streamer: EmanePathlossesStreamer) -> None: - """ - Stream EMANE pathloss events. - - :param streamer: emane pathlosses streamer - :return: nothing - :raises grpc.RpcError: when a pathloss event session or one of the nodes do not - exist - """ - self.stub.EmanePathlosses(streamer.iter()) - - 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/events.py b/daemon/core/api/grpc/events.py index aff3c5e5..b319a978 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -14,6 +14,8 @@ from core.emulator.data import ( ) from core.emulator.session import Session +logger = logging.getLogger(__name__) + def handle_node_event(node_data: NodeData) -> core_pb2.Event: """ @@ -199,7 +201,7 @@ class EventStreamer: elif isinstance(data, FileData): event = handle_file_event(data) else: - logging.error("unknown event: %s", data) + logger.error("unknown event: %s", data) except Empty: pass if event: diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 93927ec6..c585a135 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -7,10 +7,9 @@ import grpc from grpc import ServicerContext from core import utils -from core.api.grpc import common_pb2, core_pb2 -from core.api.grpc.common_pb2 import MappedConfig +from core.api.grpc import common_pb2, core_pb2, wrappers from core.api.grpc.configservices_pb2 import ConfigServiceConfig -from core.api.grpc.emane_pb2 import GetEmaneModelConfig +from core.api.grpc.emane_pb2 import NodeEmaneConfig from core.api.grpc.services_pb2 import ( NodeServiceConfig, NodeServiceData, @@ -28,9 +27,10 @@ from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.docker import DockerNode from core.nodes.interface import CoreInterface from core.nodes.lxd import LxcNode -from core.nodes.network import WlanNode +from core.nodes.network import CtrlNet, PtpNet, WlanNode from core.services.coreservices import CoreService +logger = logging.getLogger(__name__) WORKERS = 10 @@ -156,7 +156,7 @@ def create_nodes( start = time.monotonic() results, exceptions = utils.threadpool(funcs) total = time.monotonic() - start - logging.debug("grpc created nodes time: %s", total) + logger.debug("grpc created nodes time: %s", total) return results, exceptions @@ -180,7 +180,7 @@ def create_links( start = time.monotonic() results, exceptions = utils.threadpool(funcs) total = time.monotonic() - start - logging.debug("grpc created links time: %s", total) + logger.debug("grpc created links time: %s", total) return results, exceptions @@ -204,7 +204,7 @@ def edit_links( start = time.monotonic() results, exceptions = utils.threadpool(funcs) total = time.monotonic() - start - logging.debug("grpc edit links time: %s", total) + logger.debug("grpc edit links time: %s", total) return results, exceptions @@ -251,12 +251,15 @@ def get_config_options( return results -def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node: +def get_node_proto( + session: Session, node: NodeBase, emane_configs: List[NodeEmaneConfig] +) -> core_pb2.Node: """ Convert CORE node to protobuf representation. :param session: session containing node :param node: node to convert + :param emane_configs: emane configs related to node :return: node proto """ node_type = session.get_node_type(node.__class__) @@ -271,17 +274,53 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node: node_dir = None config_services = [] if isinstance(node, CoreNodeBase): - node_dir = node.nodedir + node_dir = str(node.directory) config_services = [x for x in node.config_services] channel = None if isinstance(node, CoreNode): - channel = node.ctrlchnlname + channel = str(node.ctrlchnlname) emane_model = None if isinstance(node, EmaneNet): emane_model = node.model.name image = None if isinstance(node, (DockerNode, LxcNode)): image = node.image + # check for wlan config + wlan_config = session.mobility.get_configs( + node.id, config_type=BasicRangeModel.name + ) + if wlan_config: + wlan_config = get_config_options(wlan_config, BasicRangeModel) + # check for mobility config + mobility_config = session.mobility.get_configs( + node.id, config_type=Ns2ScriptedMobility.name + ) + if mobility_config: + mobility_config = get_config_options(mobility_config, Ns2ScriptedMobility) + # check for service configs + custom_services = session.services.custom_services.get(node.id) + service_configs = {} + if custom_services: + for service in custom_services.values(): + service_proto = get_service_configuration(service) + service_configs[service.name] = NodeServiceConfig( + node_id=node.id, + service=service.name, + data=service_proto, + files=service.config_data, + ) + # check for config service configs + config_service_configs = {} + if isinstance(node, CoreNode): + for service in node.config_services.values(): + if not service.custom_templates and not service.custom_config: + continue + config_service_configs[service.name] = ConfigServiceConfig( + node_id=node.id, + name=service.name, + templates=service.custom_templates, + config=service.custom_config, + ) return core_pb2.Node( id=node.id, name=node.name, @@ -297,6 +336,11 @@ def get_node_proto(session: Session, node: NodeBase) -> core_pb2.Node: dir=node_dir, channel=channel, canvas=node.canvas, + wlan_config=wlan_config, + mobility_config=mobility_config, + service_configs=service_configs, + config_service_configs=config_service_configs, + emane_configs=emane_configs, ) @@ -529,54 +573,20 @@ def get_nem_id( return nem_id -def get_emane_model_configs(session: Session) -> List[GetEmaneModelConfig]: - configs = [] - for _id in session.emane.node_configurations: - if _id == -1: - continue - model_configs = session.emane.node_configurations[_id] +def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneConfig]]: + configs = {} + for _id, model_configs in session.emane.node_configs.items(): for model_name in model_configs: - model = session.emane.models[model_name] - current_config = session.emane.get_model_config(_id, model_name) - config = get_config_options(current_config, model) + model_class = session.emane.get_model(model_name) + current_config = session.emane.get_config(_id, model_name) + config = get_config_options(current_config, model_class) node_id, iface_id = utils.parse_iface_config_id(_id) iface_id = iface_id if iface_id is not None else -1 - model_config = GetEmaneModelConfig( - node_id=node_id, model=model_name, iface_id=iface_id, config=config + node_config = NodeEmaneConfig( + model=model_name, iface_id=iface_id, config=config ) - configs.append(model_config) - return configs - - -def get_wlan_configs(session: Session) -> Dict[int, MappedConfig]: - configs = {} - for node_id in session.mobility.node_configurations: - model_config = session.mobility.node_configurations[node_id] - if node_id == -1: - continue - for model_name in model_config: - if model_name != BasicRangeModel.name: - continue - current_config = session.mobility.get_model_config(node_id, model_name) - config = get_config_options(current_config, BasicRangeModel) - mapped_config = MappedConfig(config=config) - configs[node_id] = mapped_config - return configs - - -def get_mobility_configs(session: Session) -> Dict[int, MappedConfig]: - configs = {} - for node_id in session.mobility.node_configurations: - model_config = session.mobility.node_configurations[node_id] - if node_id == -1: - continue - for model_name in model_config: - if model_name != Ns2ScriptedMobility.name: - continue - current_config = session.mobility.get_model_config(node_id, model_name) - config = get_config_options(current_config, Ns2ScriptedMobility) - mapped_config = MappedConfig(config=config) - configs[node_id] = mapped_config + node_configs = configs.setdefault(node_id, []) + node_configs.append(node_config) return configs @@ -590,15 +600,6 @@ def get_hooks(session: Session) -> List[core_pb2.Hook]: return hooks -def get_emane_models(session: Session) -> List[str]: - emane_models = [] - for model in session.emane.models.keys(): - if len(model.split("_")) != 2: - continue - emane_models.append(model) - return emane_models - - def get_default_services(session: Session) -> List[ServiceDefaults]: default_services = [] for name, services in session.services.default_services.items(): @@ -607,45 +608,6 @@ def get_default_services(session: Session) -> List[ServiceDefaults]: return default_services -def get_node_service_configs(session: Session) -> List[NodeServiceConfig]: - configs = [] - for node_id, service_configs in session.services.custom_services.items(): - for name in service_configs: - service = session.services.get_service(node_id, name) - service_proto = get_service_configuration(service) - config = NodeServiceConfig( - node_id=node_id, - service=name, - data=service_proto, - files=service.config_data, - ) - configs.append(config) - return configs - - -def get_node_config_service_configs(session: Session) -> List[ConfigServiceConfig]: - configs = [] - for node in session.nodes.values(): - if not isinstance(node, CoreNodeBase): - continue - for name, service in node.config_services.items(): - if not service.custom_templates and not service.custom_config: - continue - config_proto = ConfigServiceConfig( - node_id=node.id, - name=name, - templates=service.custom_templates, - config=service.custom_config, - ) - configs.append(config_proto) - return configs - - -def get_emane_config(session: Session) -> Dict[str, common_pb2.ConfigOption]: - current_config = session.emane.get_configs() - return get_config_options(current_config, session.emane.emane_config) - - def get_mobility_node( session: Session, node_id: int, context: ServicerContext ) -> Union[WlanNode, EmaneNet]: @@ -656,3 +618,88 @@ def get_mobility_node( return session.get_node(node_id, EmaneNet) except CoreError: context.abort(grpc.StatusCode.NOT_FOUND, "node id is not for wlan or emane") + + +def convert_session(session: Session) -> wrappers.Session: + links = [] + nodes = [] + emane_configs = get_emane_model_configs_dict(session) + for _id in session.nodes: + node = session.nodes[_id] + if not isinstance(node, (PtpNet, CtrlNet)): + node_emane_configs = emane_configs.get(node.id, []) + node_proto = get_node_proto(session, node, node_emane_configs) + nodes.append(node_proto) + node_links = get_links(node) + links.extend(node_links) + default_services = get_default_services(session) + x, y, z = session.location.refxyz + lat, lon, alt = session.location.refgeo + location = core_pb2.SessionLocation( + x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=session.location.refscale + ) + hooks = get_hooks(session) + session_file = str(session.file_path) if session.file_path else None + options = get_config_options(session.options.get_configs(), session.options) + servers = [ + core_pb2.Server(name=x.name, host=x.host) + for x in session.distributed.servers.values() + ] + return core_pb2.Session( + id=session.id, + state=session.state.value, + nodes=nodes, + links=links, + dir=str(session.directory), + user=session.user, + default_services=default_services, + location=location, + hooks=hooks, + metadata=session.metadata, + file=session_file, + options=options, + servers=servers, + ) + + +def configure_node( + session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext +) -> None: + for emane_config in node.emane_configs: + _id = utils.iface_config_id(node.id, emane_config.iface_id) + config = {k: v.value for k, v in emane_config.config.items()} + session.emane.set_config(_id, emane_config.model, config) + if node.wlan_config: + config = {k: v.value for k, v in node.wlan_config.items()} + session.mobility.set_model_config(node.id, BasicRangeModel.name, config) + if node.mobility_config: + config = {k: v.value for k, v in node.mobility_config.items()} + session.mobility.set_model_config(node.id, Ns2ScriptedMobility.name, config) + for service_name, service_config in node.service_configs.items(): + data = service_config.data + config = ServiceConfig( + node_id=node.id, + service=service_name, + startup=data.startup, + validate=data.validate, + shutdown=data.shutdown, + files=data.configs, + directories=data.dirs, + ) + service_configuration(session, config) + for file_name, file_data in service_config.files.items(): + session.services.set_service_file( + node.id, service_name, file_name, file_data + ) + if node.config_service_configs: + if not isinstance(core_node, CoreNode): + context.abort( + grpc.StatusCode.INVALID_ARGUMENT, + "invalid node type with config service configs", + ) + for service_name, service_config in node.config_service_configs.items(): + service = core_node.config_services[service_name] + if service_config.config: + service.set_config(service_config.config) + for name, template in service_config.templates.items(): + service.set_template(name, template) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 73fa2fa6..a792c6ea 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -3,9 +3,9 @@ import logging import os import re import tempfile -import threading import time from concurrent import futures +from pathlib import Path from typing import Iterable, Optional, Pattern, Type import grpc @@ -23,16 +23,8 @@ from core.api.grpc.configservices_pb2 import ( ConfigService, GetConfigServiceDefaultsRequest, GetConfigServiceDefaultsResponse, - GetConfigServicesRequest, - GetConfigServicesResponse, - GetNodeConfigServiceConfigsRequest, - GetNodeConfigServiceConfigsResponse, GetNodeConfigServiceRequest, GetNodeConfigServiceResponse, - GetNodeConfigServicesRequest, - GetNodeConfigServicesResponse, - SetNodeConfigServiceRequest, - SetNodeConfigServiceResponse, ) from core.api.grpc.core_pb2 import ExecuteScriptResponse from core.api.grpc.emane_pb2 import ( @@ -40,18 +32,10 @@ from core.api.grpc.emane_pb2 import ( EmaneLinkResponse, EmanePathlossesRequest, EmanePathlossesResponse, - GetEmaneConfigRequest, - GetEmaneConfigResponse, GetEmaneEventChannelRequest, GetEmaneEventChannelResponse, GetEmaneModelConfigRequest, GetEmaneModelConfigResponse, - GetEmaneModelConfigsRequest, - GetEmaneModelConfigsResponse, - GetEmaneModelsRequest, - GetEmaneModelsResponse, - SetEmaneConfigRequest, - SetEmaneConfigResponse, SetEmaneModelConfigRequest, SetEmaneModelConfigResponse, ) @@ -60,8 +44,6 @@ from core.api.grpc.grpcutils import get_config_options, get_links, get_net_stats from core.api.grpc.mobility_pb2 import ( GetMobilityConfigRequest, GetMobilityConfigResponse, - GetMobilityConfigsRequest, - GetMobilityConfigsResponse, MobilityAction, MobilityActionRequest, MobilityActionResponse, @@ -69,39 +51,30 @@ from core.api.grpc.mobility_pb2 import ( SetMobilityConfigResponse, ) from core.api.grpc.services_pb2 import ( - GetNodeServiceConfigsRequest, - GetNodeServiceConfigsResponse, GetNodeServiceFileRequest, GetNodeServiceFileResponse, GetNodeServiceRequest, GetNodeServiceResponse, GetServiceDefaultsRequest, GetServiceDefaultsResponse, - GetServicesRequest, - GetServicesResponse, Service, ServiceAction, ServiceActionRequest, ServiceActionResponse, - SetNodeServiceFileRequest, - SetNodeServiceFileResponse, - SetNodeServiceRequest, - SetNodeServiceResponse, SetServiceDefaultsRequest, SetServiceDefaultsResponse, ) from core.api.grpc.wlan_pb2 import ( GetWlanConfigRequest, GetWlanConfigResponse, - GetWlanConfigsRequest, - GetWlanConfigsResponse, SetWlanConfigRequest, SetWlanConfigResponse, WlanLinkRequest, WlanLinkResponse, ) +from core.emane.modelmanager import EmaneModelManager from core.emulator.coreemu import CoreEmu -from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions +from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.enumerations import ( EventTypes, ExceptionLevels, @@ -112,9 +85,10 @@ from core.emulator.session import NT, Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNode, NodeBase -from core.nodes.network import CtrlNet, PtpNet, WlanNode +from core.nodes.network import WlanNode from core.services.coreservices import ServiceManager +logger = logging.getLogger(__name__) _ONE_DAY_IN_SECONDS: int = 60 * 60 * 24 _INTERFACE_REGEX: Pattern = re.compile(r"veth(?P[0-9a-fA-F]+)") _MAX_WORKERS = 1000 @@ -135,7 +109,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): atexit.register(self._exit_handler) def _exit_handler(self) -> None: - logging.debug("catching exit, stop running") + logger.debug("catching exit, stop running") self.running = False def _is_running(self, context) -> bool: @@ -145,7 +119,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): context.abort(grpc.StatusCode.CANCELLED, "server stopping") def listen(self, address: str) -> None: - logging.info("CORE gRPC API listening on: %s", address) + logger.info("CORE gRPC API listening on: %s", address) self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=_MAX_WORKERS)) core_pb2_grpc.add_CoreApiServicer_to_server(self, self.server) self.server.add_insecure_port(address) @@ -190,6 +164,26 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): except CoreError as e: context.abort(grpc.StatusCode.NOT_FOUND, str(e)) + def move_node( + self, + context: ServicerContext, + session_id: int, + node_id: int, + geo: core_pb2.Geo = None, + position: core_pb2.Position = None, + source: str = None, + ): + if not geo and not position: + raise CoreError("move node must provide a geo or position to move") + session = self.get_session(session_id, context) + node = self.get_node(session, node_id, context, NodeBase) + if geo: + session.set_node_geo(node, geo.lon, geo.lat, geo.alt) + else: + session.set_node_pos(node, position.x, position.y) + source = source if source else None + session.broadcast_node(node, source=source) + def validate_service( self, name: str, context: ServicerContext ) -> Type[ConfigService]: @@ -206,6 +200,38 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): context.abort(grpc.StatusCode.NOT_FOUND, f"unknown service {name}") return service + def GetConfig( + self, request: core_pb2.GetConfigRequest, context: ServicerContext + ) -> core_pb2.GetConfigResponse: + services = [] + for name in ServiceManager.services: + service = ServiceManager.services[name] + service_proto = Service(group=service.group, name=service.name) + services.append(service_proto) + config_services = [] + for service in self.coreemu.service_manager.services.values(): + service_proto = ConfigService( + name=service.name, + group=service.group, + executables=service.executables, + dependencies=service.dependencies, + directories=service.directories, + files=service.files, + startup=service.startup, + validate=service.validate, + shutdown=service.shutdown, + validation_mode=service.validation_mode.value, + validation_timer=service.validation_timer, + validation_period=service.validation_period, + ) + config_services.append(service_proto) + emane_models = [x.name for x in EmaneModelManager.models.values()] + return core_pb2.GetConfigResponse( + services=services, + config_services=config_services, + emane_models=emane_models, + ) + def StartSession( self, request: core_pb2.StartSessionRequest, context: ServicerContext ) -> core_pb2.StartSessionResponse: @@ -216,91 +242,79 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: grpc context :return: start session response """ - logging.debug("start session: %s", request) - session = self.get_session(request.session_id, context) + logger.debug("start session: %s", request) + session = self.get_session(request.session.id, context) # clear previous state and setup for creation session.clear() - if not os.path.exists(session.session_dir): - os.mkdir(session.session_dir) - session.set_state(EventTypes.CONFIGURATION_STATE) + if request.definition: + state = EventTypes.DEFINITION_STATE + else: + state = EventTypes.CONFIGURATION_STATE + session.directory.mkdir(exist_ok=True) + session.set_state(state) + session.user = request.session.user + + # session options + session.options.config_reset() + for option in request.session.options.values(): + session.options.set_config(option.name, option.value) + session.metadata = dict(request.session.metadata) + + # add servers + for server in request.session.servers: + session.distributed.add_server(server.name, server.host) # location - if request.HasField("location"): - grpcutils.session_location(session, request.location) + if request.session.HasField("location"): + grpcutils.session_location(session, request.session.location) # add all hooks - for hook in request.hooks: + for hook in request.session.hooks: state = EventTypes(hook.state) session.add_hook(state, hook.file, hook.data) # create nodes - _, exceptions = grpcutils.create_nodes(session, request.nodes) + _, exceptions = grpcutils.create_nodes(session, request.session.nodes) if exceptions: exceptions = [str(x) for x in exceptions] return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) - # emane configs - config = session.emane.get_configs() - 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_model_config(_id, config.model, config.config) - - # wlan configs - for config in request.wlan_configs: - session.mobility.set_model_config( - config.node_id, BasicRangeModel.name, config.config - ) - - # mobility configs - for config in request.mobility_configs: - session.mobility.set_model_config( - config.node_id, Ns2ScriptedMobility.name, config.config - ) - - # service configs - for config in request.service_configs: - grpcutils.service_configuration(session, config) - - # config service configs - for config in request.config_service_configs: - node = self.get_node(session, config.node_id, context, CoreNode) - service = node.config_services[config.name] - if config.config: - service.set_config(config.config) - for name, template in config.templates.items(): - service.set_template(name, template) - - # service file configs - for config in request.service_file_configs: - session.services.set_service_file( - config.node_id, config.service, config.file, config.data - ) + # check for configurations + for node in request.session.nodes: + core_node = self.get_node(session, node.id, context, NodeBase) + grpcutils.configure_node(session, node, core_node, context) # create links - _, exceptions = grpcutils.create_links(session, request.links) + links = [] + asym_links = [] + for link in request.session.links: + if link.options.unidirectional: + asym_links.append(link) + else: + links.append(link) + _, exceptions = grpcutils.create_links(session, links) if exceptions: exceptions = [str(x) for x in exceptions] return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) - - # asymmetric links - _, exceptions = grpcutils.edit_links(session, request.asymmetric_links) + _, exceptions = grpcutils.edit_links(session, asym_links) if exceptions: exceptions = [str(x) for x in exceptions] return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) # set to instantiation and start - session.set_state(EventTypes.INSTANTIATION_STATE) - - # boot services - boot_exceptions = session.instantiate() - if boot_exceptions: - exceptions = [] - for boot_exception in boot_exceptions: - for service_exception in boot_exception.args: - exceptions.append(str(service_exception)) - return core_pb2.StartSessionResponse(result=False, exceptions=exceptions) + if not request.definition: + session.set_state(EventTypes.INSTANTIATION_STATE) + # boot services + boot_exceptions = session.instantiate() + if boot_exceptions: + exceptions = [] + for boot_exception in boot_exceptions: + for service_exception in boot_exception.args: + exceptions.append(str(service_exception)) + return core_pb2.StartSessionResponse( + result=False, exceptions=exceptions + ) return core_pb2.StartSessionResponse(result=True) def StopSession( @@ -313,7 +327,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: grpc context :return: stop session response """ - logging.debug("stop session: %s", request) + logger.debug("stop session: %s", request) session = self.get_session(request.session_id, context) session.data_collect() session.shutdown() @@ -329,14 +343,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: :return: a create-session response """ - logging.debug("create session: %s", request) + logger.debug("create session: %s", request) session = self.coreemu.create_session(request.session_id) session.set_state(EventTypes.DEFINITION_STATE) session.location.setrefgeo(47.57917, -122.13232, 2.0) session.location.refscale = 150.0 - return core_pb2.CreateSessionResponse( - session_id=session.id, state=session.state.value - ) + session_proto = grpcutils.convert_session(session) + return core_pb2.CreateSessionResponse(session=session_proto) def DeleteSession( self, request: core_pb2.DeleteSessionRequest, context: ServicerContext @@ -348,7 +361,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: a delete-session response """ - logging.debug("delete session: %s", request) + logger.debug("delete session: %s", request) result = self.coreemu.delete_session(request.session_id) return core_pb2.DeleteSessionResponse(result=result) @@ -362,169 +375,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: a delete-session response """ - logging.debug("get sessions: %s", request) + logger.debug("get sessions: %s", request) sessions = [] for session_id in self.coreemu.sessions: session = self.coreemu.sessions[session_id] + session_file = str(session.file_path) if session.file_path else None session_summary = core_pb2.SessionSummary( id=session_id, state=session.state.value, nodes=session.get_node_count(), - file=session.file_name, - dir=session.session_dir, + file=session_file, + dir=str(session.directory), ) sessions.append(session_summary) return core_pb2.GetSessionsResponse(sessions=sessions) - def GetSessionLocation( - self, request: core_pb2.GetSessionLocationRequest, context: ServicerContext - ) -> core_pb2.GetSessionLocationResponse: - """ - Retrieve a requested session location - - :param request: get-session-location request - :param context: context object - :return: a get-session-location response - """ - logging.debug("get session location: %s", request) - session = self.get_session(request.session_id, context) - x, y, z = session.location.refxyz - lat, lon, alt = session.location.refgeo - scale = session.location.refscale - location = core_pb2.SessionLocation( - x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=scale - ) - return core_pb2.GetSessionLocationResponse(location=location) - - def SetSessionLocation( - self, request: core_pb2.SetSessionLocationRequest, context: ServicerContext - ) -> core_pb2.SetSessionLocationResponse: - """ - Set session location - - :param request: set-session-location request - :param context: context object - :return: a set-session-location-response - """ - logging.debug("set session location: %s", request) - session = self.get_session(request.session_id, context) - grpcutils.session_location(session, request.location) - return core_pb2.SetSessionLocationResponse(result=True) - - def SetSessionState( - self, request: core_pb2.SetSessionStateRequest, context: ServicerContext - ) -> core_pb2.SetSessionStateResponse: - """ - Set session state - - :param request: set-session-state request - :param context:context object - :return: set-session-state response - """ - logging.debug("set session state: %s", request) - session = self.get_session(request.session_id, context) - - try: - state = EventTypes(request.state) - session.set_state(state) - - if state == EventTypes.INSTANTIATION_STATE: - if not os.path.exists(session.session_dir): - os.mkdir(session.session_dir) - session.instantiate() - elif state == EventTypes.SHUTDOWN_STATE: - session.shutdown() - elif state == EventTypes.DATACOLLECT_STATE: - session.data_collect() - elif state == EventTypes.DEFINITION_STATE: - session.clear() - - result = True - except KeyError: - result = False - - return core_pb2.SetSessionStateResponse(result=result) - - def SetSessionUser( - self, request: core_pb2.SetSessionUserRequest, context: ServicerContext - ) -> core_pb2.SetSessionUserResponse: - """ - Sets the user for a session. - - :param request: set session user request - :param context: context object - :return: set session user response - """ - logging.debug("set session user: %s", request) - session = self.get_session(request.session_id, context) - session.user = request.user - return core_pb2.SetSessionUserResponse(result=True) - - def GetSessionOptions( - self, request: core_pb2.GetSessionOptionsRequest, context: ServicerContext - ) -> core_pb2.GetSessionOptionsResponse: - """ - Retrieve session options. - - :param request: - get-session-options request - :param context: context object - :return: get-session-options response about all session's options - """ - logging.debug("get session options: %s", request) - session = self.get_session(request.session_id, context) - current_config = session.options.get_configs() - default_config = session.options.default_values() - default_config.update(current_config) - config = get_config_options(default_config, session.options) - return core_pb2.GetSessionOptionsResponse(config=config) - - def SetSessionOptions( - self, request: core_pb2.SetSessionOptionsRequest, context: ServicerContext - ) -> core_pb2.SetSessionOptionsResponse: - """ - Update a session's configuration - - :param request: set-session-options request - :param context: context object - :return: set-session-options response - """ - logging.debug("set session options: %s", request) - session = self.get_session(request.session_id, context) - config = session.options.get_configs() - config.update(request.config) - return core_pb2.SetSessionOptionsResponse(result=True) - - def GetSessionMetadata( - self, request: core_pb2.GetSessionMetadataRequest, context: ServicerContext - ) -> core_pb2.GetSessionMetadataResponse: - """ - Retrieve session metadata. - - :param request: get session metadata - request - :param context: context object - :return: get session metadata response - """ - logging.debug("get session metadata: %s", request) - session = self.get_session(request.session_id, context) - return core_pb2.GetSessionMetadataResponse(config=session.metadata) - - def SetSessionMetadata( - self, request: core_pb2.SetSessionMetadataRequest, context: ServicerContext - ) -> core_pb2.SetSessionMetadataResponse: - """ - Update a session's metadata. - - :param request: set metadata request - :param context: context object - :return: set metadata response - """ - logging.debug("set session metadata: %s", request) - session = self.get_session(request.session_id, context) - session.metadata = dict(request.config) - return core_pb2.SetSessionMetadataResponse(result=True) - def CheckSession( self, request: core_pb2.GetSessionRequest, context: ServicerContext ) -> core_pb2.CheckSessionResponse: @@ -548,68 +413,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: get-session response """ - logging.debug("get session: %s", request) + logger.debug("get session: %s", request) session = self.get_session(request.session_id, context) - links = [] - nodes = [] - for _id in session.nodes: - node = session.nodes[_id] - if not isinstance(node, (PtpNet, CtrlNet)): - node_proto = grpcutils.get_node_proto(session, node) - nodes.append(node_proto) - node_links = get_links(node) - links.extend(node_links) - default_services = grpcutils.get_default_services(session) - x, y, z = session.location.refxyz - lat, lon, alt = session.location.refgeo - location = core_pb2.SessionLocation( - x=x, y=y, z=z, lat=lat, lon=lon, alt=alt, scale=session.location.refscale - ) - hooks = grpcutils.get_hooks(session) - emane_models = grpcutils.get_emane_models(session) - emane_config = grpcutils.get_emane_config(session) - emane_model_configs = grpcutils.get_emane_model_configs(session) - wlan_configs = grpcutils.get_wlan_configs(session) - mobility_configs = grpcutils.get_mobility_configs(session) - service_configs = grpcutils.get_node_service_configs(session) - config_service_configs = grpcutils.get_node_config_service_configs(session) - session_proto = core_pb2.Session( - id=session.id, - state=session.state.value, - nodes=nodes, - links=links, - dir=session.session_dir, - user=session.user, - default_services=default_services, - location=location, - hooks=hooks, - emane_models=emane_models, - emane_config=emane_config, - emane_model_configs=emane_model_configs, - wlan_configs=wlan_configs, - service_configs=service_configs, - config_service_configs=config_service_configs, - mobility_configs=mobility_configs, - metadata=session.metadata, - file=session.file_name, - ) + session_proto = grpcutils.convert_session(session) return core_pb2.GetSessionResponse(session=session_proto) - def AddSessionServer( - self, request: core_pb2.AddSessionServerRequest, context: ServicerContext - ) -> core_pb2.AddSessionServerResponse: - """ - Add distributed server to a session. - - :param request: get-session - request - :param context: context object - :return: add session server response - """ - session = self.get_session(request.session_id, context) - session.distributed.add_server(request.name, request.host) - return core_pb2.AddSessionServerResponse(result=True) - def SessionAlert( self, request: core_pb2.SessionAlertRequest, context: ServicerContext ) -> core_pb2.SessionAlertResponse: @@ -721,11 +529,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: add-node response """ - logging.debug("add node: %s", request) + logger.debug("add node: %s", request) session = self.get_session(request.session_id, context) _type, _id, options = grpcutils.add_node_data(request.node) _class = session.get_node_class(_type) node = session.add_node(_class, _id, options) + grpcutils.configure_node(session, request.node, node, context) source = request.source if request.source else None session.broadcast_node(node, MessageFlags.ADD, source) return core_pb2.AddNodeResponse(node_id=node.id) @@ -740,7 +549,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: get-node response """ - logging.debug("get node: %s", request) + logger.debug("get node: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context, NodeBase) ifaces = [] @@ -748,8 +557,28 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): iface = node.ifaces[iface_id] iface_proto = grpcutils.iface_to_proto(request.node_id, iface) ifaces.append(iface_proto) - node_proto = grpcutils.get_node_proto(session, node) - return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces) + emane_configs = grpcutils.get_emane_model_configs_dict(session) + node_emane_configs = emane_configs.get(node.id, []) + node_proto = grpcutils.get_node_proto(session, node, node_emane_configs) + links = get_links(node) + return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces, links=links) + + def MoveNode( + self, request: core_pb2.MoveNodeRequest, context: ServicerContext + ) -> core_pb2.MoveNodeResponse: + """ + Move node, either by x,y position or geospatial. + + :param request: move node request + :param context: context object + :return: move nodes response + """ + geo = request.geo if request.HasField("geo") else None + position = request.position if request.HasField("position") else None + self.move_node( + context, request.session_id, request.node_id, geo, position, request.source + ) + return core_pb2.MoveNodeResponse(result=True) def MoveNodes( self, @@ -764,27 +593,16 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :return: move nodes response """ for request in request_iterator: - if not request.WhichOneof("move_type"): - raise CoreError("move nodes must provide a move type") - session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context, NodeBase) - options = NodeOptions() - has_geo = request.HasField("geo") - if has_geo: - logging.info("has geo") - lat = request.geo.lat - lon = request.geo.lon - alt = request.geo.alt - options.set_location(lat, lon, alt) - else: - x = request.position.x - y = request.position.y - logging.info("has pos: %s,%s", x, y) - options.set_position(x, y) - session.edit_node(node.id, options) - source = request.source if request.source else None - if not has_geo: - session.broadcast_node(node, source=source) + geo = request.geo if request.HasField("geo") else None + position = request.position if request.HasField("position") else None + self.move_node( + context, + request.session_id, + request.node_id, + geo, + position, + request.source, + ) return core_pb2.MoveNodesResponse() def EditNode( @@ -797,31 +615,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: edit-node response """ - logging.debug("edit node: %s", request) + logger.debug("edit node: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context, NodeBase) - options = NodeOptions(icon=request.icon) - if request.HasField("position"): - x = request.position.x - y = request.position.y - options.set_position(x, y) - has_geo = request.HasField("geo") - if has_geo: - lat = request.geo.lat - lon = request.geo.lon - alt = request.geo.alt - options.set_location(lat, lon, alt) - result = True - try: - session.edit_node(node.id, options) - source = None - if request.source: - source = request.source - if not has_geo: - session.broadcast_node(node, source=source) - except CoreError: - result = False - return core_pb2.EditNodeResponse(result=result) + node.icon = request.icon or None + source = request.source or None + session.broadcast_node(node, source=source) + return core_pb2.EditNodeResponse(result=True) def DeleteNode( self, request: core_pb2.DeleteNodeRequest, context: ServicerContext @@ -833,7 +633,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: core.api.grpc.core_pb2.DeleteNodeResponse """ - logging.debug("delete node: %s", request) + logger.debug("delete node: %s", request) session = self.get_session(request.session_id, context) result = False if request.node_id in session.nodes: @@ -853,7 +653,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: core.api.grpc.core_pb2.NodeCommandResponse """ - logging.debug("sending node command: %s", request) + logger.debug("sending node command: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context, CoreNode) try: @@ -874,28 +674,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: get-node-terminal response """ - logging.debug("getting node terminal: %s", request) + logger.debug("getting node terminal: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context, CoreNode) terminal = node.termcmdstring("/bin/bash") return core_pb2.GetNodeTerminalResponse(terminal=terminal) - def GetNodeLinks( - self, request: core_pb2.GetNodeLinksRequest, context: ServicerContext - ) -> core_pb2.GetNodeLinksResponse: - """ - Retrieve all links form a requested node - - :param request: get-node-links request - :param context: context object - :return: get-node-links response - """ - logging.debug("get node links: %s", request) - session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context, NodeBase) - links = get_links(node) - return core_pb2.GetNodeLinksResponse(links=links) - def AddLink( self, request: core_pb2.AddLinkRequest, context: ServicerContext ) -> core_pb2.AddLinkResponse: @@ -906,7 +690,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: add-link response """ - logging.debug("add link: %s", request) + logger.debug("add link: %s", request) session = self.get_session(request.session_id, context) node1_id = request.link.node1_id node2_id = request.link.node2_id @@ -955,7 +739,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: edit-link response """ - logging.debug("edit link: %s", request) + logger.debug("edit link: %s", request) session = self.get_session(request.session_id, context) node1_id = request.node1_id node2_id = request.node2_id @@ -1001,7 +785,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: delete-link response """ - logging.debug("delete link: %s", request) + logger.debug("delete link: %s", request) session = self.get_session(request.session_id, context) node1_id = request.node1_id node2_id = request.node2_id @@ -1022,54 +806,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): session.broadcast_link(link_data) return core_pb2.DeleteLinkResponse(result=True) - def GetHooks( - self, request: core_pb2.GetHooksRequest, context: ServicerContext - ) -> core_pb2.GetHooksResponse: - """ - Retrieve all hooks from a session - - :param request: get-hook request - :param context: context object - :return: get-hooks response about all the hooks in all session states - """ - logging.debug("get hooks: %s", request) - session = self.get_session(request.session_id, context) - hooks = grpcutils.get_hooks(session) - return core_pb2.GetHooksResponse(hooks=hooks) - - def AddHook( - self, request: core_pb2.AddHookRequest, context: ServicerContext - ) -> core_pb2.AddHookResponse: - """ - Add hook to a session - - :param request: add-hook request - :param context: context object - :return: add-hook response - """ - logging.debug("add hook: %s", request) - session = self.get_session(request.session_id, context) - hook = request.hook - state = EventTypes(hook.state) - session.add_hook(state, hook.file, hook.data) - return core_pb2.AddHookResponse(result=True) - - def GetMobilityConfigs( - self, request: GetMobilityConfigsRequest, context: ServicerContext - ) -> GetMobilityConfigsResponse: - """ - Retrieve all mobility configurations from a session - - :param request: - get-mobility-configurations request - :param context: context object - :return: get-mobility-configurations response that has a list of configurations - """ - logging.debug("get mobility configs: %s", request) - session = self.get_session(request.session_id, context) - configs = grpcutils.get_mobility_configs(session) - return GetMobilityConfigsResponse(configs=configs) - def GetMobilityConfig( self, request: GetMobilityConfigRequest, context: ServicerContext ) -> GetMobilityConfigResponse: @@ -1081,7 +817,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: get-mobility-configuration response """ - logging.debug("get mobility config: %s", request) + logger.debug("get mobility config: %s", request) session = self.get_session(request.session_id, context) current_config = session.mobility.get_model_config( request.node_id, Ns2ScriptedMobility.name @@ -1100,7 +836,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: set-mobility-configuration response """ - logging.debug("set mobility config: %s", request) + logger.debug("set mobility config: %s", request) session = self.get_session(request.session_id, context) mobility_config = request.mobility_config session.mobility.set_model_config( @@ -1119,7 +855,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: mobility-action response """ - logging.debug("mobility action: %s", request) + logger.debug("mobility action: %s", request) session = self.get_session(request.session_id, context) node = grpcutils.get_mobility_node(session, request.node_id, context) if not node.mobility: @@ -1137,24 +873,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): result = False return MobilityActionResponse(result=result) - def GetServices( - self, request: GetServicesRequest, context: ServicerContext - ) -> GetServicesResponse: - """ - Retrieve all the services that are running - - :param request: get-service request - :param context: context object - :return: get-services response - """ - logging.debug("get services: %s", request) - services = [] - for name in ServiceManager.services: - service = ServiceManager.services[name] - service_proto = Service(group=service.group, name=service.name) - services.append(service_proto) - return GetServicesResponse(services=services) - def GetServiceDefaults( self, request: GetServiceDefaultsRequest, context: ServicerContext ) -> GetServiceDefaultsResponse: @@ -1165,7 +883,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: get-service-defaults response about all the available default services """ - logging.debug("get service defaults: %s", request) + logger.debug("get service defaults: %s", request) session = self.get_session(request.session_id, context) defaults = grpcutils.get_default_services(session) return GetServiceDefaultsResponse(defaults=defaults) @@ -1180,7 +898,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: set-service-defaults response """ - logging.debug("set service defaults: %s", request) + logger.debug("set service defaults: %s", request) session = self.get_session(request.session_id, context) session.services.default_services.clear() for service_defaults in request.defaults: @@ -1189,22 +907,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ] = service_defaults.services return SetServiceDefaultsResponse(result=True) - def GetNodeServiceConfigs( - self, request: GetNodeServiceConfigsRequest, context: ServicerContext - ) -> GetNodeServiceConfigsResponse: - """ - Retrieve all node service configurations. - - :param request: - get-node-service request - :param context: context object - :return: all node service configs response - """ - logging.debug("get node service configs: %s", request) - session = self.get_session(request.session_id, context) - configs = grpcutils.get_node_service_configs(session) - return GetNodeServiceConfigsResponse(configs=configs) - def GetNodeService( self, request: GetNodeServiceRequest, context: ServicerContext ) -> GetNodeServiceResponse: @@ -1216,7 +918,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: get-node-service response about the requested service """ - logging.debug("get node service: %s", request) + logger.debug("get node service: %s", request) session = self.get_session(request.session_id, context) service = session.services.get_service( request.node_id, request.service, default_service=True @@ -1235,7 +937,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: get-node-service response about the requested service """ - logging.debug("get node service file: %s", request) + logger.debug("get node service file: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context, CoreNode) file_data = session.services.get_service_file( @@ -1243,42 +945,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ) return GetNodeServiceFileResponse(data=file_data.data) - def SetNodeService( - self, request: SetNodeServiceRequest, context: ServicerContext - ) -> SetNodeServiceResponse: - """ - Set a node service for a node - - :param request: set-node-service - request that has info to set a node service - :param context: context object - :return: set-node-service response - """ - logging.debug("set node service: %s", request) - session = self.get_session(request.session_id, context) - config = request.config - grpcutils.service_configuration(session, config) - return SetNodeServiceResponse(result=True) - - def SetNodeServiceFile( - self, request: SetNodeServiceFileRequest, context: ServicerContext - ) -> SetNodeServiceFileResponse: - """ - Store the customized service file in the service config - - :param request: - set-node-service-file request - :param context: context object - :return: set-node-service-file response - """ - logging.debug("set node service file: %s", request) - session = self.get_session(request.session_id, context) - config = request.config - session.services.set_service_file( - config.node_id, config.service, config.file, config.data - ) - return SetNodeServiceFileResponse(result=True) - def ServiceAction( self, request: ServiceActionRequest, context: ServicerContext ) -> ServiceActionResponse: @@ -1290,7 +956,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: service-action response about status of action """ - logging.debug("service action: %s", request) + logger.debug("service action: %s", request) session = self.get_session(request.session_id, context) node = self.get_node(session, request.node_id, context, CoreNode) service = None @@ -1320,21 +986,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): return ServiceActionResponse(result=result) - def GetWlanConfigs( - self, request: GetWlanConfigsRequest, context: ServicerContext - ) -> GetWlanConfigsResponse: - """ - Retrieve all wireless-lan configurations. - - :param request: request - :param context: core.api.grpc.core_pb2.GetWlanConfigResponse - :return: all wlan configurations - """ - logging.debug("get wlan configs: %s", request) - session = self.get_session(request.session_id, context) - configs = grpcutils.get_wlan_configs(session) - return GetWlanConfigsResponse(configs=configs) - def GetWlanConfig( self, request: GetWlanConfigRequest, context: ServicerContext ) -> GetWlanConfigResponse: @@ -1345,7 +996,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: core.api.grpc.core_pb2.GetWlanConfigResponse :return: get-wlan-configuration response about the wlan configuration of a node """ - logging.debug("get wlan config: %s", request) + logger.debug("get wlan config: %s", request) session = self.get_session(request.session_id, context) current_config = session.mobility.get_model_config( request.node_id, BasicRangeModel.name @@ -1363,7 +1014,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: set-wlan-configuration response """ - logging.debug("set wlan config: %s", request) + logger.debug("set wlan config: %s", request) session = self.get_session(request.session_id, context) node_id = request.wlan_config.node_id config = request.wlan_config.config @@ -1373,52 +1024,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 - """ - logging.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 - """ - logging.debug("set emane config: %s", request) - session = self.get_session(request.session_id, context) - config = session.emane.get_configs() - config.update(request.config) - return SetEmaneConfigResponse(result=True) - - def GetEmaneModels( - self, request: GetEmaneModelsRequest, context: ServicerContext - ) -> GetEmaneModelsResponse: - """ - Retrieve all the EMANE models in the session - - :param request: get-emane-model request - :param context: context object - :return: get-EMANE-models response that has all the models - """ - logging.debug("get emane models: %s", request) - session = self.get_session(request.session_id, context) - models = grpcutils.get_emane_models(session) - return GetEmaneModelsResponse(models=models) - def GetEmaneModelConfig( self, request: GetEmaneModelConfigRequest, context: ServicerContext ) -> GetEmaneModelConfigResponse: @@ -1430,13 +1035,11 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: get-EMANE-model-configuration response """ - logging.debug("get emane model config: %s", request) + logger.debug("get emane model config: %s", request) session = self.get_session(request.session_id, context) - model = session.emane.models.get(request.model) - if not model: - raise CoreError(f"invalid emane model: {request.model}") + model = session.emane.get_model(request.model) _id = utils.iface_config_id(request.node_id, request.iface_id) - current_config = session.emane.get_model_config(_id, request.model) + current_config = session.emane.get_config(_id, request.model) config = get_config_options(current_config, model) return GetEmaneModelConfigResponse(config=config) @@ -1451,30 +1054,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: set-EMANE-model-configuration response """ - logging.debug("set emane model config: %s", request) + logger.debug("set emane model config: %s", request) session = self.get_session(request.session_id, context) model_config = request.emane_model_config _id = utils.iface_config_id(model_config.node_id, model_config.iface_id) - session.emane.set_model_config(_id, model_config.model, model_config.config) + session.emane.set_config(_id, model_config.model, model_config.config) return SetEmaneModelConfigResponse(result=True) - def GetEmaneModelConfigs( - self, request: GetEmaneModelConfigsRequest, context: ServicerContext - ) -> GetEmaneModelConfigsResponse: - """ - Retrieve all EMANE model configurations of a session - - :param request: - get-EMANE-model-configurations request - :param context: context object - :return: get-EMANE-model-configurations response that has all the EMANE - configurations - """ - logging.debug("get emane model configs: %s", request) - session = self.get_session(request.session_id, context) - configs = grpcutils.get_emane_model_configs(session) - return GetEmaneModelConfigsResponse(configs=configs) - def SaveXml( self, request: core_pb2.SaveXmlRequest, context: ServicerContext ) -> core_pb2.SaveXmlResponse: @@ -1485,15 +1071,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: save-xml response """ - logging.debug("save xml: %s", request) + logger.debug("save xml: %s", request) session = self.get_session(request.session_id, context) - _, temp_path = tempfile.mkstemp() session.save_xml(temp_path) - with open(temp_path, "r") as xml_file: data = xml_file.read() - return core_pb2.SaveXmlResponse(data=data) def OpenXml( @@ -1506,20 +1089,20 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: Open-XML response or raise an exception if invalid XML file """ - logging.debug("open xml: %s", request) + logger.debug("open xml: %s", request) session = self.coreemu.create_session() - temp = tempfile.NamedTemporaryFile(delete=False) temp.write(request.data.encode("utf-8")) temp.close() - + temp_path = Path(temp.name) + file_path = Path(request.file) try: - session.open_xml(temp.name, request.start) - session.name = os.path.basename(request.file) - session.file_name = request.file + session.open_xml(temp_path, request.start) + session.name = file_path.name + session.file_path = file_path return core_pb2.OpenXmlResponse(session_id=session.id, result=True) except IOError: - logging.exception("error opening session file") + logger.exception("error opening session file") self.coreemu.delete_session(session.id) context.abort(grpc.StatusCode.INVALID_ARGUMENT, "invalid xml file") finally: @@ -1552,7 +1135,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): :param context: context object :return: emane link response with success status """ - logging.debug("emane link: %s", request) + logger.debug("emane link: %s", request) session = self.get_session(request.session_id, context) nem1 = request.nem1 iface1 = session.emane.get_iface(nem1) @@ -1585,35 +1168,6 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): else: return EmaneLinkResponse(result=False) - def GetConfigServices( - self, request: GetConfigServicesRequest, context: ServicerContext - ) -> GetConfigServicesResponse: - """ - Gets all currently known configuration services. - - :param request: get config services request - :param context: grpc context - :return: get config services response - """ - services = [] - for service in self.coreemu.service_manager.services.values(): - service_proto = ConfigService( - name=service.name, - group=service.group, - executables=service.executables, - dependencies=service.dependencies, - directories=service.directories, - files=service.files, - startup=service.startup, - validate=service.validate, - shutdown=service.shutdown, - validation_mode=service.validation_mode.value, - validation_timer=service.validation_timer, - validation_period=service.validation_period, - ) - services.append(service_proto) - return GetConfigServicesResponse(services=services) - def GetNodeConfigService( self, request: GetNodeConfigServiceRequest, context: ServicerContext ) -> GetNodeConfigServiceResponse: @@ -1667,82 +1221,21 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): templates=templates, config=config, modes=modes ) - def GetNodeConfigServiceConfigs( - self, request: GetNodeConfigServiceConfigsRequest, context: ServicerContext - ) -> GetNodeConfigServiceConfigsResponse: - """ - Get current custom templates and config for configuration services for a given - node. - - :param request: get node config service configs request - :param context: grpc context - :return: get node config service configs response - """ - session = self.get_session(request.session_id, context) - configs = grpcutils.get_node_config_service_configs(session) - return GetNodeConfigServiceConfigsResponse(configs=configs) - - def GetNodeConfigServices( - self, request: GetNodeConfigServicesRequest, context: ServicerContext - ) -> GetNodeConfigServicesResponse: - """ - Get configuration services for a given node. - - :param request: get node config services request - :param context: grpc context - :return: get node config services response - """ - session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context, CoreNode) - services = node.config_services.keys() - return GetNodeConfigServicesResponse(services=services) - - def SetNodeConfigService( - self, request: SetNodeConfigServiceRequest, context: ServicerContext - ) -> SetNodeConfigServiceResponse: - """ - Set custom config, for a given configuration service, for a given node. - - :param request: set node config service request - :param context: grpc context - :return: set node config service response - """ - session = self.get_session(request.session_id, context) - node = self.get_node(session, request.node_id, context, CoreNode) - self.validate_service(request.name, context) - service = node.config_services.get(request.name) - if service: - service.set_config(request.config) - return SetNodeConfigServiceResponse(result=True) - else: - context.abort( - grpc.StatusCode.NOT_FOUND, - f"node {node.name} missing service {request.name}", - ) - def GetEmaneEventChannel( 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()) - thread = threading.Thread( - target=utils.execute_file, - args=( - request.script, - {"__file__": request.script, "coreemu": self.coreemu}, - ), - daemon=True, - ) - thread.start() - thread.join() + file_path = Path(request.script) + utils.execute_script(self.coreemu, file_path, request.args) current_sessions = set(self.coreemu.sessions.keys()) new_sessions = list(current_sessions.difference(existing_sessions)) new_session = -1 diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py index 1ef43be2..ffeb6793 100644 --- a/daemon/core/api/grpc/wrappers.py +++ b/daemon/core/api/grpc/wrappers.py @@ -171,18 +171,32 @@ class ConfigServiceData: class ConfigServiceDefaults: templates: Dict[str, str] config: Dict[str, "ConfigOption"] - modes: List[str] + modes: Dict[str, Dict[str, str]] @classmethod def from_proto( - cls, proto: configservices_pb2.GetConfigServicesResponse + cls, proto: configservices_pb2.GetConfigServiceDefaultsResponse ) -> "ConfigServiceDefaults": config = ConfigOption.from_dict(proto.config) + modes = {x.name: dict(x.config) for x in proto.modes} return ConfigServiceDefaults( - templates=dict(proto.templates), config=config, modes=list(proto.modes) + templates=dict(proto.templates), config=config, modes=modes ) +@dataclass +class Server: + name: str + host: str + + @classmethod + def from_proto(cls, proto: core_pb2.Server) -> "Server": + return Server(name=proto.name, host=proto.host) + + def to_proto(self) -> core_pb2.Server: + return core_pb2.Server(name=self.name, host=self.host) + + @dataclass class Service: group: str @@ -205,16 +219,16 @@ class ServiceDefault: @dataclass class NodeServiceData: - executables: List[str] - dependencies: List[str] - dirs: List[str] - configs: List[str] - startup: List[str] - validate: List[str] - validation_mode: ServiceValidationMode - validation_timer: int - shutdown: List[str] - meta: str + executables: List[str] = field(default_factory=list) + dependencies: List[str] = field(default_factory=list) + dirs: List[str] = field(default_factory=list) + configs: List[str] = field(default_factory=list) + startup: List[str] = field(default_factory=list) + validate: List[str] = field(default_factory=list) + validation_mode: ServiceValidationMode = ServiceValidationMode.NON_BLOCKING + validation_timer: int = 5 + shutdown: List[str] = field(default_factory=list) + meta: str = None @classmethod def from_proto(cls, proto: services_pb2.NodeServiceData) -> "NodeServiceData": @@ -225,12 +239,43 @@ class NodeServiceData: configs=proto.configs, startup=proto.startup, validate=proto.validate, - validation_mode=proto.validation_mode, + validation_mode=ServiceValidationMode(proto.validation_mode), validation_timer=proto.validation_timer, shutdown=proto.shutdown, meta=proto.meta, ) + def to_proto(self) -> services_pb2.NodeServiceData: + return services_pb2.NodeServiceData( + executables=self.executables, + dependencies=self.dependencies, + dirs=self.dirs, + configs=self.configs, + startup=self.startup, + validate=self.validate, + validation_mode=self.validation_mode.value, + validation_timer=self.validation_timer, + shutdown=self.shutdown, + meta=self.meta, + ) + + +@dataclass +class NodeServiceConfig: + node_id: int + service: str + data: NodeServiceData + files: Dict[str, str] = field(default_factory=dict) + + @classmethod + def from_proto(cls, proto: services_pb2.NodeServiceConfig) -> "NodeServiceConfig": + return NodeServiceConfig( + node_id=proto.node_id, + service=proto.service, + data=NodeServiceData.from_proto(proto.data), + files=dict(proto.files), + ) + @dataclass class ServiceConfig: @@ -254,6 +299,19 @@ class ServiceConfig: ) +@dataclass +class ServiceFileConfig: + node_id: int + service: str + file: str + data: str = field(repr=False) + + def to_proto(self) -> services_pb2.ServiceFileConfig: + return services_pb2.ServiceFileConfig( + node_id=self.node_id, service=self.service, file=self.file, data=self.data + ) + + @dataclass class BridgeThroughput: node_id: int @@ -364,11 +422,11 @@ class ExceptionEvent: @dataclass class ConfigOption: - label: str name: str value: str - type: ConfigOptionType - group: str + label: str = None + type: ConfigOptionType = None + group: str = None select: List[str] = None @classmethod @@ -386,15 +444,27 @@ class ConfigOption: @classmethod def from_proto(cls, proto: common_pb2.ConfigOption) -> "ConfigOption": + config_type = ConfigOptionType(proto.type) if proto.type is not None else None return ConfigOption( label=proto.label, name=proto.name, value=proto.value, - type=ConfigOptionType(proto.type), + type=config_type, group=proto.group, select=proto.select, ) + def to_proto(self) -> common_pb2.ConfigOption: + config_type = self.type.value if self.type is not None else None + return common_pb2.ConfigOption( + label=self.label, + name=self.name, + value=self.value, + type=config_type, + select=self.select, + group=self.group, + ) + @dataclass class Interface: @@ -598,11 +668,12 @@ class EmaneModelConfig: ) def to_proto(self) -> emane_pb2.EmaneModelConfig: + config = ConfigOption.to_dict(self.config) return emane_pb2.EmaneModelConfig( node_id=self.node_id, model=self.model, iface_id=self.iface_id, - config=self.config, + config=config, ) @@ -635,11 +706,11 @@ class Geo: @dataclass class Node: - id: int - name: str - type: NodeType + id: int = None + name: str = None + type: NodeType = NodeType.DEFAULT model: str = None - position: Position = None + position: Position = Position(x=0, y=0) services: Set[str] = field(default_factory=set) config_services: Set[str] = field(default_factory=set) emane: str = None @@ -669,6 +740,23 @@ class Node: @classmethod def from_proto(cls, proto: core_pb2.Node) -> "Node": + service_configs = {} + service_file_configs = {} + for service, node_config in proto.service_configs.items(): + service_configs[service] = NodeServiceData.from_proto(node_config.data) + service_file_configs[service] = dict(node_config.files) + emane_configs = {} + for emane_config in proto.emane_configs: + iface_id = None if emane_config.iface_id == -1 else emane_config.iface_id + model = emane_config.model + key = (model, iface_id) + emane_configs[key] = ConfigOption.from_dict(emane_config.config) + config_service_configs = {} + for service, service_config in proto.config_service_configs.items(): + config_service_configs[service] = ConfigServiceData( + templates=dict(service_config.templates), + config=dict(service_config.config), + ) return Node( id=proto.id, name=proto.name, @@ -685,9 +773,43 @@ class Node: dir=proto.dir, channel=proto.channel, canvas=proto.canvas, + wlan_config=ConfigOption.from_dict(proto.wlan_config), + mobility_config=ConfigOption.from_dict(proto.mobility_config), + service_configs=service_configs, + service_file_configs=service_file_configs, + config_service_configs=config_service_configs, + emane_model_configs=emane_configs, ) def to_proto(self) -> core_pb2.Node: + emane_configs = [] + for key, config in self.emane_model_configs.items(): + model, iface_id = key + if iface_id is None: + iface_id = -1 + config = {k: v.to_proto() for k, v in config.items()} + emane_config = emane_pb2.NodeEmaneConfig( + iface_id=iface_id, model=model, config=config + ) + emane_configs.append(emane_config) + service_configs = {} + for service, service_data in self.service_configs.items(): + service_configs[service] = services_pb2.NodeServiceConfig( + service=service, data=service_data.to_proto() + ) + for service, file_configs in self.service_file_configs.items(): + service_config = service_configs.get(service) + if service_config: + service_config.files.update(file_configs) + else: + service_configs[service] = services_pb2.NodeServiceConfig( + service=service, files=file_configs + ) + config_service_configs = {} + for service, service_config in self.config_service_configs.items(): + config_service_configs[service] = configservices_pb2.ConfigServiceConfig( + templates=service_config.templates, config=service_config.config + ) return core_pb2.Node( id=self.id, name=self.name, @@ -703,24 +825,50 @@ class Node: dir=self.dir, channel=self.channel, canvas=self.canvas, + wlan_config={k: v.to_proto() for k, v in self.wlan_config.items()}, + mobility_config={k: v.to_proto() for k, v in self.mobility_config.items()}, + service_configs=service_configs, + config_service_configs=config_service_configs, + emane_configs=emane_configs, ) + def set_wlan(self, config: Dict[str, str]) -> None: + for key, value in config.items(): + option = ConfigOption(name=key, value=value) + self.wlan_config[key] = option + + def set_mobility(self, config: Dict[str, str]) -> None: + for key, value in config.items(): + option = ConfigOption(name=key, value=value) + self.mobility_config[key] = option + + def set_emane_model( + self, model: str, config: Dict[str, str], iface_id: int = None + ) -> None: + key = (model, iface_id) + config_options = self.emane_model_configs.setdefault(key, {}) + for key, value in config.items(): + option = ConfigOption(name=key, value=value) + config_options[key] = option + @dataclass class Session: - id: int - state: SessionState - nodes: Dict[int, Node] - links: List[Link] - dir: str - user: str - default_services: Dict[str, Set[str]] - location: SessionLocation - hooks: Dict[str, Hook] - emane_models: List[str] - emane_config: Dict[str, ConfigOption] - metadata: Dict[str, str] - file: Path + id: int = None + state: SessionState = SessionState.DEFINITION + nodes: Dict[int, Node] = field(default_factory=dict) + links: List[Link] = field(default_factory=list) + dir: str = None + user: str = None + default_services: Dict[str, Set[str]] = field(default_factory=dict) + location: SessionLocation = SessionLocation( + 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) + metadata: Dict[str, str] = field(default_factory=dict) + file: Path = None + options: Dict[str, ConfigOption] = field(default_factory=dict) + servers: List[Server] = field(default_factory=list) @classmethod def from_proto(cls, proto: core_pb2.Session) -> "Session": @@ -730,33 +878,9 @@ class Session: x.node_type: set(x.services) for x in proto.default_services } hooks = {x.file: Hook.from_proto(x) for x in proto.hooks} - # update nodes with their current configurations - for model in proto.emane_model_configs: - iface_id = None - if model.iface_id != -1: - iface_id = model.iface_id - node = nodes[model.node_id] - key = (model.model, iface_id) - node.emane_model_configs[key] = ConfigOption.from_dict(model.config) - for node_id, mapped_config in proto.wlan_configs.items(): - node = nodes[node_id] - node.wlan_config = ConfigOption.from_dict(mapped_config.config) - for config in proto.service_configs: - service = config.service - node = nodes[config.node_id] - node.service_configs[service] = NodeServiceData.from_proto(config.data) - for file, data in config.files.items(): - files = node.service_file_configs.setdefault(service, {}) - files[file] = data - for config in proto.config_service_configs: - node = nodes[config.node_id] - node.config_service_configs[config.name] = ConfigServiceData( - templates=dict(config.templates), config=dict(config.config) - ) - for node_id, mapped_config in proto.mobility_configs.items(): - node = nodes[node_id] - node.mobility_config = ConfigOption.from_dict(mapped_config.config) file_path = Path(proto.file) if proto.file else None + options = ConfigOption.from_dict(proto.options) + servers = [Server.from_proto(x) for x in proto.servers] return Session( id=proto.id, state=SessionState(proto.state), @@ -767,10 +891,107 @@ class Session: default_services=default_services, location=SessionLocation.from_proto(proto.location), hooks=hooks, - emane_models=list(proto.emane_models), - emane_config=ConfigOption.from_dict(proto.emane_config), metadata=dict(proto.metadata), file=file_path, + options=options, + servers=servers, + ) + + def to_proto(self) -> core_pb2.Session: + nodes = [x.to_proto() for x in self.nodes.values()] + links = [x.to_proto() for x in self.links] + hooks = [x.to_proto() for x in self.hooks.values()] + options = {k: v.to_proto() for k, v in self.options.items()} + servers = [x.to_proto() for x in self.servers] + default_services = [] + for node_type, services in self.default_services.items(): + default_service = services_pb2.ServiceDefaults( + node_type=node_type, services=services + ) + default_services.append(default_service) + file = str(self.file) if self.file else None + return core_pb2.Session( + id=self.id, + state=self.state.value, + nodes=nodes, + links=links, + dir=self.dir, + user=self.user, + default_services=default_services, + location=self.location.to_proto(), + hooks=hooks, + metadata=self.metadata, + file=file, + options=options, + servers=servers, + ) + + def add_node( + self, + _id: int, + *, + name: str = None, + _type: NodeType = NodeType.DEFAULT, + model: str = "PC", + position: Position = None, + geo: Geo = None, + emane: str = None, + image: str = None, + server: str = None, + ) -> Node: + node = Node( + id=_id, + name=name, + type=_type, + model=model, + position=position, + geo=geo, + emane=emane, + image=image, + server=server, + ) + self.nodes[node.id] = node + return node + + def add_link( + self, + *, + node1: Node, + node2: Node, + iface1: Interface = None, + iface2: Interface = None, + options: LinkOptions = None, + ) -> Link: + link = Link( + node1_id=node1.id, + node2_id=node2.id, + iface1=iface1, + iface2=iface2, + options=options, + ) + self.links.append(link) + return link + + def set_options(self, config: Dict[str, str]) -> None: + for key, value in config.items(): + option = ConfigOption(name=key, value=value) + self.options[key] = option + + +@dataclass +class CoreConfig: + services: List[Service] = field(default_factory=list) + config_services: List[ConfigService] = field(default_factory=list) + emane_models: List[str] = field(default_factory=list) + + @classmethod + def from_proto(cls, proto: core_pb2.GetConfigResponse) -> "CoreConfig": + services = [Service.from_proto(x) for x in proto.services] + config_services = [ConfigService.from_proto(x) for x in proto.config_services] + return CoreConfig( + services=services, + config_services=config_services, + emane_models=list(proto.emane_models), ) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 65abed8c..1937aea8 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -3,7 +3,6 @@ socket server request handlers leveraged by core servers. """ import logging -import os import shlex import shutil import socketserver @@ -11,6 +10,7 @@ import sys import threading import time from itertools import repeat +from pathlib import Path from queue import Empty, Queue from typing import Optional @@ -29,6 +29,7 @@ from core.api.tlv.enumerations import ( NodeTlvs, SessionTlvs, ) +from core.emane.modelmanager import EmaneModelManager from core.emulator.data import ( ConfigData, EventData, @@ -55,6 +56,8 @@ from core.nodes.network import WlanNode from core.nodes.physical import Rj45Node from core.services.coreservices import ServiceManager, ServiceShim +logger = logging.getLogger(__name__) + class CoreHandler(socketserver.BaseRequestHandler): """ @@ -104,7 +107,7 @@ class CoreHandler(socketserver.BaseRequestHandler): :return: nothing """ - logging.debug("new TCP connection: %s", self.client_address) + logger.debug("new TCP connection: %s", self.client_address) def finish(self): """ @@ -113,40 +116,40 @@ class CoreHandler(socketserver.BaseRequestHandler): :return: nothing """ - logging.debug("finishing request handler") - logging.debug("remaining message queue size: %s", self.message_queue.qsize()) + logger.debug("finishing request handler") + logger.debug("remaining message queue size: %s", self.message_queue.qsize()) # give some time for message queue to deplete timeout = 10 wait = 0 while not self.message_queue.empty(): - logging.debug("waiting for message queue to empty: %s seconds", wait) + logger.debug("waiting for message queue to empty: %s seconds", wait) time.sleep(1) wait += 1 if wait == timeout: - logging.warning("queue failed to be empty, finishing request handler") + logger.warning("queue failed to be empty, finishing request handler") break - logging.info("client disconnected: notifying threads") + logger.info("client disconnected: notifying threads") self.done = True for thread in self.handler_threads: - logging.info("waiting for thread: %s", thread.getName()) + logger.info("waiting for thread: %s", thread.getName()) thread.join(timeout) if thread.is_alive(): - logging.warning( + logger.warning( "joining %s failed: still alive after %s sec", thread.getName(), timeout, ) - logging.info("connection closed: %s", self.client_address) + logger.info("connection closed: %s", self.client_address) if self.session: # remove client from session broker and shutdown if there are no clients self.remove_session_handlers() clients = self.session_clients[self.session.id] clients.remove(self) if not clients and not self.session.is_active(): - logging.info( + logger.info( "no session clients left and not active, initiating shutdown" ) self.coreemu.delete_session(self.session.id) @@ -167,39 +170,27 @@ class CoreHandler(socketserver.BaseRequestHandler): date_list = [] thumb_list = [] num_sessions = 0 - with self._sessions_lock: for _id in self.coreemu.sessions: session = self.coreemu.sessions[_id] num_sessions += 1 id_list.append(str(_id)) - name = session.name if not name: name = "" name_list.append(name) - - file_name = session.file_name - if not file_name: - file_name = "" - file_list.append(file_name) - + file_name = str(session.file_path) if session.file_path else "" + file_list.append(str(file_name)) node_count_list.append(str(session.get_node_count())) - date_list.append(time.ctime(session.state_time)) - - thumb = session.thumbnail - if not thumb: - thumb = "" + thumb = str(session.thumbnail) if session.thumbnail else "" thumb_list.append(thumb) - session_ids = "|".join(id_list) names = "|".join(name_list) files = "|".join(file_list) node_counts = "|".join(node_count_list) dates = "|".join(date_list) thumbs = "|".join(thumb_list) - if num_sessions > 0: tlv_data = b"" if len(session_ids) > 0: @@ -221,7 +212,6 @@ class CoreHandler(socketserver.BaseRequestHandler): message = coreapi.CoreSessionMessage.pack(flags, tlv_data) else: message = None - return message def handle_broadcast_event(self, event_data): @@ -231,7 +221,7 @@ class CoreHandler(socketserver.BaseRequestHandler): :param core.emulator.data.EventData event_data: event data to handle :return: nothing """ - logging.debug("handling broadcast event: %s", event_data) + logger.debug("handling broadcast event: %s", event_data) tlv_data = structutils.pack_values( coreapi.CoreEventTlv, @@ -249,7 +239,7 @@ class CoreHandler(socketserver.BaseRequestHandler): try: self.sendall(message) except IOError: - logging.exception("error sending event message") + logger.exception("error sending event message") def handle_broadcast_file(self, file_data): """ @@ -258,7 +248,7 @@ class CoreHandler(socketserver.BaseRequestHandler): :param core.emulator.data.FileData file_data: file data to handle :return: nothing """ - logging.debug("handling broadcast file: %s", file_data) + logger.debug("handling broadcast file: %s", file_data) tlv_data = structutils.pack_values( coreapi.CoreFileTlv, @@ -279,7 +269,7 @@ class CoreHandler(socketserver.BaseRequestHandler): try: self.sendall(message) except IOError: - logging.exception("error sending file message") + logger.exception("error sending file message") def handle_broadcast_config(self, config_data): """ @@ -288,12 +278,12 @@ class CoreHandler(socketserver.BaseRequestHandler): :param core.emulator.data.ConfigData config_data: config data to handle :return: nothing """ - logging.debug("handling broadcast config: %s", config_data) + logger.debug("handling broadcast config: %s", config_data) message = dataconversion.convert_config(config_data) try: self.sendall(message) except IOError: - logging.exception("error sending config message") + logger.exception("error sending config message") def handle_broadcast_exception(self, exception_data): """ @@ -302,7 +292,7 @@ class CoreHandler(socketserver.BaseRequestHandler): :param core.emulator.data.ExceptionData exception_data: exception data to handle :return: nothing """ - logging.debug("handling broadcast exception: %s", exception_data) + logger.debug("handling broadcast exception: %s", exception_data) tlv_data = structutils.pack_values( coreapi.CoreExceptionTlv, [ @@ -319,7 +309,7 @@ class CoreHandler(socketserver.BaseRequestHandler): try: self.sendall(message) except IOError: - logging.exception("error sending exception message") + logger.exception("error sending exception message") def handle_broadcast_node(self, node_data): """ @@ -328,12 +318,12 @@ class CoreHandler(socketserver.BaseRequestHandler): :param core.emulator.data.NodeData node_data: node data to handle :return: nothing """ - logging.debug("handling broadcast node: %s", node_data) + logger.debug("handling broadcast node: %s", node_data) message = dataconversion.convert_node(node_data) try: self.sendall(message) except IOError: - logging.exception("error sending node message") + logger.exception("error sending node message") def handle_broadcast_link(self, link_data): """ @@ -342,7 +332,7 @@ class CoreHandler(socketserver.BaseRequestHandler): :param core.emulator.data.LinkData link_data: link data to handle :return: nothing """ - logging.debug("handling broadcast link: %s", link_data) + logger.debug("handling broadcast link: %s", link_data) options_data = link_data.options loss = "" if options_data.loss is not None: @@ -394,7 +384,7 @@ class CoreHandler(socketserver.BaseRequestHandler): try: self.sendall(message) except IOError: - logging.exception("error sending Event Message") + logger.exception("error sending Event Message") def register(self): """ @@ -402,7 +392,7 @@ class CoreHandler(socketserver.BaseRequestHandler): :return: register message data """ - logging.info( + logger.info( "GUI has connected to session %d at %s", self.session.id, time.ctime() ) tlv_data = b"" @@ -430,8 +420,7 @@ class CoreHandler(socketserver.BaseRequestHandler): tlv_data += coreapi.CoreRegisterTlv.pack( self.session.emane.config_type.value, self.session.emane.name ) - for model_name in self.session.emane.models: - model_class = self.session.emane.models[model_name] + for model_name, model_class in EmaneModelManager.models.items(): tlv_data += coreapi.CoreRegisterTlv.pack( model_class.config_type.value, model_class.name ) @@ -474,14 +463,14 @@ class CoreHandler(socketserver.BaseRequestHandler): header ) if message_len == 0: - logging.warning("received message with no data") + logger.warning("received message with no data") data = b"" while len(data) < message_len: data += self.request.recv(message_len - len(data)) if len(data) > message_len: error_message = f"received message length does not match received data ({len(data)} != {message_len})" - logging.error(error_message) + logger.error(error_message) raise IOError(error_message) try: @@ -490,7 +479,7 @@ class CoreHandler(socketserver.BaseRequestHandler): except KeyError: message = coreapi.CoreMessage(message_flags, header, data) message.message_type = message_type - logging.exception("unimplemented core message type: %s", message.type_str()) + logger.exception("unimplemented core message type: %s", message.type_str()) return message @@ -501,7 +490,7 @@ class CoreHandler(socketserver.BaseRequestHandler): :param message: message to queue :return: nothing """ - logging.debug( + logger.debug( "queueing msg (queuedtimes = %s): type %s", message.queuedtimes, MessageTypes(message.message_type), @@ -531,11 +520,11 @@ class CoreHandler(socketserver.BaseRequestHandler): :param message: message to handle :return: nothing """ - logging.debug( + logger.debug( "%s handling message:\n%s", threading.currentThread().getName(), message ) if message.message_type not in self.message_handlers: - logging.error("no handler for message type: %s", message.type_str()) + logger.error("no handler for message type: %s", message.type_str()) return message_handler = self.message_handlers[message.message_type] @@ -545,7 +534,7 @@ class CoreHandler(socketserver.BaseRequestHandler): self.dispatch_replies(replies, message) except Exception as e: self.send_exception(ExceptionLevels.ERROR, "corehandler", str(e)) - logging.exception( + logger.exception( "%s: exception while handling message: %s", threading.currentThread().getName(), message, @@ -573,12 +562,12 @@ class CoreHandler(socketserver.BaseRequestHandler): # multiple TLVs of same type cause KeyError exception reply_message = f"CoreMessage (type {message_type} flags {message_flags} length {message_length})" - logging.debug("sending reply:\n%s", reply_message) + logger.debug("sending reply:\n%s", reply_message) try: self.sendall(reply) except IOError: - logging.exception("error dispatching reply") + logger.exception("error dispatching reply") def handle(self): """ @@ -593,7 +582,7 @@ class CoreHandler(socketserver.BaseRequestHandler): # TODO: add shutdown handler for session self.session = self.coreemu.create_session(port) - logging.debug("created new session for client: %s", self.session.id) + logger.debug("created new session for client: %s", self.session.id) clients = self.session_clients.setdefault(self.session.id, []) clients.append(self) @@ -607,10 +596,10 @@ class CoreHandler(socketserver.BaseRequestHandler): try: message = self.receive_message() except EOFError: - logging.info("client disconnected") + logger.info("client disconnected") break except IOError: - logging.exception("error receiving message") + logger.exception("error receiving message") break message.queuedtimes = 0 @@ -632,7 +621,7 @@ class CoreHandler(socketserver.BaseRequestHandler): if client == self: continue - logging.debug("BROADCAST TO OTHER CLIENT: %s", client) + logger.debug("BROADCAST TO OTHER CLIENT: %s", client) client.sendall(message.raw_message) def send_exception(self, level, source, text, node=None): @@ -656,7 +645,7 @@ class CoreHandler(socketserver.BaseRequestHandler): self.handle_broadcast_exception(exception_data) def add_session_handlers(self): - logging.debug("adding session broadcast handlers") + logger.debug("adding session broadcast handlers") self.session.event_handlers.append(self.handle_broadcast_event) self.session.exception_handlers.append(self.handle_broadcast_exception) self.session.node_handlers.append(self.handle_broadcast_node) @@ -665,7 +654,7 @@ class CoreHandler(socketserver.BaseRequestHandler): self.session.config_handlers.append(self.handle_broadcast_config) def remove_session_handlers(self): - logging.debug("removing session broadcast handlers") + logger.debug("removing session broadcast handlers") self.session.event_handlers.remove(self.handle_broadcast_event) self.session.exception_handlers.remove(self.handle_broadcast_exception) self.session.node_handlers.remove(self.handle_broadcast_node) @@ -685,7 +674,7 @@ class CoreHandler(socketserver.BaseRequestHandler): message.flags & MessageFlags.ADD.value and message.flags & MessageFlags.DELETE.value ): - logging.warning("ignoring invalid message: add and delete flag both set") + logger.warning("ignoring invalid message: add and delete flag both set") return () _class = CoreNode @@ -699,8 +688,8 @@ class CoreHandler(socketserver.BaseRequestHandler): options = NodeOptions( name=message.get_tlv(NodeTlvs.NAME.value), model=message.get_tlv(NodeTlvs.MODEL.value), + legacy=True, ) - options.set_position( x=message.get_tlv(NodeTlvs.X_POSITION.value), y=message.get_tlv(NodeTlvs.Y_POSITION.value), @@ -727,12 +716,15 @@ class CoreHandler(socketserver.BaseRequestHandler): if message.flags & MessageFlags.ADD.value: node = self.session.add_node(_class, node_id, options) - if node: - if message.flags & MessageFlags.STRING.value: - self.node_status_request[node.id] = True - - if self.session.state == EventTypes.RUNTIME_STATE: - self.send_node_emulation_id(node.id) + has_geo = all( + i is not None for i in [options.lon, options.lat, options.alt] + ) + if has_geo: + self.session.broadcast_node(node) + if message.flags & MessageFlags.STRING.value: + self.node_status_request[node.id] = True + if self.session.state == EventTypes.RUNTIME_STATE: + self.send_node_emulation_id(node.id) elif message.flags & MessageFlags.DELETE.value: with self._shutdown_lock: result = self.session.delete_node(node_id) @@ -750,7 +742,16 @@ class CoreHandler(socketserver.BaseRequestHandler): replies.append(coreapi.CoreNodeMessage.pack(flags, tlvdata)) # node update else: - self.session.edit_node(node_id, options) + node = self.session.get_node(node_id, NodeBase) + node.icon = options.icon + has_geo = all( + i is not None for i in [options.lon, options.lat, options.alt] + ) + if has_geo: + self.session.set_node_geo(node, options.lon, options.lat, options.alt) + self.session.broadcast_node(node) + else: + self.session.set_node_pos(node, options.x, options.y) return replies @@ -801,13 +802,19 @@ class CoreHandler(socketserver.BaseRequestHandler): if dup is not None: options.dup = int(dup) + # fix for rj45 nodes missing iface id + node1 = self.session.get_node(node1_id, NodeBase) + node2 = self.session.get_node(node2_id, NodeBase) + if isinstance(node1, Rj45Node) and iface1_data.id is None: + iface1_data.id = 0 + if isinstance(node2, Rj45Node) and iface2_data.id is None: + iface2_data.id = 0 + if message.flags & MessageFlags.ADD.value: self.session.add_link( node1_id, node2_id, iface1_data, iface2_data, options, link_type ) elif message.flags & MessageFlags.DELETE.value: - node1 = self.session.get_node(node1_id, NodeBase) - node2 = self.session.get_node(node2_id, NodeBase) if isinstance(node1, Rj45Node): iface1_data.id = node1.iface_id if isinstance(node2, Rj45Node): @@ -905,7 +912,7 @@ class CoreHandler(socketserver.BaseRequestHandler): else: node.cmd(command, wait=False) except CoreError: - logging.exception("error getting object: %s", node_id) + logger.exception("error getting object: %s", node_id) # XXX wait and queue this message to try again later # XXX maybe this should be done differently if not message.flags & MessageFlags.LOCAL.value: @@ -927,56 +934,47 @@ class CoreHandler(socketserver.BaseRequestHandler): execute_server = message.get_tlv(RegisterTlvs.EXECUTE_SERVER.value) if execute_server: try: - logging.info("executing: %s", execute_server) + logger.info("executing: %s", execute_server) + old_session_ids = set() if message.flags & MessageFlags.STRING.value: old_session_ids = set(self.coreemu.sessions.keys()) sys.argv = shlex.split(execute_server) - file_name = sys.argv[0] - - if os.path.splitext(file_name)[1].lower() == ".xml": + file_path = Path(sys.argv[0]) + if file_path.suffix == ".xml": session = self.coreemu.create_session() try: - session.open_xml(file_name) + session.open_xml(file_path) except Exception: self.coreemu.delete_session(session.id) raise else: - thread = threading.Thread( - target=utils.execute_file, - args=( - file_name, - {"__file__": file_name, "coreemu": self.coreemu}, - ), - daemon=True, - ) - thread.start() - thread.join() + utils.execute_script(self.coreemu, file_path, execute_server) if message.flags & MessageFlags.STRING.value: new_session_ids = set(self.coreemu.sessions.keys()) new_sid = new_session_ids.difference(old_session_ids) try: sid = new_sid.pop() - logging.info("executed: %s as session %d", execute_server, sid) + logger.info("executed: %s as session %d", execute_server, sid) except KeyError: - logging.info( + logger.info( "executed %s with unknown session ID", execute_server ) return replies - logging.debug("checking session %d for RUNTIME state", sid) + logger.debug("checking session %d for RUNTIME state", sid) session = self.coreemu.sessions.get(sid) retries = 10 # wait for session to enter RUNTIME state, to prevent GUI from # connecting while nodes are still being instantiated while session.state != EventTypes.RUNTIME_STATE: - logging.debug( + logger.debug( "waiting for session %d to enter RUNTIME state", sid ) time.sleep(1) retries -= 1 if retries <= 0: - logging.debug("session %d did not enter RUNTIME state", sid) + logger.debug("session %d did not enter RUNTIME state", sid) return replies tlv_data = coreapi.CoreRegisterTlv.pack( @@ -988,7 +986,7 @@ class CoreHandler(socketserver.BaseRequestHandler): message = coreapi.CoreRegMessage.pack(0, tlv_data) replies.append(message) except Exception as e: - logging.exception("error executing: %s", execute_server) + logger.exception("error executing: %s", execute_server) tlv_data = coreapi.CoreExceptionTlv.pack(ExceptionTlvs.LEVEL.value, 2) tlv_data += coreapi.CoreExceptionTlv.pack( ExceptionTlvs.TEXT.value, str(e) @@ -1000,7 +998,7 @@ class CoreHandler(socketserver.BaseRequestHandler): gui = message.get_tlv(RegisterTlvs.GUI.value) if gui is None: - logging.debug("ignoring Register message") + logger.debug("ignoring Register message") else: # register capabilities with the GUI replies.append(self.register()) @@ -1031,7 +1029,7 @@ class CoreHandler(socketserver.BaseRequestHandler): network_id=message.get_tlv(ConfigTlvs.NETWORK_ID.value), opaque=message.get_tlv(ConfigTlvs.OPAQUE.value), ) - logging.debug( + logger.debug( "configuration message for %s node %s", config_data.object, config_data.node ) message_type = ConfigFlags(config_data.type) @@ -1055,9 +1053,7 @@ 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 self.session.emane.models: + elif config_data.object in EmaneModelManager.models: replies = self.handle_config_emane_models(message_type, config_data) else: raise Exception("no handler for configuration: %s", config_data.object) @@ -1106,7 +1102,7 @@ class CoreHandler(socketserver.BaseRequestHandler): self.session.location.reset() else: if not config_data.data_values: - logging.warning("location data missing") + logger.warning("location data missing") else: values = [float(x) for x in config_data.data_values.split("|")] @@ -1119,7 +1115,7 @@ class CoreHandler(socketserver.BaseRequestHandler): # geographic reference point self.session.location.setrefgeo(lat, lon, alt) self.session.location.refscale = values[5] - logging.info( + logger.info( "location configured: %s = %s scale=%s", self.session.location.refxyz, self.session.location.refgeo, @@ -1156,7 +1152,7 @@ class CoreHandler(socketserver.BaseRequestHandler): def handle_config_broker(self, message_type, config_data): if message_type not in [ConfigFlags.REQUEST, ConfigFlags.RESET]: if not config_data.data_values: - logging.info("emulation server data missing") + logger.info("emulation server data missing") else: values = config_data.data_values.split("|") @@ -1180,7 +1176,7 @@ class CoreHandler(socketserver.BaseRequestHandler): session_id = config_data.session opaque = config_data.opaque - logging.debug( + logger.debug( "configuration request: node(%s) session(%s) opaque(%s)", node_id, session_id, @@ -1210,10 +1206,10 @@ class CoreHandler(socketserver.BaseRequestHandler): values = [] group_strings = [] start_index = 1 - logging.debug("sorted groups: %s", groups) + logger.debug("sorted groups: %s", groups) for group in groups: services = sorted(group_map[group], key=lambda x: x.name.lower()) - logging.debug("sorted services for group(%s): %s", group, services) + logger.debug("sorted services for group(%s): %s", group, services) end_index = start_index + len(services) - 1 group_strings.append(f"{group}:{start_index}-{end_index}") start_index += len(services) @@ -1237,7 +1233,7 @@ class CoreHandler(socketserver.BaseRequestHandler): node = self.session.get_node(node_id, CoreNodeBase) if node is None: - logging.warning( + logger.warning( "request to configure service for unknown node %s", node_id ) return replies @@ -1296,7 +1292,7 @@ class CoreHandler(socketserver.BaseRequestHandler): error_message = "services config message that I don't know how to handle" if values is None: - logging.error(error_message) + logger.error(error_message) else: if opaque is None: values = values.split("|") @@ -1305,11 +1301,11 @@ class CoreHandler(socketserver.BaseRequestHandler): data_types is None or data_types[0] != ConfigDataTypes.STRING.value ): - logging.info(error_message) + logger.info(error_message) return None key = values.pop(0) self.session.services.default_services[key] = values - logging.debug("default services for type %s set to %s", key, values) + logger.debug("default services for type %s set to %s", key, values) elif node_id: services = ServiceShim.servicesfromopaque(opaque) if services: @@ -1348,16 +1344,16 @@ class CoreHandler(socketserver.BaseRequestHandler): values_str = config_data.data_values node_id = utils.iface_config_id(node_id, iface_id) - logging.debug( + logger.debug( "received configure message for %s nodenum: %s", object_name, node_id ) if message_type == ConfigFlags.REQUEST: - logging.info("replying to configure request for model: %s", object_name) + logger.info("replying to configure request for model: %s", object_name) typeflags = ConfigFlags.NONE.value model_class = self.session.mobility.models.get(object_name) if not model_class: - logging.warning("model class does not exist: %s", object_name) + logger.warning("model class does not exist: %s", object_name) return [] config = self.session.mobility.get_model_config(node_id, object_name) @@ -1368,7 +1364,7 @@ class CoreHandler(socketserver.BaseRequestHandler): elif message_type != ConfigFlags.RESET: # store the configuration values for later use, when the node if not object_name: - logging.warning("no configuration object for node: %s", node_id) + logger.warning("no configuration object for node: %s", node_id) return [] parsed_config = {} @@ -1382,42 +1378,12 @@ class CoreHandler(socketserver.BaseRequestHandler): if object_name == BasicRangeModel.name: node.updatemodel(parsed_config) except CoreError: - logging.error( + logger.error( "skipping mobility configuration for unknown node: %s", node_id ) 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) - logging.debug( - "received configure message for %s nodenum: %s", object_name, node_id - ) - if message_type == ConfigFlags.REQUEST: - logging.info("replying to configure request for %s model", object_name) - typeflags = ConfigFlags.NONE.value - config = self.session.emane.get_configs() - 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: - logging.info("no configuration object for node %s", node_id) - return [] - - if values_str: - config = ConfigShim.str_to_dict(values_str) - self.session.emane.set_configs(config) - - return replies - def handle_config_emane_models(self, message_type, config_data): replies = [] node_id = config_data.node @@ -1426,19 +1392,19 @@ class CoreHandler(socketserver.BaseRequestHandler): values_str = config_data.data_values node_id = utils.iface_config_id(node_id, iface_id) - logging.debug( + logger.debug( "received configure message for %s nodenum: %s", object_name, node_id ) if message_type == ConfigFlags.REQUEST: - logging.info("replying to configure request for model: %s", object_name) + logger.info("replying to configure request for model: %s", object_name) typeflags = ConfigFlags.NONE.value - model_class = self.session.emane.models.get(object_name) + model_class = self.session.emane.get_model(object_name) if not model_class: - logging.warning("model class does not exist: %s", object_name) + logger.warning("model class does not exist: %s", object_name) return [] - config = self.session.emane.get_model_config(node_id, object_name) + config = self.session.emane.get_config(node_id, object_name) config_response = ConfigShim.config_data( 0, node_id, typeflags, model_class, config ) @@ -1446,14 +1412,13 @@ class CoreHandler(socketserver.BaseRequestHandler): elif message_type != ConfigFlags.RESET: # store the configuration values for later use, when the node if not object_name: - logging.warning("no configuration object for node: %s", node_id) + logger.warning("no configuration object for node: %s", node_id) return [] - parsed_config = {} if values_str: parsed_config = ConfigShim.str_to_dict(values_str) - - self.session.emane.set_model_config(node_id, object_name, parsed_config) + self.session.emane.node_models[node_id] = object_name + self.session.emane.set_config(node_id, object_name, parsed_config) return replies @@ -1465,21 +1430,21 @@ class CoreHandler(socketserver.BaseRequestHandler): :return: reply messages """ if message.flags & MessageFlags.ADD.value: - node_num = message.get_tlv(FileTlvs.NODE.value) + node_id = message.get_tlv(FileTlvs.NODE.value) file_name = message.get_tlv(FileTlvs.NAME.value) file_type = message.get_tlv(FileTlvs.TYPE.value) - source_name = message.get_tlv(FileTlvs.SOURCE_NAME.value) + src_path = message.get_tlv(FileTlvs.SOURCE_NAME.value) + if src_path: + src_path = Path(src_path) data = message.get_tlv(FileTlvs.DATA.value) compressed_data = message.get_tlv(FileTlvs.COMPRESSED_DATA.value) if compressed_data: - logging.warning( - "Compressed file data not implemented for File message." - ) + logger.warning("Compressed file data not implemented for File message.") return () - if source_name and data: - logging.warning( + if src_path and data: + logger.warning( "ignoring invalid File message: source and data TLVs are both present" ) return () @@ -1490,29 +1455,30 @@ class CoreHandler(socketserver.BaseRequestHandler): if file_type.startswith("service:"): _, service_name = file_type.split(":")[:2] self.session.services.set_service_file( - node_num, service_name, file_name, data + node_id, service_name, file_name, data ) return () elif file_type.startswith("hook:"): _, state = file_type.split(":")[:2] if not state.isdigit(): - logging.error("error setting hook having state '%s'", state) + logger.error("error setting hook having state '%s'", state) return () state = int(state) state = EventTypes(state) - self.session.add_hook(state, file_name, data, source_name) + self.session.add_hook(state, file_name, data, src_path) return () # writing a file to the host - if node_num is None: - if source_name is not None: - shutil.copy2(source_name, file_name) + if node_id is None: + if src_path is not None: + shutil.copy2(src_path, file_name) else: - with open(file_name, "w") as open_file: - open_file.write(data) + with file_name.open("w") as f: + f.write(data) return () - self.session.add_node_file(node_num, source_name, file_name, data) + file_path = Path(file_name) + self.session.add_node_file(node_id, src_path, file_path, data) else: raise NotImplementedError @@ -1525,7 +1491,7 @@ class CoreHandler(socketserver.BaseRequestHandler): :param message: interface message to handle :return: reply messages """ - logging.info("ignoring Interface message") + logger.info("ignoring Interface message") return () def handle_event_message(self, message): @@ -1551,7 +1517,7 @@ class CoreHandler(socketserver.BaseRequestHandler): raise NotImplementedError("Event message missing event type") node_id = event_data.node - logging.debug("handling event %s at %s", event_type.name, time.ctime()) + logger.debug("handling event %s at %s", event_type.name, time.ctime()) if event_type.value <= EventTypes.SHUTDOWN_STATE.value: if node_id is not None: node = self.session.get_node(node_id, NodeBase) @@ -1563,31 +1529,37 @@ class CoreHandler(socketserver.BaseRequestHandler): self.session.start_mobility(node_ids=[node.id]) return () - logging.warning( + logger.warning( "dropping unhandled event message for node: %s", node.name ) return () - self.session.set_state(event_type) if event_type == EventTypes.DEFINITION_STATE: + self.session.set_state(event_type) # clear all session objects in order to receive new definitions self.session.clear() + elif event_type == EventTypes.CONFIGURATION_STATE: + self.session.set_state(event_type) elif event_type == EventTypes.INSTANTIATION_STATE: + self.session.set_state(event_type) if len(self.handler_threads) > 1: # TODO: sync handler threads here before continuing time.sleep(2.0) # XXX # done receiving node/link configuration, ready to instantiate self.session.instantiate() - # after booting nodes attempt to send emulation id for nodes waiting on status + # after booting nodes attempt to send emulation id for nodes + # waiting on status for _id in self.session.nodes: self.send_node_emulation_id(_id) elif event_type == EventTypes.RUNTIME_STATE: - logging.warning("Unexpected event message: RUNTIME state received") + self.session.set_state(event_type) + logger.warning("Unexpected event message: RUNTIME state received") elif event_type == EventTypes.DATACOLLECT_STATE: self.session.data_collect() elif event_type == EventTypes.SHUTDOWN_STATE: - logging.warning("Unexpected event message: SHUTDOWN state received") + self.session.set_state(event_type) + logger.warning("Unexpected event message: SHUTDOWN state received") elif event_type in { EventTypes.START, EventTypes.STOP, @@ -1607,26 +1579,26 @@ class CoreHandler(socketserver.BaseRequestHandler): self.session.mobility_event(event_data) handled = True if not handled: - logging.warning( + logger.warning( "unhandled event message: event type %s, name %s ", event_type.name, name, ) elif event_type == EventTypes.FILE_OPEN: - filename = event_data.name - self.session.open_xml(filename, start=False) + file_path = Path(event_data.name) + self.session.open_xml(file_path, start=False) self.send_objects() return () elif event_type == EventTypes.FILE_SAVE: - filename = event_data.name - self.session.save_xml(filename) + file_path = Path(event_data.name) + self.session.save_xml(file_path) elif event_type == EventTypes.SCHEDULED: etime = event_data.time node_id = event_data.node name = event_data.name data = event_data.data if etime is None: - logging.warning("Event message scheduled event missing start time") + logger.warning("Event message scheduled event missing start time") return () if message.flags & MessageFlags.ADD.value: self.session.add_event( @@ -1652,7 +1624,7 @@ class CoreHandler(socketserver.BaseRequestHandler): try: node = self.session.get_node(node_id, CoreNodeBase) except CoreError: - logging.warning( + logger.warning( "ignoring event for service '%s', unknown node '%s'", name, node_id ) return @@ -1694,7 +1666,7 @@ class CoreHandler(socketserver.BaseRequestHandler): if num > 1: unknown_data += ", " num -= 1 - logging.warning("Event requested for unknown service(s): %s", unknown_data) + logger.warning("Event requested for unknown service(s): %s", unknown_data) unknown_data = f"Unknown:{unknown_data}" event_data = EventData( @@ -1722,7 +1694,7 @@ class CoreHandler(socketserver.BaseRequestHandler): files = coreapi.str_to_list(file_str) thumb = message.get_tlv(SessionTlvs.THUMB.value) user = message.get_tlv(SessionTlvs.USER.value) - logging.debug( + logger.debug( "SESSION message flags=0x%x sessions=%s", message.flags, session_id_str ) @@ -1733,20 +1705,16 @@ class CoreHandler(socketserver.BaseRequestHandler): session = self.session else: session = self.coreemu.sessions.get(session_id) - if session is None: - logging.warning("session %s not found", session_id) + logger.warning("session %s not found", session_id) continue - if names is not None: session.name = names[index] - if files is not None: - session.file_name = files[index] - + session.file_path = Path(files[index]) if thumb: + thumb = Path(thumb) session.set_thumbnail(thumb) - if user: session.set_user(user) elif ( @@ -1762,14 +1730,14 @@ class CoreHandler(socketserver.BaseRequestHandler): session = self.coreemu.sessions.get(session_id) if session is None: - logging.info( + logger.info( "session %s not found (flags=0x%x)", session_id, message.flags ) continue if message.flags & MessageFlags.ADD.value: # connect to the first session that exists - logging.info("request to connect to session %s", session_id) + logger.info("request to connect to session %s", session_id) # remove client from session broker and shutdown if needed self.remove_session_handlers() @@ -1786,7 +1754,7 @@ class CoreHandler(socketserver.BaseRequestHandler): clients.append(self) # add broadcast handlers - logging.info("adding session broadcast handlers") + logger.info("adding session broadcast handlers") self.add_session_handlers() if user: @@ -1796,12 +1764,10 @@ class CoreHandler(socketserver.BaseRequestHandler): self.send_objects() elif message.flags & MessageFlags.DELETE.value: # shut down the specified session(s) - logging.info("request to terminate session %s", session_id) + logger.info("request to terminate session %s", session_id) self.coreemu.delete_session(session_id) else: - logging.warning( - "unhandled session flags for session %s", session_id - ) + logger.warning("unhandled session flags for session %s", session_id) return () @@ -1823,9 +1789,7 @@ class CoreHandler(socketserver.BaseRequestHandler): try: self.sendall(reply) except IOError: - logging.exception( - "error sending node emulation id message: %s", node_id - ) + logger.exception("error sending node emulation id message: %s", node_id) del self.node_status_request[node_id] @@ -1851,7 +1815,7 @@ class CoreHandler(socketserver.BaseRequestHandler): for model_name in mobility_configs: config = mobility_configs[model_name] model_class = self.session.mobility.models[model_name] - logging.debug( + logger.debug( "mobility config: node(%s) class(%s) values(%s)", node_id, model_class, @@ -1862,21 +1826,11 @@ class CoreHandler(socketserver.BaseRequestHandler): ) self.session.broadcast_config(config_data) - # send global emane config - config = self.session.emane.get_configs() - logging.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 in self.session.emane.nodes(): - emane_configs = self.session.emane.get_all_configs(node_id) - for model_name in emane_configs: - config = emane_configs[model_name] - model_class = self.session.emane.models[model_name] - logging.debug( + for node_id, model_configs in self.session.emane.node_configs.items(): + for model_name, config in model_configs.items(): + model_class = self.session.emane.get_model(model_name) + logger.debug( "emane config: node(%s) class(%s) values(%s)", node_id, model_class, @@ -1957,7 +1911,7 @@ class CoreHandler(socketserver.BaseRequestHandler): self.session.broadcast_config(config_data) node_count = self.session.get_node_count() - logging.info( + logger.info( "informed GUI about %d nodes and %d links", node_count, len(all_links) ) @@ -1997,11 +1951,11 @@ class CoreUdpHandler(CoreHandler): header ) if message_len == 0: - logging.warning("received message with no data") + logger.warning("received message with no data") return if len(data) != coreapi.CoreMessage.header_len + message_len: - logging.error( + logger.error( "received message length does not match received data (%s != %s)", len(data), coreapi.CoreMessage.header_len + message_len, @@ -2019,7 +1973,7 @@ class CoreUdpHandler(CoreHandler): message_flags, header, data[coreapi.CoreMessage.header_len :] ) message.msgtype = message_type - logging.exception("unimplemented core message type: %s", message.type_str()) + logger.exception("unimplemented core message type: %s", message.type_str()) def handle(self): message = self.receive_message() @@ -2029,12 +1983,12 @@ class CoreUdpHandler(CoreHandler): for session_id in sessions: session = self.server.mainserver.coreemu.sessions.get(session_id) if session: - logging.debug("session handling message: %s", session.id) + logger.debug("session handling message: %s", session.id) self.session = session self.handle_message(message) self.broadcast(message) else: - logging.error( + logger.error( "session %d in %s message not found.", session_id, message.type_str(), @@ -2058,7 +2012,7 @@ class CoreUdpHandler(CoreHandler): self.handle_message(message) self.broadcast(message) else: - logging.error( + logger.error( "no active session, dropping %s message.", message.type_str() ) @@ -2071,7 +2025,7 @@ class CoreUdpHandler(CoreHandler): try: client.sendall(message.raw_message) except IOError: - logging.error("error broadcasting") + logger.error("error broadcasting") def finish(self): return socketserver.BaseRequestHandler.finish(self) diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py index 8a26300a..d625a615 100644 --- a/daemon/core/api/tlv/dataconversion.py +++ b/daemon/core/api/tlv/dataconversion.py @@ -10,6 +10,8 @@ from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs from core.config import ConfigGroup, ConfigurableOptions from core.emulator.data import ConfigData, NodeData +logger = logging.getLogger(__name__) + def convert_node(node_data: NodeData): """ @@ -139,9 +141,9 @@ class ConfigShim: captions = None data_types = [] possible_values = [] - logging.debug("configurable: %s", configurable_options) - logging.debug("configuration options: %s", configurable_options.configurations) - logging.debug("configuration data: %s", config) + logger.debug("configurable: %s", configurable_options) + logger.debug("configuration options: %s", configurable_options.configurations) + logger.debug("configuration data: %s", config) for configuration in configurable_options.configurations(): if not captions: captions = configuration.label diff --git a/daemon/core/api/tlv/structutils.py b/daemon/core/api/tlv/structutils.py index 41358848..d67f388e 100644 --- a/daemon/core/api/tlv/structutils.py +++ b/daemon/core/api/tlv/structutils.py @@ -4,6 +4,8 @@ Utilities for working with python struct data. import logging +logger = logging.getLogger(__name__) + def pack_values(clazz, packers): """ @@ -15,7 +17,7 @@ def pack_values(clazz, packers): """ # iterate through tuples of values to pack - logging.debug("packing: %s", packers) + logger.debug("packing: %s", packers) data = b"" for packer in packers: # check if a transformer was provided for valid values @@ -37,7 +39,7 @@ def pack_values(clazz, packers): value = transformer(value) # pack and add to existing data - logging.debug("packing: %s - %s type(%s)", tlv_type, value, type(value)) + logger.debug("packing: %s - %s type(%s)", tlv_type, value, type(value)) data += clazz.pack(tlv_type.value, value) return data diff --git a/daemon/core/config.py b/daemon/core/config.py index 222abf01..b705e8b6 100644 --- a/daemon/core/config.py +++ b/daemon/core/config.py @@ -4,73 +4,107 @@ Common support for configurable CORE objects. import logging from collections import OrderedDict -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Type, Union from core.emane.nodes import EmaneNet from core.emulator.enumerations import ConfigDataTypes +from core.errors import CoreConfigError from core.nodes.network import WlanNode +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.location.mobility import WirelessModel WirelessModelType = Type[WirelessModel] +_BOOL_OPTIONS: Set[str] = {"0", "1"} + +@dataclass class ConfigGroup: """ Defines configuration group tabs used for display by ConfigurationOptions. """ - def __init__(self, name: str, start: int, stop: int) -> None: - """ - Creates a ConfigGroup object. - - :param name: configuration group display name - :param start: configurations start index for this group - :param stop: configurations stop index for this group - """ - self.name: str = name - self.start: int = start - self.stop: int = stop + name: str + start: int + stop: int +@dataclass class Configuration: """ - Represents a configuration options. + Represents a configuration option. """ - def __init__( - self, - _id: str, - _type: ConfigDataTypes, - label: str = None, - default: str = "", - options: List[str] = None, - ) -> None: - """ - Creates a Configuration object. + id: str + type: ConfigDataTypes + label: str = None + default: str = "" + options: List[str] = field(default_factory=list) - :param _id: unique name for configuration - :param _type: configuration data type - :param label: configuration label for display - :param default: default value for configuration - :param options: list options if this is a configuration with a combobox - """ - self.id: str = _id - self.type: ConfigDataTypes = _type - self.default: str = default - if not options: - options = [] - self.options: List[str] = options - if not label: - label = _id - self.label: str = label + def __post_init__(self) -> None: + self.label = self.label if self.label else self.id + if self.type == ConfigDataTypes.BOOL: + if self.default and self.default not in _BOOL_OPTIONS: + raise CoreConfigError( + f"{self.id} bool value must be one of: {_BOOL_OPTIONS}: " + f"{self.default}" + ) + elif self.type == ConfigDataTypes.FLOAT: + if self.default: + try: + float(self.default) + except ValueError: + raise CoreConfigError( + f"{self.id} is not a valid float: {self.default}" + ) + elif self.type != ConfigDataTypes.STRING: + if self.default: + try: + int(self.default) + except ValueError: + raise CoreConfigError( + f"{self.id} is not a valid int: {self.default}" + ) - def __str__(self): - return ( - f"{self.__class__.__name__}(id={self.id}, type={self.type}, " - f"default={self.default}, options={self.options})" - ) + +@dataclass +class ConfigBool(Configuration): + """ + Represents a boolean configuration option. + """ + + type: ConfigDataTypes = ConfigDataTypes.BOOL + + +@dataclass +class ConfigFloat(Configuration): + """ + Represents a float configuration option. + """ + + type: ConfigDataTypes = ConfigDataTypes.FLOAT + + +@dataclass +class ConfigInt(Configuration): + """ + Represents an integer configuration option. + """ + + type: ConfigDataTypes = ConfigDataTypes.INT32 + + +@dataclass +class ConfigString(Configuration): + """ + Represents a string configuration option. + """ + + type: ConfigDataTypes = ConfigDataTypes.STRING class ConfigurableOptions: @@ -182,7 +216,7 @@ class ConfigurableManager: :param config_type: configuration type to store configuration for :return: nothing """ - logging.debug( + logger.debug( "setting config for node(%s) type(%s): %s", node_id, config_type, config ) node_configs = self.node_configurations.setdefault(node_id, OrderedDict()) @@ -314,7 +348,7 @@ class ModelManager(ConfigurableManager): :param config: model configuration, None for default configuration :return: nothing """ - logging.debug( + logger.debug( "setting model(%s) for node(%s): %s", model_class.name, node.id, config ) self.set_model_config(node.id, model_class.name, config) @@ -343,5 +377,5 @@ class ModelManager(ConfigurableManager): model_class = self.models[model_name] models.append((model_class, config)) - logging.debug("models for node(%s): %s", node.id, models) + logger.debug("models for node(%s): %s", node.id, models) return models diff --git a/daemon/core/configservice/base.py b/daemon/core/configservice/base.py index bb97e321..386ab26d 100644 --- a/daemon/core/configservice/base.py +++ b/daemon/core/configservice/base.py @@ -2,9 +2,10 @@ import abc import enum import inspect import logging -import pathlib import time -from typing import Any, Dict, List +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Optional from mako import exceptions from mako.lookup import TemplateLookup @@ -14,6 +15,7 @@ from core.config import Configuration from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNode +logger = logging.getLogger(__name__) TEMPLATES_DIR: str = "templates" @@ -27,6 +29,14 @@ class ConfigServiceBootError(Exception): pass +@dataclass +class ShadowDir: + path: str + src: Optional[str] = None + templates: bool = False + has_node_paths: bool = False + + class ConfigService(abc.ABC): """ Base class for creating configurable services. @@ -38,6 +48,9 @@ class ConfigService(abc.ABC): # time to wait in seconds for determining if service started successfully validation_timer: int = 5 + # directories to shadow and copy files from + shadow_directories: List[ShadowDir] = [] + def __init__(self, node: CoreNode) -> None: """ Create ConfigService instance. @@ -46,7 +59,7 @@ class ConfigService(abc.ABC): """ self.node: CoreNode = node class_file = inspect.getfile(self.__class__) - templates_path = pathlib.Path(class_file).parent.joinpath(TEMPLATES_DIR) + templates_path = Path(class_file).parent.joinpath(TEMPLATES_DIR) self.templates: TemplateLookup = TemplateLookup(directories=templates_path) self.config: Dict[str, Configuration] = {} self.custom_templates: Dict[str, str] = {} @@ -133,7 +146,8 @@ class ConfigService(abc.ABC): :return: nothing :raises ConfigServiceBootError: when there is an error starting service """ - logging.info("node(%s) service(%s) starting...", self.node.name, self.name) + logger.info("node(%s) service(%s) starting...", self.node.name, self.name) + self.create_shadow_dirs() self.create_dirs() self.create_files() wait = self.validation_mode == ConfigServiceMode.BLOCKING @@ -154,7 +168,7 @@ class ConfigService(abc.ABC): try: self.node.cmd(cmd) except CoreCommandError: - logging.exception( + logger.exception( f"node({self.node.name}) service({self.name}) " f"failed shutdown: {cmd}" ) @@ -168,6 +182,64 @@ class ConfigService(abc.ABC): self.stop() self.start() + def create_shadow_dirs(self) -> None: + """ + Creates a shadow of a host system directory recursively + to be mapped and live within a node. + + :return: nothing + :raises CoreError: when there is a failure creating a directory or file + """ + for shadow_dir in self.shadow_directories: + # setup shadow and src paths, using node unique paths when configured + shadow_path = Path(shadow_dir.path) + if shadow_dir.src is None: + src_path = shadow_path + else: + src_path = Path(shadow_dir.src) + if shadow_dir.has_node_paths: + src_path = src_path / self.node.name + # validate shadow and src paths + if not shadow_path.is_absolute(): + raise CoreError(f"shadow dir({shadow_path}) is not absolute") + if not src_path.is_absolute(): + raise CoreError(f"shadow source dir({src_path}) is not absolute") + if not src_path.is_dir(): + raise CoreError(f"shadow source dir({src_path}) does not exist") + # create root of the shadow path within node + logger.info( + "node(%s) creating shadow directory(%s) src(%s) node paths(%s) " + "templates(%s)", + self.node.name, + shadow_path, + src_path, + shadow_dir.has_node_paths, + shadow_dir.templates, + ) + self.node.create_dir(shadow_path) + # find all directories and files to create + dir_paths = [] + file_paths = [] + for path in src_path.rglob("*"): + shadow_src_path = shadow_path / path.relative_to(src_path) + if path.is_dir(): + dir_paths.append(shadow_src_path) + else: + file_paths.append((path, shadow_src_path)) + # create all directories within node + for path in dir_paths: + self.node.create_dir(path) + # create all files within node, from templates when configured + data = self.data() + templates = TemplateLookup(directories=src_path) + for path, dst_path in file_paths: + if shadow_dir.templates: + template = templates.get_template(path.name) + rendered = self._render(template, data) + self.node.create_file(dst_path, rendered) + else: + self.node.copy_file(path, dst_path) + def create_dirs(self) -> None: """ Creates directories for service. @@ -175,10 +247,12 @@ class ConfigService(abc.ABC): :return: nothing :raises CoreError: when there is a failure creating a directory """ - for directory in self.directories: + logger.debug("creating config service directories") + for directory in sorted(self.directories): + dir_path = Path(directory) try: - self.node.privatedir(directory) - except (CoreCommandError, ValueError): + self.node.create_dir(dir_path) + except (CoreCommandError, CoreError): raise CoreError( f"node({self.node.name}) service({self.name}) " f"failure to create service directory: {directory}" @@ -219,17 +293,21 @@ class ConfigService(abc.ABC): :return: mapping of files to templates """ templates = {} - for name in self.files: - basename = pathlib.Path(name).name - if name in self.custom_templates: - template = self.custom_templates[name] - template = self.clean_text(template) - elif self.templates.has_template(basename): - template = self.templates.get_template(basename).source + for file in self.files: + file_path = Path(file) + if file_path.is_absolute(): + template_path = str(file_path.relative_to("/")) else: - template = self.get_text_template(name) + template_path = str(file_path) + if file in self.custom_templates: + template = self.custom_templates[file] template = self.clean_text(template) - templates[name] = template + elif self.templates.has_template(template_path): + template = self.templates.get_template(template_path).source + else: + template = self.get_text_template(file) + template = self.clean_text(template) + templates[file] = template return templates def create_files(self) -> None: @@ -239,24 +317,20 @@ class ConfigService(abc.ABC): :return: nothing """ data = self.data() - for name in self.files: - basename = pathlib.Path(name).name - if name in self.custom_templates: - text = self.custom_templates[name] - rendered = self.render_text(text, data) - elif self.templates.has_template(basename): - rendered = self.render_template(basename, data) - else: - text = self.get_text_template(name) - rendered = self.render_text(text, data) - logging.debug( - "node(%s) service(%s) template(%s): \n%s", - self.node.name, - self.name, - name, - rendered, + for file in sorted(self.files): + logger.debug( + "node(%s) service(%s) template(%s)", self.node.name, self.name, file ) - self.node.nodefile(name, rendered) + file_path = Path(file) + if file in self.custom_templates: + text = self.custom_templates[file] + rendered = self.render_text(text, data) + elif self.templates.has_template(file_path.name): + rendered = self.render_template(file_path.name, data) + else: + text = self.get_text_template(file) + rendered = self.render_text(text, data) + self.node.create_file(file_path, rendered) def run_startup(self, wait: bool) -> None: """ @@ -300,7 +374,7 @@ class ConfigService(abc.ABC): del cmds[index] index += 1 except CoreCommandError: - logging.debug( + logger.debug( f"node({self.node.name}) service({self.name}) " f"validate command failed: {cmd}" ) diff --git a/daemon/core/configservice/dependencies.py b/daemon/core/configservice/dependencies.py index be1c45e7..b24c83c6 100644 --- a/daemon/core/configservice/dependencies.py +++ b/daemon/core/configservice/dependencies.py @@ -1,6 +1,8 @@ import logging from typing import TYPE_CHECKING, Dict, List, Set +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.configservice.base import ConfigService @@ -41,7 +43,7 @@ class ConfigServiceDependencies: for name in self.node_services: service = self.node_services[name] if service.name in self.started: - logging.debug( + logger.debug( "skipping service that will already be started: %s", service.name ) continue @@ -75,7 +77,7 @@ class ConfigServiceDependencies: :param service: service to check dependencies for :return: list of config services to start in order """ - logging.debug("starting service dependency check: %s", service.name) + logger.debug("starting service dependency check: %s", service.name) self._reset() return self._visit(service) @@ -86,7 +88,7 @@ class ConfigServiceDependencies: :param current_service: service being visited :return: list of dependent services for a visited service """ - logging.debug("visiting service(%s): %s", current_service.name, self.path) + logger.debug("visiting service(%s): %s", current_service.name, self.path) self.visited.add(current_service.name) self.visiting.add(current_service.name) @@ -109,7 +111,7 @@ class ConfigServiceDependencies: self._visit(service) # add service when bottom is found - logging.debug("adding service to startup path: %s", current_service.name) + logger.debug("adding service to startup path: %s", current_service.name) self.started.add(current_service.name) self.path.append(current_service) self.visiting.remove(current_service.name) diff --git a/daemon/core/configservice/manager.py b/daemon/core/configservice/manager.py index 83657655..1fd26e43 100644 --- a/daemon/core/configservice/manager.py +++ b/daemon/core/configservice/manager.py @@ -1,11 +1,15 @@ import logging import pathlib +import pkgutil +from pathlib import Path from typing import Dict, List, Type -from core import utils +from core import configservices, utils from core.configservice.base import ConfigService from core.errors import CoreError +logger = logging.getLogger(__name__) + class ConfigServiceManager: """ @@ -28,7 +32,7 @@ class ConfigServiceManager: """ service_class = self.services.get(name) if service_class is None: - raise CoreError(f"service does not exit {name}") + raise CoreError(f"service does not exist {name}") return service_class def add(self, service: Type[ConfigService]) -> None: @@ -40,7 +44,7 @@ class ConfigServiceManager: :raises CoreError: when service is a duplicate or has unmet executables """ name = service.name - logging.debug( + logger.debug( "loading service: class(%s) name(%s)", service.__class__.__name__, name ) @@ -55,27 +59,46 @@ class ConfigServiceManager: except CoreError as e: raise CoreError(f"config service({service.name}): {e}") - # make service available + # make service available self.services[name] = service - def load(self, path: str) -> List[str]: + def load_locals(self) -> List[str]: """ - Search path provided for configurable services and add them for being managed. + Search and add config service from local core module. + + :return: list of errors when loading services + """ + errors = [] + for module_info in pkgutil.walk_packages( + configservices.__path__, f"{configservices.__name__}." + ): + services = utils.load_module(module_info.name, ConfigService) + for service in services: + try: + self.add(service) + except CoreError as e: + errors.append(service.name) + logger.debug("not loading config service(%s): %s", service.name, e) + return errors + + def load(self, path: Path) -> List[str]: + """ + Search path provided for config services and add them for being managed. :param path: path to search configurable services - :return: list errors when loading and adding services + :return: list errors when loading services """ path = pathlib.Path(path) subdirs = [x for x in path.iterdir() if x.is_dir()] subdirs.append(path) service_errors = [] for subdir in subdirs: - logging.debug("loading config services from: %s", subdir) - services = utils.load_classes(str(subdir), ConfigService) + logger.debug("loading config services from: %s", subdir) + services = utils.load_classes(subdir, ConfigService) for service in services: try: self.add(service) except CoreError as e: service_errors.append(service.name) - logging.debug("not loading service(%s): %s", service.name, e) + logger.debug("not loading service(%s): %s", service.name, e) return service_errors diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index 8ea9fcf4..fba892b4 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -9,6 +9,7 @@ from core.nodes.base import CoreNodeBase from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.network import WlanNode +logger = logging.getLogger(__name__) GROUP: str = "Quagga" QUAGGA_STATE_DIR: str = "/var/run/quagga" @@ -101,9 +102,9 @@ class Zebra(ConfigService): ip4s = [] ip6s = [] for ip4 in iface.ip4s: - ip4s.append(str(ip4.ip)) + ip4s.append(str(ip4)) for ip6 in iface.ip6s: - ip6s.append(str(ip6.ip)) + ip6s.append(str(ip6)) ifaces.append((iface, ip4s, ip6s, iface.control)) return dict( @@ -226,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)) - logging.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/configservices/securityservices/services.py b/daemon/core/configservices/securityservices/services.py index c656f5ca..e866617c 100644 --- a/daemon/core/configservices/securityservices/services.py +++ b/daemon/core/configservices/securityservices/services.py @@ -1,8 +1,7 @@ from typing import Any, Dict, List -from core.config import Configuration +from core.config import ConfigString, Configuration from core.configservice.base import ConfigService, ConfigServiceMode -from core.emulator.enumerations import ConfigDataTypes GROUP_NAME: str = "Security" @@ -19,24 +18,9 @@ class VpnClient(ConfigService): shutdown: List[str] = ["killall openvpn"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING default_configs: List[Configuration] = [ - Configuration( - _id="keydir", - _type=ConfigDataTypes.STRING, - label="Key Dir", - default="/etc/core/keys", - ), - Configuration( - _id="keyname", - _type=ConfigDataTypes.STRING, - label="Key Name", - default="client1", - ), - Configuration( - _id="server", - _type=ConfigDataTypes.STRING, - label="Server", - default="10.0.2.10", - ), + ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"), + ConfigString(id="keyname", label="Key Name", default="client1"), + ConfigString(id="server", label="Server", default="10.0.2.10"), ] modes: Dict[str, Dict[str, str]] = {} @@ -53,24 +37,9 @@ class VpnServer(ConfigService): shutdown: List[str] = ["killall openvpn"] validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING default_configs: List[Configuration] = [ - Configuration( - _id="keydir", - _type=ConfigDataTypes.STRING, - label="Key Dir", - default="/etc/core/keys", - ), - Configuration( - _id="keyname", - _type=ConfigDataTypes.STRING, - label="Key Name", - default="server", - ), - Configuration( - _id="subnet", - _type=ConfigDataTypes.STRING, - label="Subnet", - default="10.0.200.0", - ), + ConfigString(id="keydir", label="Key Dir", default="/etc/core/keys"), + ConfigString(id="keyname", label="Key Name", default="server"), + ConfigString(id="subnet", label="Subnet", default="10.0.200.0"), ] modes: Dict[str, Dict[str, str]] = {} diff --git a/daemon/core/configservices/simpleservice.py b/daemon/core/configservices/simpleservice.py deleted file mode 100644 index c2e7242f..00000000 --- a/daemon/core/configservices/simpleservice.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import Dict, List - -from core.config import Configuration -from core.configservice.base import ConfigService, ConfigServiceMode -from core.emulator.enumerations import ConfigDataTypes - - -class SimpleService(ConfigService): - name: str = "Simple" - group: str = "SimpleGroup" - directories: List[str] = ["/etc/quagga", "/usr/local/lib"] - files: List[str] = ["test1.sh", "test2.sh"] - executables: List[str] = [] - dependencies: List[str] = [] - startup: List[str] = [] - validate: List[str] = [] - shutdown: List[str] = [] - validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING - default_configs: List[Configuration] = [ - Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"), - Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"), - Configuration( - _id="value3", - _type=ConfigDataTypes.STRING, - label="Multiple Choice", - options=["value1", "value2", "value3"], - ), - ] - modes: Dict[str, Dict[str, str]] = { - "mode1": {"value1": "value1", "value2": "0", "value3": "value2"}, - "mode2": {"value1": "value2", "value2": "1", "value3": "value3"}, - "mode3": {"value1": "value3", "value2": "0", "value3": "value1"}, - } - - def get_text_template(self, name: str) -> str: - if name == "test1.sh": - return """ - # sample script 1 - # node id(${node.id}) name(${node.name}) - # config: ${config} - echo hello - """ - elif name == "test2.sh": - return """ - # sample script 2 - # node id(${node.id}) name(${node.name}) - # config: ${config} - echo hello2 - """ diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py index 633da333..3a4addfe 100644 --- a/daemon/core/configservices/utilservices/services.py +++ b/daemon/core/configservices/utilservices/services.py @@ -149,11 +149,13 @@ class DhcpService(ConfigService): subnets = [] for iface in self.node.get_ifaces(control=False): for ip4 in iface.ip4s: + if ip4.size == 1: + continue # divide the address space in half index = (ip4.size - 2) / 2 rangelow = ip4[index] rangehigh = ip4[-2] - subnets.append((ip4.ip, ip4.netmask, rangelow, rangehigh, str(ip4.ip))) + subnets.append((ip4.cidr.ip, ip4.netmask, rangelow, rangehigh, ip4.ip)) return dict(subnets=subnets) diff --git a/daemon/core/constants.py.in b/daemon/core/constants.py.in index cb566e40..1ade8287 100644 --- a/daemon/core/constants.py.in +++ b/daemon/core/constants.py.in @@ -1,3 +1,5 @@ -COREDPY_VERSION = "@PACKAGE_VERSION@" -CORE_CONF_DIR = "@CORE_CONF_DIR@" -CORE_DATA_DIR = "@CORE_DATA_DIR@" +from pathlib import Path + +COREDPY_VERSION: str = "@PACKAGE_VERSION@" +CORE_CONF_DIR: Path = Path("@CORE_CONF_DIR@") +CORE_DATA_DIR: Path = Path("@CORE_DATA_DIR@") diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 6ae66b93..4ffed725 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -1,66 +1,53 @@ """ -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 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, ModelManager -from core.emane import emanemanifest -from core.emane.bypass import EmaneBypassModel -from core.emane.commeffect import EmaneCommEffectModel from core.emane.emanemodel import EmaneModel -from core.emane.ieee80211abg import EmaneIeee80211abgModel from core.emane.linkmonitor import EmaneLinkMonitor +from core.emane.modelmanager import EmaneModelManager from core.emane.nodes import EmaneNet -from core.emane.rfpipe import EmaneRfPipeModel -from core.emane.tdma import EmaneTdmaModel 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 +logger = logging.getLogger(__name__) + 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 EventServiceException = None - logging.debug("compatible emane python bindings not installed") + logger.debug("compatible emane python bindings not installed") -EMANE_MODELS = [ - EmaneRfPipeModel, - EmaneIeee80211abgModel, - EmaneCommEffectModel, - EmaneBypassModel, - EmaneTdmaModel, -] DEFAULT_EMANE_PREFIX = "/usr" DEFAULT_DEV = "ctrl0" +DEFAULT_LOG_LEVEL: int = 3 class EmaneState(Enum): @@ -69,13 +56,60 @@ 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(ModelManager): +class EmaneManager: """ EMANE controller object. Lives in a Session instance and is used for building EMANE config files for all EMANE networks in this emulation, and for @@ -84,9 +118,6 @@ class EmaneManager(ModelManager): name: str = "emane" config_type: RegisterTlvs = RegisterTlvs.EMULATION_SERVER - NOT_READY: int = 2 - EVENTCFGVAR: str = "LIBEMANEEVENTSERVICECONFIG" - DEFAULT_LOG_LEVEL: int = 3 def __init__(self, session: "Session") -> None: """ @@ -112,23 +143,74 @@ class EmaneManager(ModelManager): self.eventmonthread: Optional[threading.Thread] = None # model for global EMANE configuration options - self.emane_config: EmaneGlobalModel = EmaneGlobalModel(session) - self.set_configs(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 - self.emane_check() - - def next_nem_id(self) -> int: - nem_id = int(self.get_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( + self, key: int, model: str, default: bool = True + ) -> Optional[Dict[str, str]]: + """ + Get the current or default configuration for an emane model. + + :param key: key to get configuration for + :param model: emane model to get configuration for + :param default: True to return default configuration when none exists, False + otherwise + :return: emane model configuration + :raises CoreError: when model does not exist + """ + model_class = self.get_model(model) + model_configs = self.node_configs.get(key) + config = None + if model_configs: + config = model_configs.get(model) + if config is None and default: + config = model_class.default_values() + return config + + def set_config(self, key: int, model: str, config: Dict[str, str] = None) -> None: + """ + Sets and update the provided configuration against the default model + or currently set emane model configuration. + + :param key: configuration key to set + :param model: model to set configuration for + :param config: configuration to update current configuration with + :return: nothing + :raises CoreError: when model does not exist + """ + self.get_model(model) + model_config = self.get_config(key, model) + config = config if config else {} + model_config.update(config) + model_configs = self.node_configs.setdefault(key, {}) + model_configs[model] = model_config + + def get_model(self, model_name: str) -> Type[EmaneModel]: + """ + Convenience method for getting globally loaded emane models. + + :param model_name: name of model to retrieve + :return: emane model class + :raises CoreError: when model does not exist + """ + return EmaneModelManager.get(model_name) + def get_iface_config( self, emane_net: EmaneNet, iface: CoreInterface ) -> Dict[str, str]: @@ -146,108 +228,27 @@ class EmaneManager(ModelManager): # try to retrieve interface specific configuration if iface.node_id is not None: key = utils.iface_config_id(iface.node.id, iface.node_id) - config = self.get_configs(node_id=key, config_type=model_name) + config = self.get_config(key, model_name, default=False) # attempt to retrieve node specific config, when iface config is not present if not config: - config = self.get_configs(node_id=iface.node.id, config_type=model_name) + config = self.get_config(iface.node.id, model_name, default=False) # 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) + config = self.get_config(emane_net.id, model_name, default=False) # 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: - super().config_reset(node_id) - self.set_configs(self.emane_config.default_values()) - - def emane_check(self) -> None: - """ - Check if emane is installed and load models. - - :return: nothing - """ - # check for emane - path = utils.which("emane", required=False) - if not path: - logging.info("emane is not installed") - return - - # get version - emane_version = utils.cmd("emane --version") - logging.info("using emane: %s", emane_version) - - # load default emane models - self.load_models(EMANE_MODELS) - - # load custom models - custom_models_path = self.session.options.get_config("emane_models_dir") - if custom_models_path: - emane_models = utils.load_classes(custom_models_path, EmaneModel) - self.load_models(emane_models) - - 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.get_config("eventservicegroup").split(":") - self.event_device = self.get_config("eventservicedevice") - eventnetidx = self.session.get_control_net_index(self.event_device) - if eventnetidx < 0: - logging.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 - logging.info("using %s for event service traffic", self.event_device) - try: - self.service = EventService(eventchannel=self.eventchannel, otachannel=None) - except EventServiceException: - logging.exception("error instantiating emane EventService") - - def load_models(self, emane_models: List[Type[EmaneModel]]) -> None: - """ - Load EMANE models and make them available. - """ - for emane_model in emane_models: - logging.debug("loading emane model: %s", emane_model.__name__) - emane_prefix = self.session.options.get_config( - "emane_prefix", default=DEFAULT_EMANE_PREFIX - ) - emane_model.load(emane_prefix) - self.models[emane_model.name] = emane_model + if node_id is None: + self.node_configs.clear() + self.node_models.clear() + else: + self.node_configs.get(node_id, {}).clear() + self.node_models.pop(node_id, None) def add_node(self, emane_net: EmaneNet) -> None: """ @@ -281,53 +282,21 @@ class EmaneManager(ModelManager): :return: SUCCESS, NOT_NEEDED, NOT_READY in order to delay session instantiation """ - logging.debug("emane setup") + logger.debug("emane setup") with self.session.nodes_lock: for node_id in self.session.nodes: node = self.session.nodes[node_id] if isinstance(node, EmaneNet): - logging.debug( + logger.debug( "adding emane node: id(%s) name(%s)", node.id, node.name ) self.add_node(node) if not self._emane_nets: - logging.debug("no emane nodes in session") + 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.get_config("otamanagerdevice") - netidx = self.session.get_control_net_index(otadev) - logging.debug("emane ota manager device: index(%s) otadev(%s)", netidx, otadev) - if netidx < 0: - logging.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.get_config("eventservicedevice") - logging.debug("emane event service device: eventdev(%s)", eventdev) - if eventdev != otadev: - netidx = self.session.get_control_net_index(eventdev) - logging.debug("emane event service device index: %s", netidx) - if netidx < 0: - logging.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 @@ -343,53 +312,100 @@ class EmaneManager(ModelManager): status = self.setup() if status != EmaneState.SUCCESS: return status - self.starteventmonitor() - self.buildeventservicexml() - with self._emane_node_lock: - logging.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: - logging.error("emane net(%s) has no model", emane_net.name) + logger.error("emane net(%s) has no model", emane_net.name) continue for iface in emane_net.get_ifaces(): if not iface.node: - logging.error( + logger.error( "emane net(%s) connected interface(%s) missing node", emane_net.name, 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) @@ -397,32 +413,94 @@ class EmaneManager(ModelManager): 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 = os.path.join(self.session.session_dir, "emane_nems") + path = self.session.directory / "emane_nems" try: - with open(path, "a") as f: + with path.open("a") as f: f.write(f"{iface.node.name} {iface.name} {nem_id}\n") except IOError: - logging.exception("error writing to emane nem file") + logger.exception("error writing to emane nem file") def links_enabled(self) -> bool: - return self.get_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] - logging.debug( + 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: """ @@ -433,6 +511,8 @@ class EmaneManager(ModelManager): 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: """ @@ -441,25 +521,26 @@ class EmaneManager(ModelManager): with self._emane_node_lock: if not self._emane_nets: return - logging.info("stopping EMANE daemons") + 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: """ @@ -467,25 +548,23 @@ class EmaneManager(ModelManager): """ for node_id in self._emane_nets: emane_net = self._emane_nets[node_id] - logging.debug("checking emane model for node: %s", node_id) - + logger.debug("checking emane model for node: %s", node_id) # skip nodes that already have a model set if emane_net.model: - logging.debug( + logger.debug( "node(%s) already has model(%s)", emane_net.id, emane_net.model.name ) continue - # set model configured for node, due to legacy messaging configuration # before nodes exist model_name = self.node_models.get(node_id) if not model_name: - logging.error("emane node(%s) has no node model", node_id) + logger.error("emane node(%s) has no node model", node_id) raise ValueError("emane node has no model set") - config = self.get_model_config(node_id=node_id, model_name=model_name) - logging.debug("setting emane model(%s) config(%s)", model_name, config) - model_class = self.models[model_name] + config = self.get_config(node_id, model_name) + logger.debug("setting emane model(%s) config(%s)", model_name, config) + model_class = self.get_model(model_name) emane_net.setmodel(model_class, config) def get_nem_link( @@ -493,12 +572,12 @@ class EmaneManager(ModelManager): ) -> Optional[LinkData]: iface1 = self.get_iface(nem1) if not iface1: - logging.error("invalid nem: %s", nem1) + logger.error("invalid nem: %s", nem1) return None node1 = iface1.node iface2 = self.get_iface(nem2) if not iface2: - logging.error("invalid nem: %s", nem2) + logger.error("invalid nem: %s", nem2) return None node2 = iface2.node if iface1.net != iface2.net: @@ -514,109 +593,43 @@ class EmaneManager(ModelManager): 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. + Start emane daemon for a given nem/interface. + + :param iface: interface to start emane daemon for + :return: nothing """ - need_xml = False - default_values = self.emane_config.default_values() - for name in ["eventservicegroup", "eventservicedevice"]: - a = default_values[name] - b = self.get_config(name) - if a != b: - need_xml = True - - if not need_xml: - # reset to using default config - self.initeventservice() - return - - try: - group, port = self.get_config("eventservicegroup").split(":") - except ValueError: - logging.exception("invalid eventservicegroup in EMANE config") - return - - dev = self.get_config("eventservicedevice") - emanexml.create_event_service_xml(group, port, dev, self.session.session_dir) - self.session.distributed.execute( - lambda x: emanexml.create_event_service_xml( - group, port, dev, self.session.session_dir, x - ) - ) - - def start_daemon(self, node: CoreNodeBase) -> None: - """ - Start one EMANE daemon per node having a radio. - Add a control network even if the user has not configured one. - """ - logging.info("starting emane daemons...") - loglevel = str(EmaneManager.DEFAULT_LOG_LEVEL) + 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) if cfgloglevel: - logging.info("setting user-defined emane log level: %d", cfgloglevel) + logger.info("setting user-defined emane log level: %d", cfgloglevel) loglevel = str(cfgloglevel) emanecmd = f"emane -d -l {loglevel}" if realtime: emanecmd += " -r" if isinstance(node, CoreNode): - otagroup, _otaport = self.get_config("otamanagergroup").split(":") - otadev = self.get_config("otamanagerdevice") - otanetidx = self.session.get_control_net_index(otadev) - eventgroup, _eventport = self.get_config("eventservicegroup").split(":") - eventdev = self.get_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: - logging.info("adding ota device ctrl%d", otanetidx) - self.session.add_remove_control_iface( - node, otanetidx, remove=False, conf_required=False - ) - if eventservicenetidx >= 0: - logging.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 - logging.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 = os.path.join(node.nodedir, f"{node.name}-emane.log") - platform_xml = os.path.join(node.nodedir, 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) - logging.info("node(%s) emane daemon running: %s", node.name, args) else: - path = self.session.session_dir - log_file = os.path.join(path, f"{node.name}-emane.log") - platform_xml = os.path.join(path, f"{node.name}-platform.xml") - emanecmd += f" -f {log_file} {platform_xml}" - node.host_cmd(emanecmd, cwd=path) - logging.info("node(%s) host emane daemon running: %s", node.name, emanecmd) + 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) - 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: @@ -638,68 +651,6 @@ class EmaneManager(ModelManager): tmp = not self.doeventmonitor() return tmp - def starteventmonitor(self) -> None: - """ - Start monitoring EMANE location events if configured to do so. - """ - logging.info("emane start event monitor") - if not self.doeventmonitor(): - return - if self.service is None: - logging.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 - logging.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) - - logging.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. @@ -713,14 +664,13 @@ class EmaneManager(ModelManager): or "longitude" not in attrs or "altitude" not in attrs ): - logging.warning("dropped invalid location event") + logger.warning("dropped invalid location event") continue - # yaw,pitch,roll,azimuth,elevation,velocity are unhandled lat = attrs["latitude"] lon = attrs["longitude"] alt = attrs["altitude"] - logging.debug("emane location event: %s,%s,%s", lat, lon, alt) + logger.debug("emane location event: %s,%s,%s", lat, lon, alt) self.handlelocationeventtoxyz(txnemid, lat, lon, alt) def handlelocationeventtoxyz( @@ -734,7 +684,7 @@ class EmaneManager(ModelManager): # convert nemid to node number iface = self.get_iface(nemid) if iface is None: - logging.info("location event for unknown NEM %s", nemid) + logger.info("location event for unknown NEM %s", nemid) return False n = iface.node.id @@ -743,7 +693,7 @@ class EmaneManager(ModelManager): x = int(x) y = int(y) z = int(z) - logging.debug( + logger.debug( "location event NEM %s (%s, %s, %s) -> (%s, %s, %s)", nemid, lat, @@ -757,7 +707,7 @@ class EmaneManager(ModelManager): ybit_check = y.bit_length() > 16 or y < 0 zbit_check = z.bit_length() > 16 or z < 0 if any([xbit_check, ybit_check, zbit_check]): - logging.error( + logger.error( "Unable to build node location message, received lat/long/alt " "exceeds coordinate space: NEM %s (%d, %d, %d)", nemid, @@ -771,7 +721,7 @@ class EmaneManager(ModelManager): try: node = self.session.get_node(n, NodeBase) except CoreError: - logging.exception( + logger.exception( "location event NEM %s has no corresponding node %s", nemid, n ) return False @@ -810,86 +760,19 @@ class EmaneManager(ModelManager): 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 - ) - emulator_xml = os.path.join(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/emanemanifest.py b/daemon/core/emane/emanemanifest.py index 41dc7beb..0fb5bc17 100644 --- a/daemon/core/emane/emanemanifest.py +++ b/daemon/core/emane/emanemanifest.py @@ -1,9 +1,12 @@ import logging +from pathlib import Path from typing import Dict, List from core.config import Configuration from core.emulator.enumerations import ConfigDataTypes +logger = logging.getLogger(__name__) + manifest = None try: from emane.shell import manifest @@ -12,7 +15,7 @@ except ImportError: from emanesh import manifest except ImportError: manifest = None - logging.debug("compatible emane python bindings not installed") + logger.debug("compatible emane python bindings not installed") def _type_value(config_type: str) -> ConfigDataTypes: @@ -71,9 +74,10 @@ def _get_default(config_type_name: str, config_value: List[str]) -> str: return config_default -def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]: +def parse(manifest_path: Path, defaults: Dict[str, str]) -> List[Configuration]: """ - Parses a valid emane manifest file and converts the provided configuration values into ones used by core. + Parses a valid emane manifest file and converts the provided configuration values + into ones used by core. :param manifest_path: absolute manifest file path :param defaults: used to override default values for configurations @@ -85,7 +89,7 @@ def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]: return [] # load configuration file - manifest_file = manifest.Manifest(manifest_path) + manifest_file = manifest.Manifest(str(manifest_path)) manifest_configurations = manifest_file.getAllConfiguration() configurations = [] @@ -116,8 +120,8 @@ def parse(manifest_path: str, defaults: Dict[str, str]) -> List[Configuration]: config_descriptions = f"{config_descriptions} file" configuration = Configuration( - _id=config_name, - _type=config_type_value, + id=config_name, + type=config_type_value, default=config_default, options=possible, label=config_descriptions, diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 755f07aa..cc5b0f4d 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -2,19 +2,21 @@ Defines Emane Models used within CORE. """ import logging -import os +from pathlib import Path from typing import Dict, List, Optional, Set -from core.config import ConfigGroup, Configuration +from core.config import ConfigBool, ConfigGroup, ConfigString, 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 from core.location.mobility import WirelessModel 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): """ @@ -23,6 +25,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 @@ -41,35 +54,45 @@ class EmaneModel(WirelessModel): # support for external configurations external_config: List[Configuration] = [ - Configuration("external", ConfigDataTypes.BOOL, default="0"), - Configuration( - "platformendpoint", ConfigDataTypes.STRING, default="127.0.0.1:40001" - ), - Configuration( - "transportendpoint", ConfigDataTypes.STRING, default="127.0.0.1:50002" - ), + ConfigBool(id="external", default="0"), + ConfigString(id="platformendpoint", default="127.0.0.1:40001"), + ConfigString(id="transportendpoint", default="127.0.0.1:50002"), ] config_ignore: Set[str] = set() @classmethod - def load(cls, emane_prefix: str) -> None: + 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 = os.path.join(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 = os.path.join(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]: """ @@ -77,7 +100,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]: @@ -86,11 +111,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), ] @@ -110,13 +137,14 @@ 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 """ - logging.debug("emane model(%s) has no post setup tasks", self.name) + logger.debug("emane model(%s) has no post setup tasks", self.name) def update(self, moved_ifaces: List[CoreInterface]) -> None: """ @@ -128,10 +156,9 @@ 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: - logging.exception("error during update") + logger.exception("error during update") def linkconfig( self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None @@ -144,4 +171,4 @@ class EmaneModel(WirelessModel): :param iface2: interface two :return: nothing """ - logging.warning("emane model(%s) does not support link config", self.name) + logger.warning("emane model(%s) does not support link config", self.name) diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py index 56473f62..9b18bae2 100644 --- a/daemon/core/emane/linkmonitor.py +++ b/daemon/core/emane/linkmonitor.py @@ -6,10 +6,13 @@ 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 +logger = logging.getLogger(__name__) + try: from emane import shell except ImportError: @@ -17,12 +20,11 @@ except ImportError: from emanesh import shell except ImportError: shell = None - logging.debug("compatible emane python bindings not installed") + logger.debug("compatible emane python bindings not installed") 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" @@ -77,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() @@ -91,7 +93,7 @@ class EmaneClient: # get mac config mac_id, _, emane_model = components[MAC_COMPONENT_INDEX] mac_config = self.client.getConfiguration(mac_id) - logging.debug( + logger.debug( "address(%s) nem(%s) emane(%s)", self.address, nem_id, emane_model ) @@ -101,9 +103,9 @@ class EmaneClient: elif emane_model == EMANE_RFPIPE: loss_table = self.handle_rfpipe(mac_config) else: - logging.warning("unknown emane link model: %s", emane_model) + logger.warning("unknown emane link model: %s", emane_model) continue - logging.info("monitoring links nem(%s) model(%s)", nem_id, emane_model) + logger.info("monitoring links nem(%s) model(%s)", nem_id, emane_model) loss_table.mac_id = mac_id self.nems[nem_id] = loss_table @@ -138,12 +140,12 @@ class EmaneClient: def handle_tdma(self, config: Dict[str, Tuple]): pcr = config["pcrcurveuri"][0][0] - logging.debug("tdma pcr: %s", pcr) + logger.debug("tdma pcr: %s", pcr) def handle_80211(self, config: Dict[str, Tuple]) -> LossTable: unicastrate = config["unicastrate"][0][0] pcr = config["pcrcurveuri"][0][0] - logging.debug("80211 pcr: %s", pcr) + logger.debug("80211 pcr: %s", pcr) tree = etree.parse(pcr) root = tree.getroot() table = root.find("table") @@ -159,7 +161,7 @@ class EmaneClient: def handle_rfpipe(self, config: Dict[str, Tuple]) -> LossTable: pcr = config["pcrcurveuri"][0][0] - logging.debug("rfpipe pcr: %s", pcr) + logger.debug("rfpipe pcr: %s", pcr) tree = etree.parse(pcr) root = tree.getroot() table = root.find("table") @@ -187,12 +189,13 @@ class EmaneLinkMonitor: self.running: bool = False def start(self) -> None: - self.loss_threshold = int(self.emane_manager.get_config("loss_threshold")) - self.link_interval = int(self.emane_manager.get_config("link_interval")) - self.link_timeout = int(self.emane_manager.get_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: - logging.info("no valid emane models to monitor links") + logger.info("no valid emane models to monitor links") return self.scheduler = sched.scheduler() self.scheduler.enter(0, 0, self.check_links) @@ -202,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: @@ -228,7 +237,7 @@ class EmaneLinkMonitor: client.check_links(self.links, self.loss_threshold) except shell.ControlPortException: if self.running: - logging.exception("link monitor error") + logger.exception("link monitor error") # find new links current_links = set(self.links.keys()) diff --git a/daemon/core/emane/modelmanager.py b/daemon/core/emane/modelmanager.py new file mode 100644 index 00000000..989802c4 --- /dev/null +++ b/daemon/core/emane/modelmanager.py @@ -0,0 +1,70 @@ +import logging +import pkgutil +from pathlib import Path +from typing import Dict, List, Type + +from core import utils +from core.emane import models as emane_models +from core.emane.emanemodel import EmaneModel +from core.errors import CoreError + +logger = logging.getLogger(__name__) + + +class EmaneModelManager: + models: Dict[str, Type[EmaneModel]] = {} + + @classmethod + def load_locals(cls, emane_prefix: Path) -> List[str]: + """ + Load local core emane models and make them available. + + :param emane_prefix: installed emane prefix + :return: list of errors encountered loading emane models + """ + errors = [] + for module_info in pkgutil.walk_packages( + emane_models.__path__, f"{emane_models.__name__}." + ): + models = utils.load_module(module_info.name, EmaneModel) + for model in models: + logger.debug("loading emane model: %s", model.name) + try: + model.load(emane_prefix) + cls.models[model.name] = model + except CoreError as e: + errors.append(model.name) + logger.debug("not loading emane model(%s): %s", model.name, e) + return errors + + @classmethod + def load(cls, path: Path, emane_prefix: Path) -> List[str]: + """ + Search and load custom emane models and make them available. + + :param path: path to search for custom emane models + :param emane_prefix: installed emane prefix + :return: list of errors encountered loading emane models + """ + subdirs = [x for x in path.iterdir() if x.is_dir()] + subdirs.append(path) + errors = [] + for subdir in subdirs: + logger.debug("loading emane models from: %s", subdir) + models = utils.load_classes(subdir, EmaneModel) + for model in models: + logger.debug("loading emane model: %s", model.name) + try: + model.load(emane_prefix) + cls.models[model.name] = model + except CoreError as e: + errors.append(model.name) + logger.debug("not loading emane model(%s): %s", model.name, e) + return errors + + @classmethod + def get(cls, name: str) -> Type[EmaneModel]: + model = cls.models.get(name) + if model is None: + raise CoreError(f"emame model does not exist {name}") + return model diff --git a/daemon/core/emane/models/__init__.py b/daemon/core/emane/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/daemon/core/emane/bypass.py b/daemon/core/emane/models/bypass.py similarity index 68% rename from daemon/core/emane/bypass.py rename to daemon/core/emane/models/bypass.py index 8aabc3f9..25841114 100644 --- a/daemon/core/emane/bypass.py +++ b/daemon/core/emane/models/bypass.py @@ -1,11 +1,11 @@ """ EMANE Bypass model for CORE """ +from pathlib import Path from typing import List, Set -from core.config import Configuration +from core.config import ConfigBool, Configuration from core.emane import emanemodel -from core.emulator.enumerations import ConfigDataTypes class EmaneBypassModel(emanemodel.EmaneModel): @@ -17,9 +17,8 @@ class EmaneBypassModel(emanemodel.EmaneModel): # mac definitions mac_library: str = "bypassmaclayer" mac_config: List[Configuration] = [ - Configuration( - _id="none", - _type=ConfigDataTypes.BOOL, + ConfigBool( + id="none", default="0", label="There are no parameters for the bypass model.", ) @@ -30,6 +29,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/commeffect.py b/daemon/core/emane/models/commeffect.py similarity index 83% rename from daemon/core/emane/commeffect.py rename to daemon/core/emane/models/commeffect.py index 13ec53f7..c3f0b07b 100644 --- a/daemon/core/emane/commeffect.py +++ b/daemon/core/emane/models/commeffect.py @@ -3,7 +3,7 @@ commeffect.py: EMANE CommEffect model for CORE """ import logging -import os +from pathlib import Path from typing import Dict, List from lxml import etree @@ -14,6 +14,8 @@ from core.emulator.data import LinkOptions from core.nodes.interface import CoreInterface from core.xml import emanexml +logger = logging.getLogger(__name__) + try: from emane.events.commeffectevent import CommEffectEvent except ImportError: @@ -21,7 +23,7 @@ except ImportError: from emanesh.events.commeffectevent import CommEffectEvent except ImportError: CommEffectEvent = None - logging.debug("compatible emane python bindings not installed") + logger.debug("compatible emane python bindings not installed") def convert_none(x: float) -> int: @@ -48,17 +50,26 @@ class EmaneCommEffectModel(emanemodel.EmaneModel): external_config: List[Configuration] = [] @classmethod - def load(cls, emane_prefix: str) -> None: - shim_xml_path = os.path.join(emane_prefix, "share/emane/manifest", cls.shim_xml) + 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: """ @@ -111,21 +122,15 @@ 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: - logging.warning("%s: EMANE event service unavailable", self.name) - return - if iface is None or iface2 is None: - logging.warning("%s: missing NEM information", self.name) + 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() nem1 = self.session.emane.get_nem_id(iface) nem2 = self.session.emane.get_nem_id(iface2) - logging.info("sending comm effect event") + logger.info("sending comm effect event") event.append( nem1, latency=convert_none(options.delay), @@ -135,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/ieee80211abg.py b/daemon/core/emane/models/ieee80211abg.py similarity index 65% rename from daemon/core/emane/ieee80211abg.py rename to daemon/core/emane/models/ieee80211abg.py index 0d58ec9e..f6b32264 100644 --- a/daemon/core/emane/ieee80211abg.py +++ b/daemon/core/emane/models/ieee80211abg.py @@ -1,7 +1,7 @@ """ ieee80211abg.py: EMANE IEEE 802.11abg model for CORE """ -import os +from pathlib import Path from core.emane import emanemodel @@ -15,8 +15,8 @@ class EmaneIeee80211abgModel(emanemodel.EmaneModel): mac_xml: str = "ieee80211abgmaclayer.xml" @classmethod - def load(cls, emane_prefix: str) -> None: - cls.mac_defaults["pcrcurveuri"] = os.path.join( - emane_prefix, "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml" + def load(cls, emane_prefix: Path) -> None: + cls.mac_defaults["pcrcurveuri"] = str( + emane_prefix / "share/emane/xml/models/mac/ieee80211abg/ieee80211pcr.xml" ) super().load(emane_prefix) diff --git a/daemon/core/emane/rfpipe.py b/daemon/core/emane/models/rfpipe.py similarity index 63% rename from daemon/core/emane/rfpipe.py rename to daemon/core/emane/models/rfpipe.py index 068ef800..7dace8c7 100644 --- a/daemon/core/emane/rfpipe.py +++ b/daemon/core/emane/models/rfpipe.py @@ -1,7 +1,7 @@ """ rfpipe.py: EMANE RF-PIPE model for CORE """ -import os +from pathlib import Path from core.emane import emanemodel @@ -15,8 +15,8 @@ class EmaneRfPipeModel(emanemodel.EmaneModel): mac_xml: str = "rfpipemaclayer.xml" @classmethod - def load(cls, emane_prefix: str) -> None: - cls.mac_defaults["pcrcurveuri"] = os.path.join( - emane_prefix, "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" + def load(cls, emane_prefix: Path) -> None: + cls.mac_defaults["pcrcurveuri"] = str( + emane_prefix / "share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" ) super().load(emane_prefix) diff --git a/daemon/core/emane/models/tdma.py b/daemon/core/emane/models/tdma.py new file mode 100644 index 00000000..c6ac631b --- /dev/null +++ b/daemon/core/emane/models/tdma.py @@ -0,0 +1,66 @@ +""" +tdma.py: EMANE TDMA model bindings for CORE +""" + +import logging +from pathlib import Path +from typing import Set + +from core import constants, utils +from core.config import ConfigString +from core.emane import emanemodel +from core.emane.nodes import EmaneNet +from core.nodes.interface import CoreInterface + +logger = logging.getLogger(__name__) + + +class EmaneTdmaModel(emanemodel.EmaneModel): + # model name + name: str = "emane_tdma" + + # mac configuration + mac_library: str = "tdmaeventschedulerradiomodel" + mac_xml: str = "tdmaeventschedulerradiomodel.xml" + + # add custom schedule options and ignore it when writing emane xml + schedule_name: str = "schedule" + default_schedule: Path = ( + constants.CORE_DATA_DIR / "examples" / "tdma" / "schedule.xml" + ) + config_ignore: Set[str] = {schedule_name} + + @classmethod + def load(cls, emane_prefix: Path) -> None: + cls.mac_defaults["pcrcurveuri"] = str( + emane_prefix + / "share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml" + ) + super().load(emane_prefix) + config_item = ConfigString( + id=cls.schedule_name, + default=str(cls.default_schedule), + label="TDMA schedule file (core)", + ) + cls.mac_config.insert(0, config_item) + + def post_startup(self, iface: CoreInterface) -> None: + # get configured schedule + 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.error("ignoring invalid tdma schedule: %s", schedule) + return + # initiate tdma 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 5791f46a..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 @@ -19,6 +19,8 @@ from core.errors import CoreError from core.nodes.base import CoreNetworkBase, CoreNode from core.nodes.interface import CoreInterface +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.emane.emanemodel import EmaneModel from core.emulator.session import Session @@ -34,7 +36,7 @@ except ImportError: from emanesh.events import LocationEvent except ImportError: LocationEvent = None - logging.debug("compatible emane python bindings not installed") + logger.debug("compatible emane python bindings not installed") class EmaneNet(CoreNetworkBase): @@ -92,9 +94,7 @@ class EmaneNet(CoreNetworkBase): def updatemodel(self, config: Dict[str, str]) -> None: if not self.model: raise CoreError(f"no model set to update for node({self.name})") - logging.info( - "node(%s) updating model(%s): %s", self.id, self.model.name, config - ) + logger.info("node(%s) updating model(%s): %s", self.id, self.model.name, config) self.model.update_config(config) def setmodel(self, model: "WirelessModelType", config: Dict[str, str]) -> None: @@ -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: - logging.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: - logging.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: - logging.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/emane/tdma.py b/daemon/core/emane/tdma.py deleted file mode 100644 index ee80f3d7..00000000 --- a/daemon/core/emane/tdma.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -tdma.py: EMANE TDMA model bindings for CORE -""" - -import logging -import os -from typing import Set - -from core import constants, utils -from core.config import Configuration -from core.emane import emanemodel -from core.emulator.enumerations import ConfigDataTypes - - -class EmaneTdmaModel(emanemodel.EmaneModel): - # model name - name: str = "emane_tdma" - - # mac configuration - mac_library: str = "tdmaeventschedulerradiomodel" - mac_xml: str = "tdmaeventschedulerradiomodel.xml" - - # add custom schedule options and ignore it when writing emane xml - schedule_name: str = "schedule" - default_schedule: str = os.path.join( - constants.CORE_DATA_DIR, "examples", "tdma", "schedule.xml" - ) - config_ignore: Set[str] = {schedule_name} - - @classmethod - def load(cls, emane_prefix: str) -> None: - cls.mac_defaults["pcrcurveuri"] = os.path.join( - emane_prefix, - "share/emane/xml/models/mac/tdmaeventscheduler/tdmabasemodelpcr.xml", - ) - super().load(emane_prefix) - cls.mac_config.insert( - 0, - Configuration( - _id=cls.schedule_name, - _type=ConfigDataTypes.STRING, - default=cls.default_schedule, - label="TDMA schedule file (core)", - ), - ) - - def post_startup(self) -> None: - """ - Logic to execute after the emane manager is finished with startup. - - :return: nothing - """ - # get configured schedule - config = self.session.emane.get_configs(node_id=self.id, config_type=self.name) - if not config: - return - schedule = config[self.schedule_name] - - # get the set event device - event_device = self.session.emane.event_device - - # initiate tdma schedule - logging.info( - "setting up tdma schedule: schedule(%s) device(%s)", schedule, event_device - ) - args = f"emaneevent-tdmaschedule -i {event_device} {schedule}" - utils.cmd(args) diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index 885fb431..179faf9c 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -3,15 +3,21 @@ import logging import os import signal import sys +from pathlib import Path from typing import Dict, List, Type import core.services -from core import configservices, utils +from core import utils from core.configservice.manager import ConfigServiceManager +from core.emane.modelmanager import EmaneModelManager from core.emulator.session import Session from core.executables import get_requirements from core.services.coreservices import ServiceManager +logger = logging.getLogger(__name__) + +DEFAULT_EMANE_PREFIX: str = "/usr" + def signal_handler(signal_number: int, _) -> None: """ @@ -21,7 +27,7 @@ def signal_handler(signal_number: int, _) -> None: :param _: ignored :return: nothing """ - logging.info("caught signal: %s", signal_number) + logger.info("caught signal: %s", signal_number) sys.exit(signal_number) @@ -47,8 +53,7 @@ class CoreEmu: os.umask(0) # configuration - if config is None: - config = {} + config = config if config else {} self.config: Dict[str, str] = config # session management @@ -56,15 +61,12 @@ class CoreEmu: # load services self.service_errors: List[str] = [] - self.load_services() - - # config services self.service_manager: ConfigServiceManager = ConfigServiceManager() - config_services_path = os.path.abspath(os.path.dirname(configservices.__file__)) - self.service_manager.load(config_services_path) - custom_dir = self.config.get("custom_config_services_dir") - if custom_dir: - self.service_manager.load(custom_dir) + self._load_services() + + # check and load emane + self.has_emane: bool = False + self._load_emane() # check executables exist on path self._validate_env() @@ -83,7 +85,7 @@ class CoreEmu: for requirement in get_requirements(use_ovs): utils.which(requirement, required=True) - def load_services(self) -> None: + def _load_services(self) -> None: """ Loads default and custom services for use within CORE. @@ -91,15 +93,46 @@ class CoreEmu: """ # load default services self.service_errors = core.services.load() - # load custom services service_paths = self.config.get("custom_services_dir") - logging.debug("custom service paths: %s", service_paths) - if service_paths: + logger.debug("custom service paths: %s", service_paths) + if service_paths is not None: for service_path in service_paths.split(","): - service_path = service_path.strip() + service_path = Path(service_path.strip()) custom_service_errors = ServiceManager.add_services(service_path) self.service_errors.extend(custom_service_errors) + # load default config services + self.service_manager.load_locals() + # load custom config services + custom_dir = self.config.get("custom_config_services_dir") + if custom_dir is not None: + custom_dir = Path(custom_dir) + self.service_manager.load(custom_dir) + + def _load_emane(self) -> None: + """ + Check if emane is installed and load models. + + :return: nothing + """ + # check for emane + path = utils.which("emane", required=False) + self.has_emane = path is not None + if not self.has_emane: + logger.info("emane is not installed, emane functionality disabled") + return + # get version + emane_version = utils.cmd("emane --version") + logger.info("using emane: %s", emane_version) + emane_prefix = self.config.get("emane_prefix", DEFAULT_EMANE_PREFIX) + emane_prefix = Path(emane_prefix) + EmaneModelManager.load_locals(emane_prefix) + # load custom models + custom_path = self.config.get("emane_models_dir") + if custom_path is not None: + logger.info("loading custom emane models: %s", custom_path) + custom_path = Path(custom_path) + EmaneModelManager.load(custom_path, emane_prefix) def shutdown(self) -> None: """ @@ -107,7 +140,7 @@ class CoreEmu: :return: nothing """ - logging.info("shutting down all sessions") + logger.info("shutting down all sessions") sessions = self.sessions.copy() self.sessions.clear() for _id in sessions: @@ -128,7 +161,7 @@ class CoreEmu: _id += 1 session = _cls(_id, config=self.config) session.service_manager = self.service_manager - logging.info("created session: %s", _id) + logger.info("created session: %s", _id) self.sessions[_id] = session return session @@ -139,14 +172,14 @@ class CoreEmu: :param _id: session id to delete :return: True if deleted, False otherwise """ - logging.info("deleting session: %s", _id) + logger.info("deleting session: %s", _id) session = self.sessions.pop(_id, None) result = False if session: - logging.info("shutting session down: %s", _id) + logger.info("shutting session down: %s", _id) session.data_collect() session.shutdown() result = True else: - logging.error("session to delete did not exist: %s", _id) + logger.error("session to delete did not exist: %s", _id) return result diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 68a92eea..4ce92d0b 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -91,6 +91,7 @@ class NodeOptions: server: str = None image: str = None emane: str = None + legacy: bool = False def set_position(self, x: float, y: float) -> None: """ @@ -141,6 +142,7 @@ class InterfaceData: ip4_mask: int = None ip6: str = None ip6_mask: int = None + mtu: int = None def get_ips(self) -> List[str]: """ diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index a5e1009f..2b4830ad 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -6,6 +6,7 @@ import logging import os import threading from collections import OrderedDict +from pathlib import Path from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING, Callable, Dict, Tuple @@ -19,6 +20,8 @@ from core.executables import get_requirements from core.nodes.interface import GreTap from core.nodes.network import CoreNetwork, CtrlNet +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.emulator.session import Session @@ -61,7 +64,7 @@ class DistributedServer: replace_env = env is not None if not wait: cmd += " &" - logging.debug( + logger.debug( "remote cmd server(%s) cwd(%s) wait(%s): %s", self.host, cwd, wait, cmd ) try: @@ -79,23 +82,23 @@ class DistributedServer: stdout, stderr = e.streams_for_display() raise CoreCommandError(e.result.exited, cmd, stdout, stderr) - def remote_put(self, source: str, destination: str) -> None: + def remote_put(self, src_path: Path, dst_path: Path) -> None: """ Push file to remote server. - :param source: source file to push - :param destination: destination file location + :param src_path: source file to push + :param dst_path: destination file location :return: nothing """ with self.lock: - self.conn.put(source, destination) + self.conn.put(str(src_path), str(dst_path)) - def remote_put_temp(self, destination: str, data: str) -> None: + def remote_put_temp(self, dst_path: Path, data: str) -> None: """ Remote push file contents to a remote server, using a temp file as an intermediate step. - :param destination: file destination for data + :param dst_path: file destination for data :param data: data to store in remote file :return: nothing """ @@ -103,7 +106,7 @@ class DistributedServer: temp = NamedTemporaryFile(delete=False) temp.write(data.encode("utf-8")) temp.close() - self.conn.put(temp.name, destination) + self.conn.put(temp.name, str(dst_path)) os.unlink(temp.name) @@ -144,7 +147,7 @@ class DistributedController: f"command({requirement})" ) self.servers[name] = server - cmd = f"mkdir -p {self.session.session_dir}" + cmd = f"mkdir -p {self.session.directory}" server.remote_cmd(cmd) def execute(self, func: Callable[[DistributedServer], None]) -> None: @@ -170,13 +173,11 @@ class DistributedController: tunnels = self.tunnels[key] for tunnel in tunnels: tunnel.shutdown() - # remove all remote session directories for name in self.servers: server = self.servers[name] - cmd = f"rm -rf {self.session.session_dir}" + cmd = f"rm -rf {self.session.directory}" server.remote_cmd(cmd) - # clear tunnels self.tunnels.clear() @@ -186,6 +187,7 @@ class DistributedController: :return: nothing """ + mtu = self.session.options.get_config_int("mtu") for node_id in self.session.nodes: node = self.session.nodes[node_id] if not isinstance(node, CoreNetwork): @@ -194,17 +196,18 @@ class DistributedController: continue for name in self.servers: server = self.servers[name] - self.create_gre_tunnel(node, server) + self.create_gre_tunnel(node, server, mtu, True) def create_gre_tunnel( - self, node: CoreNetwork, server: DistributedServer + self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool ) -> Tuple[GreTap, GreTap]: """ Create gre tunnel using a pair of gre taps between the local and remote server. :param node: node to create gre tunnel for - :param server: server to create - tunnel for + :param server: server to create tunnel for + :param mtu: mtu for gre taps + :param start: True to start gre taps, False otherwise :return: local and remote gre taps created for tunnel """ host = server.host @@ -212,23 +215,20 @@ class DistributedController: tunnel = self.tunnels.get(key) if tunnel is not None: return tunnel - # local to server - logging.info( - "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key - ) - local_tap = GreTap(session=self.session, remoteip=host, key=key) - local_tap.net_client.set_iface_master(node.brname, local_tap.localname) - + logger.info("local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key) + local_tap = GreTap(self.session, host, key=key, mtu=mtu) + if start: + local_tap.startup() + local_tap.net_client.set_iface_master(node.brname, local_tap.localname) # server to local - logging.info( + logger.info( "remote tunnel node(%s) to local(%s) key(%s)", node.name, self.address, key ) - remote_tap = GreTap( - session=self.session, remoteip=self.address, key=key, server=server - ) - remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname) - + remote_tap = GreTap(self.session, self.address, key=key, server=server, mtu=mtu) + if start: + remote_tap.startup() + remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname) # save tunnels for shutdown tunnel = (local_tap, remote_tap) self.tunnels[key] = tunnel @@ -244,7 +244,7 @@ class DistributedController: :param node2_id: node two id :return: tunnel key for the node pair """ - logging.debug("creating tunnel key for: %s, %s", node1_id, node2_id) + logger.debug("creating tunnel key for: %s, %s", node1_id, node2_id) key = ( (self.session.id << 16) ^ utils.hashkey(node1_id) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index cb5f3722..4f41a5e5 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -4,6 +4,7 @@ that manages a CORE session. """ import logging +import math import os import pwd import shutil @@ -45,7 +46,7 @@ from core.location.geo import GeoLocation from core.location.mobility import BasicRangeModel, MobilityManager from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase from core.nodes.docker import DockerNode -from core.nodes.interface import CoreInterface +from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.lxd import LxcNode from core.nodes.network import ( CtrlNet, @@ -62,6 +63,8 @@ from core.services.coreservices import CoreServices from core.xml import corexml, corexmldeployment from core.xml.corexml import CoreXmlReader, CoreXmlWriter +logger = logging.getLogger(__name__) + # maps for converting from API call node type values to classes and vice versa NODES: Dict[NodeTypes, Type[NodeBase]] = { NodeTypes.DEFAULT: CoreNode, @@ -103,13 +106,13 @@ class Session: self.id: int = _id # define and create session directory when desired - self.session_dir: str = os.path.join(tempfile.gettempdir(), f"pycore.{self.id}") + self.directory: Path = Path(tempfile.gettempdir()) / f"pycore.{self.id}" if mkdir: - os.mkdir(self.session_dir) + self.directory.mkdir() self.name: Optional[str] = None - self.file_name: Optional[str] = None - self.thumbnail: Optional[str] = None + self.file_path: Optional[Path] = None + self.thumbnail: Optional[Path] = None self.user: Optional[str] = None self.event_loop: EventLoop = EventLoop() self.link_colors: Dict[int, str] = {} @@ -197,7 +200,7 @@ class Session: :raises core.CoreError: when objects to link is less than 2, or no common networks are found """ - logging.info( + logger.info( "handling wireless linking node1(%s) node2(%s): %s", node1.name, node2.name, @@ -208,7 +211,7 @@ class Session: raise CoreError("no common network found for wireless link/unlink") for common_network, iface1, iface2 in common_networks: if not isinstance(common_network, (WlanNode, EmaneNet)): - logging.info( + logger.info( "skipping common network that is not wireless/emane: %s", common_network, ) @@ -250,7 +253,12 @@ class Session: node2 = self.get_node(node2_id, NodeBase) iface1 = None iface2 = None - + # set mtu + mtu = self.options.get_config_int("mtu") or DEFAULT_MTU + if iface1_data: + iface1_data.mtu = mtu + if iface2_data: + iface2_data.mtu = mtu # wireless link if link_type == LinkTypes.WIRELESS: if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): @@ -263,7 +271,7 @@ class Session: else: # peer to peer link if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): - logging.info("linking ptp: %s - %s", node1.name, node2.name) + logger.info("linking ptp: %s - %s", node1.name, node2.name) start = self.state.should_start() ptp = self.create_node(PtpNet, start) iface1 = node1.new_iface(ptp, iface1_data) @@ -286,7 +294,7 @@ class Session: elif isinstance(node1, CoreNetworkBase) and isinstance( node2, CoreNetworkBase ): - logging.info( + logger.info( "linking network to network: %s - %s", node1.name, node2.name ) iface1 = node1.linknet(node2) @@ -303,10 +311,10 @@ class Session: # configure tunnel nodes key = options.key if isinstance(node1, TunnelNode): - logging.info("setting tunnel key for: %s", node1.name) + logger.info("setting tunnel key for: %s", node1.name) node1.setkey(key, iface1_data) if isinstance(node2, TunnelNode): - logging.info("setting tunnel key for: %s", node2.name) + logger.info("setting tunnel key for: %s", node2.name) node2.setkey(key, iface2_data) self.sdt.add_link(node1_id, node2_id) return iface1, iface2 @@ -332,7 +340,7 @@ class Session: """ node1 = self.get_node(node1_id, NodeBase) node2 = self.get_node(node2_id, NodeBase) - logging.info( + logger.info( "deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)", link_type.name, node1.name, @@ -409,7 +417,7 @@ class Session: options = LinkOptions() node1 = self.get_node(node1_id, NodeBase) node2 = self.get_node(node2_id, NodeBase) - logging.info( + logger.info( "update link(%s) node(%s):interface(%s) node(%s):interface(%s)", link_type.name, node1.name, @@ -525,7 +533,7 @@ class Session: raise CoreError(f"invalid distributed server: {options.server}") # create node - logging.info( + logger.info( "creating node(%s) id(%s) name(%s) start(%s)", _class.__name__, _id, @@ -542,30 +550,40 @@ class Session: node.canvas = options.canvas # set node position and broadcast it - self.set_node_position(node, options) + has_geo = all(i is not None for i in [options.lon, options.lat, options.alt]) + if has_geo: + self.set_node_geo(node, options.lon, options.lat, options.alt) + else: + self.set_node_pos(node, options.x, options.y) # add services to needed nodes if isinstance(node, (CoreNode, PhysicalNode)): node.type = options.model - logging.debug("set node type: %s", node.type) - self.services.add_services(node, node.type, options.services) + if options.legacy or options.services: + logger.debug("set node type: %s", node.type) + self.services.add_services(node, node.type, options.services) # add config services - logging.info("setting node config services: %s", options.config_services) - for name in options.config_services: + config_services = options.config_services + if not options.legacy and not config_services and not node.services: + config_services = self.services.default_services.get(node.type, []) + logger.info("setting node config services: %s", config_services) + for name in config_services: service_class = self.service_manager.get_service(name) node.add_config_service(service_class) + # set network mtu, if configured + mtu = self.options.get_config_int("mtu") + if isinstance(node, CoreNetworkBase) and mtu > 0: + node.mtu = mtu + # ensure default emane configuration if isinstance(node, EmaneNet) and 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.model = model(self, node.id) + model_class = self.emane.get_model(options.emane) + node.model = model_class(self, node.id) if self.state == EventTypes.RUNTIME_STATE: self.emane.add_node(node) + # set default wlan config if needed if isinstance(node, WlanNode): self.mobility.set_model_config(_id, BasicRangeModel.name) @@ -575,51 +593,26 @@ class Session: if self.state == EventTypes.RUNTIME_STATE and is_boot_node: self.write_nodes() self.add_remove_control_iface(node, remove=False) - self.services.boot_services(node) + self.boot_node(node) self.sdt.add_node(node) return node - def edit_node(self, node_id: int, options: NodeOptions) -> None: - """ - Edit node information. + def set_node_pos(self, node: NodeBase, x: float, y: float) -> None: + node.setposition(x, y, None) + self.sdt.edit_node( + node, node.position.lon, node.position.lat, node.position.alt + ) - :param node_id: id of node to update - :param options: data to update node with - :return: nothing - :raises core.CoreError: when node to update does not exist - """ - node = self.get_node(node_id, NodeBase) - node.icon = options.icon - self.set_node_position(node, options) - self.sdt.edit_node(node, options.lon, options.lat, options.alt) - - def set_node_position(self, node: NodeBase, options: NodeOptions) -> None: - """ - Set position for a node, use lat/lon/alt if needed. - - :param node: node to set position for - :param options: data for node - :return: nothing - """ - # extract location values - x = options.x - y = options.y - lat = options.lat - lon = options.lon - alt = options.alt - - # check if we need to generate position from lat/lon/alt - has_empty_position = all(i is None for i in [x, y]) - has_lat_lon_alt = all(i is not None for i in [lat, lon, alt]) - using_lat_lon_alt = has_empty_position and has_lat_lon_alt - if using_lat_lon_alt: - x, y, _ = self.location.getxyz(lat, lon, alt) - node.setposition(x, y, None) - node.position.set_geo(lon, lat, alt) - self.broadcast_node(node) - elif not has_empty_position: - node.setposition(x, y, None) + def set_node_geo(self, node: NodeBase, lon: float, lat: float, alt: float) -> None: + x, y, _ = self.location.getxyz(lat, lon, alt) + if math.isinf(x) or math.isinf(y): + raise CoreError( + f"invalid geo for current reference/scale: {lon},{lat},{alt}" + ) + node.setposition(x, y, None) + node.position.set_geo(lon, lat, alt) + self.sdt.edit_node(node, lon, lat, alt) def start_mobility(self, node_ids: List[int] = None) -> None: """ @@ -638,45 +631,42 @@ class Session: :return: True if active, False otherwise """ result = self.state in {EventTypes.RUNTIME_STATE, EventTypes.DATACOLLECT_STATE} - logging.info("session(%s) checking if active: %s", self.id, result) + logger.info("session(%s) checking if active: %s", self.id, result) return result - def open_xml(self, file_name: str, start: bool = False) -> None: + def open_xml(self, file_path: Path, start: bool = False) -> None: """ Import a session from the EmulationScript XML format. - :param file_name: xml file to load session from + :param file_path: xml file to load session from :param start: instantiate session if true, false otherwise :return: nothing """ - logging.info("opening xml: %s", file_name) - + logger.info("opening xml: %s", file_path) # clear out existing session self.clear() - # set state and read xml state = EventTypes.CONFIGURATION_STATE if start else EventTypes.DEFINITION_STATE self.set_state(state) - self.name = os.path.basename(file_name) - self.file_name = file_name - CoreXmlReader(self).read(file_name) - + self.name = file_path.name + self.file_path = file_path + CoreXmlReader(self).read(file_path) # start session if needed if start: self.set_state(EventTypes.INSTANTIATION_STATE) self.instantiate() - def save_xml(self, file_name: str) -> None: + def save_xml(self, file_path: Path) -> None: """ Export a session to the EmulationScript XML format. - :param file_name: file name to write session xml to + :param file_path: file name to write session xml to :return: nothing """ - CoreXmlWriter(self).write(file_name) + CoreXmlWriter(self).write(file_path) def add_hook( - self, state: EventTypes, file_name: str, data: str, source_name: str = None + self, state: EventTypes, file_name: str, data: str, src_name: str = None ) -> None: """ Store a hook from a received file message. @@ -684,11 +674,11 @@ class Session: :param state: when to run hook :param file_name: file name for hook :param data: hook data - :param source_name: source name + :param src_name: source name :return: nothing """ - logging.info( - "setting state hook: %s - %s source(%s)", state, file_name, source_name + logger.info( + "setting state hook: %s - %s source(%s)", state, file_name, src_name ) hook = file_name, data state_hooks = self.hooks.setdefault(state, []) @@ -696,26 +686,26 @@ class Session: # immediately run a hook if it is in the current state if self.state == state: - logging.info("immediately running new state hook") + logger.info("immediately running new state hook") self.run_hook(hook) def add_node_file( - self, node_id: int, source_name: str, file_name: str, data: str + self, node_id: int, src_path: Path, file_path: Path, data: str ) -> None: """ Add a file to a node. :param node_id: node to add file to - :param source_name: source file name - :param file_name: file name to add + :param src_path: source file path + :param file_path: file path to add :param data: file data :return: nothing """ - node = self.get_node(node_id, CoreNodeBase) - if source_name is not None: - node.addfile(source_name, file_name) + node = self.get_node(node_id, CoreNode) + if src_path is not None: + node.addfile(src_path, file_path) elif data is not None: - node.nodefile(file_name, data) + node.create_file(file_path, data) def clear(self) -> None: """ @@ -769,9 +759,9 @@ class Session: Shutdown all session nodes and remove the session directory. """ if self.state == EventTypes.SHUTDOWN_STATE: - logging.info("session(%s) state(%s) already shutdown", self.id, self.state) + logger.info("session(%s) state(%s) already shutdown", self.id, self.state) else: - logging.info("session(%s) state(%s) shutting down", self.id, self.state) + logger.info("session(%s) state(%s) shutting down", self.id, self.state) self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True) # clear out current core session self.clear() @@ -780,7 +770,7 @@ class Session: # remove this sessions working directory preserve = self.options.get_config("preservedir") == "1" if not preserve: - shutil.rmtree(self.session_dir, ignore_errors=True) + shutil.rmtree(self.directory, ignore_errors=True) def broadcast_event(self, event_data: EventData) -> None: """ @@ -864,7 +854,7 @@ class Session: return self.state = state self.state_time = time.monotonic() - logging.info("changing session(%s) to state %s", self.id, state.name) + logger.info("changing session(%s) to state %s", self.id, state.name) self.write_state(state) self.run_hooks(state) self.run_state_hooks(state) @@ -879,12 +869,12 @@ class Session: :param state: state to write to file :return: nothing """ - state_file = os.path.join(self.session_dir, "state") + state_file = self.directory / "state" try: - with open(state_file, "w") as f: + with state_file.open("w") as f: f.write(f"{state.value} {state.name}\n") except IOError: - logging.exception("error writing state file: %s", state.name) + logger.exception("error writing state file: %s", state.name) def run_hooks(self, state: EventTypes) -> None: """ @@ -906,24 +896,24 @@ class Session: :return: nothing """ file_name, data = hook - logging.info("running hook %s", file_name) - file_path = os.path.join(self.session_dir, file_name) - log_path = os.path.join(self.session_dir, f"{file_name}.log") + logger.info("running hook %s", file_name) + file_path = self.directory / file_name + log_path = self.directory / f"{file_name}.log" try: - with open(file_path, "w") as f: + with file_path.open("w") as f: f.write(data) - with open(log_path, "w") as f: + with log_path.open("w") as f: args = ["/bin/sh", file_name] subprocess.check_call( args, stdout=f, stderr=subprocess.STDOUT, close_fds=True, - cwd=self.session_dir, + cwd=self.directory, env=self.get_environment(), ) except (IOError, subprocess.CalledProcessError): - logging.exception("error running hook: %s", file_path) + logger.exception("error running hook: %s", file_path) def run_state_hooks(self, state: EventTypes) -> None: """ @@ -940,7 +930,7 @@ class Session: hook(state) except Exception: message = f"exception occurred when running {state.name} state hook: {hook}" - logging.exception(message) + logger.exception(message) self.exception(ExceptionLevels.ERROR, "Session.run_state_hooks", message) def add_state_hook( @@ -983,10 +973,10 @@ class Session: """ self.emane.poststartup() # create session deployed xml - xml_file_name = os.path.join(self.session_dir, "session-deployed.xml") xml_writer = corexml.CoreXmlWriter(self) corexmldeployment.CoreXmlDeployment(self, xml_writer.scenario) - xml_writer.write(xml_file_name) + xml_file_path = self.directory / "session-deployed.xml" + xml_writer.write(xml_file_path) def get_environment(self, state: bool = True) -> Dict[str, str]: """ @@ -1001,9 +991,9 @@ class Session: env["CORE_PYTHON"] = sys.executable env["SESSION"] = str(self.id) env["SESSION_SHORT"] = self.short_session_id() - env["SESSION_DIR"] = self.session_dir + env["SESSION_DIR"] = str(self.directory) env["SESSION_NAME"] = str(self.name) - env["SESSION_FILENAME"] = str(self.file_name) + env["SESSION_FILENAME"] = str(self.file_path) env["SESSION_USER"] = str(self.user) if state: env["SESSION_STATE"] = str(self.state) @@ -1011,8 +1001,8 @@ class Session: # /etc/core/environment # /home/user/.core/environment # /tmp/pycore./environment - core_env_path = Path(constants.CORE_CONF_DIR) / "environment" - session_env_path = Path(self.session_dir) / "environment" + core_env_path = constants.CORE_CONF_DIR / "environment" + session_env_path = self.directory / "environment" if self.user: user_home_path = Path(f"~{self.user}").expanduser() user_env1 = user_home_path / ".core" / "environment" @@ -1025,23 +1015,23 @@ class Session: try: utils.load_config(path, env) except IOError: - logging.exception("error reading environment file: %s", path) + logger.exception("error reading environment file: %s", path) return env - def set_thumbnail(self, thumb_file: str) -> None: + def set_thumbnail(self, thumb_file: Path) -> None: """ Set the thumbnail filename. Move files from /tmp to session dir. :param thumb_file: tumbnail file to set for session :return: nothing """ - if not os.path.exists(thumb_file): - logging.error("thumbnail file to set does not exist: %s", thumb_file) + if not thumb_file.is_file(): + logger.error("thumbnail file to set does not exist: %s", thumb_file) self.thumbnail = None return - destination_file = os.path.join(self.session_dir, os.path.basename(thumb_file)) - shutil.copy(thumb_file, destination_file) - self.thumbnail = destination_file + dst_path = self.directory / thumb_file.name + shutil.copy(thumb_file, dst_path) + self.thumbnail = dst_path def set_user(self, user: str) -> None: """ @@ -1054,10 +1044,10 @@ class Session: if user: try: uid = pwd.getpwnam(user).pw_uid - gid = os.stat(self.session_dir).st_gid - os.chown(self.session_dir, uid, gid) + gid = self.directory.stat().st_gid + os.chown(self.directory, uid, gid) except IOError: - logging.exception("failed to set permission on %s", self.session_dir) + logger.exception("failed to set permission on %s", self.directory) self.user = user def create_node( @@ -1114,7 +1104,7 @@ class Session: with self.nodes_lock: if _id in self.nodes: node = self.nodes.pop(_id) - logging.info("deleted node(%s)", node.name) + logger.info("deleted node(%s)", node.name) if node: node.shutdown() self.sdt.delete_node(_id) @@ -1140,14 +1130,14 @@ class Session: Write nodes to a 'nodes' file in the session dir. The 'nodes' file lists: number, name, api-type, class-type """ - file_path = os.path.join(self.session_dir, "nodes") + file_path = self.directory / "nodes" try: with self.nodes_lock: - with open(file_path, "w") as f: + with file_path.open("w") as f: for _id, node in self.nodes.items(): f.write(f"{_id} {node.name} {node.apitype} {type(node)}\n") except IOError: - logging.exception("error writing nodes file") + logger.exception("error writing nodes file") def exception( self, level: ExceptionLevels, source: str, text: str, node_id: int = None @@ -1238,13 +1228,13 @@ class Session: """ # this is called from instantiate() after receiving an event message # for the instantiation state - logging.debug( + logger.debug( "session(%s) checking if not in runtime state, current state: %s", self.id, self.state.name, ) if self.state == EventTypes.RUNTIME_STATE: - logging.info("valid runtime state found, returning") + logger.info("valid runtime state found, returning") return # start event loop and set to runtime self.event_loop.run() @@ -1258,25 +1248,23 @@ class Session: :return: nothing """ if self.state.already_collected(): - logging.info( + logger.info( "session(%s) state(%s) already data collected", self.id, self.state ) return - logging.info("session(%s) state(%s) data collection", self.id, self.state) + logger.info("session(%s) state(%s) data collection", self.id, self.state) self.set_state(EventTypes.DATACOLLECT_STATE, send_event=True) # stop event loop self.event_loop.stop() - # stop node services + # stop mobility and node services with self.nodes_lock: funcs = [] - for node_id in self.nodes: - node = self.nodes[node_id] - if not isinstance(node, CoreNodeBase) or not node.up: - continue - args = (node,) - funcs.append((self.services.stop_services, args, {})) + for node in self.nodes.values(): + if isinstance(node, CoreNodeBase) and node.up: + args = (node,) + funcs.append((self.services.stop_services, args, {})) utils.threadpool(funcs) # shutdown emane @@ -1307,7 +1295,7 @@ class Session: :param node: node to boot :return: nothing """ - logging.info("booting node(%s): %s", node.name, [x.name for x in node.services]) + logger.info("booting node(%s): %s", node.name, [x.name for x in node.services]) self.services.boot_services(node) node.start_config_services() @@ -1328,7 +1316,7 @@ class Session: funcs.append((self.boot_node, (node,), {})) results, exceptions = utils.threadpool(funcs) total = time.monotonic() - start - logging.debug("boot run time: %s", total) + logger.debug("boot run time: %s", total) if not exceptions: self.update_control_iface_hosts() return exceptions @@ -1356,7 +1344,7 @@ class Session: """ d0 = self.options.get_config("controlnetif0") if d0: - logging.error("controlnet0 cannot be assigned with a host interface") + logger.error("controlnet0 cannot be assigned with a host interface") d1 = self.options.get_config("controlnetif1") d2 = self.options.get_config("controlnetif2") d3 = self.options.get_config("controlnetif3") @@ -1401,7 +1389,7 @@ class Session: :param conf_required: flag to check if conf is required :return: control net node """ - logging.debug( + logger.debug( "add/remove control net: index(%s) remove(%s) conf_required(%s)", net_index, remove, @@ -1415,7 +1403,7 @@ class Session: return None else: prefix_spec = CtrlNet.DEFAULT_PREFIX_LIST[net_index] - logging.debug("prefix spec: %s", prefix_spec) + logger.debug("prefix spec: %s", prefix_spec) server_iface = self.get_control_net_server_ifaces()[net_index] # return any existing controlnet bridge @@ -1438,7 +1426,7 @@ class Session: if net_index == 0: updown_script = self.options.get_config("controlnet_updown_script") if not updown_script: - logging.debug("controlnet updown script not configured") + logger.debug("controlnet updown script not configured") prefixes = prefix_spec.split() if len(prefixes) > 1: @@ -1452,7 +1440,7 @@ class Session: else: prefix = prefixes[0] - logging.info( + logger.info( "controlnet(%s) prefix(%s) updown(%s) serverintf(%s)", _id, prefix, @@ -1508,6 +1496,7 @@ class Session: mac=utils.random_mac(), ip4=ip4, ip4_mask=ip4_mask, + mtu=DEFAULT_MTU, ) iface = node.new_iface(control_net, iface_data) iface.control = True @@ -1515,7 +1504,7 @@ class Session: msg = f"Control interface not added to node {node.id}. " msg += f"Invalid control network prefix ({control_net.prefix}). " msg += "A longer prefix length may be required for this many nodes." - logging.exception(msg) + logger.exception(msg) def update_control_iface_hosts( self, net_index: int = 0, remove: bool = False @@ -1533,12 +1522,12 @@ class Session: try: control_net = self.get_control_net(net_index) except CoreError: - logging.exception("error retrieving control net node") + logger.exception("error retrieving control net node") return header = f"CORE session {self.id} host entries" if remove: - logging.info("Removing /etc/hosts file entries.") + logger.info("Removing /etc/hosts file entries.") utils.file_demunge("/etc/hosts", header) return @@ -1548,7 +1537,7 @@ class Session: for ip in iface.ips(): entries.append(f"{ip.ip} {name}") - logging.info("Adding %d /etc/hosts file entries.", len(entries)) + logger.info("Adding %d /etc/hosts file entries.", len(entries)) utils.file_munge("/etc/hosts", header, "\n".join(entries) + "\n") def runtime(self) -> float: @@ -1577,7 +1566,7 @@ class Session: current_time = self.runtime() if current_time > 0: if event_time <= current_time: - logging.warning( + logger.warning( "could not schedule past event for time %s (run time is now %s)", event_time, current_time, @@ -1589,7 +1578,7 @@ class Session: ) if not name: name = "" - logging.info( + logger.info( "scheduled event %s at time %s data=%s", name, event_time + current_time, @@ -1608,12 +1597,12 @@ class Session: :return: nothing """ if data is None: - logging.warning("no data for event node(%s) name(%s)", node_id, name) + logger.warning("no data for event node(%s) name(%s)", node_id, name) return now = self.runtime() if not name: name = "" - logging.info("running event %s at time %s cmd=%s", name, now, data) + logger.info("running event %s at time %s cmd=%s", name, now, data) if not node_id: utils.mute_detach(data) else: diff --git a/daemon/core/emulator/sessionconfig.py b/daemon/core/emulator/sessionconfig.py index 9b22bcc7..028d4e66 100644 --- a/daemon/core/emulator/sessionconfig.py +++ b/daemon/core/emulator/sessionconfig.py @@ -1,7 +1,14 @@ from typing import Any, List -from core.config import ConfigurableManager, ConfigurableOptions, Configuration -from core.emulator.enumerations import ConfigDataTypes, RegisterTlvs +from core.config import ( + ConfigBool, + ConfigInt, + ConfigString, + ConfigurableManager, + ConfigurableOptions, + Configuration, +) +from core.emulator.enumerations import RegisterTlvs from core.plugins.sdt import Sdt @@ -12,53 +19,28 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): name: str = "session" options: List[Configuration] = [ - Configuration( - _id="controlnet", _type=ConfigDataTypes.STRING, label="Control Network" + ConfigString(id="controlnet", label="Control Network"), + ConfigString(id="controlnet0", label="Control Network 0"), + ConfigString(id="controlnet1", label="Control Network 1"), + ConfigString(id="controlnet2", label="Control Network 2"), + ConfigString(id="controlnet3", label="Control Network 3"), + ConfigString(id="controlnet_updown_script", label="Control Network Script"), + ConfigBool(id="enablerj45", default="1", label="Enable RJ45s"), + ConfigBool(id="preservedir", default="0", label="Preserve session dir"), + ConfigBool(id="enablesdt", default="0", label="Enable SDT3D output"), + ConfigString(id="sdturl", default=Sdt.DEFAULT_SDT_URL, label="SDT3D URL"), + ConfigBool(id="ovs", default="0", label="Enable OVS"), + ConfigInt(id="platform_id_start", default="1", label="EMANE Platform ID Start"), + ConfigInt(id="nem_id_start", default="1", label="EMANE NEM ID Start"), + ConfigBool(id="link_enabled", default="1", label="EMANE Links?"), + ConfigInt( + id="loss_threshold", default="30", label="EMANE Link Loss Threshold (%)" ), - Configuration( - _id="controlnet0", _type=ConfigDataTypes.STRING, label="Control Network 0" - ), - Configuration( - _id="controlnet1", _type=ConfigDataTypes.STRING, label="Control Network 1" - ), - Configuration( - _id="controlnet2", _type=ConfigDataTypes.STRING, label="Control Network 2" - ), - Configuration( - _id="controlnet3", _type=ConfigDataTypes.STRING, label="Control Network 3" - ), - Configuration( - _id="controlnet_updown_script", - _type=ConfigDataTypes.STRING, - label="Control Network Script", - ), - Configuration( - _id="enablerj45", - _type=ConfigDataTypes.BOOL, - default="1", - label="Enable RJ45s", - ), - Configuration( - _id="preservedir", - _type=ConfigDataTypes.BOOL, - default="0", - label="Preserve session dir", - ), - Configuration( - _id="enablesdt", - _type=ConfigDataTypes.BOOL, - default="0", - label="Enable SDT3D output", - ), - Configuration( - _id="sdturl", - _type=ConfigDataTypes.STRING, - default=Sdt.DEFAULT_SDT_URL, - label="SDT3D URL", - ), - Configuration( - _id="ovs", _type=ConfigDataTypes.BOOL, default="0", label="Enable OVS" + ConfigInt( + id="link_interval", default="1", label="EMANE Link Check Interval (sec)" ), + ConfigInt(id="link_timeout", default="4", label="EMANE Link Timeout (sec)"), + ConfigInt(id="mtu", default="0", label="MTU for All Devices"), ] config_type: RegisterTlvs = RegisterTlvs.UTILITY @@ -112,3 +94,13 @@ class SessionConfig(ConfigurableManager, ConfigurableOptions): if value is not None: value = int(value) return value + + def config_reset(self, node_id: int = None) -> None: + """ + Clear prior configuration files and reset to default values. + + :param node_id: node id to store configuration for + :return: nothing + """ + super().config_reset(node_id) + self.set_configs(self.default_values()) diff --git a/daemon/core/errors.py b/daemon/core/errors.py index a75bd536..20ffc3a9 100644 --- a/daemon/core/errors.py +++ b/daemon/core/errors.py @@ -46,3 +46,11 @@ class CoreServiceBootError(Exception): """ pass + + +class CoreConfigError(Exception): + """ + Used when there is an error defining a configurable option. + """ + + pass diff --git a/daemon/core/executables.py b/daemon/core/executables.py index 16f159fc..3d0e80f6 100644 --- a/daemon/core/executables.py +++ b/daemon/core/executables.py @@ -7,15 +7,15 @@ SYSCTL: str = "sysctl" IP: str = "ip" ETHTOOL: str = "ethtool" TC: str = "tc" -EBTABLES: str = "ebtables" MOUNT: str = "mount" UMOUNT: str = "umount" OVS_VSCTL: str = "ovs-vsctl" TEST: str = "test" +NFTABLES: str = "nft" COMMON_REQUIREMENTS: List[str] = [ BASH, - EBTABLES, + NFTABLES, ETHTOOL, IP, MOUNT, diff --git a/daemon/core/gui/app.py b/daemon/core/gui/app.py index eddb60d3..d905bff3 100644 --- a/daemon/core/gui/app.py +++ b/daemon/core/gui/app.py @@ -22,6 +22,7 @@ from core.gui.statusbar import StatusBar from core.gui.themes import PADY from core.gui.toolbar import Toolbar +logger = logging.getLogger(__name__) WIDTH: int = 1000 HEIGHT: int = 800 @@ -171,7 +172,7 @@ class Application(ttk.Frame): def show_grpc_exception( self, message: str, e: grpc.RpcError, blocking: bool = False ) -> None: - logging.exception("app grpc exception", exc_info=e) + logger.exception("app grpc exception", exc_info=e) dialog = ErrorDialog(self, "GRPC Exception", message, e.details()) if blocking: dialog.show() @@ -179,7 +180,7 @@ class Application(ttk.Frame): self.after(0, lambda: dialog.show()) def show_exception(self, message: str, e: Exception) -> None: - logging.exception("app exception", exc_info=e) + logger.exception("app exception", exc_info=e) self.after( 0, lambda: ErrorDialog(self, "App Exception", message, str(e)).show() ) diff --git a/daemon/core/gui/appconfig.py b/daemon/core/gui/appconfig.py index 8ce7b2b0..04f2fdcb 100644 --- a/daemon/core/gui/appconfig.py +++ b/daemon/core/gui/appconfig.py @@ -120,25 +120,18 @@ class IpConfigs(yaml.YAMLObject): yaml_tag: str = "!IpConfigs" yaml_loader: Type[yaml.SafeLoader] = yaml.SafeLoader - def __init__( - self, - ip4: str = None, - ip6: str = None, - ip4s: List[str] = None, - ip6s: List[str] = None, - ) -> None: - if ip4s is None: - ip4s = ["10.0.0.0", "192.168.0.0", "172.16.0.0"] - self.ip4s: List[str] = ip4s - if ip6s is None: - ip6s = ["2001::", "2002::", "a::"] - self.ip6s: List[str] = ip6s - if ip4 is None: - ip4 = self.ip4s[0] - self.ip4: str = ip4 - if ip6 is None: - ip6 = self.ip6s[0] - self.ip6: str = ip6 + def __init__(self, **kwargs) -> None: + self.__setstate__(kwargs) + + def __setstate__(self, kwargs): + self.ip4s: List[str] = kwargs.get( + "ip4s", ["10.0.0.0", "192.168.0.0", "172.16.0.0"] + ) + self.ip4: str = kwargs.get("ip4", self.ip4s[0]) + self.ip6s: List[str] = kwargs.get("ip6s", ["2001::", "2002::", "a::"]) + self.ip6: str = kwargs.get("ip6", self.ip6s[0]) + self.enable_ip4: bool = kwargs.get("enable_ip4", True) + self.enable_ip6: bool = kwargs.get("enable_ip6", True) class GuiConfig(yaml.YAMLObject): @@ -223,7 +216,7 @@ def check_directory() -> None: def read() -> GuiConfig: with CONFIG_PATH.open("r") as f: - return yaml.load(f, Loader=yaml.SafeLoader) + return yaml.safe_load(f) def save(config: GuiConfig) -> None: diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index d2f0cb87..4905ac8a 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -6,23 +6,18 @@ import json import logging import os import tkinter as tk +from pathlib import Path from tkinter import messagebox from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple import grpc -from core.api.grpc import ( - client, - configservices_pb2, - core_pb2, - emane_pb2, - mobility_pb2, - services_pb2, - wlan_pb2, -) +from core.api.grpc import client, configservices_pb2, core_pb2 from core.api.grpc.wrappers import ( ConfigOption, ConfigService, + EmaneModelConfig, + Event, ExceptionEvent, Link, LinkEvent, @@ -33,6 +28,9 @@ from core.api.grpc.wrappers import ( NodeServiceData, NodeType, Position, + Server, + ServiceConfig, + ServiceFileConfig, Session, SessionLocation, SessionState, @@ -45,10 +43,11 @@ from core.gui.dialogs.mobilityplayer import MobilityPlayer from core.gui.dialogs.sessions import SessionsDialog from core.gui.graph.edges import CanvasEdge from core.gui.graph.node import CanvasNode -from core.gui.graph.shape import Shape from core.gui.interface import InterfaceManager from core.gui.nodeutils import NodeDraw +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -77,6 +76,7 @@ class CoreClient: self.config_services: Dict[str, ConfigService] = {} # loaded configuration data + self.emane_models: List[str] = [] self.servers: Dict[str, CoreServer] = {} self.custom_nodes: Dict[str, NodeDraw] = {} self.custom_observers: Dict[str, Observer] = {} @@ -98,8 +98,7 @@ class CoreClient: @property def client(self) -> client.CoreGrpcClient: if self.session: - response = self._client.check_session(self.session.id) - if not response.result: + if not self._client.check_session(self.session.id): throughputs_enabled = self.handling_throughputs is not None self.cancel_throughputs() self.cancel_events() @@ -150,22 +149,20 @@ class CoreClient: for observer in self.app.guiconfig.observers: self.custom_observers[observer.name] = observer - def handle_events(self, event: core_pb2.Event) -> None: + def handle_events(self, event: Event) -> None: if not self.session or event.source == GUI_SOURCE: return if event.session_id != self.session.id: - logging.warning( + logger.warning( "ignoring event session(%s) current(%s)", event.session_id, self.session.id, ) return - - if event.HasField("link_event"): - link_event = LinkEvent.from_proto(event.link_event) - self.app.after(0, self.handle_link_event, link_event) - elif event.HasField("session_event"): - logging.info("session event: %s", event) + if event.link_event: + self.app.after(0, self.handle_link_event, event.link_event) + elif event.session_event: + logger.info("session event: %s", event) session_event = event.session_event if session_event.event <= SessionState.SHUTDOWN.value: self.session.state = SessionState(session_event.event) @@ -180,24 +177,22 @@ class CoreClient: else: dialog.set_pause() else: - logging.warning("unknown session event: %s", session_event) - elif event.HasField("node_event"): - node_event = NodeEvent.from_proto(event.node_event) - self.app.after(0, self.handle_node_event, node_event) - elif event.HasField("config_event"): - logging.info("config event: %s", event) - elif event.HasField("exception_event"): - event = ExceptionEvent.from_proto(event.session_id, event.exception_event) - self.handle_exception_event(event) + logger.warning("unknown session event: %s", session_event) + elif event.node_event: + self.app.after(0, self.handle_node_event, event.node_event) + elif event.config_event: + logger.info("config event: %s", event) + elif event.exception_event: + self.handle_exception_event(event.exception_event) else: - logging.info("unhandled event: %s", event) + logger.info("unhandled event: %s", event) def handle_link_event(self, event: LinkEvent) -> None: - logging.debug("Link event: %s", event) + logger.debug("Link event: %s", event) node1_id = event.link.node1_id node2_id = event.link.node2_id if node1_id == node2_id: - logging.warning("ignoring links with loops: %s", event) + logger.warning("ignoring links with loops: %s", event) return canvas_node1 = self.canvas_nodes[node1_id] canvas_node2 = self.canvas_nodes[node2_id] @@ -215,7 +210,7 @@ class CoreClient: canvas_node1, canvas_node2, event.link ) else: - logging.warning("unknown link event: %s", event) + logger.warning("unknown link event: %s", event) else: if event.message_type == MessageType.ADD: self.app.manager.add_wired_edge(canvas_node1, canvas_node2, event.link) @@ -224,10 +219,10 @@ class CoreClient: elif event.message_type == MessageType.NONE: self.app.manager.update_wired_edge(event.link) else: - logging.warning("unknown link event: %s", event) + logger.warning("unknown link event: %s", event) def handle_node_event(self, event: NodeEvent) -> None: - logging.debug("node event: %s", event) + logger.debug("node event: %s", event) node = event.node if event.message_type == MessageType.NONE: canvas_node = self.canvas_nodes[node.id] @@ -238,15 +233,13 @@ class CoreClient: canvas_node.update_icon(node.icon) elif event.message_type == MessageType.DELETE: canvas_node = self.canvas_nodes[node.id] - canvas_node.canvas.clear_selection() - canvas_node.canvas.select_object(canvas_node.id) - canvas_node.canvas.delete_selected_objects() + canvas_node.canvas_delete() elif event.message_type == MessageType.ADD: if node.id in self.session.nodes: - logging.error("core node already exists: %s", node) + logger.error("core node already exists: %s", node) self.app.manager.add_core_node(node) else: - logging.warning("unknown node event: %s", event) + logger.warning("unknown node event: %s", event) def enable_throughputs(self) -> None: self.handling_throughputs = self.client.throughputs( @@ -278,34 +271,35 @@ class CoreClient: CPU_USAGE_DELAY, self.handle_cpu_event ) - def handle_throughputs(self, event: core_pb2.ThroughputsEvent) -> None: - event = ThroughputsEvent.from_proto(event) + def handle_throughputs(self, event: ThroughputsEvent) -> None: if event.session_id != self.session.id: - logging.warning( + logger.warning( "ignoring throughput event session(%s) current(%s)", event.session_id, self.session.id, ) return - logging.debug("handling throughputs event: %s", event) + logger.debug("handling throughputs event: %s", event) self.app.after(0, self.app.manager.set_throughputs, event) def handle_cpu_event(self, event: core_pb2.CpuUsageEvent) -> None: self.app.after(0, self.app.statusbar.set_cpu, event.usage) def handle_exception_event(self, event: ExceptionEvent) -> None: - logging.info("exception event: %s", event) + logger.info("exception event: %s", event) self.app.statusbar.add_alert(event) + def update_session_title(self) -> None: + title_file = self.session.file.name if self.session.file else "" + self.master.title(f"CORE Session({self.session.id}) {title_file}") + def join_session(self, session_id: int) -> None: - logging.info("joining session(%s)", session_id) + logger.info("joining session(%s)", session_id) self.reset() try: - response = self.client.get_session(session_id) - self.session = Session.from_proto(response.session) - self.client.set_session_user(self.session.id, self.user) - title_file = self.session.file.name if self.session.file else "" - self.master.title(f"CORE Session({self.session.id}) {title_file}") + self.session = self.client.get_session(session_id) + self.session.user = self.user + self.update_session_title() self.handling_events = self.client.events( self.session.id, self.handle_events ) @@ -320,53 +314,14 @@ class CoreClient: def is_runtime(self) -> bool: return self.session and self.session.state == SessionState.RUNTIME - def parse_metadata(self) -> None: - # canvas setting - config = self.session.metadata - canvas_config = config.get("canvas") - logging.debug("canvas metadata: %s", canvas_config) - if canvas_config: - canvas_config = json.loads(canvas_config) - self.app.manager.parse_metadata(canvas_config) - - # load saved shapes - shapes_config = config.get("shapes") - if shapes_config: - shapes_config = json.loads(shapes_config) - for shape_config in shapes_config: - logging.debug("loading shape: %s", shape_config) - Shape.from_metadata(self.app, shape_config) - - # load edges config - edges_config = config.get("edges") - if edges_config: - edges_config = json.loads(edges_config) - logging.info("edges config: %s", edges_config) - for edge_config in edges_config: - edge = self.links[edge_config["token"]] - edge.width = edge_config["width"] - edge.color = edge_config["color"] - edge.redraw() - - # read hidden nodes - hidden = config.get("hidden") - if hidden: - hidden = json.loads(hidden) - for _id in hidden: - canvas_node = self.canvas_nodes.get(_id) - if canvas_node: - canvas_node.hide() - else: - logging.warning("invalid node to hide: %s", _id) - def create_new_session(self) -> None: """ Create a new session """ try: - response = self.client.create_session() - logging.info("created session: %s", response) - self.join_session(response.session_id) + session = self.client.create_session() + logger.info("created session: %s", session.id) + self.join_session(session.id) location_config = self.app.guiconfig.location self.session.location = SessionLocation( x=location_config.x, @@ -387,7 +342,7 @@ class CoreClient: session_id = self.session.id try: response = self.client.delete_session(session_id) - logging.info("deleted session(%s), Result: %s", session_id, response) + logger.info("deleted session(%s), Result: %s", session_id, response) except grpc.RpcError as e: self.app.show_grpc_exception("Delete Session Error", e) @@ -397,23 +352,21 @@ class CoreClient: """ try: self.client.connect() - # get all available services - response = self.client.get_services() - for service in response.services: + # get current core configurations services/config services + core_config = self.client.get_config() + self.emane_models = sorted(core_config.emane_models) + for service in core_config.services: group_services = self.services.setdefault(service.group, set()) group_services.add(service.name) - # get config service informations - response = self.client.get_config_services() - for service in response.services: - self.config_services[service.name] = ConfigService.from_proto(service) + for service in core_config.config_services: + self.config_services[service.name] = service group_services = self.config_services_groups.setdefault( service.group, set() ) group_services.add(service.name) # join provided session, create new session, or show dialog to select an # existing session - response = self.client.get_sessions() - sessions = response.sessions + sessions = self.client.get_sessions() if session_id: session_ids = set(x.id for x in sessions) if session_id not in session_ids: @@ -432,71 +385,50 @@ class CoreClient: dialog = SessionsDialog(self.app, True) dialog.show() except grpc.RpcError as e: - logging.exception("core setup error") + logger.exception("core setup error") self.app.show_grpc_exception("Setup Error", e, blocking=True) self.app.close() def edit_node(self, core_node: Node) -> None: try: - position = core_node.position.to_proto() - self.client.edit_node( - self.session.id, core_node.id, position, source=GUI_SOURCE + self.client.move_node( + self.session.id, core_node.id, core_node.position, source=GUI_SOURCE ) except grpc.RpcError as e: self.app.show_grpc_exception("Edit Node Error", e) - def send_servers(self) -> None: - for server in self.servers.values(): - self.client.add_session_server(self.session.id, server.name, server.address) - - def start_session(self) -> Tuple[bool, List[str]]: - self.ifaces_manager.set_macs([x.link for x in self.links.values()]) - nodes = [x.to_proto() for x in self.session.nodes.values()] + def get_links(self, definition: bool = False) -> List[Link]: + if not definition: + self.ifaces_manager.set_macs([x.link for x in self.links.values()]) links = [] - asymmetric_links = [] for edge in self.links.values(): link = edge.link - if link.iface1 and not link.iface1.mac: - link.iface1.mac = self.ifaces_manager.next_mac() - if link.iface2 and not link.iface2.mac: - link.iface2.mac = self.ifaces_manager.next_mac() - links.append(link.to_proto()) + if not definition: + if link.iface1 and not link.iface1.mac: + link.iface1.mac = self.ifaces_manager.next_mac() + if link.iface2 and not link.iface2.mac: + link.iface2.mac = self.ifaces_manager.next_mac() + links.append(link) if edge.asymmetric_link: - asymmetric_links.append(edge.asymmetric_link.to_proto()) - wlan_configs = self.get_wlan_configs_proto() - mobility_configs = self.get_mobility_configs_proto() - emane_model_configs = self.get_emane_model_configs_proto() - hooks = [x.to_proto() for x in self.session.hooks.values()] - service_configs = self.get_service_configs_proto() - file_configs = self.get_service_file_configs_proto() - config_service_configs = self.get_config_service_configs_proto() - emane_config = to_dict(self.session.emane_config) + links.append(edge.asymmetric_link) + return links + + def start_session(self, definition: bool = False) -> Tuple[bool, List[str]]: + self.session.links = self.get_links(definition) + self.session.metadata = self.get_metadata() + self.session.servers.clear() + for server in self.servers.values(): + self.session.servers.append(Server(name=server.name, host=server.address)) result = False exceptions = [] try: - self.send_servers() - response = self.client.start_session( + result, exceptions = self.client.start_session(self.session, definition) + logger.info( + "start session(%s) definition(%s), result: %s", self.session.id, - nodes, - links, - self.session.location.to_proto(), - hooks, - emane_config, - emane_model_configs, - wlan_configs, - mobility_configs, - service_configs, - file_configs, - asymmetric_links, - config_service_configs, + definition, + result, ) - logging.info( - "start session(%s), result: %s", self.session.id, response.result - ) - if response.result: - self.set_metadata() - result = response.result - exceptions = response.exceptions except grpc.RpcError as e: self.app.show_grpc_exception("Start Session Error", e) return result, exceptions @@ -506,9 +438,8 @@ class CoreClient: session_id = self.session.id result = False try: - response = self.client.stop_session(session_id) - logging.info("stopped session(%s), result: %s", session_id, response) - result = response.result + result = self.client.stop_session(session_id) + logger.info("stopped session(%s), result: %s", session_id, result) except grpc.RpcError as e: self.app.show_grpc_exception("Stop Session Error", e) return result @@ -522,7 +453,7 @@ class CoreClient: self.mobility_players[node.id] = mobility_player mobility_player.show() - def set_metadata(self) -> None: + def get_metadata(self) -> Dict[str, str]: # create canvas data canvas_config = self.app.manager.get_metadata() canvas_config = json.dumps(canvas_config) @@ -548,11 +479,9 @@ class CoreClient: hidden = json.dumps(hidden) # save metadata - metadata = dict( + return dict( canvas=canvas_config, shapes=shapes, edges=edges_config, hidden=hidden ) - response = self.client.set_session_metadata(self.session.id, metadata) - logging.debug("set session metadata %s, result: %s", metadata, response) def launch_terminal(self, node_id: int) -> None: try: @@ -564,9 +493,9 @@ class CoreClient: parent=self.app, ) return - response = self.client.get_node_terminal(self.session.id, node_id) - cmd = f"{terminal} {response.terminal} &" - logging.info("launching terminal %s", cmd) + node_term = self.client.get_node_terminal(self.session.id, node_id) + cmd = f"{terminal} {node_term} &" + logger.info("launching terminal %s", cmd) os.system(cmd) except grpc.RpcError as e: self.app.show_grpc_exception("Node Terminal Error", e) @@ -574,189 +503,82 @@ class CoreClient: def get_xml_dir(self) -> str: return str(self.session.file.parent) if self.session.file else str(XMLS_PATH) - def save_xml(self, file_path: str = None) -> None: + def save_xml(self, file_path: Path = None) -> bool: """ Save core session as to an xml file """ if not file_path and not self.session.file: - logging.error("trying to save xml for session with no file") - return + logger.error("trying to save xml for session with no file") + return False if not file_path: - file_path = str(self.session.file) + file_path = self.session.file + result = False try: if not self.is_runtime(): - logging.debug("Send session data to the daemon") - self.send_data() - response = self.client.save_xml(self.session.id, file_path) - logging.info("saved xml file %s, result: %s", file_path, response) + logger.debug("sending session data to the daemon") + result, exceptions = self.start_session(definition=True) + if not result: + message = "\n".join(exceptions) + self.app.show_exception_data( + "Session Definition Exception", + "Failed to define session", + message, + ) + self.client.save_xml(self.session.id, str(file_path)) + if self.session.file != file_path: + self.session.file = file_path + self.update_session_title() + logger.info("saved xml file %s", file_path) + result = True except grpc.RpcError as e: self.app.show_grpc_exception("Save XML Error", e) + return result - def open_xml(self, file_path: str) -> None: + def open_xml(self, file_path: Path) -> None: """ Open core xml """ try: - response = self._client.open_xml(file_path) - logging.info("open xml file %s, response: %s", file_path, response) - self.join_session(response.session_id) + result, session_id = self._client.open_xml(file_path) + logger.info( + "open xml file %s, result(%s) session(%s)", + file_path, + result, + session_id, + ) + self.join_session(session_id) except grpc.RpcError as e: self.app.show_grpc_exception("Open XML Error", e) def get_node_service(self, node_id: int, service_name: str) -> NodeServiceData: - response = self.client.get_node_service(self.session.id, node_id, service_name) - logging.debug( - "get node(%s) %s service, response: %s", node_id, service_name, response + node_service = self.client.get_node_service( + self.session.id, node_id, service_name ) - return NodeServiceData.from_proto(response.service) - - def set_node_service( - self, - node_id: int, - service_name: str, - dirs: List[str], - files: List[str], - startups: List[str], - validations: List[str], - shutdowns: List[str], - ) -> NodeServiceData: - response = self.client.set_node_service( - self.session.id, - node_id, - service_name, - directories=dirs, - files=files, - startup=startups, - validate=validations, - shutdown=shutdowns, + logger.debug( + "get node(%s) service(%s): %s", node_id, service_name, node_service ) - logging.info( - "Set %s service for node(%s), files: %s, Startup: %s, " - "Validation: %s, Shutdown: %s, Result: %s", - service_name, - node_id, - files, - startups, - validations, - shutdowns, - response, - ) - response = self.client.get_node_service(self.session.id, node_id, service_name) - return NodeServiceData.from_proto(response.service) + return node_service def get_node_service_file( self, node_id: int, service_name: str, file_name: str ) -> str: - response = self.client.get_node_service_file( + data = self.client.get_node_service_file( self.session.id, node_id, service_name, file_name ) - logging.debug( - "get service file for node(%s), service: %s, file: %s, result: %s", - node_id, - service_name, - file_name, - response, - ) - return response.data - - def set_node_service_file( - self, node_id: int, service_name: str, file_name: str, data: str - ) -> None: - response = self.client.set_node_service_file( - self.session.id, node_id, service_name, file_name, data - ) - logging.info( - "set node(%s) service file, service: %s, file: %s, data: %s, result: %s", + logger.debug( + "get service file for node(%s), service: %s, file: %s, data: %s", node_id, service_name, file_name, data, - response, ) - - def create_nodes_and_links(self) -> None: - """ - create nodes and links that have not been created yet - """ - self.client.set_session_state(self.session.id, SessionState.DEFINITION.value) - for node in self.session.nodes.values(): - response = self.client.add_node( - self.session.id, node.to_proto(), source=GUI_SOURCE - ) - logging.debug("created node: %s", response) - asymmetric_links = [] - for edge in self.links.values(): - self.add_link(edge.link) - if edge.asymmetric_link: - asymmetric_links.append(edge.asymmetric_link) - for link in asymmetric_links: - self.add_link(link) - - def send_data(self) -> None: - """ - Send to daemon all session info, but don't start the session - """ - self.send_servers() - self.create_nodes_and_links() - for config_proto in self.get_wlan_configs_proto(): - self.client.set_wlan_config( - self.session.id, config_proto.node_id, config_proto.config - ) - for config_proto in self.get_mobility_configs_proto(): - self.client.set_mobility_config( - self.session.id, config_proto.node_id, config_proto.config - ) - for config_proto in self.get_service_configs_proto(): - self.client.set_node_service( - self.session.id, - config_proto.node_id, - config_proto.service, - config_proto.files, - config_proto.directories, - config_proto.startup, - config_proto.validate, - config_proto.shutdown, - ) - for config_proto in self.get_service_file_configs_proto(): - self.client.set_node_service_file( - self.session.id, - config_proto.node_id, - config_proto.service, - config_proto.file, - config_proto.data, - ) - for hook in self.session.hooks.values(): - self.client.add_hook( - self.session.id, hook.state.value, hook.file, hook.data - ) - for config_proto in self.get_emane_model_configs_proto(): - self.client.set_emane_model_config( - self.session.id, - config_proto.node_id, - config_proto.model, - config_proto.config, - config_proto.iface_id, - ) - config = to_dict(self.session.emane_config) - self.client.set_emane_config(self.session.id, config) - location = self.session.location - self.client.set_session_location( - self.session.id, - location.x, - location.y, - location.z, - location.lat, - location.lon, - location.alt, - location.scale, - ) - self.set_metadata() + return data def close(self) -> None: """ Clean ups when done using grpc """ - logging.debug("close grpc") + logger.debug("close grpc") self.client.close() def next_node_id(self) -> int: @@ -783,11 +605,11 @@ class CoreClient: image = "ubuntu:latest" emane = None if node_type == NodeType.EMANE: - if not self.session.emane_models: + if not self.emane_models: dialog = EmaneInstallDialog(self.app) dialog.show() return - emane = self.session.emane_models[0] + emane = self.emane_models[0] name = f"emane{node_id}" elif node_type == NodeType.WIRELESS_LAN: name = f"wlan{node_id}" @@ -806,13 +628,13 @@ class CoreClient: ) if nutils.is_custom(node): services = nutils.get_custom_services(self.app.guiconfig, model) - node.services = set(services) + node.config_services = set(services) # assign default services to CORE node else: services = self.session.default_services.get(model) if services: - node.services = services.copy() - logging.info( + node.config_services = set(services) + logger.info( "add node(%s) to session(%s), coordinates(%s, %s)", node.name, self.session.id, @@ -850,7 +672,7 @@ class CoreClient: dst_iface_id = edge.link.iface2.id self.iface_to_edge[(dst_node.id, dst_iface_id)] = edge - def get_wlan_configs_proto(self) -> List[wlan_pb2.WlanConfig]: + def get_wlan_configs(self) -> List[Tuple[int, Dict[str, str]]]: configs = [] for node in self.session.nodes.values(): if node.type != NodeType.WIRELESS_LAN: @@ -858,11 +680,10 @@ class CoreClient: if not node.wlan_config: continue config = ConfigOption.to_dict(node.wlan_config) - wlan_config = wlan_pb2.WlanConfig(node_id=node.id, config=config) - configs.append(wlan_config) + configs.append((node.id, config)) return configs - def get_mobility_configs_proto(self) -> List[mobility_pb2.MobilityConfig]: + def get_mobility_configs(self) -> List[Tuple[int, Dict[str, str]]]: configs = [] for node in self.session.nodes.values(): if not nutils.is_mobility(node): @@ -870,27 +691,24 @@ class CoreClient: if not node.mobility_config: continue config = ConfigOption.to_dict(node.mobility_config) - mobility_config = mobility_pb2.MobilityConfig( - node_id=node.id, config=config - ) - configs.append(mobility_config) + configs.append((node.id, config)) return configs - def get_emane_model_configs_proto(self) -> List[emane_pb2.EmaneModelConfig]: + def get_emane_model_configs(self) -> List[EmaneModelConfig]: configs = [] for node in self.session.nodes.values(): for key, config in node.emane_model_configs.items(): model, iface_id = key - config = ConfigOption.to_dict(config) + # config = ConfigOption.to_dict(config) if iface_id is None: iface_id = -1 - config_proto = emane_pb2.EmaneModelConfig( - node_id=node.id, iface_id=iface_id, model=model, config=config + config = EmaneModelConfig( + node_id=node.id, model=model, iface_id=iface_id, config=config ) - configs.append(config_proto) + configs.append(config) return configs - def get_service_configs_proto(self) -> List[services_pb2.ServiceConfig]: + def get_service_configs(self) -> List[ServiceConfig]: configs = [] for node in self.session.nodes.values(): if not nutils.is_container(node): @@ -898,19 +716,19 @@ class CoreClient: if not node.service_configs: continue for name, config in node.service_configs.items(): - config_proto = services_pb2.ServiceConfig( + config = ServiceConfig( node_id=node.id, service=name, - directories=config.dirs, files=config.configs, + directories=config.dirs, startup=config.startup, validate=config.validate, shutdown=config.shutdown, ) - configs.append(config_proto) + configs.append(config) return configs - def get_service_file_configs_proto(self) -> List[services_pb2.ServiceFileConfig]: + def get_service_file_configs(self) -> List[ServiceFileConfig]: configs = [] for node in self.session.nodes.values(): if not nutils.is_container(node): @@ -919,10 +737,8 @@ class CoreClient: continue for service, file_configs in node.service_file_configs.items(): for file, data in file_configs.items(): - config_proto = services_pb2.ServiceFileConfig( - node_id=node.id, service=service, file=file, data=data - ) - configs.append(config_proto) + config = ServiceFileConfig(node.id, service, file, data) + configs.append(config) return configs def get_config_service_configs_proto( @@ -945,39 +761,37 @@ class CoreClient: return config_service_protos def run(self, node_id: int) -> str: - logging.info("running node(%s) cmd: %s", node_id, self.observer) - return self.client.node_command(self.session.id, node_id, self.observer).output + logger.info("running node(%s) cmd: %s", node_id, self.observer) + _, output = self.client.node_command(self.session.id, node_id, self.observer) + return output def get_wlan_config(self, node_id: int) -> Dict[str, ConfigOption]: - response = self.client.get_wlan_config(self.session.id, node_id) - config = response.config - logging.debug( + config = self.client.get_wlan_config(self.session.id, node_id) + logger.debug( "get wlan configuration from node %s, result configuration: %s", node_id, config, ) - return ConfigOption.from_dict(config) + return config def get_mobility_config(self, node_id: int) -> Dict[str, ConfigOption]: - response = self.client.get_mobility_config(self.session.id, node_id) - config = response.config - logging.debug( + config = self.client.get_mobility_config(self.session.id, node_id) + logger.debug( "get mobility config from node %s, result configuration: %s", node_id, config, ) - return ConfigOption.from_dict(config) + return config def get_emane_model_config( self, node_id: int, model: str, iface_id: int = None ) -> Dict[str, ConfigOption]: if iface_id is None: iface_id = -1 - response = self.client.get_emane_model_config( + config = self.client.get_emane_model_config( self.session.id, node_id, model, iface_id ) - config = response.config - logging.debug( + logger.debug( "get emane model config: node id: %s, EMANE model: %s, " "interface: %s, config: %s", node_id, @@ -985,42 +799,21 @@ class CoreClient: iface_id, config, ) - return ConfigOption.from_dict(config) + return config - def execute_script(self, script) -> None: - response = self.client.execute_script(script) - logging.info("execute python script %s", response) - if response.session_id != -1: - self.join_session(response.session_id) + def execute_script(self, script: str, options: str) -> None: + session_id = self.client.execute_script(script, options) + logger.info("execute python script %s", session_id) + if session_id != -1: + self.join_session(session_id) def add_link(self, link: Link) -> None: - iface1 = link.iface1.to_proto() if link.iface1 else None - iface2 = link.iface2.to_proto() if link.iface2 else None - options = link.options.to_proto() if link.options else None - response = self.client.add_link( - self.session.id, - link.node1_id, - link.node2_id, - iface1, - iface2, - options, - source=GUI_SOURCE, - ) - logging.debug("added link: %s", response) - if not response.result: - logging.error("error adding link: %s", link) + result, _, _ = self.client.add_link(self.session.id, link, source=GUI_SOURCE) + logger.debug("added link: %s", result) + if not result: + logger.error("error adding link: %s", link) def edit_link(self, link: Link) -> None: - iface1_id = link.iface1.id if link.iface1 else None - iface2_id = link.iface2.id if link.iface2 else None - response = self.client.edit_link( - self.session.id, - link.node1_id, - link.node2_id, - link.options.to_proto(), - iface1_id, - iface2_id, - source=GUI_SOURCE, - ) - if not response.result: - logging.error("error editing link: %s", link) + result = self.client.edit_link(self.session.id, link, source=GUI_SOURCE) + if not result: + logger.error("error editing link: %s", link) diff --git a/daemon/core/gui/data/xmls/sample1.xml b/daemon/core/gui/data/xmls/sample1.xml index c9e12940..c4f75c47 100644 --- a/daemon/core/gui/data/xmls/sample1.xml +++ b/daemon/core/gui/data/xmls/sample1.xml @@ -223,1634 +223,6 @@ - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga -]]> - - - - - - - pidof ospfd - - - killall ospfd - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga -]]> - - - - - - - pidof ospfd - - - killall ospfd - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga -]]> - - - - - - - pidof ospfd - - - killall ospfd - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga -]]> - - - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga -]]> - - - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga -]]> - - - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga -]]> - - - - - - - pidof ospfd - - - killall ospfd - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga -]]> - - - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - - - - - - sh defaultroute.sh - - - - - - - - sh defaultroute.sh - - - - - - - - sh defaultroute.sh - - - - - - - - sh defaultroute.sh - - - - - - - - /etc/ssh - /var/run/sshd - - - sh startsshd.sh - - - killall sshd - - - - - - - - - /usr/local/etc/quagga - /var/run/quagga - - - sh quaggaboot.sh zebra - - - pidof zebra - - - killall zebra - - - CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then - ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf - fi - # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR - if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then - ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf - fi -} - -bootdaemon() -{ - QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) - if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then - echo "ERROR: Quagga's '$1' daemon not found in search path:" - echo " $QUAGGA_SBIN_SEARCH" - return 1 - fi - - flags="" - - if [ "$1" = "xpimd" ] && \ - grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then - flags="$flags -6" - fi - - $QUAGGA_SBIN_DIR/$1 $flags -d - if [ "$?" != "0" ]; then - echo "ERROR: Quagga's '$1' daemon failed to start!:" - return 1 - fi -} - -bootquagga() -{ - QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) - if [ "z$QUAGGA_BIN_DIR" = "z" ]; then - echo "ERROR: Quagga's 'vtysh' program not found in search path:" - echo " $QUAGGA_BIN_SEARCH" - return 1 - fi - - # fix /var/run/quagga permissions - id -u quagga 2>/dev/null >/dev/null - if [ "$?" = "0" ]; then - chown quagga $QUAGGA_STATE_DIR - fi - - bootdaemon "zebra" - for r in rip ripng ospf6 ospf bgp babel; do - if grep -q "^router \<${r}\>" $QUAGGA_CONF; then - bootdaemon "${r}d" - fi - done - - if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then - bootdaemon "xpimd" - fi - - $QUAGGA_BIN_DIR/vtysh -b -} - -if [ "$1" != "zebra" ]; then - echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" - exit 1 -fi -confcheck -bootquagga -]]> - - - - - - - pidof ospfd - - - killall ospfd - - - - - pidof ospf6d - - - killall ospf6d - - - - - sh ipforward.sh - - - - - diff --git a/daemon/core/gui/dialogs/canvassizeandscale.py b/daemon/core/gui/dialogs/canvassizeandscale.py index 8155cb57..863d1174 100644 --- a/daemon/core/gui/dialogs/canvassizeandscale.py +++ b/daemon/core/gui/dialogs/canvassizeandscale.py @@ -24,7 +24,7 @@ class SizeAndScaleDialog(Dialog): super().__init__(app, "Canvas Size and Scale") self.manager: CanvasManager = self.app.manager self.section_font: font.Font = font.Font(weight=font.BOLD) - width, height = self.manager.current_dimensions + width, height = self.manager.current().current_dimensions self.pixel_width: tk.IntVar = tk.IntVar(value=width) self.pixel_height: tk.IntVar = tk.IntVar(value=height) location = self.app.core.session.location @@ -189,7 +189,7 @@ class SizeAndScaleDialog(Dialog): def click_apply(self) -> None: width, height = self.pixel_width.get(), self.pixel_height.get() - self.manager.redraw_canvases((width, height)) + self.manager.redraw_canvas((width, height)) location = self.app.core.session.location location.x = self.x.get() location.y = self.y.get() diff --git a/daemon/core/gui/dialogs/canvaswallpaper.py b/daemon/core/gui/dialogs/canvaswallpaper.py index 871de2f7..0ef294c7 100644 --- a/daemon/core/gui/dialogs/canvaswallpaper.py +++ b/daemon/core/gui/dialogs/canvaswallpaper.py @@ -13,6 +13,8 @@ from core.gui.graph.graph import CanvasGraph from core.gui.themes import PADX, PADY from core.gui.widgets import image_chooser +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -167,5 +169,5 @@ class CanvasWallpaperDialog(Dialog): try: self.canvas.set_wallpaper(filename) except FileNotFoundError: - logging.error("invalid background: %s", filename) + logger.error("invalid background: %s", filename) self.destroy() diff --git a/daemon/core/gui/dialogs/configserviceconfig.py b/daemon/core/gui/dialogs/configserviceconfig.py index 14388f5a..870f9639 100644 --- a/daemon/core/gui/dialogs/configserviceconfig.py +++ b/daemon/core/gui/dialogs/configserviceconfig.py @@ -18,6 +18,8 @@ from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ConfigFrame, ListboxScroll +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application from core.gui.coreclient import CoreClient @@ -73,7 +75,7 @@ class ConfigServiceConfigDialog(Dialog): def load(self) -> None: try: - self.core.create_nodes_and_links() + self.core.start_session(definition=True) service = self.core.config_services[self.service_name] self.dependencies = service.dependencies[:] self.executables = service.executables[:] @@ -86,18 +88,18 @@ class ConfigServiceConfigDialog(Dialog): self.validation_time = service.validation_timer self.validation_period.set(service.validation_period) - response = self.core.client.get_config_service_defaults(self.service_name) - self.original_service_files = response.templates + defaults = self.core.client.get_config_service_defaults(self.service_name) + self.original_service_files = defaults.templates self.temp_service_files = dict(self.original_service_files) - self.modes = sorted(x.name for x in response.modes) - self.mode_configs = {x.name: x.config for x in response.modes} - self.config = ConfigOption.from_dict(response.config) + self.modes = sorted(defaults.modes) + self.mode_configs = defaults.modes + self.config = ConfigOption.from_dict(defaults.config) self.default_config = {x.name: x.value for x in self.config.values()} service_config = self.node.config_service_configs.get(self.service_name) if service_config: for key, value in service_config.config.items(): self.config[key].value = value - logging.info("default config: %s", self.default_config) + logger.info("default config: %s", self.default_config) for file, data in service_config.templates.items(): self.modified_files.add(file) self.temp_service_files[file] = data @@ -181,7 +183,7 @@ class ConfigServiceConfigDialog(Dialog): self.modes_combobox.bind("<>", self.handle_mode_changed) self.modes_combobox.grid(row=0, column=1, sticky=tk.EW, pady=PADY) - logging.info("config service config: %s", self.config) + logger.info("config service config: %s", self.config) self.config_frame = ConfigFrame(tab, self.app, self.config) self.config_frame.draw_config() self.config_frame.grid(sticky=tk.NSEW, pady=PADY) @@ -308,9 +310,9 @@ class ConfigServiceConfigDialog(Dialog): current_listbox.itemconfig(current_listbox.curselection()[0], bg="") self.destroy() return - service_config = self.node.config_service_configs.get(self.service_name) - if not service_config: - service_config = ConfigServiceData() + service_config = self.node.config_service_configs.setdefault( + self.service_name, ConfigServiceData() + ) if self.config_frame: self.config_frame.parse_config() service_config.config = {x.name: x.value for x in self.config.values()} @@ -328,7 +330,7 @@ class ConfigServiceConfigDialog(Dialog): def handle_mode_changed(self, event: tk.Event) -> None: mode = self.modes_combobox.get() config = self.mode_configs[mode] - logging.info("mode config: %s", config) + logger.info("mode config: %s", config) self.config_frame.set_values(config) def update_template_file_data(self, event: tk.Event) -> None: @@ -350,7 +352,7 @@ class ConfigServiceConfigDialog(Dialog): def click_defaults(self) -> None: self.node.config_service_configs.pop(self.service_name, None) - logging.info( + logger.info( "cleared config service config: %s", self.node.config_service_configs ) self.temp_service_files = dict(self.original_service_files) @@ -358,7 +360,7 @@ class ConfigServiceConfigDialog(Dialog): self.template_text.text.delete(1.0, "end") self.template_text.text.insert("end", self.temp_service_files[filename]) if self.config_frame: - logging.info("resetting defaults: %s", self.default_config) + logger.info("resetting defaults: %s", self.default_config) self.config_frame.set_values(self.default_config) def click_copy(self) -> None: diff --git a/daemon/core/gui/dialogs/customnodes.py b/daemon/core/gui/dialogs/customnodes.py index d10bf30c..065cc43e 100644 --- a/daemon/core/gui/dialogs/customnodes.py +++ b/daemon/core/gui/dialogs/customnodes.py @@ -13,6 +13,8 @@ from core.gui.nodeutils import NodeDraw from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CheckboxList, ListboxScroll, image_chooser +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -209,7 +211,7 @@ class CustomNodesDialog(Dialog): name, node_draw.image_file, list(node_draw.services) ) self.app.guiconfig.nodes.append(custom_node) - logging.info("saving custom nodes: %s", self.app.guiconfig.nodes) + logger.info("saving custom nodes: %s", self.app.guiconfig.nodes) self.app.save_config() self.destroy() @@ -219,7 +221,7 @@ class CustomNodesDialog(Dialog): image_file = str(Path(self.image_file).absolute()) custom_node = CustomNode(name, image_file, list(self.services)) node_draw = NodeDraw.from_custom(custom_node) - logging.info( + logger.info( "created new custom node (%s), image file (%s), services: (%s)", name, image_file, @@ -239,7 +241,7 @@ class CustomNodesDialog(Dialog): node_draw.image_file = str(Path(self.image_file).absolute()) node_draw.image = self.image node_draw.services = set(self.services) - logging.debug( + logger.debug( "edit custom node (%s), image: (%s), services (%s)", node_draw.model, node_draw.image_file, diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py index 35de0d66..b3f6d9ce 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, @@ -115,7 +81,7 @@ class EmaneConfigDialog(Dialog): self.radiovar: tk.IntVar = tk.IntVar() self.radiovar.set(1) self.emane_models: List[str] = [ - x.split("_")[1] for x in self.app.core.session.emane_models + x.split("_")[1] for x in self.app.core.emane_models ] model = self.node.emane.split("_")[1] self.emane_model: tk.StringVar = tk.StringVar(value=model) @@ -179,9 +145,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( frame, @@ -191,18 +155,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) @@ -215,10 +168,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/gui/dialogs/executepython.py b/daemon/core/gui/dialogs/executepython.py index 0bef9dc1..8c9b31ba 100644 --- a/daemon/core/gui/dialogs/executepython.py +++ b/daemon/core/gui/dialogs/executepython.py @@ -7,6 +7,8 @@ from core.gui.appconfig import SCRIPT_PATH from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -83,6 +85,6 @@ class ExecutePythonDialog(Dialog): def script_execute(self) -> None: file = self.file_entry.get() options = self.option_entry.get() - logging.info("Execute %s with options %s", file, options) - self.app.core.execute_script(file) + logger.info("Execute %s with options %s", file, options) + self.app.core.execute_script(file, options) self.destroy() diff --git a/daemon/core/gui/dialogs/find.py b/daemon/core/gui/dialogs/find.py index 3b899ef8..54be81b0 100644 --- a/daemon/core/gui/dialogs/find.py +++ b/daemon/core/gui/dialogs/find.py @@ -6,6 +6,8 @@ from typing import TYPE_CHECKING, Optional from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -139,8 +141,8 @@ class FindDialog(Dialog): _x, _y, _, _ = canvas_node.canvas.bbox(canvas_node.id) oid = canvas_node.canvas.find_withtag("rectangle") x0, y0, x1, y1 = canvas_node.canvas.bbox(oid[0]) - logging.debug("Dist to most left: %s", abs(x0 - _x)) - logging.debug("White canvas width: %s", abs(x0 - x1)) + logger.debug("Dist to most left: %s", abs(x0 - _x)) + logger.debug("White canvas width: %s", abs(x0 - x1)) # calculate the node's location # (as fractions of white canvas's width and height) diff --git a/daemon/core/gui/dialogs/ipdialog.py b/daemon/core/gui/dialogs/ipdialog.py index a09ca097..68b5ab36 100644 --- a/daemon/core/gui/dialogs/ipdialog.py +++ b/daemon/core/gui/dialogs/ipdialog.py @@ -23,6 +23,8 @@ class IpConfigDialog(Dialog): self.ip4_listbox: Optional[ListboxScroll] = None self.ip6_entry: Optional[ttk.Entry] = None self.ip6_listbox: Optional[ListboxScroll] = None + self.enable_ip4 = tk.BooleanVar(value=self.app.guiconfig.ips.enable_ip4) + self.enable_ip6 = tk.BooleanVar(value=self.app.guiconfig.ips.enable_ip6) self.draw() def draw(self) -> None: @@ -36,10 +38,19 @@ class IpConfigDialog(Dialog): frame.rowconfigure(0, weight=1) frame.grid(sticky=tk.NSEW, pady=PADY) + ip4_checkbox = ttk.Checkbutton( + frame, text="Enable IP4?", variable=self.enable_ip4 + ) + ip4_checkbox.grid(row=0, column=0, sticky=tk.EW) + ip6_checkbox = ttk.Checkbutton( + frame, text="Enable IP6?", variable=self.enable_ip6 + ) + ip6_checkbox.grid(row=0, column=1, sticky=tk.EW) + ip4_frame = ttk.LabelFrame(frame, text="IPv4", padding=FRAME_PAD) ip4_frame.columnconfigure(0, weight=1) - ip4_frame.rowconfigure(0, weight=1) - ip4_frame.grid(row=0, column=0, stick="nsew") + ip4_frame.rowconfigure(1, weight=1) + ip4_frame.grid(row=1, column=0, stick=tk.NSEW) self.ip4_listbox = ListboxScroll(ip4_frame) self.ip4_listbox.listbox.bind("<>", self.select_ip4) self.ip4_listbox.grid(sticky=tk.NSEW, pady=PADY) @@ -63,7 +74,7 @@ class IpConfigDialog(Dialog): ip6_frame = ttk.LabelFrame(frame, text="IPv6", padding=FRAME_PAD) ip6_frame.columnconfigure(0, weight=1) ip6_frame.rowconfigure(0, weight=1) - ip6_frame.grid(row=0, column=1, stick="nsew") + ip6_frame.grid(row=1, column=1, stick=tk.NSEW) self.ip6_listbox = ListboxScroll(ip6_frame) self.ip6_listbox.listbox.bind("<>", self.select_ip6) self.ip6_listbox.grid(sticky=tk.NSEW, pady=PADY) @@ -86,7 +97,7 @@ class IpConfigDialog(Dialog): # draw buttons frame = ttk.Frame(self.top) - frame.grid(stick="ew") + frame.grid(stick=tk.EW) for i in range(2): frame.columnconfigure(i, weight=1) button = ttk.Button(frame, text="Save", command=self.click_save) @@ -142,10 +153,18 @@ class IpConfigDialog(Dialog): ip6 = self.ip6_listbox.listbox.get(index) ip6s.append(ip6) ip_config = self.app.guiconfig.ips - ip_config.ip4 = self.ip4 - ip_config.ip6 = self.ip6 + ip_changed = False + if ip_config.ip4 != self.ip4: + ip_config.ip4 = self.ip4 + ip_changed = True + if ip_config.ip6 != self.ip6: + ip_config.ip6 = self.ip6 + ip_changed = True ip_config.ip4s = ip4s ip_config.ip6s = ip6s - self.app.core.ifaces_manager.update_ips(self.ip4, self.ip6) + ip_config.enable_ip4 = self.enable_ip4.get() + ip_config.enable_ip6 = self.enable_ip6.get() + if ip_changed: + self.app.core.ifaces_manager.update_ips(self.ip4, self.ip6) self.app.save_config() self.destroy() diff --git a/daemon/core/gui/dialogs/mobilityplayer.py b/daemon/core/gui/dialogs/mobilityplayer.py index c27cd2a5..7b6c4d9f 100644 --- a/daemon/core/gui/dialogs/mobilityplayer.py +++ b/daemon/core/gui/dialogs/mobilityplayer.py @@ -134,7 +134,7 @@ class MobilityPlayerDialog(Dialog): session_id = self.app.core.session.id try: self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.START.value + session_id, self.node.id, MobilityAction.START ) except grpc.RpcError as e: self.app.show_grpc_exception("Mobility Error", e) @@ -144,7 +144,7 @@ class MobilityPlayerDialog(Dialog): session_id = self.app.core.session.id try: self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.PAUSE.value + session_id, self.node.id, MobilityAction.PAUSE ) except grpc.RpcError as e: self.app.show_grpc_exception("Mobility Error", e) @@ -154,7 +154,7 @@ class MobilityPlayerDialog(Dialog): session_id = self.app.core.session.id try: self.app.core.client.mobility_action( - session_id, self.node.id, MobilityAction.STOP.value + session_id, self.node.id, MobilityAction.STOP ) except grpc.RpcError as e: self.app.show_grpc_exception("Mobility Error", e) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index a78df050..ee0d7b81 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -17,6 +17,8 @@ from core.gui.dialogs.emaneconfig import EmaneModelDialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import ListboxScroll, image_chooser +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.node import CanvasNode @@ -260,17 +262,17 @@ class NodeConfigDialog(Dialog): row += 1 if nutils.is_rj45(self.node): - response = self.app.core.client.get_ifaces() - logging.debug("host machine available interfaces: %s", response) - ifaces = ListboxScroll(frame) - ifaces.listbox.config(state=state) - ifaces.grid( + ifaces = self.app.core.client.get_ifaces() + logger.debug("host machine available interfaces: %s", ifaces) + ifaces_scroll = ListboxScroll(frame) + ifaces_scroll.listbox.config(state=state) + ifaces_scroll.grid( row=row, column=0, columnspan=2, sticky=tk.EW, padx=PADX, pady=PADY ) - for inf in sorted(response.ifaces[:]): - ifaces.listbox.insert(tk.END, inf) + for inf in sorted(ifaces): + ifaces_scroll.listbox.insert(tk.END, inf) row += 1 - ifaces.listbox.bind("<>", self.iface_select) + ifaces_scroll.listbox.bind("<>", self.iface_select) # interfaces if self.canvas_node.ifaces: @@ -296,10 +298,9 @@ class NodeConfigDialog(Dialog): emane_node = self.canvas_node.has_emane_link(iface.id) if emane_node: emane_model = emane_node.emane.split("_")[1] + command = partial(self.click_emane_config, emane_model, iface.id) button = ttk.Button( - tab, - text=f"Configure EMANE {emane_model}", - command=lambda: self.click_emane_config(emane_model, iface.id), + tab, text=f"Configure EMANE {emane_model}", command=command ) button.grid(row=row, sticky=tk.EW, columnspan=3, pady=PADY) row += 1 @@ -365,6 +366,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: + logger.info("configuring emane: %s - %s", emane_model, iface_id) dialog = EmaneModelDialog(self, self.app, self.node, emane_model, iface_id) dialog.show() diff --git a/daemon/core/gui/dialogs/nodeconfigservice.py b/daemon/core/gui/dialogs/nodeconfigservice.py index 2141b3dc..cddbdb03 100644 --- a/daemon/core/gui/dialogs/nodeconfigservice.py +++ b/daemon/core/gui/dialogs/nodeconfigservice.py @@ -12,6 +12,8 @@ from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CheckboxList, ListboxScroll +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -29,6 +31,7 @@ class NodeConfigServiceDialog(Dialog): if services is None: services = set(node.config_services) self.current_services: Set[str] = services + self.protocol("WM_DELETE_WINDOW", self.click_cancel) self.draw() def draw(self) -> None: @@ -100,6 +103,7 @@ class NodeConfigServiceDialog(Dialog): self.current_services.add(name) elif not var.get() and name in self.current_services: self.current_services.remove(name) + self.node.config_service_configs.pop(name, None) self.draw_current_services() self.node.config_services = self.current_services.copy() @@ -131,7 +135,7 @@ class NodeConfigServiceDialog(Dialog): def click_save(self) -> None: self.node.config_services = self.current_services.copy() - logging.info("saved node config services: %s", self.node.config_services) + logger.info("saved node config services: %s", self.node.config_services) self.destroy() def click_cancel(self) -> None: @@ -144,6 +148,7 @@ class NodeConfigServiceDialog(Dialog): service = self.current.listbox.get(cur[0]) self.current.listbox.delete(cur[0]) self.current_services.remove(service) + self.node.config_service_configs.pop(service, None) for checkbutton in self.services.frame.winfo_children(): if checkbutton["text"] == service: checkbutton.invoke() diff --git a/daemon/core/gui/dialogs/nodeservice.py b/daemon/core/gui/dialogs/nodeservice.py index 09732e73..431d5c3d 100644 --- a/daemon/core/gui/dialogs/nodeservice.py +++ b/daemon/core/gui/dialogs/nodeservice.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: class NodeServiceDialog(Dialog): def __init__(self, app: "Application", node: Node) -> None: - title = f"{node.name} Services" + title = f"{node.name} Services (Deprecated)" super().__init__(app, title) self.node: Node = node self.groups: Optional[ListboxScroll] = None @@ -25,6 +25,7 @@ class NodeServiceDialog(Dialog): self.current: Optional[ListboxScroll] = None services = set(node.services) self.current_services: Set[str] = services + self.protocol("WM_DELETE_WINDOW", self.click_cancel) self.draw() def draw(self) -> None: @@ -77,7 +78,7 @@ class NodeServiceDialog(Dialog): button.grid(row=0, column=1, sticky=tk.EW, padx=PADX) button = ttk.Button(frame, text="Remove", command=self.click_remove) button.grid(row=0, column=2, sticky=tk.EW, padx=PADX) - button = ttk.Button(frame, text="Cancel", command=self.destroy) + button = ttk.Button(frame, text="Cancel", command=self.click_cancel) button.grid(row=0, column=3, sticky=tk.EW) # trigger group change @@ -98,6 +99,8 @@ class NodeServiceDialog(Dialog): self.current_services.add(name) elif not var.get() and name in self.current_services: self.current_services.remove(name) + self.node.service_configs.pop(name, None) + self.node.service_file_configs.pop(name, None) self.current.listbox.delete(0, tk.END) for name in sorted(self.current_services): self.current.listbox.insert(tk.END, name) @@ -125,6 +128,9 @@ class NodeServiceDialog(Dialog): "Service Configuration", "Select a service to configure", parent=self ) + def click_cancel(self) -> None: + self.destroy() + def click_save(self) -> None: self.node.services = self.current_services.copy() self.destroy() @@ -135,6 +141,8 @@ class NodeServiceDialog(Dialog): service = self.current.listbox.get(cur[0]) self.current.listbox.delete(cur[0]) self.current_services.remove(service) + self.node.service_configs.pop(service, None) + self.node.service_file_configs.pop(service, None) for checkbutton in self.services.frame.winfo_children(): if checkbutton["text"] == service: checkbutton.invoke() diff --git a/daemon/core/gui/dialogs/preferences.py b/daemon/core/gui/dialogs/preferences.py index f78e5c48..4a6a1c08 100644 --- a/daemon/core/gui/dialogs/preferences.py +++ b/daemon/core/gui/dialogs/preferences.py @@ -9,6 +9,8 @@ from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts from core.gui.validation import LARGEST_SCALE, SMALLEST_SCALE +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -102,7 +104,7 @@ class PreferencesDialog(Dialog): def theme_change(self, event: tk.Event) -> None: theme = self.theme.get() - logging.info("changing theme: %s", theme) + logger.info("changing theme: %s", theme) self.app.style.theme_use(theme) def click_save(self) -> None: diff --git a/daemon/core/gui/dialogs/runtool.py b/daemon/core/gui/dialogs/runtool.py index 707ac2b8..494020e3 100644 --- a/daemon/core/gui/dialogs/runtool.py +++ b/daemon/core/gui/dialogs/runtool.py @@ -106,10 +106,8 @@ class RunToolDialog(Dialog): for selection in self.node_list.listbox.curselection(): node_name = self.node_list.listbox.get(selection) node_id = self.executable_nodes[node_name] - response = self.app.core.client.node_command( + _, output = self.app.core.client.node_command( self.app.core.session.id, node_id, command ) - self.result.text.insert( - tk.END, f"> {node_name} > {command}:\n{response.output}\n" - ) + self.result.text.insert(tk.END, f"> {node_name} > {command}:\n{output}\n") self.result.text.config(state=tk.DISABLED) diff --git a/daemon/core/gui/dialogs/serviceconfig.py b/daemon/core/gui/dialogs/serviceconfig.py index 2f788dce..6f6b0d24 100644 --- a/daemon/core/gui/dialogs/serviceconfig.py +++ b/daemon/core/gui/dialogs/serviceconfig.py @@ -1,7 +1,7 @@ import logging -import os import tkinter as tk -from tkinter import filedialog, ttk +from pathlib import Path +from tkinter import filedialog, messagebox, ttk from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple import grpc @@ -15,6 +15,8 @@ from core.gui.images import ImageEnum from core.gui.themes import FRAME_PAD, PADX, PADY from core.gui.widgets import CodeText, ListboxScroll +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application from core.gui.coreclient import CoreClient @@ -26,7 +28,7 @@ class ServiceConfigDialog(Dialog): def __init__( self, master: tk.BaseWidget, app: "Application", service_name: str, node: Node ) -> None: - title = f"{service_name} Service" + title = f"{service_name} Service (Deprecated)" super().__init__(app, title, master=master) self.core: "CoreClient" = app.core self.node: Node = node @@ -76,7 +78,7 @@ class ServiceConfigDialog(Dialog): def load(self) -> None: try: - self.app.core.create_nodes_and_links() + self.core.start_session(definition=True) default_config = self.app.core.get_node_service( self.node.id, self.service_name ) @@ -388,7 +390,7 @@ class ServiceConfigDialog(Dialog): 1.0, "end" ) else: - logging.debug("file already existed") + logger.debug("file already existed") def delete_filename(self) -> None: cbb = self.filename_combobox @@ -447,36 +449,31 @@ class ServiceConfigDialog(Dialog): self.current_service_color("") self.destroy() return - - try: - if ( - self.is_custom_command() - or self.has_new_files() - or self.is_custom_directory() - ): - startup, validate, shutdown = self.get_commands() - config = self.core.set_node_service( - self.node.id, - self.service_name, - dirs=self.temp_directories, - files=list(self.filename_combobox["values"]), - startups=startup, - validations=validate, - shutdowns=shutdown, - ) - self.node.service_configs[self.service_name] = config - for file in self.modified_files: - file_configs = self.node.service_file_configs.setdefault( - self.service_name, {} - ) - file_configs[file] = self.temp_service_files[file] - # TODO: check if this is really needed - self.app.core.set_node_service_file( - self.node.id, self.service_name, file, self.temp_service_files[file] - ) - self.current_service_color("green") - except grpc.RpcError as e: - self.app.show_grpc_exception("Save Service Config Error", e) + files = set(self.filenames) + if ( + self.is_custom_command() + or self.has_new_files() + or self.is_custom_directory() + ): + startup, validate, shutdown = self.get_commands() + files = set(self.filename_combobox["values"]) + service_data = NodeServiceData( + configs=list(files), + dirs=self.temp_directories, + startup=startup, + validate=validate, + shutdown=shutdown, + ) + logger.info("setting service data: %s", service_data) + self.node.service_configs[self.service_name] = service_data + for file in self.modified_files: + if file not in files: + continue + file_configs = self.node.service_file_configs.setdefault( + self.service_name, {} + ) + file_configs[file] = self.temp_service_files[file] + self.current_service_color("green") self.destroy() def display_service_file_data(self, event: tk.Event) -> None: @@ -579,11 +576,13 @@ class ServiceConfigDialog(Dialog): self.directory_entry.insert("end", d) def add_directory(self) -> None: - d = self.directory_entry.get() - if os.path.isdir(d): - if d not in self.temp_directories: - self.dir_list.listbox.insert("end", d) - self.temp_directories.append(d) + directory = Path(self.directory_entry.get()) + if directory.is_absolute(): + if str(directory) not in self.temp_directories: + self.dir_list.listbox.insert("end", directory) + self.temp_directories.append(str(directory)) + else: + messagebox.showerror("Add Directory", "Path must be absolute!", parent=self) def remove_directory(self) -> None: d = self.directory_entry.get() @@ -594,7 +593,7 @@ class ServiceConfigDialog(Dialog): i = dirs.index(d) self.dir_list.listbox.delete(i) except ValueError: - logging.debug("directory is not in the list") + logger.debug("directory is not in the list") self.directory_entry.delete(0, "end") def directory_select(self, event) -> None: diff --git a/daemon/core/gui/dialogs/sessionoptions.py b/daemon/core/gui/dialogs/sessionoptions.py index 4b086d67..28d780dc 100644 --- a/daemon/core/gui/dialogs/sessionoptions.py +++ b/daemon/core/gui/dialogs/sessionoptions.py @@ -1,15 +1,14 @@ import logging import tkinter as tk from tkinter import ttk -from typing import TYPE_CHECKING, Dict, Optional +from typing import TYPE_CHECKING, Optional -import grpc - -from core.api.grpc.wrappers import ConfigOption from core.gui.dialogs.dialog import Dialog from core.gui.themes import PADX, PADY from core.gui.widgets import ConfigFrame +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -19,25 +18,15 @@ class SessionOptionsDialog(Dialog): super().__init__(app, "Session Options") self.config_frame: Optional[ConfigFrame] = None self.has_error: bool = False - self.config: Dict[str, ConfigOption] = self.get_config() self.enabled: bool = not self.app.core.is_runtime() if not self.has_error: self.draw() - def get_config(self) -> Dict[str, ConfigOption]: - try: - session_id = self.app.core.session.id - response = self.app.core.client.get_session_options(session_id) - return ConfigOption.from_dict(response.config) - except grpc.RpcError as e: - self.app.show_grpc_exception("Get Session Options Error", e) - self.has_error = True - self.destroy() - def draw(self) -> None: self.top.columnconfigure(0, weight=1) self.top.rowconfigure(0, weight=1) - self.config_frame = ConfigFrame(self.top, self.app, self.config, self.enabled) + options = self.app.core.session.options + self.config_frame = ConfigFrame(self.top, self.app, options, self.enabled) self.config_frame.draw_config() self.config_frame.grid(sticky=tk.NSEW, pady=PADY) @@ -53,10 +42,6 @@ class SessionOptionsDialog(Dialog): def save(self) -> None: config = self.config_frame.parse_config() - try: - session_id = self.app.core.session.id - response = self.app.core.client.set_session_options(session_id, config) - logging.info("saved session config: %s", response) - except grpc.RpcError as e: - self.app.show_grpc_exception("Set Session Options Error", e) + for key, value in config.items(): + self.app.core.session.options[key].value = value self.destroy() diff --git a/daemon/core/gui/dialogs/sessions.py b/daemon/core/gui/dialogs/sessions.py index efcbbeea..deca7404 100644 --- a/daemon/core/gui/dialogs/sessions.py +++ b/daemon/core/gui/dialogs/sessions.py @@ -12,6 +12,8 @@ from core.gui.images import ImageEnum from core.gui.task import ProgressTask from core.gui.themes import PADX, PADY +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -30,10 +32,9 @@ class SessionsDialog(Dialog): def get_sessions(self) -> List[SessionSummary]: try: - response = self.app.core.client.get_sessions() - logging.info("sessions: %s", response) - sessions = sorted(response.sessions, key=lambda x: x.id) - return [SessionSummary.from_proto(x) for x in sessions] + sessions = self.app.core.client.get_sessions() + logger.info("sessions: %s", sessions) + return sorted(sessions, key=lambda x: x.id) except grpc.RpcError as e: self.app.show_grpc_exception("Get Sessions Error", e) self.destroy() @@ -176,7 +177,7 @@ class SessionsDialog(Dialog): self.selected_id = None self.delete_button.config(state=tk.DISABLED) self.connect_button.config(state=tk.DISABLED) - logging.debug("selected session: %s", self.selected_session) + logger.debug("selected session: %s", self.selected_session) def click_connect(self) -> None: if not self.selected_session: @@ -200,7 +201,7 @@ class SessionsDialog(Dialog): def click_delete(self) -> None: if not self.selected_session: return - logging.info("click delete session: %s", self.selected_session) + logger.info("click delete session: %s", self.selected_session) self.tree.delete(self.selected_id) self.app.core.delete_session(self.selected_session) session_id = None diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 509b67ec..405ef658 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -11,6 +11,8 @@ from core.gui.frames.link import EdgeInfoFrame, WirelessEdgeInfoFrame from core.gui.graph import tags from core.gui.utils import bandwidth_text, delay_jitter_text +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.graph import CanvasGraph @@ -393,7 +395,7 @@ class Edge: self.dst.canvas.coords(self.dst_label2, *dst_pos) def delete(self) -> None: - logging.debug("deleting canvas edge, id: %s", self.id) + logger.debug("deleting canvas edge, id: %s", self.id) self.src.canvas.delete(self.id) self.src.canvas.delete(self.src_label) self.src.canvas.delete(self.dst_label) @@ -488,7 +490,7 @@ class CanvasWirelessEdge(Edge): token: str, link: Link, ) -> None: - logging.debug("drawing wireless link from node %s to node %s", src, dst) + logger.debug("drawing wireless link from node %s to node %s", src, dst) super().__init__(app, src, dst) self.src.wireless_edges.add(self) self.dst.wireless_edges.add(self) @@ -622,7 +624,7 @@ class CanvasEdge(Edge): self.draw_link_options() def complete(self, dst: "CanvasNode", link: Link = None) -> None: - logging.debug( + logger.debug( "completing wired link from node(%s) to node(%s)", self.src.core_node.name, dst.core_node.name, diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 7dd68215..e3225a4d 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -18,6 +18,8 @@ from core.gui.graph.node import CanvasNode, ShadowNode from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.manager import CanvasManager @@ -101,10 +103,15 @@ class CanvasGraph(tk.Canvas): """ Bind any mouse events or hot keys to the matching action """ + self.bind("", self.copy_selected) + self.bind("", self.paste_selected) + self.bind("", self.cut_selected) + self.bind("", self.delete_selected) + self.bind("", self.hide_selected) self.bind("", self.click_press) self.bind("", self.click_release) self.bind("", self.click_motion) - self.bind("", self.press_delete) + self.bind("", self.delete_selected) self.bind("", self.ctrl_click) self.bind("", self.double_click) self.bind("", self.zoom) @@ -184,7 +191,7 @@ class CanvasGraph(tk.Canvas): """ Draw a node or finish drawing an edge according to the current graph mode """ - logging.debug("click release") + logger.debug("click release") x, y = self.canvas_xy(event) if not self.inside_canvas(x, y): return @@ -210,7 +217,7 @@ class CanvasGraph(tk.Canvas): else: self.focus_set() self.selected = self.get_selected(event) - logging.debug( + logger.debug( "click release selected(%s) mode(%s)", self.selected, self.manager.mode ) if self.manager.mode == GraphMode.EDGE: @@ -228,7 +235,7 @@ class CanvasGraph(tk.Canvas): edge = self.drawing_edge self.drawing_edge = None # edge dst must be a node - logging.debug("current selected: %s", self.selected) + logger.debug("current selected: %s", self.selected) dst_node = self.nodes.get(self.selected) if not dst_node: edge.delete() @@ -275,7 +282,7 @@ class CanvasGraph(tk.Canvas): if select_id is not None: self.move(select_id, x_offset, y_offset) - def delete_selected_objects(self) -> None: + def delete_selected_objects(self, _event: tk.Event = None) -> None: edges = set() nodes = [] for object_id in self.selection: @@ -305,7 +312,7 @@ class CanvasGraph(tk.Canvas): self.selection.clear() self.core.deleted_canvas_nodes(nodes) - def hide_selected_objects(self) -> None: + def hide_selected(self, _event: tk.Event = None) -> None: for object_id in self.selection: # delete selection box selection_id = self.selection[object_id] @@ -331,8 +338,8 @@ class CanvasGraph(tk.Canvas): self.offset[0] * factor + event.x * (1 - factor), self.offset[1] * factor + event.y * (1 - factor), ) - logging.debug("ratio: %s", self.ratio) - logging.debug("offset: %s", self.offset) + logger.debug("ratio: %s", self.ratio) + logger.debug("offset: %s", self.offset) self.app.statusbar.set_zoom(self.ratio) if self.wallpaper: self.redraw_wallpaper() @@ -347,10 +354,10 @@ class CanvasGraph(tk.Canvas): self.cursor = x, y selected = self.get_selected(event) - logging.debug("click press(%s): %s", self.cursor, selected) + logger.debug("click press(%s): %s", self.cursor, selected) x_check = self.cursor[0] - self.offset[0] y_check = self.cursor[1] - self.offset[1] - logging.debug("click press offset(%s, %s)", x_check, y_check) + logger.debug("click press offset(%s, %s)", x_check, y_check) is_node = selected in self.nodes if self.manager.mode == GraphMode.EDGE and is_node: node = self.nodes[selected] @@ -387,7 +394,7 @@ class CanvasGraph(tk.Canvas): node = self.nodes[selected] self.select_object(node.id) self.selected = selected - logging.debug( + logger.debug( "selected node(%s), coords: (%s, %s)", node.core_node.name, node.core_node.position.x, @@ -397,7 +404,7 @@ class CanvasGraph(tk.Canvas): shadow_node = self.shadow_nodes[selected] self.select_object(shadow_node.id) self.selected = selected - logging.debug( + logger.debug( "selected shadow node(%s), coords: (%s, %s)", shadow_node.node.core_node.name, shadow_node.node.core_node.position.x, @@ -418,7 +425,7 @@ class CanvasGraph(tk.Canvas): self.cursor = x, y # handle multiple selections - logging.debug("control left click: %s", event) + logger.debug("control left click: %s", event) selected = self.get_selected(event) if ( selected not in self.selection @@ -485,17 +492,6 @@ class CanvasGraph(tk.Canvas): if self.select_box and self.manager.mode == GraphMode.SELECT: self.select_box.shape_motion(x, y) - def press_delete(self, _event: tk.Event) -> None: - """ - delete selected nodes and any data that relates to it - """ - logging.debug("press delete key") - if not self.app.core.is_runtime(): - self.delete_selected_objects() - self.app.default_info() - else: - logging.debug("node deletion is disabled during runtime state") - def double_click(self, event: tk.Event) -> None: selected = self.get_selected(event) if selected is not None and selected in self.shapes: @@ -606,10 +602,10 @@ class CanvasGraph(tk.Canvas): self.draw_wallpaper(image) def redraw_canvas(self, dimensions: Tuple[int, int] = None) -> None: - logging.debug("redrawing canvas to dimensions: %s", dimensions) + logger.debug("redrawing canvas to dimensions: %s", dimensions) # reset scale and move back to original position - logging.debug("resetting scaling: %s %s", self.ratio, self.offset) + logger.debug("resetting scaling: %s %s", self.ratio, self.offset) factor = 1 / self.ratio self.scale(tk.ALL, self.offset[0], self.offset[1], factor, factor) self.move(tk.ALL, -self.offset[0], -self.offset[1]) @@ -628,11 +624,11 @@ class CanvasGraph(tk.Canvas): def redraw_wallpaper(self) -> None: if self.adjust_to_dim.get(): - logging.debug("drawing wallpaper to canvas dimensions") + logger.debug("drawing wallpaper to canvas dimensions") self.resize_to_wallpaper() else: option = ScaleOption(self.scale_option.get()) - logging.debug("drawing canvas using scaling option: %s", option) + logger.debug("drawing canvas using scaling option: %s", option) if option == ScaleOption.UPPER_LEFT: self.wallpaper_upper_left() elif option == ScaleOption.CENTERED: @@ -640,7 +636,7 @@ class CanvasGraph(tk.Canvas): elif option == ScaleOption.SCALED: self.wallpaper_scaled() elif option == ScaleOption.TILED: - logging.warning("tiled background not implemented yet") + logger.warning("tiled background not implemented yet") self.organize() def organize(self) -> None: @@ -648,7 +644,7 @@ class CanvasGraph(tk.Canvas): self.tag_raise(tag) def set_wallpaper(self, filename: Optional[str]) -> None: - logging.info("setting canvas(%s) background: %s", self.id, filename) + logger.info("setting canvas(%s) background: %s", self.id, filename) if filename: img = Image.open(filename) self.wallpaper = img @@ -671,20 +667,38 @@ class CanvasGraph(tk.Canvas): edge.complete(dst) return edge - def copy(self) -> None: + def copy_selected(self, _event: tk.Event = None) -> None: if self.core.is_runtime(): - logging.debug("copy is disabled during runtime state") + logger.debug("copy is disabled during runtime state") return if self.selection: - logging.debug("to copy nodes: %s", self.selection) + logger.debug("to copy nodes: %s", self.selection) self.to_copy.clear() for node_id in self.selection.keys(): canvas_node = self.nodes[node_id] self.to_copy.append(canvas_node) - def paste(self) -> None: + def cut_selected(self, _event: tk.Event = None) -> None: if self.core.is_runtime(): - logging.debug("paste is disabled during runtime state") + logger.debug("cut is disabled during runtime state") + return + self.copy_selected() + self.delete_selected() + + def delete_selected(self, _event: tk.Event = None) -> None: + """ + delete selected nodes and any data that relates to it + """ + logger.debug("press delete key") + if self.core.is_runtime(): + logger.debug("node deletion is disabled during runtime state") + return + self.delete_selected_objects() + self.app.default_info() + + def paste_selected(self, _event: tk.Event = None) -> None: + if self.core.is_runtime(): + logger.debug("paste is disabled during runtime state") return # maps original node canvas id to copy node canvas id copy_map = {} @@ -813,6 +827,7 @@ class CanvasGraph(tk.Canvas): wallpaper=wallpaper_path, wallpaper_style=self.scale_option.get(), fit_image=self.adjust_to_dim.get(), + dimensions=self.current_dimensions, ) def parse_metadata(self, config: Dict[str, Any]) -> None: @@ -820,12 +835,15 @@ class CanvasGraph(tk.Canvas): self.adjust_to_dim.set(fit_image) wallpaper_style = config.get("wallpaper_style", 1) self.scale_option.set(wallpaper_style) + dimensions = config.get("dimensions") + if dimensions: + self.redraw_canvas(dimensions) wallpaper = config.get("wallpaper") if wallpaper: wallpaper = Path(wallpaper) if not wallpaper.is_file(): wallpaper = appconfig.BACKGROUNDS_PATH.joinpath(wallpaper) - logging.info("canvas(%s), wallpaper: %s", self.id, wallpaper) + logger.info("canvas(%s), wallpaper: %s", self.id, wallpaper) if wallpaper.is_file(): self.set_wallpaper(str(wallpaper)) else: diff --git a/daemon/core/gui/graph/manager.py b/daemon/core/gui/graph/manager.py index 5f4be2e2..8acce4c9 100644 --- a/daemon/core/gui/graph/manager.py +++ b/daemon/core/gui/graph/manager.py @@ -1,3 +1,4 @@ +import json import logging import tkinter as tk from copy import deepcopy @@ -16,9 +17,12 @@ from core.gui.graph.edges import ( from core.gui.graph.enums import GraphMode from core.gui.graph.graph import CanvasGraph from core.gui.graph.node import CanvasNode +from core.gui.graph.shape import Shape from core.gui.graph.shapeutils import ShapeType from core.gui.nodeutils import NodeDraw +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application from core.gui.coreclient import CoreClient @@ -85,7 +89,6 @@ class CanvasManager: self.app.guiconfig.preferences.width, self.app.guiconfig.preferences.height, ) - self.current_dimensions: Tuple[int, int] = self.default_dimensions self.show_node_labels: ShowVar = ShowNodeLabels( self, tags.NODE_LABEL, value=True ) @@ -166,7 +169,7 @@ class CanvasManager: canvas_id = self._next_id() self.notebook.add(tab, text=f"Canvas {canvas_id}") unique_id = self.notebook.tabs()[-1] - logging.info("creating canvas(%s)", canvas_id) + logger.info("creating canvas(%s)", canvas_id) self.canvas_ids[unique_id] = canvas_id self.unique_ids[canvas_id] = unique_id @@ -205,7 +208,7 @@ class CanvasManager: edge.delete() def join(self, session: Session) -> None: - # clear out all canvas + # clear out all canvases for canvas_id in self.notebook.tabs(): self.notebook.forget(canvas_id) self.canvases.clear() @@ -213,7 +216,7 @@ class CanvasManager: self.unique_ids.clear() self.edges.clear() self.wireless_edges.clear() - logging.info("cleared canvases") + logger.info("cleared canvases") # reset settings self.show_node_labels.set(True) @@ -232,6 +235,10 @@ class CanvasManager: self.draw_session(session) def draw_session(self, session: Session) -> None: + # draw canvas configurations and shapes + self.parse_metadata_canvas(session.metadata) + self.parse_metadata_shapes(session.metadata) + # create session nodes for core_node in session.nodes.values(): # add node, avoiding ignored nodes @@ -254,50 +261,92 @@ class CanvasManager: else: self.add_wired_edge(node1, node2, link) - # parse metadata and organize canvases - self.core.parse_metadata() + # organize canvas order for canvas in self.canvases.values(): canvas.organize() + # parse metada for edge configs and hidden nodes + self.parse_metadata_edges(session.metadata) + self.parse_metadata_hidden(session.metadata) + # create a default canvas if none were created prior if not self.canvases: self.add_canvas() - def redraw_canvases(self, dimensions: Tuple[int, int]) -> None: - for canvas in self.canvases.values(): - canvas.redraw_canvas(dimensions) - if canvas.wallpaper: - canvas.redraw_wallpaper() + def redraw_canvas(self, dimensions: Tuple[int, int]) -> None: + canvas = self.current() + canvas.redraw_canvas(dimensions) + if canvas.wallpaper: + canvas.redraw_wallpaper() def get_metadata(self) -> Dict[str, Any]: canvases = [x.get_metadata() for x in self.all()] - return dict( - gridlines=self.app.manager.show_grid.get(), - dimensions=self.app.manager.current_dimensions, - canvases=canvases, - ) + return dict(gridlines=self.show_grid.get(), canvases=canvases) - def parse_metadata(self, config: Dict[str, Any]) -> None: + def parse_metadata_canvas(self, metadata: Dict[str, Any]) -> None: + # canvas setting + canvas_config = metadata.get("canvas") + logger.debug("canvas metadata: %s", canvas_config) + if not canvas_config: + return + canvas_config = json.loads(canvas_config) # get configured dimensions and gridlines option - dimensions = self.default_dimensions - dimensions = config.get("dimensions", dimensions) - gridlines = config.get("gridlines", True) + gridlines = canvas_config.get("gridlines", True) self.show_grid.set(gridlines) - self.redraw_canvases(dimensions) # get background configurations - for canvas_config in config.get("canvases", []): + for canvas_config in canvas_config.get("canvases", []): canvas_id = canvas_config.get("id") if canvas_id is None: - logging.error("canvas config id not provided") + logger.error("canvas config id not provided") continue canvas = self.get(canvas_id) canvas.parse_metadata(canvas_config) + def parse_metadata_shapes(self, metadata: Dict[str, Any]) -> None: + # load saved shapes + shapes_config = metadata.get("shapes") + if not shapes_config: + return + shapes_config = json.loads(shapes_config) + for shape_config in shapes_config: + logger.debug("loading shape: %s", shape_config) + Shape.from_metadata(self.app, shape_config) + + def parse_metadata_edges(self, metadata: Dict[str, Any]) -> None: + # load edges config + edges_config = metadata.get("edges") + if not edges_config: + return + edges_config = json.loads(edges_config) + logger.info("edges config: %s", edges_config) + for edge_config in edges_config: + edge_token = edge_config["token"] + edge = self.core.links.get(edge_token) + if edge: + edge.width = edge_config["width"] + edge.color = edge_config["color"] + edge.redraw() + else: + logger.warning("invalid edge token to configure: %s", edge_token) + + def parse_metadata_hidden(self, metadata: Dict[str, Any]) -> None: + # read hidden nodes + hidden_config = metadata.get("hidden") + if not hidden_config: + return + hidden_config = json.loads(hidden_config) + for node_id in hidden_config: + canvas_node = self.core.canvas_nodes.get(node_id) + if canvas_node: + canvas_node.hide() + else: + logger.warning("invalid node to hide: %s", node_id) + def add_core_node(self, core_node: Node) -> None: # get canvas tab for node canvas_id = core_node.canvas if core_node.canvas > 0 else 1 - logging.info("adding core node canvas(%s): %s", core_node.name, canvas_id) + logger.info("adding core node canvas(%s): %s", core_node.name, canvas_id) canvas = self.get(canvas_id) image = nutils.get_icon(core_node, self.app) x = core_node.position.x @@ -354,7 +403,7 @@ class CanvasManager: network_id = link.network_id if link.network_id else None token = create_wireless_token(src.id, dst.id, network_id) if token in self.wireless_edges: - logging.warning("ignoring link that already exists: %s", link) + logger.warning("ignoring link that already exists: %s", link) return edge = CanvasWirelessEdge(self.app, src, dst, network_id, token, link) self.wireless_edges[token] = edge diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index c58741ba..b0a78268 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -24,6 +24,8 @@ from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge from core.gui.graph.tooltip import CanvasTooltip from core.gui.images import ImageEnum +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.graph import CanvasGraph @@ -87,7 +89,7 @@ class CanvasNode: self.canvas.tag_bind(self.id, "", self.show_info) def delete(self) -> None: - logging.debug("Delete canvas node for %s", self.core_node) + logger.debug("Delete canvas node for %s", self.core_node) self.canvas.delete(self.id) self.canvas.delete(self.text_id) self.delete_antennas() @@ -110,7 +112,7 @@ class CanvasNode: """ delete one antenna """ - logging.debug("Delete an antenna on %s", self.core_node.name) + logger.debug("Delete an antenna on %s", self.core_node.name) if self.antennas: antenna_id = self.antennas.pop() self.canvas.delete(antenna_id) @@ -120,7 +122,7 @@ class CanvasNode: """ delete all antennas """ - logging.debug("Remove all antennas for %s", self.core_node.name) + logger.debug("Remove all antennas for %s", self.core_node.name) for antenna_id in self.antennas: self.canvas.delete(antenna_id) self.antennas.clear() @@ -253,10 +255,12 @@ class CanvasNode: else: self.context.add_command(label="Configure", command=self.show_config) if nutils.is_container(self.core_node): - self.context.add_command(label="Services", command=self.show_services) self.context.add_command( label="Config Services", command=self.show_config_services ) + self.context.add_command( + label="Services (Deprecated)", command=self.show_services + ) if is_emane: self.context.add_command( label="EMANE Config", command=self.show_emane_config @@ -334,7 +338,7 @@ class CanvasNode: def canvas_copy(self) -> None: self.canvas.clear_selection() self.canvas.select_object(self.id) - self.canvas.copy() + self.canvas.copy_selected() def show_config(self) -> None: dialog = NodeConfigDialog(self.app, self) @@ -400,7 +404,7 @@ class CanvasNode: def update_icon(self, icon_path: str) -> None: if not Path(icon_path).exists(): - logging.error(f"node icon does not exist: {icon_path}") + logger.error(f"node icon does not exist: {icon_path}") return self.core_node.icon = icon_path self.image = images.from_file(icon_path, width=images.NODE_SIZE) @@ -459,10 +463,10 @@ class CanvasNode: def _service_action(self, service: str, action: ServiceAction) -> None: session_id = self.app.core.session.id try: - response = self.app.core.client.service_action( + result = self.app.core.client.service_action( session_id, self.core_node.id, service, action ) - if not response.result: + if not result: self.app.show_error("Service Action Error", "Action Failed!") except grpc.RpcError as e: self.app.show_grpc_exception("Service Error", e) diff --git a/daemon/core/gui/graph/shape.py b/daemon/core/gui/graph/shape.py index 24786b04..7db18b5b 100644 --- a/daemon/core/gui/graph/shape.py +++ b/daemon/core/gui/graph/shape.py @@ -5,6 +5,8 @@ from core.gui.dialogs.shapemod import ShapeDialog from core.gui.graph import tags from core.gui.graph.shapeutils import ShapeType +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application from core.gui.graph.graph import CanvasGraph @@ -92,7 +94,7 @@ class Shape: shape = Shape(app, canvas, shape_type, *coords, data=data) canvas.shapes[shape.id] = shape except ValueError: - logging.exception("unknown shape: %s", shape_type) + logger.exception("unknown shape: %s", shape_type) def draw(self) -> None: if self.created: @@ -139,7 +141,7 @@ class Shape: state=self.app.manager.show_annotations.state(), ) else: - logging.error("unknown shape type: %s", self.shape_type) + logger.error("unknown shape type: %s", self.shape_type) self.created = True def get_font(self) -> List[Union[int, str]]: @@ -192,7 +194,7 @@ class Shape: self.canvas.move(self.text_id, x_offset, y_offset) def delete(self) -> None: - logging.debug("Delete shape, id(%s)", self.id) + logger.debug("Delete shape, id(%s)", self.id) self.canvas.delete(self.id) self.canvas.delete(self.text_id) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index 5fa70326..d4d09443 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -9,6 +9,8 @@ from core.gui import nodeutils as nutils from core.gui.graph.edges import CanvasEdge from core.gui.graph.node import CanvasNode +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -158,11 +160,18 @@ class InterfaceManager: index += 1 return index - def get_ips(self, node: Node) -> [str, str]: + def get_ips(self, node: Node) -> [Optional[str], Optional[str]]: + enable_ip4 = self.app.guiconfig.ips.enable_ip4 + enable_ip6 = self.app.guiconfig.ips.enable_ip6 + ip4, ip6 = None, None + if not enable_ip4 and not enable_ip6: + return ip4, ip6 index = self.next_index(node) - ip4 = self.current_subnets.ip4[index] - ip6 = self.current_subnets.ip6[index] - return str(ip4), str(ip6) + if enable_ip4: + ip4 = str(self.current_subnets.ip4[index]) + if enable_ip6: + ip6 = str(self.current_subnets.ip6[index]) + return ip4, ip6 def get_subnets(self, iface: Interface) -> Subnets: ip4_subnet = self.ip4_subnets @@ -196,12 +205,12 @@ class InterfaceManager: else: self.current_subnets = self.next_subnets() else: - logging.info("ignoring subnet change for link between network nodes") + logger.info("ignoring subnet change for link between network nodes") def find_subnets( self, canvas_node: CanvasNode, visited: Set[int] = None ) -> Optional[IPNetwork]: - logging.info("finding subnet for node: %s", canvas_node.core_node.name) + logger.info("finding subnet for node: %s", canvas_node.core_node.name) subnets = None if not visited: visited = set() @@ -220,7 +229,7 @@ class InterfaceManager: else: subnets = self.find_subnets(check_node, visited) if subnets: - logging.info("found subnets: %s", subnets) + logger.info("found subnets: %s", subnets) break return subnets @@ -244,7 +253,7 @@ class InterfaceManager: iface1=src_iface, iface2=dst_iface, ) - logging.info("added link between %s and %s", src_node.name, dst_node.name) + logger.info("added link between %s and %s", src_node.name, dst_node.name) return link def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface: @@ -266,5 +275,5 @@ class InterfaceManager: ip6=ip6, ip6_mask=ip6_mask, ) - logging.info("create node(%s) interface(%s)", node.name, iface) + logger.info("create node(%s) interface(%s)", node.name, iface) return iface diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index c7764f1a..e2df2f92 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -1,8 +1,8 @@ import logging -import os import tkinter as tk import webbrowser from functools import partial +from pathlib import Path from tkinter import filedialog, messagebox from typing import TYPE_CHECKING, Optional @@ -27,6 +27,8 @@ from core.gui.graph.manager import CanvasManager from core.gui.observers import ObserversMenu from core.gui.task import ProgressTask +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -76,7 +78,7 @@ class Menubar(tk.Menu): self.app.bind_all("", lambda e: self.click_new()) menu.add_command(label="Save", accelerator="Ctrl+S", command=self.click_save) self.app.bind_all("", self.click_save) - menu.add_command(label="Save As...", command=self.click_save_xml) + menu.add_command(label="Save As...", command=self.click_save_as) menu.add_command( label="Open...", command=self.click_open_xml, accelerator="Ctrl+O" ) @@ -84,7 +86,7 @@ class Menubar(tk.Menu): self.recent_menu = tk.Menu(menu) for i in self.app.guiconfig.recentfiles: self.recent_menu.add_command( - label=i, command=partial(self.open_recent_files, i) + label=i, command=partial(self.open_recent_files, Path(i)) ) menu.add_cascade(label="Recent Files", menu=self.recent_menu) menu.add_separator() @@ -120,11 +122,6 @@ class Menubar(tk.Menu): ) menu.add_command(label="Hide", accelerator="Ctrl+H", command=self.click_hide) self.add_cascade(label="Edit", menu=menu) - self.app.master.bind_all("", self.click_cut) - self.app.master.bind_all("", self.click_copy) - self.app.master.bind_all("", self.click_paste) - self.app.master.bind_all("", self.click_delete) - self.app.master.bind_all("", self.click_hide) self.edit_menu = menu def draw_canvas_menu(self) -> None: @@ -272,27 +269,28 @@ class Menubar(tk.Menu): menu.add_command(label="About", command=self.click_about) self.add_cascade(label="Help", menu=menu) - def open_recent_files(self, filename: str) -> None: - if os.path.isfile(filename): - logging.debug("Open recent file %s", filename) - self.open_xml_task(filename) + def open_recent_files(self, file_path: Path) -> None: + if file_path.is_file(): + logger.debug("Open recent file %s", file_path) + self.open_xml_task(file_path) else: - logging.warning("File does not exist %s", filename) + logger.warning("File does not exist %s", file_path) def update_recent_files(self) -> None: self.recent_menu.delete(0, tk.END) for i in self.app.guiconfig.recentfiles: self.recent_menu.add_command( - label=i, command=partial(self.open_recent_files, i) + label=i, command=partial(self.open_recent_files, Path(i)) ) - def click_save(self, _event=None) -> None: + def click_save(self, _event: tk.Event = None) -> None: if self.core.session.file: - self.core.save_xml() + if self.core.save_xml(): + self.add_recent_file_to_gui_config(self.core.session.file) else: - self.click_save_xml() + self.click_save_as() - def click_save_xml(self, _event: tk.Event = None) -> None: + def click_save_as(self, _event: tk.Event = None) -> None: init_dir = self.core.get_xml_dir() file_path = filedialog.asksaveasfilename( initialdir=init_dir, @@ -301,8 +299,9 @@ class Menubar(tk.Menu): defaultextension=".xml", ) if file_path: - self.add_recent_file_to_gui_config(file_path) - self.core.save_xml(file_path) + file_path = Path(file_path) + if self.core.save_xml(file_path): + self.add_recent_file_to_gui_config(file_path) def click_open_xml(self, _event: tk.Event = None) -> None: init_dir = self.core.get_xml_dir() @@ -312,9 +311,10 @@ class Menubar(tk.Menu): filetypes=(("XML Files", "*.xml"), ("All Files", "*")), ) if file_path: + file_path = Path(file_path) self.open_xml_task(file_path) - def open_xml_task(self, file_path: str) -> None: + def open_xml_task(self, file_path: Path) -> None: self.add_recent_file_to_gui_config(file_path) self.prompt_save_running_session() task = ProgressTask(self.app, "Open XML", self.core.open_xml, args=(file_path,)) @@ -324,21 +324,14 @@ class Menubar(tk.Menu): dialog = ExecutePythonDialog(self.app) dialog.show() - def add_recent_file_to_gui_config(self, file_path) -> None: + def add_recent_file_to_gui_config(self, file_path: Path) -> None: recent_files = self.app.guiconfig.recentfiles - num_files = len(recent_files) - if num_files == 0: - recent_files.insert(0, file_path) - elif 0 < num_files <= MAX_FILES: - if file_path in recent_files: - recent_files.remove(file_path) - recent_files.insert(0, file_path) - else: - if num_files == MAX_FILES: - recent_files.pop() - recent_files.insert(0, file_path) - else: - logging.error("unexpected number of recent files") + file_path = str(file_path) + if file_path in recent_files: + recent_files.remove(file_path) + recent_files.insert(0, file_path) + if len(recent_files) > MAX_FILES: + recent_files.pop() self.app.save_config() self.app.menubar.update_recent_files() @@ -411,47 +404,47 @@ class Menubar(tk.Menu): def click_copy(self, _event: tk.Event = None) -> None: canvas = self.manager.current() - canvas.copy() + canvas.copy_selected() - def click_paste(self, _event: tk.Event = None) -> None: + def click_paste(self, event: tk.Event = None) -> None: canvas = self.manager.current() - canvas.paste() + canvas.paste_selected(event) - def click_delete(self, _event: tk.Event = None) -> None: + def click_delete(self, event: tk.Event = None) -> None: canvas = self.manager.current() - canvas.delete_selected_objects() + canvas.delete_selected(event) - def click_hide(self, _event: tk.Event = None) -> None: + def click_hide(self, event: tk.Event = None) -> None: canvas = self.manager.current() - canvas.hide_selected_objects() + canvas.hide_selected(event) - def click_cut(self, _event: tk.Event = None) -> None: + def click_cut(self, event: tk.Event = None) -> None: canvas = self.manager.current() - canvas.copy() - canvas.delete_selected_objects() + canvas.copy_selected(event) + canvas.delete_selected(event) def click_show_hidden(self, _event: tk.Event = None) -> None: for canvas in self.manager.all(): canvas.show_hidden() def click_session_options(self) -> None: - logging.debug("Click options") + logger.debug("Click options") dialog = SessionOptionsDialog(self.app) if not dialog.has_error: dialog.show() def click_sessions(self) -> None: - logging.debug("Click change sessions") + logger.debug("Click change sessions") dialog = SessionsDialog(self.app) dialog.show() def click_hooks(self) -> None: - logging.debug("Click hooks") + logger.debug("Click hooks") dialog = HooksDialog(self.app) dialog.show() def click_servers(self) -> None: - logging.debug("Click emulation servers") + logger.debug("Click emulation servers") dialog = ServersDialog(self.app) dialog.show() @@ -460,11 +453,11 @@ class Menubar(tk.Menu): dialog.show() def click_autogrid(self) -> None: - width, height = self.manager.current_dimensions + width, height = self.manager.current().current_dimensions padding = (images.NODE_SIZE / 2) + 10 layout_size = padding + images.NODE_SIZE col_count = width // layout_size - logging.info( + logger.info( "auto grid layout: dimension(%s, %s) col(%s)", width, height, col_count ) canvas = self.manager.current() diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 251ed275..537cedf2 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -8,6 +8,8 @@ from core.gui import images from core.gui.appconfig import CustomNode, GuiConfig from core.gui.images import ImageEnum +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -29,10 +31,9 @@ ANTENNA_ICON: Optional[PhotoImage] = None def setup() -> None: global ANTENNA_ICON nodes = [ - (ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"), - (ImageEnum.HOST, NodeType.DEFAULT, "Host", "host"), (ImageEnum.PC, NodeType.DEFAULT, "PC", "PC"), (ImageEnum.MDR, NodeType.DEFAULT, "MDR", "mdr"), + (ImageEnum.ROUTER, NodeType.DEFAULT, "Router", "router"), (ImageEnum.PROUTER, NodeType.DEFAULT, "PRouter", "prouter"), (ImageEnum.DOCKER, NodeType.DOCKER, "Docker", None), (ImageEnum.LXC, NodeType.LXC, "LXC", None), @@ -118,11 +119,11 @@ def get_icon(node: Node, app: "Application") -> PhotoImage: try: image = images.from_file(node.icon, width=images.NODE_SIZE, scale=scale) except OSError: - logging.error("invalid icon: %s", node.icon) + logger.error("invalid icon: %s", node.icon) # custom node elif is_custom(node): image_file = _get_custom_file(app.guiconfig, node.model) - logging.info("custom node file: %s", image_file) + logger.info("custom node file: %s", image_file) if image_file: image = images.from_file(image_file, width=images.NODE_SIZE, scale=scale) # built in node diff --git a/daemon/core/gui/task.py b/daemon/core/gui/task.py index 02148f5a..2623136d 100644 --- a/daemon/core/gui/task.py +++ b/daemon/core/gui/task.py @@ -4,6 +4,8 @@ import time import tkinter as tk from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -43,7 +45,7 @@ class ProgressTask: if self.callback: self.app.after(0, self.callback, *values) except Exception as e: - logging.exception("progress task exception") + logger.exception("progress task exception") self.app.show_exception("Task Error", e) finally: self.app.after(0, self.complete) diff --git a/daemon/core/gui/toolbar.py b/daemon/core/gui/toolbar.py index 844a68f2..faf8ab36 100644 --- a/daemon/core/gui/toolbar.py +++ b/daemon/core/gui/toolbar.py @@ -20,6 +20,8 @@ from core.gui.task import ProgressTask from core.gui.themes import Styles from core.gui.tooltip import Tooltip +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -304,7 +306,6 @@ class Toolbar(ttk.Frame): def start_callback(self, result: bool, exceptions: List[str]) -> None: if result: self.set_runtime() - self.app.core.set_metadata() self.app.core.show_mobility_players() else: enable_buttons(self.design_frame, enabled=True) @@ -338,7 +339,7 @@ class Toolbar(ttk.Frame): type_enum: NodeTypeEnum, image: PhotoImage, ) -> None: - logging.debug("update button(%s): %s", button, node_draw) + logger.debug("update button(%s): %s", button, node_draw) button.configure(image=image) button.image = image self.app.manager.node_draw = node_draw @@ -399,7 +400,7 @@ class Toolbar(ttk.Frame): """ redraw buttons on the toolbar, send node and link messages to grpc server """ - logging.info("clicked stop button") + logger.info("clicked stop button") self.app.menubar.set_state(is_runtime=False) self.app.core.close_mobility_players() enable_buttons(self.runtime_frame, enabled=False) @@ -415,7 +416,7 @@ class Toolbar(ttk.Frame): def update_annotation( self, shape_type: ShapeType, image_enum: ImageEnum, image: PhotoImage ) -> None: - logging.debug("clicked annotation") + logger.debug("clicked annotation") self.annotation_button.configure(image=image) self.annotation_button.image = image self.app.manager.annotation_type = shape_type @@ -433,7 +434,7 @@ class Toolbar(ttk.Frame): self.marker_frame.grid() def click_run_button(self) -> None: - logging.debug("Click on RUN button") + logger.debug("Click on RUN button") dialog = RunToolDialog(self.app) dialog.show() diff --git a/daemon/core/gui/widgets.py b/daemon/core/gui/widgets.py index 004aa7b7..1f6cd637 100644 --- a/daemon/core/gui/widgets.py +++ b/daemon/core/gui/widgets.py @@ -10,6 +10,8 @@ from core.gui import appconfig, themes, validation from core.gui.dialogs.dialog import Dialog from core.gui.themes import FRAME_PAD, PADX, PADY +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.gui.app import Application @@ -161,7 +163,7 @@ class ConfigFrame(ttk.Notebook): ) entry.grid(row=index, column=1, sticky=tk.EW) else: - logging.error("unhandled config option type: %s", option.type) + logger.error("unhandled config option type: %s", option.type) self.values[option.name] = value def parse_config(self) -> Dict[str, str]: diff --git a/daemon/core/location/geo.py b/daemon/core/location/geo.py index af463595..5896e074 100644 --- a/daemon/core/location/geo.py +++ b/daemon/core/location/geo.py @@ -10,6 +10,7 @@ from pyproj import Transformer from core.emulator.enumerations import RegisterTlvs +logger = logging.getLogger(__name__) SCALE_FACTOR: float = 100.0 CRS_WGS84: int = 4326 CRS_PROJ: int = 3857 @@ -92,7 +93,7 @@ class GeoLocation: :param alt: altitude value :return: x,y,z representation of provided values """ - logging.debug("input lon,lat,alt(%s, %s, %s)", lon, lat, alt) + logger.debug("input lon,lat,alt(%s, %s, %s)", lon, lat, alt) px, py = self.to_pixels.transform(lon, lat) px -= self.refproj[0] py -= self.refproj[1] @@ -100,7 +101,7 @@ class GeoLocation: x = self.meters2pixels(px) + self.refxyz[0] y = -(self.meters2pixels(py) + self.refxyz[1]) z = self.meters2pixels(pz) + self.refxyz[2] - logging.debug("result x,y,z(%s, %s, %s)", x, y, z) + logger.debug("result x,y,z(%s, %s, %s)", x, y, z) return x, y, z def getgeo(self, x: float, y: float, z: float) -> Tuple[float, float, float]: @@ -112,7 +113,7 @@ class GeoLocation: :param z: z value :return: lat,lon,alt representation of provided values """ - logging.debug("input x,y(%s, %s)", x, y) + logger.debug("input x,y(%s, %s)", x, y) x -= self.refxyz[0] y = -(y - self.refxyz[1]) if z is None: @@ -123,5 +124,5 @@ class GeoLocation: py = self.refproj[1] + self.pixels2meters(y) lon, lat = self.to_geo.transform(px, py) alt = self.refgeo[2] + self.pixels2meters(z) - logging.debug("result lon,lat,alt(%s, %s, %s)", lon, lat, alt) + logger.debug("result lon,lat,alt(%s, %s, %s)", lon, lat, alt) return lat, lon, alt diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 688910c5..4c61c065 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -12,22 +12,27 @@ from pathlib import Path from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, Union from core import utils -from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager +from core.config import ( + ConfigBool, + ConfigFloat, + ConfigGroup, + ConfigInt, + ConfigString, + ConfigurableOptions, + Configuration, + ModelManager, +) from core.emane.nodes import EmaneNet from core.emulator.data import EventData, LinkData, LinkOptions -from core.emulator.enumerations import ( - ConfigDataTypes, - EventTypes, - LinkTypes, - MessageFlags, - RegisterTlvs, -) +from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags, RegisterTlvs from core.errors import CoreError from core.executables import BASH from core.nodes.base import CoreNode from core.nodes.interface import CoreInterface from core.nodes.network import WlanNode +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.emulator.session import Session @@ -42,6 +47,43 @@ def get_mobility_node(session: "Session", node_id: int) -> Union[WlanNode, Emane return session.get_node(node_id, EmaneNet) +def get_config_int(current: int, config: Dict[str, str], name: str) -> Optional[int]: + """ + Convenience function to get config values as int. + + :param current: current config value to use when one is not provided + :param config: config to get values from + :param name: name of config value to get + :return: current config value when not provided, new value otherwise + """ + value = get_config_float(current, config, name) + if value is not None: + value = int(value) + return value + + +def get_config_float( + current: Union[int, float], config: Dict[str, str], name: str +) -> Optional[float]: + """ + Convenience function to get config values as float. + + :param current: current config value to use when one is not provided + :param config: config to get values from + :param name: name of config value to get + :return: current config value when not provided, new value otherwise + """ + value = config.get(name) + if value is not None: + if value == "": + value = None + else: + value = float(value) + else: + value = current + return value + + class MobilityManager(ModelManager): """ Member of session class for handling configuration data for mobility and @@ -81,7 +123,7 @@ class MobilityManager(ModelManager): if node_ids is None: node_ids = self.nodes() for node_id in node_ids: - logging.debug( + logger.debug( "node(%s) mobility startup: %s", node_id, self.get_all_configs(node_id) ) try: @@ -95,8 +137,8 @@ class MobilityManager(ModelManager): if node.mobility: self.session.event_loop.add_event(0.0, node.mobility.startup) except CoreError: - logging.exception("mobility startup error") - logging.warning( + logger.exception("mobility startup error") + logger.warning( "skipping mobility configuration for unknown node: %s", node_id ) @@ -114,7 +156,7 @@ class MobilityManager(ModelManager): try: node = get_mobility_node(self.session, node_id) except CoreError: - logging.exception( + logger.exception( "ignoring event for model(%s), unknown node(%s)", name, node_id ) return @@ -124,17 +166,17 @@ class MobilityManager(ModelManager): for model in models: cls = self.models.get(model) if not cls: - logging.warning("ignoring event for unknown model '%s'", model) + logger.warning("ignoring event for unknown model '%s'", model) continue if cls.config_type in [RegisterTlvs.WIRELESS, RegisterTlvs.MOBILITY]: model = node.mobility else: continue if model is None: - logging.warning("ignoring event, %s has no model", node.name) + logger.warning("ignoring event, %s has no model", node.name) continue if cls.name != model.name: - logging.warning( + logger.warning( "ignoring event for %s wrong model %s,%s", node.name, cls.name, @@ -235,39 +277,12 @@ class BasicRangeModel(WirelessModel): name: str = "basic_range" options: List[Configuration] = [ - Configuration( - _id="range", - _type=ConfigDataTypes.UINT32, - default="275", - label="wireless range (pixels)", - ), - Configuration( - _id="bandwidth", - _type=ConfigDataTypes.UINT64, - default="54000000", - label="bandwidth (bps)", - ), - Configuration( - _id="jitter", - _type=ConfigDataTypes.UINT64, - default="0", - label="transmission jitter (usec)", - ), - Configuration( - _id="delay", - _type=ConfigDataTypes.UINT64, - default="5000", - label="transmission delay (usec)", - ), - Configuration( - _id="error", _type=ConfigDataTypes.STRING, default="0", label="loss (%)" - ), - Configuration( - _id="promiscuous", - _type=ConfigDataTypes.BOOL, - default="0", - label="promiscuous mode", - ), + ConfigInt(id="range", default="275", label="wireless range (pixels)"), + ConfigInt(id="bandwidth", default="54000000", label="bandwidth (bps)"), + ConfigInt(id="jitter", default="0", label="transmission jitter (usec)"), + ConfigInt(id="delay", default="5000", label="transmission delay (usec)"), + ConfigFloat(id="error", default="0.0", label="loss (%)"), + ConfigBool(id="promiscuous", default="0", label="promiscuous mode"), ] @classmethod @@ -293,25 +308,6 @@ class BasicRangeModel(WirelessModel): self.jitter: Optional[int] = None self.promiscuous: bool = False - def _get_config(self, current_value: int, config: Dict[str, str], name: str) -> int: - """ - Convenience for updating value to use from a provided configuration. - - :param current_value: current config value to use when one is not provided - :param config: config to get values from - :param name: name of config value to get - :return: current config value when not provided, new value otherwise - """ - value = config.get(name) - if value is not None: - if value == "": - value = None - else: - value = int(float(value)) - else: - value = current_value - return value - def setlinkparams(self) -> None: """ Apply link parameters to all interfaces. This is invoked from @@ -406,20 +402,20 @@ class BasicRangeModel(WirelessModel): a = min(iface, iface2) b = max(iface, iface2) - with self.wlan._linked_lock: - linked = self.wlan.linked(a, b) + with self.wlan.linked_lock: + linked = self.wlan.is_linked(a, b) if d > self.range: if linked: - logging.debug("was linked, unlinking") + logger.debug("was linked, unlinking") self.wlan.unlink(a, b) self.sendlinkmsg(a, b, unlink=True) else: if not linked: - logging.debug("was not linked, linking") + logger.debug("was not linked, linking") self.wlan.link(a, b) self.sendlinkmsg(a, b) except KeyError: - logging.exception("error getting interfaces during calclinkS") + logger.exception("error getting interfaces during calclink") @staticmethod def calcdistance( @@ -446,15 +442,15 @@ class BasicRangeModel(WirelessModel): :param config: values to update configuration :return: nothing """ - self.range = self._get_config(self.range, config, "range") + self.range = get_config_int(self.range, config, "range") if self.range is None: self.range = 0 - logging.debug("wlan %s set range to %s", self.wlan.name, self.range) - self.bw = self._get_config(self.bw, config, "bandwidth") - self.delay = self._get_config(self.delay, config, "delay") - self.loss = self._get_config(self.loss, config, "error") - self.jitter = self._get_config(self.jitter, config, "jitter") - promiscuous = config["promiscuous"] == "1" + logger.debug("wlan %s set range to %s", self.wlan.name, self.range) + self.bw = get_config_int(self.bw, config, "bandwidth") + self.delay = get_config_int(self.delay, config, "delay") + self.loss = get_config_float(self.loss, config, "error") + self.jitter = get_config_int(self.jitter, config, "jitter") + promiscuous = config.get("promiscuous", "0") == "1" if self.promiscuous and not promiscuous: self.wlan.net_client.set_mac_learning(self.wlan.brname, LEARNING_ENABLED) elif not self.promiscuous and promiscuous: @@ -506,10 +502,10 @@ class BasicRangeModel(WirelessModel): :return: all link data """ all_links = [] - with self.wlan._linked_lock: - for a in self.wlan._linked: - for b in self.wlan._linked[a]: - if self.wlan._linked[a][b]: + with self.wlan.linked_lock: + for a in self.wlan.linked: + for b in self.wlan.linked[a]: + if self.wlan.linked[a][b]: all_links.append(self.create_link_data(a, b, flags)) return all_links @@ -867,43 +863,14 @@ class Ns2ScriptedMobility(WayPointMobility): name: str = "ns2script" options: List[Configuration] = [ - Configuration( - _id="file", _type=ConfigDataTypes.STRING, label="mobility script file" - ), - Configuration( - _id="refresh_ms", - _type=ConfigDataTypes.UINT32, - default="50", - label="refresh time (ms)", - ), - Configuration( - _id="loop", _type=ConfigDataTypes.BOOL, default="1", label="loop" - ), - Configuration( - _id="autostart", - _type=ConfigDataTypes.STRING, - label="auto-start seconds (0.0 for runtime)", - ), - Configuration( - _id="map", - _type=ConfigDataTypes.STRING, - label="node mapping (optional, e.g. 0:1,1:2,2:3)", - ), - Configuration( - _id="script_start", - _type=ConfigDataTypes.STRING, - label="script file to run upon start", - ), - Configuration( - _id="script_pause", - _type=ConfigDataTypes.STRING, - label="script file to run upon pause", - ), - Configuration( - _id="script_stop", - _type=ConfigDataTypes.STRING, - label="script file to run upon stop", - ), + ConfigString(id="file", label="mobility script file"), + ConfigInt(id="refresh_ms", default="50", label="refresh time (ms)"), + ConfigBool(id="loop", default="1", label="loop"), + ConfigString(id="autostart", label="auto-start seconds (0.0 for runtime)"), + ConfigString(id="map", label="node mapping (optional, e.g. 0:1,1:2,2:3)"), + ConfigString(id="script_start", label="script file to run upon start"), + ConfigString(id="script_pause", label="script file to run upon pause"), + ConfigString(id="script_stop", label="script file to run upon stop"), ] @classmethod @@ -920,7 +887,7 @@ class Ns2ScriptedMobility(WayPointMobility): :param _id: object id """ super().__init__(session, _id) - self.file: Optional[str] = None + self.file: Optional[Path] = None self.autostart: Optional[str] = None self.nodemap: Dict[int, int] = {} self.script_start: Optional[str] = None @@ -928,8 +895,8 @@ class Ns2ScriptedMobility(WayPointMobility): self.script_stop: Optional[str] = None def update_config(self, config: Dict[str, str]) -> None: - self.file = config["file"] - logging.info( + self.file = Path(config["file"]) + logger.info( "ns-2 scripted mobility configured for WLAN %d using file: %s", self.id, self.file, @@ -953,15 +920,15 @@ class Ns2ScriptedMobility(WayPointMobility): :return: nothing """ - filename = self.findfile(self.file) + file_path = self.findfile(self.file) try: - f = open(filename, "r") + f = file_path.open("r") except IOError: - logging.exception( + logger.exception( "ns-2 scripted mobility failed to load file: %s", self.file ) return - logging.info("reading ns-2 script file: %s", filename) + logger.info("reading ns-2 script file: %s", file_path) ln = 0 ix = iy = iz = None inodenum = None @@ -977,13 +944,13 @@ class Ns2ScriptedMobility(WayPointMobility): # waypoints: # $ns_ at 1.00 "$node_(6) setdest 500.0 178.0 25.0" parts = line.split() - time = float(parts[2]) + line_time = float(parts[2]) nodenum = parts[3][1 + parts[3].index("(") : parts[3].index(")")] x = float(parts[5]) y = float(parts[6]) z = None speed = float(parts[7].strip('"')) - self.addwaypoint(time, self.map(nodenum), x, y, z, speed) + self.addwaypoint(line_time, self.map(nodenum), x, y, z, speed) elif line[:7] == "$node_(": # initial position (time=0, speed=0): # $node_(6) set X_ 780.0 @@ -1004,38 +971,38 @@ class Ns2ScriptedMobility(WayPointMobility): else: raise ValueError except ValueError: - logging.exception( + logger.exception( "skipping line %d of file %s '%s'", ln, self.file, line ) continue if ix is not None and iy is not None: self.addinitial(self.map(inodenum), ix, iy, iz) - def findfile(self, file_name: str) -> str: + def findfile(self, file_path: Path) -> Path: """ Locate a script file. If the specified file doesn't exist, look in the same directory as the scenario file, or in gui directories. - :param file_name: file name to find + :param file_path: file name to find :return: absolute path to the file :raises CoreError: when file is not found """ - file_path = Path(file_name).expanduser() + file_path = file_path.expanduser() if file_path.exists(): - return str(file_path) - if self.session.file_name: - file_path = Path(self.session.file_name).parent / file_name - if file_path.exists(): - return str(file_path) + return file_path + if self.session.file_path: + session_file_path = self.session.file_path.parent / file_path + if session_file_path.exists(): + return session_file_path if self.session.user: user_path = Path(f"~{self.session.user}").expanduser() - file_path = user_path / ".core" / "configs" / file_name - if file_path.exists(): - return str(file_path) - file_path = user_path / ".coregui" / "mobility" / file_name - if file_path.exists(): - return str(file_path) - raise CoreError(f"invalid file: {file_name}") + configs_path = user_path / ".core" / "configs" / file_path + if configs_path.exists(): + return configs_path + mobility_path = user_path / ".coregui" / "mobility" / file_path + if mobility_path.exists(): + return mobility_path + raise CoreError(f"invalid file: {file_path}") def parsemap(self, mapstr: str) -> None: """ @@ -1047,7 +1014,6 @@ class Ns2ScriptedMobility(WayPointMobility): self.nodemap = {} if mapstr.strip() == "": return - for pair in mapstr.split(","): parts = pair.split(":") try: @@ -1055,7 +1021,7 @@ class Ns2ScriptedMobility(WayPointMobility): raise ValueError self.nodemap[int(parts[0])] = int(parts[1]) except ValueError: - logging.exception("ns-2 mobility node map error") + logger.exception("ns-2 mobility node map error") def map(self, nodenum: str) -> int: """ @@ -1077,19 +1043,19 @@ class Ns2ScriptedMobility(WayPointMobility): :return: nothing """ if self.autostart == "": - logging.info("not auto-starting ns-2 script for %s", self.net.name) + logger.info("not auto-starting ns-2 script for %s", self.net.name) return try: t = float(self.autostart) except ValueError: - logging.exception( + logger.exception( "Invalid auto-start seconds specified '%s' for %s", self.autostart, self.net.name, ) return self.movenodesinitial() - logging.info("scheduling ns-2 script for %s autostart at %s", self.net.name, t) + logger.info("scheduling ns-2 script for %s autostart at %s", self.net.name, t) self.state = self.STATE_RUNNING self.session.event_loop.add_event(t, self.run) @@ -1099,7 +1065,7 @@ class Ns2ScriptedMobility(WayPointMobility): :return: nothing """ - logging.info("starting script: %s", self.file) + logger.info("starting script: %s", self.file) laststate = self.state super().start() if laststate == self.STATE_PAUSED: @@ -1120,7 +1086,7 @@ class Ns2ScriptedMobility(WayPointMobility): :return: nothing """ - logging.info("pausing script: %s", self.file) + logger.info("pausing script: %s", self.file) super().pause() self.statescript("pause") @@ -1132,7 +1098,7 @@ class Ns2ScriptedMobility(WayPointMobility): position :return: nothing """ - logging.info("stopping script: %s", self.file) + logger.info("stopping script: %s", self.file) super().stop(move_initial=move_initial) self.statescript("stop") @@ -1152,8 +1118,7 @@ class Ns2ScriptedMobility(WayPointMobility): filename = self.script_stop if filename is None or filename == "": return + filename = Path(filename) filename = self.findfile(filename) args = f"{BASH} {filename} {typestr}" - utils.cmd( - args, cwd=self.session.session_dir, env=self.session.get_environment() - ) + utils.cmd(args, cwd=self.session.directory, env=self.session.get_environment()) diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 0a952e04..91c1fdcc 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -3,9 +3,9 @@ Defines the base logic for nodes used within core. """ import abc import logging -import os import shutil import threading +from pathlib import Path from threading import RLock from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union @@ -18,9 +18,11 @@ from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.errors import CoreCommandError, CoreError from core.executables import MOUNT, TEST, VNODED from core.nodes.client import VnodeClient -from core.nodes.interface import CoreInterface, TunTap, Veth +from core.nodes.interface import DEFAULT_MTU, CoreInterface, TunTap, Veth from core.nodes.netclient import LinuxNetClient, get_net_client +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.emulator.distributed import DistributedServer from core.emulator.session import Session @@ -30,6 +32,8 @@ if TYPE_CHECKING: CoreServices = List[Union[CoreService, Type[CoreService]]] ConfigServiceType = Type[ConfigService] +PRIVATE_DIRS: List[Path] = [Path("/var/run"), Path("/var/log")] + class NodeBase(abc.ABC): """ @@ -97,7 +101,7 @@ class NodeBase(abc.ABC): self, args: str, env: Dict[str, str] = None, - cwd: str = None, + cwd: Path = None, wait: bool = True, shell: bool = False, ) -> str: @@ -221,7 +225,7 @@ class CoreNodeBase(NodeBase): """ super().__init__(session, _id, name, server) self.config_services: Dict[str, "ConfigService"] = {} - self.nodedir: Optional[str] = None + self.directory: Optional[Path] = None self.tmpnodedir: bool = False @abc.abstractmethod @@ -233,11 +237,21 @@ class CoreNodeBase(NodeBase): raise NotImplementedError @abc.abstractmethod - def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: + def create_dir(self, dir_path: Path) -> None: + """ + Create a node private directory. + + :param dir_path: path to create + :return: nothing + """ + raise NotImplementedError + + @abc.abstractmethod + def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None: """ Create a node file with a given mode. - :param filename: name of file to create + :param file_path: name of file to create :param contents: contents of file :param mode: mode for file :return: nothing @@ -245,12 +259,25 @@ class CoreNodeBase(NodeBase): raise NotImplementedError @abc.abstractmethod - def addfile(self, srcname: str, filename: str) -> None: + def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None: + """ + Copy source file to node host destination, updating the file mode when + provided. + + :param src_path: source file to copy + :param dst_path: node host destination + :param mode: file mode + :return: nothing + """ + raise NotImplementedError + + @abc.abstractmethod + def addfile(self, src_path: Path, file_path: Path) -> None: """ Add a file. - :param srcname: source file name - :param filename: file name to add + :param src_path: source file path + :param file_path: file name to add :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ @@ -302,6 +329,21 @@ class CoreNodeBase(NodeBase): """ raise NotImplementedError + def host_path(self, path: Path, is_dir: bool = False) -> Path: + """ + Return the name of a node"s file on the host filesystem. + + :param path: path to translate to host path + :param is_dir: True if path is a directory path, False otherwise + :return: path to file + """ + if is_dir: + directory = str(path).strip("/").replace("/", ".") + return self.directory / directory + else: + directory = str(path.parent).strip("/").replace("/", ".") + return self.directory / directory / path.name + def add_config_service(self, service_class: "ConfigServiceType") -> None: """ Adds a configuration service to the node. @@ -345,9 +387,9 @@ class CoreNodeBase(NodeBase): :return: nothing """ - if self.nodedir is None: - self.nodedir = os.path.join(self.session.session_dir, self.name + ".conf") - self.host_cmd(f"mkdir -p {self.nodedir}") + if self.directory is None: + self.directory = self.session.directory / f"{self.name}.conf" + self.host_cmd(f"mkdir -p {self.directory}") self.tmpnodedir = True else: self.tmpnodedir = False @@ -362,7 +404,7 @@ class CoreNodeBase(NodeBase): if preserve: return if self.tmpnodedir: - self.host_cmd(f"rm -rf {self.nodedir}") + self.host_cmd(f"rm -rf {self.directory}") def add_iface(self, iface: CoreInterface, iface_id: int) -> None: """ @@ -387,7 +429,7 @@ class CoreNodeBase(NodeBase): if iface_id not in self.ifaces: raise CoreError(f"node({self.name}) interface({iface_id}) does not exist") iface = self.ifaces.pop(iface_id) - logging.info("node(%s) removing interface(%s)", self.name, iface.name) + logger.info("node(%s) removing interface(%s)", self.name, iface.name) iface.detachnet() iface.shutdown() @@ -458,7 +500,7 @@ class CoreNode(CoreNodeBase): session: "Session", _id: int = None, name: str = None, - nodedir: str = None, + directory: Path = None, server: "DistributedServer" = None, ) -> None: """ @@ -467,19 +509,17 @@ class CoreNode(CoreNodeBase): :param session: core session instance :param _id: object id :param name: object name - :param nodedir: node directory + :param directory: node directory :param server: remote server node will run on, default is None for localhost """ super().__init__(session, _id, name, server) - self.nodedir: Optional[str] = nodedir - self.ctrlchnlname: str = os.path.abspath( - os.path.join(self.session.session_dir, self.name) - ) + self.directory: Optional[Path] = directory + self.ctrlchnlname: Path = self.session.directory / self.name self.client: Optional[VnodeClient] = None self.pid: Optional[int] = None self.lock: RLock = RLock() - self._mounts: List[Tuple[str, str]] = [] + self._mounts: List[Tuple[Path, Path]] = [] self.node_net_client: LinuxNetClient = self.create_node_net_client( self.session.use_ovs() ) @@ -524,33 +564,33 @@ class CoreNode(CoreNodeBase): f"{VNODED} -v -c {self.ctrlchnlname} -l {self.ctrlchnlname}.log " f"-p {self.ctrlchnlname}.pid" ) - if self.nodedir: - vnoded += f" -C {self.nodedir}" + if self.directory: + vnoded += f" -C {self.directory}" env = self.session.get_environment(state=False) env["NODE_NUMBER"] = str(self.id) env["NODE_NAME"] = str(self.name) output = self.host_cmd(vnoded, env=env) self.pid = int(output) - logging.debug("node(%s) pid: %s", self.name, self.pid) + logger.debug("node(%s) pid: %s", self.name, self.pid) # create vnode client self.client = VnodeClient(self.name, self.ctrlchnlname) # bring up the loopback interface - logging.debug("bringing up loopback interface") + logger.debug("bringing up loopback interface") self.node_net_client.device_up("lo") # set hostname for node - logging.debug("setting hostname: %s", self.name) + logger.debug("setting hostname: %s", self.name) self.node_net_client.set_hostname(self.name) # mark node as up self.up = True # create private directories - self.privatedir("/var/run") - self.privatedir("/var/log") + for dir_path in PRIVATE_DIRS: + self.create_dir(dir_path) def shutdown(self) -> None: """ @@ -561,35 +601,30 @@ class CoreNode(CoreNodeBase): # nothing to do if node is not up if not self.up: return - with self.lock: try: # unmount all targets (NOTE: non-persistent mount namespaces are # removed by the kernel when last referencing process is killed) self._mounts = [] - # shutdown all interfaces for iface in self.get_ifaces(): iface.shutdown() - # kill node process if present try: self.host_cmd(f"kill -9 {self.pid}") except CoreCommandError: - logging.exception("error killing process") - + logger.exception("error killing process") # remove node directory if present try: self.host_cmd(f"rm -rf {self.ctrlchnlname}") except CoreCommandError: - logging.exception("error removing node directory") - + logger.exception("error removing node directory") # clear interface data, close client, and mark self and not up self.ifaces.clear() self.client.close() self.up = False except OSError: - logging.exception("error during shutdown") + logger.exception("error during shutdown") finally: self.rmnodedir() @@ -636,35 +671,37 @@ class CoreNode(CoreNodeBase): else: return f"ssh -X -f {self.server.host} xterm -e {terminal}" - def privatedir(self, path: str) -> None: + def create_dir(self, dir_path: Path) -> None: """ - Create a private directory. + Create a node private directory. - :param path: path to create + :param dir_path: path to create :return: nothing """ - if path[0] != "/": - raise ValueError(f"path not fully qualified: {path}") - hostpath = os.path.join( - self.nodedir, os.path.normpath(path).strip("/").replace("/", ".") - ) - self.host_cmd(f"mkdir -p {hostpath}") - self.mount(hostpath, path) + if not dir_path.is_absolute(): + raise CoreError(f"private directory path not fully qualified: {dir_path}") + logger.debug("node(%s) creating private directory: %s", self.name, dir_path) + parent_path = self._find_parent_path(dir_path) + if parent_path: + self.host_cmd(f"mkdir -p {parent_path}") + else: + host_path = self.host_path(dir_path, is_dir=True) + self.host_cmd(f"mkdir -p {host_path}") + self.mount(host_path, dir_path) - def mount(self, source: str, target: str) -> None: + def mount(self, src_path: Path, target_path: Path) -> None: """ Create and mount a directory. - :param source: source directory to mount - :param target: target directory to create + :param src_path: source directory to mount + :param target_path: target directory to create :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ - source = os.path.abspath(source) - logging.debug("node(%s) mounting: %s at %s", self.name, source, target) - self.cmd(f"mkdir -p {target}") - self.cmd(f"{MOUNT} -n --bind {source} {target}") - self._mounts.append((source, target)) + logger.debug("node(%s) mounting: %s at %s", self.name, src_path, target_path) + self.cmd(f"mkdir -p {target_path}") + self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}") + self._mounts.append((src_path, target_path)) def next_iface_id(self) -> int: """ @@ -675,64 +712,28 @@ class CoreNode(CoreNodeBase): with self.lock: return super().next_iface_id() - def newveth(self, iface_id: int = None, ifname: str = None) -> int: + def newveth(self, iface_id: int = None, ifname: str = None, mtu: int = None) -> int: """ Create a new interface. :param iface_id: id for the new interface :param ifname: name for the new interface + :param mtu: mtu for interface :return: nothing """ with self.lock: - if iface_id is None: - iface_id = self.next_iface_id() - - if ifname is None: - ifname = f"eth{iface_id}" - + mtu = mtu if mtu is not None else DEFAULT_MTU + iface_id = iface_id if iface_id is not None else self.next_iface_id() + ifname = ifname if ifname is not None else f"eth{iface_id}" sessionid = self.session.short_session_id() - try: suffix = f"{self.id:x}.{iface_id}.{sessionid}" except TypeError: suffix = f"{self.id}.{iface_id}.{sessionid}" - localname = f"veth{suffix}" - if len(localname) >= 16: - raise ValueError(f"interface local name ({localname}) too long") - - name = localname + "p" - if len(name) >= 16: - raise ValueError(f"interface name ({name}) too long") - - veth = Veth( - self.session, self, name, localname, start=self.up, server=self.server - ) - - if self.up: - self.net_client.device_ns(veth.name, str(self.pid)) - self.node_net_client.device_name(veth.name, ifname) - self.node_net_client.checksums_off(ifname) - - veth.name = ifname - - if self.up: - flow_id = self.node_net_client.get_ifindex(veth.name) - veth.flow_id = int(flow_id) - logging.debug("interface flow index: %s - %s", veth.name, veth.flow_id) - mac = self.node_net_client.get_mac(veth.name) - logging.debug("interface mac: %s - %s", veth.name, mac) - veth.set_mac(mac) - - try: - # add network interface to the node. If unsuccessful, destroy the - # network interface and raise exception. - self.add_iface(veth, iface_id) - except ValueError as e: - veth.shutdown() - del veth - raise e - + name = f"{localname}p" + veth = Veth(self.session, name, localname, mtu, self.server, self) + veth.adopt_node(iface_id, ifname, self.up) return iface_id def newtuntap(self, iface_id: int = None, ifname: str = None) -> int: @@ -744,24 +745,19 @@ class CoreNode(CoreNodeBase): :return: interface index """ with self.lock: - if iface_id is None: - iface_id = self.next_iface_id() - - if ifname is None: - ifname = f"eth{iface_id}" - + iface_id = iface_id if iface_id is not None else self.next_iface_id() + ifname = ifname if ifname is not None else f"eth{iface_id}" sessionid = self.session.short_session_id() localname = f"tap{self.id}.{iface_id}.{sessionid}" name = ifname - tuntap = TunTap(self.session, self, name, localname, start=self.up) - + tuntap = TunTap(self.session, name, localname, node=self) + if self.up: + tuntap.startup() try: self.add_iface(tuntap, iface_id) - except ValueError as e: + except CoreError as e: tuntap.shutdown() - del tuntap raise e - return iface_id def set_mac(self, iface_id: int, mac: str) -> None: @@ -842,7 +838,7 @@ class CoreNode(CoreNodeBase): raise CoreError( f"node({self.name}) already has interface({iface_id})" ) - iface_id = self.newveth(iface_id, iface_data.name) + iface_id = self.newveth(iface_id, iface_data.name, iface_data.mtu) self.attachnet(iface_id, net) if iface_data.mac: self.set_mac(iface_id, iface_data.mac) @@ -851,86 +847,99 @@ class CoreNode(CoreNodeBase): self.ifup(iface_id) return self.get_iface(iface_id) - def addfile(self, srcname: str, filename: str) -> None: + def addfile(self, src_path: Path, file_path: Path) -> None: """ Add a file. - :param srcname: source file name - :param filename: file name to add + :param src_path: source file path + :param file_path: file name to add :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ - logging.info("adding file from %s to %s", srcname, filename) - directory = os.path.dirname(filename) + logger.info("adding file from %s to %s", src_path, file_path) + directory = file_path.parent if self.server is None: self.client.check_cmd(f"mkdir -p {directory}") - self.client.check_cmd(f"mv {srcname} {filename}") + self.client.check_cmd(f"mv {src_path} {file_path}") self.client.check_cmd("sync") else: self.host_cmd(f"mkdir -p {directory}") - self.server.remote_put(srcname, filename) + self.server.remote_put(src_path, file_path) - def hostfilename(self, filename: str) -> str: + def _find_parent_path(self, path: Path) -> Optional[Path]: """ - Return the name of a node"s file on the host filesystem. + Check if there is an existing mounted parent directory created for this node. - :param filename: host file name - :return: path to file + :param path: existing parent path to use + :return: exist parent path if exists, None otherwise """ - dirname, basename = os.path.split(filename) - if not basename: - raise ValueError(f"no basename for filename: {filename}") - if dirname and dirname[0] == "/": - dirname = dirname[1:] - dirname = dirname.replace("/", ".") - dirname = os.path.join(self.nodedir, dirname) - return os.path.join(dirname, basename) + logger.debug("looking for existing parent: %s", path) + existing_path = None + for parent in path.parents: + node_path = self.host_path(parent, is_dir=True) + if node_path == self.directory: + break + if self.path_exists(str(node_path)): + relative_path = path.relative_to(parent) + existing_path = node_path / relative_path + break + return existing_path - def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: + def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None: """ - Create a node file with a given mode. + Create file within a node at the given path, using contents and mode. - :param filename: name of file to create + :param file_path: desired path for file :param contents: contents of file - :param mode: mode for file + :param mode: mode to create file with :return: nothing """ - hostfilename = self.hostfilename(filename) - dirname, _basename = os.path.split(hostfilename) - if self.server is None: - if not os.path.isdir(dirname): - os.makedirs(dirname, mode=0o755) - with open(hostfilename, "w") as open_file: - open_file.write(contents) - os.chmod(open_file.name, mode) + logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode) + host_path = self._find_parent_path(file_path) + if host_path: + self.host_cmd(f"mkdir -p {host_path.parent}") else: - self.host_cmd(f"mkdir -m {0o755:o} -p {dirname}") - self.server.remote_put_temp(hostfilename, contents) - self.host_cmd(f"chmod {mode:o} {hostfilename}") - logging.debug( - "node(%s) added file: %s; mode: 0%o", self.name, hostfilename, mode + host_path = self.host_path(file_path) + directory = host_path.parent + if self.server is None: + if not directory.exists(): + directory.mkdir(parents=True, mode=0o755) + with host_path.open("w") as f: + f.write(contents) + host_path.chmod(mode) + else: + self.host_cmd(f"mkdir -m {0o755:o} -p {directory}") + self.server.remote_put_temp(host_path, contents) + self.host_cmd(f"chmod {mode:o} {host_path}") + + def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None: + """ + Copy source file to node host destination, updating the file mode when + provided. + + :param src_path: source file to copy + :param dst_path: node host destination + :param mode: file mode + :return: nothing + """ + logger.debug( + "node(%s) copying file src(%s) to dst(%s) mode(%o)", + self.name, + src_path, + dst_path, + mode or 0, ) - - def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None: - """ - Copy a file to a node, following symlinks and preserving metadata. - Change file mode if specified. - - :param filename: file name to copy file to - :param srcfilename: file to copy - :param mode: mode to copy to - :return: nothing - """ - hostfilename = self.hostfilename(filename) - if self.server is None: - shutil.copy2(srcfilename, hostfilename) + host_path = self._find_parent_path(dst_path) + if host_path: + self.host_cmd(f"mkdir -p {host_path.parent}") else: - self.server.remote_put(srcfilename, hostfilename) + host_path = self.host_path(dst_path) + if self.server is None: + shutil.copy2(src_path, host_path) + else: + self.server.remote_put(src_path, host_path) if mode is not None: - self.host_cmd(f"chmod {mode:o} {hostfilename}") - logging.info( - "node(%s) copied file: %s; mode: %s", self.name, hostfilename, mode - ) + self.host_cmd(f"chmod {mode:o} {host_path}") class CoreNetworkBase(NodeBase): @@ -951,16 +960,17 @@ class CoreNetworkBase(NodeBase): """ Create a CoreNetworkBase instance. - :param session: CORE session object + :param session: session object :param _id: object id :param name: object name :param server: remote server node will run on, default is None for localhost """ super().__init__(session, _id, name, server) - self.brname = None - self._linked = {} - self._linked_lock = threading.Lock() + self.mtu: int = DEFAULT_MTU + self.brname: Optional[str] = None + self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {} + self.linked_lock: threading.Lock = threading.Lock() @abc.abstractmethod def startup(self) -> None: @@ -1029,8 +1039,8 @@ class CoreNetworkBase(NodeBase): i = self.next_iface_id() self.ifaces[i] = iface iface.net_id = i - with self._linked_lock: - self._linked[iface] = {} + with self.linked_lock: + self.linked[iface] = {} def detach(self, iface: CoreInterface) -> None: """ @@ -1041,8 +1051,8 @@ class CoreNetworkBase(NodeBase): """ del self.ifaces[iface.net_id] iface.net_id = None - with self._linked_lock: - del self._linked[iface] + with self.linked_lock: + del self.linked[iface] def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ diff --git a/daemon/core/nodes/client.py b/daemon/core/nodes/client.py index 710724b1..c3afb907 100644 --- a/daemon/core/nodes/client.py +++ b/daemon/core/nodes/client.py @@ -3,6 +3,7 @@ client.py: implementation of the VnodeClient class for issuing commands over a control channel to the vnoded process running in a network namespace. The control channel can be accessed via calls using the vcmd shell. """ +from pathlib import Path from core import utils from core.executables import BASH, VCMD @@ -13,7 +14,7 @@ class VnodeClient: Provides client functionality for interacting with a virtual node. """ - def __init__(self, name: str, ctrlchnlname: str) -> None: + def __init__(self, name: str, ctrlchnlname: Path) -> None: """ Create a VnodeClient instance. @@ -21,7 +22,7 @@ class VnodeClient: :param ctrlchnlname: control channel name """ self.name: str = name - self.ctrlchnlname: str = ctrlchnlname + self.ctrlchnlname: Path = ctrlchnlname def _verify_connection(self) -> None: """ diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index ce34bd98..6dca41e1 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -1,6 +1,6 @@ import json import logging -import os +from pathlib import Path from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING, Callable, Dict, Optional @@ -11,6 +11,8 @@ from core.errors import CoreCommandError from core.nodes.base import CoreNode from core.nodes.netclient import LinuxNetClient, get_net_client +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.emulator.session import Session @@ -50,7 +52,7 @@ class DockerClient: self.run(f"docker rm -f {self.name}") def check_cmd(self, cmd: str, wait: bool = True, shell: bool = False) -> str: - logging.info("docker cmd output: %s", cmd) + logger.info("docker cmd output: %s", cmd) return utils.cmd(f"docker exec {self.name} {cmd}", wait=wait, shell=shell) def create_ns_cmd(self, cmd: str) -> str: @@ -60,11 +62,11 @@ class DockerClient: args = f"docker inspect -f '{{{{.State.Pid}}}}' {self.name}" output = self.run(args) self.pid = output - logging.debug("node(%s) pid: %s", self.name, self.pid) + logger.debug("node(%s) pid: %s", self.name, self.pid) return output - def copy_file(self, source: str, destination: str) -> str: - args = f"docker cp {source} {self.name}:{destination}" + def copy_file(self, src_path: Path, dst_path: Path) -> str: + args = f"docker cp {src_path} {self.name}:{dst_path}" return self.run(args) @@ -76,7 +78,7 @@ class DockerNode(CoreNode): session: "Session", _id: int = None, name: str = None, - nodedir: str = None, + directory: str = None, server: DistributedServer = None, image: str = None, ) -> None: @@ -86,7 +88,7 @@ class DockerNode(CoreNode): :param session: core session instance :param _id: object id :param name: object name - :param nodedir: node directory + :param directory: node directory :param server: remote server node will run on, default is None for localhost :param image: image to start container with @@ -94,7 +96,7 @@ class DockerNode(CoreNode): if image is None: image = "ubuntu" self.image: str = image - super().__init__(session, _id, name, nodedir, server) + super().__init__(session, _id, name, directory, server) def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: """ @@ -162,77 +164,73 @@ class DockerNode(CoreNode): """ return f"docker exec -it {self.name} bash" - def privatedir(self, path: str) -> None: + def create_dir(self, dir_path: Path) -> None: """ Create a private directory. - :param path: path to create + :param dir_path: path to create :return: nothing """ - logging.debug("creating node dir: %s", path) - args = f"mkdir -p {path}" + logger.debug("creating node dir: %s", dir_path) + args = f"mkdir -p {dir_path}" self.cmd(args) - def mount(self, source: str, target: str) -> None: + def mount(self, src_path: str, target_path: str) -> None: """ Create and mount a directory. - :param source: source directory to mount - :param target: target directory to create + :param src_path: source directory to mount + :param target_path: target directory to create :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ - logging.debug("mounting source(%s) target(%s)", source, target) + logger.debug("mounting source(%s) target(%s)", src_path, target_path) raise Exception("not supported") - def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: + def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None: """ Create a node file with a given mode. - :param filename: name of file to create + :param file_path: name of file to create :param contents: contents of file :param mode: mode for file :return: nothing """ - logging.debug("nodefile filename(%s) mode(%s)", filename, mode) - directory = os.path.dirname(filename) + logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode) temp = NamedTemporaryFile(delete=False) temp.write(contents.encode("utf-8")) temp.close() - - if directory: + temp_path = Path(temp.name) + directory = file_path.name + if str(directory) != ".": self.cmd(f"mkdir -m {0o755:o} -p {directory}") if self.server is not None: - self.server.remote_put(temp.name, temp.name) - self.client.copy_file(temp.name, filename) - self.cmd(f"chmod {mode:o} {filename}") + self.server.remote_put(temp_path, temp_path) + self.client.copy_file(temp_path, file_path) + self.cmd(f"chmod {mode:o} {file_path}") if self.server is not None: - self.host_cmd(f"rm -f {temp.name}") - os.unlink(temp.name) - logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode) + self.host_cmd(f"rm -f {temp_path}") + temp_path.unlink() - def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None: + def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None: """ Copy a file to a node, following symlinks and preserving metadata. Change file mode if specified. - :param filename: file name to copy file to - :param srcfilename: file to copy + :param dst_path: file name to copy file to + :param src_path: file to copy :param mode: mode to copy to :return: nothing """ - logging.info( - "node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode + logger.info( + "node file copy file(%s) source(%s) mode(%o)", dst_path, src_path, mode or 0 ) - directory = os.path.dirname(filename) - self.cmd(f"mkdir -p {directory}") - - if self.server is None: - source = srcfilename - else: + self.cmd(f"mkdir -p {dst_path.parent}") + if self.server: temp = NamedTemporaryFile(delete=False) - source = temp.name - self.server.remote_put(source, temp.name) - - self.client.copy_file(source, filename) - self.cmd(f"chmod {mode:o} {filename}") + temp_path = Path(temp.name) + src_path = temp_path + self.server.remote_put(src_path, temp_path) + self.client.copy_file(src_path, dst_path) + if mode is not None: + self.cmd(f"chmod {mode:o} {dst_path}") diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 32d47af2..7fda18c7 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -4,6 +4,7 @@ virtual ethernet classes that implement the interfaces available under Linux. import logging import time +from pathlib import Path from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple import netaddr @@ -14,6 +15,8 @@ from core.emulator.enumerations import TransportType from core.errors import CoreCommandError, CoreError from core.nodes.netclient import LinuxNetClient, get_net_client +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.emulator.distributed import DistributedServer from core.emulator.session import Session @@ -30,25 +33,28 @@ class CoreInterface: def __init__( self, session: "Session", - node: "CoreNode", name: str, localname: str, - mtu: int, + mtu: int = DEFAULT_MTU, server: "DistributedServer" = None, + node: "CoreNode" = None, ) -> None: """ Creates a CoreInterface instance. :param session: core session instance - :param node: node for interface :param name: interface name :param localname: interface local name :param mtu: mtu value - :param server: remote server node - will run on, default is None for localhost + :param server: remote server node will run on, default is None for localhost + :param node: node for interface """ + if len(name) >= 16: + raise CoreError(f"interface name ({name}) too long, max 16") + if len(localname) >= 16: + raise CoreError(f"interface local name ({localname}) too long, max 16") self.session: "Session" = session - self.node: "CoreNode" = node + self.node: Optional["CoreNode"] = node self.name: str = name self.localname: str = localname self.up: bool = False @@ -79,7 +85,7 @@ class CoreInterface: self, args: str, env: Dict[str, str] = None, - cwd: str = None, + cwd: Path = None, wait: bool = True, shell: bool = False, ) -> str: @@ -125,7 +131,6 @@ class CoreInterface: if self.net: self.detachnet() self.net = None - net.attach(self) self.net = net @@ -273,14 +278,12 @@ class CoreInterface: :return: True if parameter changed, False otherwise """ # treat None and 0 as unchanged values - logging.debug("setting param: %s - %s", key, value) + logger.debug("setting param: %s - %s", key, value) if value is None or value < 0: return False - current_value = self._params.get(key) if current_value is not None and current_value == value: return False - self._params[key] = value return True @@ -339,33 +342,32 @@ class Veth(CoreInterface): Provides virtual ethernet functionality for core nodes. """ - def __init__( - self, - session: "Session", - node: "CoreNode", - name: str, - localname: str, - mtu: int = DEFAULT_MTU, - server: "DistributedServer" = None, - start: bool = True, - ) -> None: + def adopt_node(self, iface_id: int, name: str, start: bool) -> None: """ - Creates a VEth instance. + Adopt this interface to the provided node, configuring and associating + with the node as needed. - :param session: core session instance - :param node: related core node - :param name: interface name - :param localname: interface local name - :param mtu: interface mtu - :param server: remote server node - will run on, default is None for localhost - :param start: start flag - :raises CoreCommandError: when there is a command exception + :param iface_id: interface id for node + :param name: name of interface fo rnode + :param start: True to start interface, False otherwise + :return: nothing """ - # note that net arg is ignored - super().__init__(session, node, name, localname, mtu, server) if start: self.startup() + self.net_client.device_ns(self.name, str(self.node.pid)) + self.node.node_net_client.checksums_off(self.name) + self.flow_id = self.node.node_net_client.get_ifindex(self.name) + logger.debug("interface flow index: %s - %s", self.name, self.flow_id) + mac = self.node.node_net_client.get_mac(self.name) + logger.debug("interface mac: %s - %s", self.name, mac) + self.set_mac(mac) + self.node.node_net_client.device_name(self.name, name) + self.name = name + try: + self.node.add_iface(self, iface_id) + except CoreError as e: + self.shutdown() + raise e def startup(self) -> None: """ @@ -375,6 +377,9 @@ class Veth(CoreInterface): :raises CoreCommandError: when there is a command exception """ self.net_client.create_veth(self.localname, self.name) + if self.mtu > 0: + self.net_client.set_mtu(self.name, self.mtu) + self.net_client.set_mtu(self.localname, self.mtu) self.net_client.device_up(self.localname) self.up = True @@ -404,32 +409,6 @@ class TunTap(CoreInterface): TUN/TAP virtual device in TAP mode """ - def __init__( - self, - session: "Session", - node: "CoreNode", - name: str, - localname: str, - mtu: int = DEFAULT_MTU, - server: "DistributedServer" = None, - start: bool = True, - ) -> None: - """ - Create a TunTap instance. - - :param session: core session instance - :param node: related core node - :param name: interface name - :param localname: local interface name - :param mtu: interface mtu - :param server: remote server node - will run on, default is None for localhost - :param start: start flag - """ - super().__init__(session, node, name, localname, mtu, server) - if start: - self.startup() - def startup(self) -> None: """ Startup logic for a tunnel tap. @@ -452,12 +431,10 @@ class TunTap(CoreInterface): """ if not self.up: return - try: self.node.node_net_client.device_flush(self.name) except CoreCommandError: - logging.exception("error shutting down tunnel tap") - + logger.exception("error shutting down tunnel tap") self.up = False def waitfor( @@ -481,14 +458,14 @@ class TunTap(CoreInterface): msg = f"attempt {i} failed with nonzero exit status {r}" if i < attempts + 1: msg += ", retrying..." - logging.info(msg) + logger.info(msg) time.sleep(delay) delay += delay if delay > maxretrydelay: delay = maxretrydelay else: msg += ", giving up" - logging.info(msg) + logger.info(msg) return result @@ -499,7 +476,7 @@ class TunTap(CoreInterface): :return: wait for device local response """ - logging.debug("waiting for device local: %s", self.localname) + logger.debug("waiting for device local: %s", self.localname) def localdevexists(): try: @@ -516,7 +493,7 @@ class TunTap(CoreInterface): :return: nothing """ - logging.debug("waiting for device node: %s", self.name) + logger.debug("waiting for device node: %s", self.name) def nodedevexists(): try: @@ -578,47 +555,55 @@ class GreTap(CoreInterface): def __init__( self, + session: "Session", + remoteip: str, + key: int = None, node: "CoreNode" = None, - name: str = None, - session: "Session" = None, - mtu: int = 1458, - remoteip: str = None, + mtu: int = DEFAULT_MTU, _id: int = None, localip: str = None, ttl: int = 255, - key: int = None, - start: bool = True, server: "DistributedServer" = None, ) -> None: """ Creates a GreTap instance. - :param node: related core node - :param name: interface name :param session: core session instance - :param mtu: interface mtu :param remoteip: remote address + :param key: gre tap key + :param node: related core node + :param mtu: interface mtu :param _id: object id :param localip: local address :param ttl: ttl value - :param key: gre tap key - :param start: start flag :param server: remote server node will run on, default is None for localhost :raises CoreCommandError: when there is a command exception """ if _id is None: _id = ((id(self) >> 16) ^ (id(self) & 0xFFFF)) & 0xFFFF - self.id = _id + self.id: int = _id sessionid = session.short_session_id() localname = f"gt.{self.id}.{sessionid}" - super().__init__(session, node, name, localname, mtu, server) - self.transport_type = TransportType.RAW - if not start: - return - if remoteip is None: - raise CoreError("missing remote IP required for GRE TAP device") - self.net_client.create_gretap(self.localname, remoteip, localip, ttl, key) + name = f"{localname}p" + super().__init__(session, name, localname, mtu, server, node) + self.transport_type: TransportType = TransportType.RAW + self.remote_ip: str = remoteip + self.ttl: int = ttl + self.key: Optional[int] = key + self.local_ip: Optional[str] = localip + + def startup(self) -> None: + """ + Startup logic for a GreTap. + + :return: nothing + """ + self.net_client.create_gretap( + self.localname, self.remote_ip, self.local_ip, self.ttl, self.key + ) + if self.mtu > 0: + self.net_client.set_mtu(self.localname, self.mtu) self.net_client.device_up(self.localname) self.up = True @@ -633,5 +618,5 @@ class GreTap(CoreInterface): self.net_client.device_down(self.localname) self.net_client.delete_device(self.localname) except CoreCommandError: - logging.exception("error during shutdown") + logger.exception("error during shutdown") self.localname = None diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 9773cb95..54fc8341 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -1,7 +1,7 @@ import json import logging -import os import time +from pathlib import Path from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING, Callable, Dict, Optional @@ -12,6 +12,8 @@ from core.errors import CoreCommandError from core.nodes.base import CoreNode from core.nodes.interface import CoreInterface +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.emulator.session import Session @@ -57,11 +59,10 @@ class LxdClient: args = self.create_cmd(cmd) return utils.cmd(args, wait=wait, shell=shell) - def copy_file(self, source: str, destination: str) -> None: - if destination[0] != "/": - destination = os.path.join("/root/", destination) - - args = f"lxc file push {source} {self.name}/{destination}" + def copy_file(self, src_path: Path, dst_path: Path) -> None: + if not str(dst_path).startswith("/"): + dst_path = Path("/root/") / dst_path + args = f"lxc file push {src_path} {self.name}/{dst_path}" self.run(args) @@ -73,7 +74,7 @@ class LxcNode(CoreNode): session: "Session", _id: int = None, name: str = None, - nodedir: str = None, + directory: str = None, server: DistributedServer = None, image: str = None, ) -> None: @@ -83,7 +84,7 @@ class LxcNode(CoreNode): :param session: core session instance :param _id: object id :param name: object name - :param nodedir: node directory + :param directory: node directory :param server: remote server node will run on, default is None for localhost :param image: image to start container with @@ -91,7 +92,7 @@ class LxcNode(CoreNode): if image is None: image = "ubuntu" self.image: str = image - super().__init__(session, _id, name, nodedir, server) + super().__init__(session, _id, name, directory, server) def alive(self) -> bool: """ @@ -139,81 +140,77 @@ class LxcNode(CoreNode): """ return f"lxc exec {self.name} -- {sh}" - def privatedir(self, path: str) -> None: + def create_dir(self, dir_path: Path) -> None: """ Create a private directory. - :param path: path to create + :param dir_path: path to create :return: nothing """ - logging.info("creating node dir: %s", path) - args = f"mkdir -p {path}" + logger.info("creating node dir: %s", dir_path) + args = f"mkdir -p {dir_path}" self.cmd(args) - def mount(self, source: str, target: str) -> None: + def mount(self, src_path: Path, target_path: Path) -> None: """ Create and mount a directory. - :param source: source directory to mount - :param target: target directory to create + :param src_path: source directory to mount + :param target_path: target directory to create :return: nothing :raises CoreCommandError: when a non-zero exit status occurs """ - logging.debug("mounting source(%s) target(%s)", source, target) + logger.debug("mounting source(%s) target(%s)", src_path, target_path) raise Exception("not supported") - def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: + def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None: """ Create a node file with a given mode. - :param filename: name of file to create + :param file_path: name of file to create :param contents: contents of file :param mode: mode for file :return: nothing """ - logging.debug("nodefile filename(%s) mode(%s)", filename, mode) - - directory = os.path.dirname(filename) + logger.debug("node(%s) create file(%s) mode(%o)", self.name, file_path, mode) temp = NamedTemporaryFile(delete=False) temp.write(contents.encode("utf-8")) temp.close() - - if directory: + temp_path = Path(temp.name) + directory = file_path.parent + if str(directory) != ".": self.cmd(f"mkdir -m {0o755:o} -p {directory}") if self.server is not None: - self.server.remote_put(temp.name, temp.name) - self.client.copy_file(temp.name, filename) - self.cmd(f"chmod {mode:o} {filename}") + self.server.remote_put(temp_path, temp_path) + self.client.copy_file(temp_path, file_path) + self.cmd(f"chmod {mode:o} {file_path}") if self.server is not None: - self.host_cmd(f"rm -f {temp.name}") - os.unlink(temp.name) - logging.debug("node(%s) added file: %s; mode: 0%o", self.name, filename, mode) + self.host_cmd(f"rm -f {temp_path}") + temp_path.unlink() + logger.debug("node(%s) added file: %s; mode: 0%o", self.name, file_path, mode) - def nodefilecopy(self, filename: str, srcfilename: str, mode: int = None) -> None: + def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None: """ Copy a file to a node, following symlinks and preserving metadata. Change file mode if specified. - :param filename: file name to copy file to - :param srcfilename: file to copy + :param dst_path: file name to copy file to + :param src_path: file to copy :param mode: mode to copy to :return: nothing """ - logging.info( - "node file copy file(%s) source(%s) mode(%s)", filename, srcfilename, mode + logger.info( + "node file copy file(%s) source(%s) mode(%o)", dst_path, src_path, mode or 0 ) - directory = os.path.dirname(filename) - self.cmd(f"mkdir -p {directory}") - - if self.server is None: - source = srcfilename - else: + self.cmd(f"mkdir -p {dst_path.parent}") + if self.server: temp = NamedTemporaryFile(delete=False) - source = temp.name - self.server.remote_put(source, temp.name) - - self.client.copy_file(source, filename) - self.cmd(f"chmod {mode:o} {filename}") + temp_path = Path(temp.name) + src_path = temp_path + self.server.remote_put(src_path, temp_path) + self.client.copy_file(src_path, dst_path) + if mode is not None: + self.cmd(f"chmod {mode:o} {dst_path}") def add_iface(self, iface: CoreInterface, iface_id: int) -> None: super().add_iface(iface, iface_id) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 729550b6..09cf94ec 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: """ @@ -95,14 +95,14 @@ class LinuxNetClient: """ return self.run(f"cat /sys/class/net/{device}/address") - def get_ifindex(self, device: str) -> str: + def get_ifindex(self, device: str) -> int: """ Retrieve ifindex for a given device. :param device: device to get ifindex for :return: ifindex """ - return self.run(f"cat /sys/class/net/{device}/ifindex") + return int(self.run(f"cat /sys/class/net/{device}/ifindex")) def device_ns(self, device: str, namespace: str) -> None: """ @@ -296,6 +296,16 @@ class LinuxNetClient: """ self.run(f"{IP} link set {name} type bridge ageing_time {value}") + def set_mtu(self, name: str, value: int) -> None: + """ + Sets the mtu value for a device. + + :param name: name of device to set value for + :param value: mtu value to set + :return: nothing + """ + self.run(f"{IP} link set {name} mtu {value}") + class OvsNetClient(LinuxNetClient): """ @@ -361,14 +371,15 @@ class OvsNetClient(LinuxNetClient): return True return False - def disable_mac_learning(self, name: str) -> None: + def set_mac_learning(self, name: str, value: int) -> None: """ - Disable mac learning for a OVS bridge. + Set mac learning for an OVS bridge. :param name: bridge name + :param value: ageing time value :return: nothing """ - self.run(f"{OVS_VSCTL} set bridge {name} other_config:mac-aging-time=0") + self.run(f"{OVS_VSCTL} set bridge {name} other_config:mac-aging-time={value}") def get_net_client(use_ovs: bool, run: Callable[..., str]) -> LinuxNetClient: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index cb3aca79..34f0f878 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -6,7 +6,10 @@ import logging import math import threading import time -from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Type +from collections import OrderedDict +from pathlib import Path +from queue import Queue +from typing import TYPE_CHECKING, Dict, List, Optional, Type import netaddr @@ -20,11 +23,13 @@ from core.emulator.enumerations import ( RegisterTlvs, ) from core.errors import CoreCommandError, CoreError -from core.executables import EBTABLES, TC +from core.executables import NFTABLES, TC from core.nodes.base import CoreNetworkBase from core.nodes.interface import CoreInterface, GreTap, Veth from core.nodes.netclient import get_net_client +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.emulator.distributed import DistributedServer from core.emulator.session import Session @@ -33,224 +38,194 @@ if TYPE_CHECKING: WirelessModelType = Type[WirelessModel] LEARNING_DISABLED: int = 0 -ebtables_lock: threading.Lock = threading.Lock() -class EbtablesQueue: +class SetQueue(Queue): """ - Helper class for queuing up ebtables commands into rate-limited + Set backed queue to avoid duplicate submissions. + """ + + def _init(self, maxsize): + self.queue: OrderedDict = OrderedDict() + + def _put(self, item): + self.queue[item] = None + + def _get(self): + key, _ = self.queue.popitem(last=False) + return key + + +class NftablesQueue: + """ + Helper class for queuing up nftables commands into rate-limited atomic commits. This improves performance and reliability when there are many WLAN link updates. """ # update rate is every 300ms rate: float = 0.3 - # ebtables - atomic_file: str = "/tmp/pycore.ebtables.atomic" + atomic_file: str = "/tmp/pycore.nftables.atomic" + chain: str = "forward" def __init__(self) -> None: """ Initialize the helper class, but don't start the update thread until a WLAN is instantiated. """ - self.doupdateloop: bool = False - self.updatethread: Optional[threading.Thread] = None + self.running: bool = False + self.run_thread: Optional[threading.Thread] = None # this lock protects cmds and updates lists - self.updatelock: threading.Lock = threading.Lock() - # list of pending ebtables commands + self.lock: threading.Lock = threading.Lock() + # list of pending nftables commands self.cmds: List[str] = [] # list of WLANs requiring update - self.updates: List["CoreNetwork"] = [] + self.updates: SetQueue = SetQueue() # timestamps of last WLAN update; this keeps track of WLANs that are # using this queue self.last_update_time: Dict["CoreNetwork", float] = {} - def startupdateloop(self, wlan: "CoreNetwork") -> None: + def start(self, net: "CoreNetwork") -> None: """ - Kick off the update loop; only needs to be invoked once. - + Start thread to listen for updates for the provided network. + :param net: network to start checking updates :return: nothing """ - with self.updatelock: - self.last_update_time[wlan] = time.monotonic() - if self.doupdateloop: + with self.lock: + self.last_update_time[net] = time.monotonic() + if self.running: return - self.doupdateloop = True - self.updatethread = threading.Thread(target=self.updateloop, daemon=True) - self.updatethread.start() + self.running = True + self.run_thread = threading.Thread(target=self.run, daemon=True) + self.run_thread.start() - def stopupdateloop(self, wlan: "CoreNetwork") -> None: + def stop(self, net: "CoreNetwork") -> None: """ - Kill the update loop thread if there are no more WLANs using it. + Stop updates for network, when no networks remain, stop update thread. + :param net: network to stop watching updates + :return: nothing + """ + with self.lock: + self.last_update_time.pop(net, None) + if self.last_update_time: + return + self.running = False + if self.run_thread: + self.updates.put(None) + self.run_thread.join() + self.run_thread = None + + def last_update(self, net: "CoreNetwork") -> float: + """ + Return the time elapsed since this network was last updated. + :param net: network node + :return: elapsed time + """ + now = time.monotonic() + last_update = self.last_update_time.setdefault(net, now) + return now - last_update + + def run(self) -> None: + """ + Thread target that looks for networks needing update, and + rate limits the amount of nftables activity. Only one userspace program + should use nftables at any given time, or results can be unpredictable. :return: nothing """ - with self.updatelock: - try: - del self.last_update_time[wlan] - except KeyError: - logging.exception( - "error deleting last update time for wlan, ignored before: %s", wlan - ) - if len(self.last_update_time) > 0: + while self.running: + net = self.updates.get() + if net is None: + break + if not net.up: + self.last_update_time[net] = time.monotonic() + elif self.last_update(net) > self.rate: + with self.lock: + self.build_cmds(net) + self.commit(net) + self.last_update_time[net] = time.monotonic() + + def commit(self, net: "CoreNetwork") -> None: + """ + Commit changes to nftables for the provided network. + :param net: network to commit nftables changes + :return: nothing + """ + if not self.cmds: return - self.doupdateloop = False - if self.updatethread: - self.updatethread.join() - self.updatethread = None + # write out nft commands to file + for cmd in self.cmds: + net.host_cmd(f"echo {cmd} >> {self.atomic_file}", shell=True) + # read file as atomic change + net.host_cmd(f"{NFTABLES} -f {self.atomic_file}") + # remove file + net.host_cmd(f"rm -f {self.atomic_file}") + self.cmds.clear() - def ebatomiccmd(self, cmd: str) -> str: + def update(self, net: "CoreNetwork") -> None: """ - Helper for building ebtables atomic file command list. - - :param cmd: ebtable command - :return: ebtable atomic command - """ - return f"{EBTABLES} --atomic-file {self.atomic_file} {cmd}" - - def lastupdate(self, wlan: "CoreNetwork") -> float: - """ - Return the time elapsed since this WLAN was last updated. - - :param wlan: wlan entity - :return: elpased time - """ - try: - elapsed = time.monotonic() - self.last_update_time[wlan] - except KeyError: - self.last_update_time[wlan] = time.monotonic() - elapsed = 0.0 - - return elapsed - - def updated(self, wlan: "CoreNetwork") -> None: - """ - Keep track of when this WLAN was last updated. - - :param wlan: wlan entity + Flag this network has an update, so the nftables chain will be rebuilt. + :param net: wlan network :return: nothing """ - self.last_update_time[wlan] = time.monotonic() - self.updates.remove(wlan) + self.updates.put(net) - def updateloop(self) -> None: + def delete_table(self, net: "CoreNetwork") -> None: """ - Thread target that looks for WLANs needing update, and - rate limits the amount of ebtables activity. Only one userspace program - should use ebtables at any given time, or results can be unpredictable. + Delete nftable bridge rule table. + :param net: network to delete table for :return: nothing """ - while self.doupdateloop: - with self.updatelock: - for wlan in self.updates: - # Check if wlan is from a previously closed session. Because of the - # rate limiting scheme employed here, this may happen if a new session - # is started soon after closing a previous session. - # TODO: if these are WlanNodes, this will never throw an exception - try: - wlan.session - except Exception: - # Just mark as updated to remove from self.updates. - self.updated(wlan) - continue + with self.lock: + net.host_cmd(f"{NFTABLES} delete table bridge {net.brname}") - if self.lastupdate(wlan) > self.rate: - self.buildcmds(wlan) - self.ebcommit(wlan) - self.updated(wlan) - - time.sleep(self.rate) - - def ebcommit(self, wlan: "CoreNetwork") -> None: + def build_cmds(self, net: "CoreNetwork") -> None: """ - Perform ebtables atomic commit using commands built in the self.cmds list. - + Inspect linked nodes for a network, and rebuild the nftables chain commands. + :param net: network to build commands for :return: nothing """ - # save kernel ebtables snapshot to a file - args = self.ebatomiccmd("--atomic-save") - wlan.host_cmd(args) - - # modify the table file using queued ebtables commands - for c in self.cmds: - args = self.ebatomiccmd(c) - wlan.host_cmd(args) - self.cmds = [] - - # commit the table file to the kernel - args = self.ebatomiccmd("--atomic-commit") - wlan.host_cmd(args) - - try: - wlan.host_cmd(f"rm -f {self.atomic_file}") - except CoreCommandError: - logging.exception("error removing atomic file: %s", self.atomic_file) - - def ebchange(self, wlan: "CoreNetwork") -> None: - """ - Flag a change to the given WLAN's _linked dict, so the ebtables - chain will be rebuilt at the next interval. - - :return: nothing - """ - with self.updatelock: - if wlan not in self.updates: - self.updates.append(wlan) - - def buildcmds(self, wlan: "CoreNetwork") -> None: - """ - Inspect a _linked dict from a wlan, and rebuild the ebtables chain for that WLAN. - - :return: nothing - """ - with wlan._linked_lock: - if wlan.has_ebtables_chain: - # flush the chain - self.cmds.append(f"-F {wlan.brname}") + with net.linked_lock: + if net.has_nftables_chain: + self.cmds.append(f"flush table bridge {net.brname}") else: - wlan.has_ebtables_chain = True - self.cmds.extend( - [ - f"-N {wlan.brname} -P {wlan.policy.value}", - f"-A FORWARD --logical-in {wlan.brname} -j {wlan.brname}", - ] + net.has_nftables_chain = True + policy = net.policy.value.lower() + self.cmds.append(f"add table bridge {net.brname}") + self.cmds.append( + f"add chain bridge {net.brname} {self.chain} {{type filter hook " + f"forward priority 0\\; policy {policy}\\;}}" ) + # add default rule to accept all traffic not for this bridge + self.cmds.append( + f"add rule bridge {net.brname} {self.chain} " + f"ibriport != {net.brname} accept" + ) # rebuild the chain - for iface1, v in wlan._linked.items(): - for oface2, linked in v.items(): - if wlan.policy == NetworkPolicy.DROP and linked: - self.cmds.extend( - [ - f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j ACCEPT", - f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j ACCEPT", - ] + for iface1, v in net.linked.items(): + for iface2, linked in v.items(): + policy = None + if net.policy == NetworkPolicy.DROP and linked: + policy = "accept" + elif net.policy == NetworkPolicy.ACCEPT and not linked: + policy = "drop" + if policy: + self.cmds.append( + f"add rule bridge {net.brname} {self.chain} " + f"iif {iface1.localname} oif {iface2.localname} " + f"{policy}" ) - elif wlan.policy == NetworkPolicy.ACCEPT and not linked: - self.cmds.extend( - [ - f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j DROP", - f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j DROP", - ] + self.cmds.append( + f"add rule bridge {net.brname} {self.chain} " + f"oif {iface1.localname} iif {iface2.localname} " + f"{policy}" ) -# a global object because all WLANs share the same queue -# cannot have multiple threads invoking the ebtables commnd -ebq: EbtablesQueue = EbtablesQueue() - - -def ebtablescmds(call: Callable[..., str], cmds: List[str]) -> None: - """ - Run ebtable commands. - - :param call: function to call commands - :param cmds: commands to call - :return: nothing - """ - with ebtables_lock: - for args in cmds: - call(args) +# a global object because all networks share the same queue +# cannot have multiple threads invoking the nftables commnd +nft_queue: NftablesQueue = NftablesQueue() class CoreNetwork(CoreNetworkBase): @@ -282,17 +257,17 @@ class CoreNetwork(CoreNetworkBase): if name is None: name = str(self.id) if policy is not None: - self.policy = policy + self.policy: NetworkPolicy = policy self.name: Optional[str] = name sessionid = self.session.short_session_id() self.brname: str = f"b.{self.id}.{sessionid}" - self.has_ebtables_chain: bool = False + self.has_nftables_chain: bool = False def host_cmd( self, args: str, env: Dict[str, str] = None, - cwd: str = None, + cwd: Path = None, wait: bool = True, shell: bool = False, ) -> str: @@ -308,22 +283,24 @@ class CoreNetwork(CoreNetworkBase): :return: combined stdout and stderr :raises CoreCommandError: when a non-zero exit status occurs """ - logging.debug("network node(%s) cmd", self.name) + logger.debug("network node(%s) cmd", self.name) output = utils.cmd(args, env, cwd, wait, shell) self.session.distributed.execute(lambda x: x.remote_cmd(args, env, cwd, wait)) return output def startup(self) -> None: """ - Linux bridge starup logic. + Linux bridge startup logic. :return: nothing :raises CoreCommandError: when there is a command exception """ self.net_client.create_bridge(self.brname) - self.has_ebtables_chain = False + if self.mtu > 0: + self.net_client.set_mtu(self.brname, self.mtu) + self.has_nftables_chain = False self.up = True - ebq.startupdateloop(self) + nft_queue.start(self) def shutdown(self) -> None: """ @@ -333,27 +310,18 @@ class CoreNetwork(CoreNetworkBase): """ if not self.up: return - - ebq.stopupdateloop(self) - + nft_queue.stop(self) try: self.net_client.delete_bridge(self.brname) - if self.has_ebtables_chain: - cmds = [ - f"{EBTABLES} -D FORWARD --logical-in {self.brname} -j {self.brname}", - f"{EBTABLES} -X {self.brname}", - ] - ebtablescmds(self.host_cmd, cmds) + if self.has_nftables_chain: + nft_queue.delete_table(self) except CoreCommandError: logging.exception("error during shutdown") - # removes veth pairs used for bridge-to-bridge connections for iface in self.get_ifaces(): iface.shutdown() - self.ifaces.clear() - self._linked.clear() - del self.session + self.linked.clear() self.up = False def attach(self, iface: CoreInterface) -> None: @@ -378,7 +346,7 @@ class CoreNetwork(CoreNetworkBase): iface.net_client.delete_iface(self.brname, iface.localname) super().detach(iface) - def linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool: + def is_linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool: """ Determine if the provided network interfaces are linked. @@ -389,12 +357,10 @@ class CoreNetwork(CoreNetworkBase): # check if the network interfaces are attached to this network if self.ifaces[iface1.net_id] != iface1: raise ValueError(f"inconsistency for interface {iface1.name}") - if self.ifaces[iface2.net_id] != iface2: raise ValueError(f"inconsistency for interface {iface2.name}") - try: - linked = self._linked[iface1][iface2] + linked = self.linked[iface1][iface2] except KeyError: if self.policy == NetworkPolicy.ACCEPT: linked = True @@ -402,41 +368,37 @@ class CoreNetwork(CoreNetworkBase): linked = False else: raise Exception(f"unknown policy: {self.policy.value}") - self._linked[iface1][iface2] = linked - + self.linked[iface1][iface2] = linked return linked def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None: """ - Unlink two interfaces, resulting in adding or removing ebtables + Unlink two interfaces, resulting in adding or removing filtering rules. + + :param iface1: interface one + :param iface2: interface two + :return: nothing + """ + with self.linked_lock: + if not self.is_linked(iface1, iface2): + return + self.linked[iface1][iface2] = False + nft_queue.update(self) + + def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None: + """ + Link two interfaces together, resulting in adding or removing filtering rules. :param iface1: interface one :param iface2: interface two :return: nothing """ - with self._linked_lock: - if not self.linked(iface1, iface2): + with self.linked_lock: + if self.is_linked(iface1, iface2): return - self._linked[iface1][iface2] = False - - ebq.ebchange(self) - - def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None: - """ - Link two interfaces together, resulting in adding or removing - ebtables filtering rules. - - :param iface1: interface one - :param iface2: interface two - :return: nothing - """ - with self._linked_lock: - if self.linked(iface1, iface2): - return - self._linked[iface1][iface2] = True - - ebq.ebchange(self) + self.linked[iface1][iface2] = True + nft_queue.update(self) def linkconfig( self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None @@ -522,28 +484,22 @@ class CoreNetwork(CoreNetworkBase): _id = f"{self.id:x}" except TypeError: _id = str(self.id) - try: net_id = f"{net.id:x}" except TypeError: net_id = str(net.id) - localname = f"veth{_id}.{net_id}.{sessionid}" - if len(localname) >= 16: - raise ValueError(f"interface local name {localname} too long") - name = f"veth{net_id}.{_id}.{sessionid}" - if len(name) >= 16: - raise ValueError(f"interface name {name} too long") - - iface = Veth(self.session, None, name, localname, start=self.up) + iface = Veth(self.session, name, localname) + if self.up: + iface.startup() self.attach(iface) if net.up and net.brname: iface.net_client.set_iface_master(net.brname, iface.name) i = net.next_iface_id() net.ifaces[i] = iface - with net._linked_lock: - net._linked[iface] = {} + with net.linked_lock: + net.linked[iface] = {} iface.net = self iface.othernet = net return iface @@ -616,14 +572,15 @@ class GreTapBridge(CoreNetwork): self.localip: Optional[str] = localip self.ttl: int = ttl self.gretap: Optional[GreTap] = None - if remoteip is not None: + if self.remoteip is not None: self.gretap = GreTap( + session, + remoteip, + key=self.grekey, node=self, - session=session, - remoteip=remoteip, localip=localip, ttl=ttl, - key=self.grekey, + mtu=self.mtu, ) def startup(self) -> None: @@ -634,6 +591,7 @@ class GreTapBridge(CoreNetwork): """ super().startup() if self.gretap: + self.gretap.startup() self.attach(self.gretap) def shutdown(self) -> None: @@ -659,18 +617,20 @@ class GreTapBridge(CoreNetwork): :return: nothing """ if self.gretap: - raise ValueError(f"gretap already exists for {self.name}") + raise CoreError(f"gretap already exists for {self.name}") remoteip = ips[0].split("/")[0] localip = None if len(ips) > 1: localip = ips[1].split("/")[0] self.gretap = GreTap( - session=self.session, - remoteip=remoteip, + self.session, + remoteip, + key=self.grekey, localip=localip, ttl=self.ttl, - key=self.grekey, + mtu=self.mtu, ) + self.startup() self.attach(self.gretap) def setkey(self, key: int, iface_data: InterfaceData) -> None: @@ -769,7 +729,7 @@ class CtrlNet(CoreNetwork): raise CoreError(f"old bridges exist for node: {self.id}") super().startup() - logging.info("added control network bridge: %s %s", self.brname, self.prefix) + logger.info("added control network bridge: %s %s", self.brname, self.prefix) if self.hostid and self.assign_address: self.add_addresses(self.hostid) @@ -777,7 +737,7 @@ class CtrlNet(CoreNetwork): self.add_addresses(-2) if self.updown_script: - logging.info( + logger.info( "interface %s updown script (%s startup) called", self.brname, self.updown_script, @@ -797,7 +757,7 @@ class CtrlNet(CoreNetwork): try: self.net_client.delete_iface(self.brname, self.serverintf) except CoreCommandError: - logging.exception( + logger.exception( "error deleting server interface %s from bridge %s", self.serverintf, self.brname, @@ -805,14 +765,14 @@ class CtrlNet(CoreNetwork): if self.updown_script is not None: try: - logging.info( + logger.info( "interface %s updown script (%s shutdown) called", self.brname, self.updown_script, ) self.host_cmd(f"{self.updown_script} {self.brname} shutdown") except CoreCommandError: - logging.exception("error issuing shutdown script shutdown") + logger.exception("error issuing shutdown script shutdown") super().shutdown() @@ -990,7 +950,7 @@ class WlanNode(CoreNetwork): :return: nothing """ super().startup() - ebq.ebchange(self) + nft_queue.update(self) def attach(self, iface: CoreInterface) -> None: """ @@ -1012,7 +972,7 @@ class WlanNode(CoreNetwork): :param config: configuration for model being set :return: nothing """ - logging.debug("node(%s) setting model: %s", self.name, model.name) + logger.debug("node(%s) setting model: %s", self.name, model.name) if model.config_type == RegisterTlvs.WIRELESS: self.model = model(session=self.session, _id=self.id) for iface in self.get_ifaces(): @@ -1031,7 +991,7 @@ class WlanNode(CoreNetwork): def updatemodel(self, config: Dict[str, str]) -> None: if not self.model: raise CoreError(f"no model set to update for node({self.name})") - logging.debug( + logger.debug( "node(%s) updating model(%s): %s", self.id, self.model.name, config ) self.model.update_config(config) diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 4e8c9464..dab2a954 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -3,9 +3,9 @@ PhysicalNode class for including real systems in the emulated network. """ import logging -import os import threading -from typing import IO, TYPE_CHECKING, List, Optional, Tuple +from pathlib import Path +from typing import TYPE_CHECKING, List, Optional, Tuple from core.emulator.data import InterfaceData, LinkOptions from core.emulator.distributed import DistributedServer @@ -14,7 +14,9 @@ from core.errors import CoreCommandError, CoreError from core.executables import MOUNT, TEST, UMOUNT from core.nodes.base import CoreNetworkBase, CoreNodeBase from core.nodes.interface import DEFAULT_MTU, CoreInterface -from core.nodes.network import CoreNetwork, GreTap +from core.nodes.network import CoreNetwork + +logger = logging.getLogger(__name__) if TYPE_CHECKING: from core.emulator.session import Session @@ -26,15 +28,15 @@ class PhysicalNode(CoreNodeBase): session: "Session", _id: int = None, name: str = None, - nodedir: str = None, + directory: Path = None, server: DistributedServer = None, ) -> None: super().__init__(session, _id, name, server) if not self.server: raise CoreError("physical nodes must be assigned to a remote server") - self.nodedir: Optional[str] = nodedir + self.directory: Optional[Path] = directory self.lock: threading.RLock = threading.RLock() - self._mounts: List[Tuple[str, str]] = [] + self._mounts: List[Tuple[Path, Path]] = [] def startup(self) -> None: with self.lock: @@ -44,15 +46,12 @@ class PhysicalNode(CoreNodeBase): def shutdown(self) -> None: if not self.up: return - with self.lock: while self._mounts: - _source, target = self._mounts.pop(-1) - self.umount(target) - + _, target_path = self._mounts.pop(-1) + self.umount(target_path) for iface in self.get_ifaces(): iface.shutdown() - self.rmnodedir() def path_exists(self, path: str) -> bool: @@ -166,7 +165,7 @@ class PhysicalNode(CoreNodeBase): def new_iface( self, net: CoreNetworkBase, iface_data: InterfaceData ) -> CoreInterface: - logging.info("creating interface") + logger.info("creating interface") ips = iface_data.get_ips() iface_id = iface_data.id if iface_id is None: @@ -174,67 +173,46 @@ class PhysicalNode(CoreNodeBase): name = iface_data.name if name is None: name = f"gt{iface_id}" - if self.up: - # this is reached when this node is linked to a network node - # tunnel to net not built yet, so build it now and adopt it - _, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server) - self.adopt_iface(remote_tap, iface_id, iface_data.mac, ips) - return remote_tap - else: - # this is reached when configuring services (self.up=False) - iface = GreTap(node=self, name=name, session=self.session, start=False) - self.adopt_iface(iface, iface_id, iface_data.mac, ips) - return iface - - def privatedir(self, path: str) -> None: - if path[0] != "/": - raise ValueError(f"path not fully qualified: {path}") - hostpath = os.path.join( - self.nodedir, os.path.normpath(path).strip("/").replace("/", ".") + _, remote_tap = self.session.distributed.create_gre_tunnel( + net, self.server, iface_data.mtu, self.up ) - os.mkdir(hostpath) - self.mount(hostpath, path) + self.adopt_iface(remote_tap, iface_id, iface_data.mac, ips) + return remote_tap - def mount(self, source: str, target: str) -> None: - source = os.path.abspath(source) - logging.info("mounting %s at %s", source, target) - os.makedirs(target) - self.host_cmd(f"{MOUNT} --bind {source} {target}", cwd=self.nodedir) - self._mounts.append((source, target)) + def privatedir(self, dir_path: Path) -> None: + if not str(dir_path).startswith("/"): + raise CoreError(f"private directory path not fully qualified: {dir_path}") + host_path = self.host_path(dir_path, is_dir=True) + self.host_cmd(f"mkdir -p {host_path}") + self.mount(host_path, dir_path) - def umount(self, target: str) -> None: - logging.info("unmounting '%s'", target) + def mount(self, src_path: Path, target_path: Path) -> None: + logger.debug("node(%s) mounting: %s at %s", self.name, src_path, target_path) + self.cmd(f"mkdir -p {target_path}") + self.host_cmd(f"{MOUNT} --bind {src_path} {target_path}", cwd=self.directory) + self._mounts.append((src_path, target_path)) + + def umount(self, target_path: Path) -> None: + logger.info("unmounting '%s'", target_path) try: - self.host_cmd(f"{UMOUNT} -l {target}", cwd=self.nodedir) + self.host_cmd(f"{UMOUNT} -l {target_path}", cwd=self.directory) except CoreCommandError: - logging.exception("unmounting failed for %s", target) + logger.exception("unmounting failed for %s", target_path) - def opennodefile(self, filename: str, mode: str = "w") -> IO: - dirname, basename = os.path.split(filename) - if not basename: - raise ValueError("no basename for filename: " + filename) - - if dirname and dirname[0] == "/": - dirname = dirname[1:] - - dirname = dirname.replace("/", ".") - dirname = os.path.join(self.nodedir, dirname) - if not os.path.isdir(dirname): - os.makedirs(dirname, mode=0o755) - - hostfilename = os.path.join(dirname, basename) - return open(hostfilename, mode) - - def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: - with self.opennodefile(filename, "w") as f: + def nodefile(self, file_path: Path, contents: str, mode: int = 0o644) -> None: + host_path = self.host_path(file_path) + directory = host_path.parent + if not directory.is_dir(): + directory.mkdir(parents=True, mode=0o755) + with host_path.open("w") as f: f.write(contents) - os.chmod(f.name, mode) - logging.info("created nodefile: '%s'; mode: 0%o", f.name, mode) + host_path.chmod(mode) + logger.info("created nodefile: '%s'; mode: 0%o", host_path, mode) def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: return self.host_cmd(args, wait=wait) - def addfile(self, srcname: str, filename: str) -> None: + def addfile(self, src_path: str, file_path: str) -> None: raise CoreError("physical node does not support addfile") @@ -433,7 +411,7 @@ class Rj45Node(CoreNodeBase): if items[1][:4] == "fe80": continue self.old_addrs.append((items[1], None)) - logging.info("saved rj45 state: addrs(%s) up(%s)", self.old_addrs, self.old_up) + logger.info("saved rj45 state: addrs(%s) up(%s)", self.old_addrs, self.old_up) def restorestate(self) -> None: """ @@ -443,7 +421,7 @@ class Rj45Node(CoreNodeBase): :raises CoreCommandError: when there is a command exception """ localname = self.iface.localname - logging.info("restoring rj45 state: %s", localname) + logger.info("restoring rj45 state: %s", localname) for addr in self.old_addrs: self.net_client.create_address(localname, addr[0], addr[1]) if self.old_up: @@ -464,10 +442,10 @@ class Rj45Node(CoreNodeBase): def termcmdstring(self, sh: str) -> str: raise CoreError("rj45 does not support terminal commands") - def addfile(self, srcname: str, filename: str) -> None: + def addfile(self, src_path: str, file_path: str) -> None: raise CoreError("rj45 does not support addfile") - def nodefile(self, filename: str, contents: str, mode: int = 0o644) -> None: + def nodefile(self, file_path: str, contents: str, mode: int = 0o644) -> None: raise CoreError("rj45 does not support nodefile") def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 45e09255..575f9257 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -15,6 +15,8 @@ from core.errors import CoreError from core.nodes.base import CoreNetworkBase, NodeBase from core.nodes.network import WlanNode +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.emulator.session import Session @@ -109,7 +111,7 @@ class Sdt: return False self.seturl() - logging.info("connecting to SDT at %s://%s", self.protocol, self.address) + logger.info("connecting to SDT at %s://%s", self.protocol, self.address) if self.sock is None: try: if self.protocol.lower() == "udp": @@ -119,7 +121,7 @@ class Sdt: # Default to tcp self.sock = socket.create_connection(self.address, 5) except IOError: - logging.exception("SDT socket connect error") + logger.exception("SDT socket connect error") return False if not self.initialize(): @@ -157,7 +159,7 @@ class Sdt: try: self.sock.close() except IOError: - logging.error("error closing socket") + logger.error("error closing socket") finally: self.sock = None @@ -191,11 +193,11 @@ class Sdt: try: cmd = f"{cmdstr}\n".encode() - logging.debug("sdt cmd: %s", cmd) + logger.debug("sdt cmd: %s", cmd) self.sock.sendall(cmd) return True except IOError: - logging.exception("SDT connection error") + logger.exception("SDT connection error") self.sock = None self.connected = False return False @@ -250,7 +252,7 @@ class Sdt: :param node: node to add :return: nothing """ - logging.debug("sdt add node: %s - %s", node.id, node.name) + logger.debug("sdt add node: %s - %s", node.id, node.name) if not self.connect(): return pos = self.get_node_position(node) @@ -262,8 +264,8 @@ class Sdt: icon = node.icon if icon: node_type = node.name - icon = icon.replace("$CORE_DATA_DIR", CORE_DATA_DIR) - icon = icon.replace("$CORE_CONF_DIR", CORE_CONF_DIR) + icon = icon.replace("$CORE_DATA_DIR", str(CORE_DATA_DIR)) + icon = icon.replace("$CORE_CONF_DIR", str(CORE_CONF_DIR)) self.cmd(f"sprite {node_type} image {icon}") self.cmd( f'node {node.id} nodeLayer "{NODE_LAYER}" ' @@ -280,7 +282,7 @@ class Sdt: :param alt: node altitude :return: nothing """ - logging.debug("sdt update node: %s - %s", node.id, node.name) + logger.debug("sdt update node: %s - %s", node.id, node.name) if not self.connect(): return @@ -300,7 +302,7 @@ class Sdt: :param node_id: node id to delete :return: nothing """ - logging.debug("sdt delete node: %s", node_id) + logger.debug("sdt delete node: %s", node_id) if not self.connect(): return self.cmd(f"delete node,{node_id}") @@ -315,7 +317,7 @@ class Sdt: if not self.connect(): return node = node_data.node - logging.debug("sdt handle node update: %s - %s", node.id, node.name) + logger.debug("sdt handle node update: %s - %s", node.id, node.name) if node_data.message_type == MessageFlags.DELETE: self.cmd(f"delete node,{node.id}") else: @@ -356,7 +358,7 @@ class Sdt: :param label: label for link :return: nothing """ - logging.debug("sdt add link: %s, %s, %s", node1_id, node2_id, network_id) + logger.debug("sdt add link: %s, %s, %s", node1_id, node2_id, network_id) if not self.connect(): return if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): @@ -396,7 +398,7 @@ class Sdt: :param network_id: network link is associated with, None otherwise :return: nothing """ - logging.debug("sdt delete link: %s, %s, %s", node1_id, node2_id, network_id) + logger.debug("sdt delete link: %s, %s, %s", node1_id, node2_id, network_id) if not self.connect(): return if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): @@ -416,7 +418,7 @@ class Sdt: :param label: label to update :return: nothing """ - logging.debug("sdt edit link: %s, %s, %s", node1_id, node2_id, network_id) + logger.debug("sdt edit link: %s, %s, %s", node1_id, node2_id, network_id) if not self.connect(): return if self.wireless_net_check(node1_id) or self.wireless_net_check(node2_id): diff --git a/daemon/core/services/__init__.py b/daemon/core/services/__init__.py index 94e1e9d1..4d9b90a2 100644 --- a/daemon/core/services/__init__.py +++ b/daemon/core/services/__init__.py @@ -4,11 +4,11 @@ Services Services available to nodes can be put in this directory. Everything listed in __all__ is automatically loaded by the main core module. """ -import os +from pathlib import Path from core.services.coreservices import ServiceManager -_PATH = os.path.abspath(os.path.dirname(__file__)) +_PATH: Path = Path(__file__).resolve().parent def load(): diff --git a/daemon/core/services/coreservices.py b/daemon/core/services/coreservices.py index b4c33990..b12f21c4 100644 --- a/daemon/core/services/coreservices.py +++ b/daemon/core/services/coreservices.py @@ -10,6 +10,7 @@ services. import enum import logging import time +from pathlib import Path from typing import ( TYPE_CHECKING, Dict, @@ -33,6 +34,8 @@ from core.errors import ( ) from core.nodes.base import CoreNode +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.emulator.session import Session @@ -160,7 +163,7 @@ class ServiceShim: cls.setvalue(service, key, values[cls.keys.index(key)]) except IndexError: # old config does not need to have new keys - logging.exception("error indexing into key") + logger.exception("error indexing into key") @classmethod def setvalue(cls, service: "CoreService", key: str, value: str) -> None: @@ -227,7 +230,7 @@ class ServiceManager: :raises ValueError: when service cannot be loaded """ name = service.name - logging.debug("loading service: class(%s) name(%s)", service.__name__, name) + logger.debug("loading service: class(%s) name(%s)", service.__name__, name) # avoid duplicate services if name in cls.services: @@ -244,7 +247,7 @@ class ServiceManager: try: service.on_load() except Exception as e: - logging.exception("error during service(%s) on load", service.name) + logger.exception("error during service(%s) on load", service.name) raise ValueError(e) # make service available @@ -264,7 +267,7 @@ class ServiceManager: return service @classmethod - def add_services(cls, path: str) -> List[str]: + def add_services(cls, path: Path) -> List[str]: """ Method for retrieving all CoreServices from a given path. @@ -276,12 +279,11 @@ class ServiceManager: for service in services: if not service.name: continue - try: cls.add(service) except (CoreError, ValueError) as e: service_errors.append(service.name) - logging.debug("not loading service(%s): %s", service.name, e) + logger.debug("not loading service(%s): %s", service.name, e) return service_errors @@ -329,14 +331,14 @@ class CoreServices: :param node_type: node type to get default services for :return: default services """ - logging.debug("getting default services for type: %s", node_type) + logger.debug("getting default services for type: %s", node_type) results = [] defaults = self.default_services.get(node_type, []) for name in defaults: - logging.debug("checking for service with service manager: %s", name) + logger.debug("checking for service with service manager: %s", name) service = ServiceManager.get(name) if not service: - logging.warning("default service %s is unknown", name) + logger.warning("default service %s is unknown", name) else: results.append(service) return results @@ -369,7 +371,7 @@ class CoreServices: :param service_name: name of service to set :return: nothing """ - logging.debug("setting custom service(%s) for node: %s", service_name, node_id) + logger.debug("setting custom service(%s) for node: %s", service_name, node_id) service = self.get_service(node_id, service_name) if not service: service_class = ServiceManager.get(service_name) @@ -391,15 +393,15 @@ class CoreServices: :return: nothing """ if not services: - logging.info( + logger.info( "using default services for node(%s) type(%s)", node.name, node_type ) services = self.default_services.get(node_type, []) - logging.info("setting services for node(%s): %s", node.name, services) + logger.info("setting services for node(%s): %s", node.name, services) for service_name in services: service = self.get_service(node.id, service_name, default_service=True) if not service: - logging.warning( + logger.warning( "unknown service(%s) for node(%s)", service_name, node.name ) continue @@ -457,7 +459,7 @@ class CoreServices: raise CoreServiceBootError(*exceptions) def _boot_service_path(self, node: CoreNode, boot_path: List["CoreServiceType"]): - logging.info( + logger.info( "booting node(%s) services: %s", node.name, " -> ".join([x.name for x in boot_path]), @@ -467,7 +469,7 @@ class CoreServices: try: self.boot_service(node, service) except Exception as e: - logging.exception("exception booting service: %s", service.name) + logger.exception("exception booting service: %s", service.name) raise CoreServiceBootError(e) def boot_service(self, node: CoreNode, service: "CoreServiceType") -> None: @@ -479,7 +481,7 @@ class CoreServices: :param service: service to start :return: nothing """ - logging.info( + logger.info( "starting node(%s) service(%s) validation(%s)", node.name, service.name, @@ -488,10 +490,11 @@ class CoreServices: # create service directories for directory in service.dirs: + dir_path = Path(directory) try: - node.privatedir(directory) - except (CoreCommandError, ValueError) as e: - logging.warning( + node.create_dir(dir_path) + except (CoreCommandError, CoreError) as e: + logger.warning( "error mounting private dir '%s' for service '%s': %s", directory, service.name, @@ -534,14 +537,14 @@ class CoreServices: "node(%s) service(%s) failed validation" % (node.name, service.name) ) - def copy_service_file(self, node: CoreNode, filename: str, cfg: str) -> bool: + def copy_service_file(self, node: CoreNode, file_path: Path, cfg: str) -> bool: """ Given a configured service filename and config, determine if the config references an existing file that should be copied. Returns True for local files, False for generated. :param node: node to copy service for - :param filename: file name for a configured service + :param file_path: file name for a configured service :param cfg: configuration string :return: True if successful, False otherwise """ @@ -550,7 +553,7 @@ class CoreServices: src = src.split("\n")[0] src = utils.expand_corepath(src, node.session, node) # TODO: glob here - node.nodefilecopy(filename, src, mode=0o644) + node.copy_file(src, file_path, mode=0o644) return True return False @@ -562,21 +565,21 @@ class CoreServices: :param service: service to validate :return: service validation status """ - logging.debug("validating node(%s) service(%s)", node.name, service.name) + logger.debug("validating node(%s) service(%s)", node.name, service.name) cmds = service.validate if not service.custom: cmds = service.get_validate(node) status = 0 for cmd in cmds: - logging.debug("validating service(%s) using: %s", service.name, cmd) + logger.debug("validating service(%s) using: %s", service.name, cmd) try: node.cmd(cmd) except CoreCommandError as e: - logging.debug( + logger.debug( "node(%s) service(%s) validate failed", node.name, service.name ) - logging.debug("cmd(%s): %s", e.cmd, e.output) + logger.debug("cmd(%s): %s", e.cmd, e.output) status = -1 break @@ -611,7 +614,7 @@ class CoreServices: f"error stopping service {service.name}: {e.stderr}", node.id, ) - logging.exception("error running stop command %s", args) + logger.exception("error running stop command %s", args) status = -1 return status @@ -679,13 +682,13 @@ class CoreServices: # retrieve custom service service = self.get_service(node_id, service_name) if service is None: - logging.warning("received file name for unknown service: %s", service_name) + logger.warning("received file name for unknown service: %s", service_name) return # validate file being set is valid config_files = service.configs if file_name not in config_files: - logging.warning( + logger.warning( "received unknown file(%s) for service(%s)", file_name, service_name ) return @@ -713,7 +716,7 @@ class CoreServices: try: node.cmd(cmd, wait) except CoreCommandError: - logging.exception("error starting command") + logger.exception("error starting command") status = -1 return status @@ -729,27 +732,25 @@ class CoreServices: config_files = service.configs if not service.custom: config_files = service.get_configs(node) - for file_name in config_files: - logging.debug( + file_path = Path(file_name) + logger.debug( "generating service config custom(%s): %s", service.custom, file_name ) if service.custom: cfg = service.config_data.get(file_name) if cfg is None: cfg = service.generate_config(node, file_name) - # cfg may have a file:/// url for copying from a file try: - if self.copy_service_file(node, file_name, cfg): + if self.copy_service_file(node, file_path, cfg): continue except IOError: - logging.exception("error copying service file: %s", file_name) + logger.exception("error copying service file: %s", file_name) continue else: cfg = service.generate_config(node, file_name) - - node.nodefile(file_name, cfg) + node.create_file(file_path, cfg) def service_reconfigure(self, node: CoreNode, service: "CoreService") -> None: """ @@ -762,17 +763,15 @@ class CoreServices: config_files = service.configs if not service.custom: config_files = service.get_configs(node) - for file_name in config_files: + file_path = Path(file_name) if file_name[:7] == "file:///": # TODO: implement this raise NotImplementedError - cfg = service.config_data.get(file_name) if cfg is None: cfg = service.generate_config(node, file_name) - - node.nodefile(file_name, cfg) + node.create_file(file_path, cfg) class CoreService: diff --git a/daemon/core/services/security.py b/daemon/core/services/security.py index 788988c9..f53e8533 100644 --- a/daemon/core/services/security.py +++ b/daemon/core/services/security.py @@ -11,6 +11,8 @@ from core.nodes.base import CoreNode from core.nodes.interface import CoreInterface from core.services.coreservices import CoreService +logger = logging.getLogger(__name__) + class VPNClient(CoreService): name: str = "VPNClient" @@ -33,7 +35,7 @@ class VPNClient(CoreService): with open(fname, "r") as f: cfg += f.read() except IOError: - logging.exception( + logger.exception( "error opening VPN client configuration template (%s)", fname ) return cfg @@ -61,7 +63,7 @@ class VPNServer(CoreService): with open(fname, "r") as f: cfg += f.read() except IOError: - logging.exception( + logger.exception( "Error opening VPN server configuration template (%s)", fname ) return cfg @@ -89,7 +91,7 @@ class IPsec(CoreService): with open(fname, "r") as f: cfg += f.read() except IOError: - logging.exception("Error opening IPsec configuration template (%s)", fname) + logger.exception("Error opening IPsec configuration template (%s)", fname) return cfg @@ -112,7 +114,7 @@ class Firewall(CoreService): with open(fname, "r") as f: cfg += f.read() except IOError: - logging.exception( + logger.exception( "Error opening Firewall configuration template (%s)", fname ) return cfg diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index 8ff93c28..54a58b2a 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -236,7 +236,7 @@ max-lease-time 7200; ddns-update-style none; """ for iface in node.get_ifaces(control=False): - cfg += "\n".join(map(cls.subnetentry, iface.ips())) + cfg += "\n".join(map(cls.subnetentry, iface.ip4s)) cfg += "\n" return cfg @@ -246,15 +246,13 @@ ddns-update-style none; Generate a subnet declaration block given an IPv4 prefix string for inclusion in the dhcpd3 config file. """ - address = str(ip.ip) - if netaddr.valid_ipv6(address): + if ip.size == 1: return "" - else: - # divide the address space in half - index = (ip.size - 2) / 2 - rangelow = ip[index] - rangehigh = ip[-2] - return """ + # divide the address space in half + index = (ip.size - 2) / 2 + rangelow = ip[index] + rangehigh = ip[-2] + return """ subnet %s netmask %s { pool { range %s %s; @@ -263,12 +261,12 @@ subnet %s netmask %s { } } """ % ( - ip.ip, - ip.netmask, - rangelow, - rangehigh, - address, - ) + ip.cidr.ip, + ip.netmask, + rangelow, + rangehigh, + ip.ip, + ) class DhcpClientService(UtilService): diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 4a9d6ca6..fcc37842 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -15,6 +15,7 @@ import random import shlex import shutil import sys +import threading from pathlib import Path from subprocess import PIPE, STDOUT, Popen from typing import ( @@ -36,7 +37,10 @@ import netaddr from core.errors import CoreCommandError, CoreError +logger = logging.getLogger(__name__) + if TYPE_CHECKING: + from core.emulator.coreemu import CoreEmu from core.emulator.session import Session from core.nodes.base import CoreNode T = TypeVar("T") @@ -45,12 +49,29 @@ DEVNULL = open(os.devnull, "wb") IFACE_CONFIG_FACTOR: int = 1000 +def execute_script(coreemu: "CoreEmu", file_path: Path, args: str) -> None: + """ + Provides utility function to execute a python script in context of the + provide coreemu instance. + + :param coreemu: coreemu to provide to script + :param file_path: python script to execute + :param args: args to provide script + :return: nothing + """ + sys.argv = shlex.split(args) + thread = threading.Thread( + target=execute_file, args=(file_path, {"coreemu": coreemu}), daemon=True + ) + thread.start() + thread.join() + + def execute_file( - path: str, exec_globals: Dict[str, str] = None, exec_locals: Dict[str, str] = None + path: Path, exec_globals: Dict[str, str] = None, exec_locals: Dict[str, str] = None ) -> None: """ - Provides an alternative way to run execfile to be compatible for - both python2/3. + Provides a way to execute a file. :param path: path of file to execute :param exec_globals: globals values to pass to execution @@ -59,10 +80,10 @@ def execute_file( """ if exec_globals is None: exec_globals = {} - exec_globals.update({"__file__": path, "__name__": "__main__"}) - with open(path, "rb") as f: + exec_globals.update({"__file__": str(path), "__name__": "__main__"}) + with path.open("rb") as f: data = compile(f.read(), path, "exec") - exec(data, exec_globals, exec_locals) + exec(data, exec_globals, exec_locals) def hashkey(value: Union[str, int]) -> int: @@ -92,24 +113,19 @@ def _detach_init() -> None: os.setsid() -def _valid_module(path: str, file_name: str) -> bool: +def _valid_module(path: Path) -> bool: """ Check if file is a valid python module. :param path: path to file - :param file_name: file name to check :return: True if a valid python module file, False otherwise """ - file_path = os.path.join(path, file_name) - if not os.path.isfile(file_path): + if not path.is_file(): return False - - if file_name.startswith("_"): + if path.name.startswith("_"): return False - - if not file_name.endswith(".py"): + if not path.suffix == ".py": return False - return True @@ -124,13 +140,10 @@ def _is_class(module: Any, member: Type, clazz: Type) -> bool: """ if not inspect.isclass(member): return False - if not issubclass(member, clazz): return False - if member.__module__ != module.__name__: return False - return True @@ -196,7 +209,7 @@ def mute_detach(args: str, **kwargs: Dict[str, Any]) -> int: def cmd( args: str, env: Dict[str, str] = None, - cwd: str = None, + cwd: Path = None, wait: bool = True, shell: bool = False, ) -> str: @@ -213,7 +226,7 @@ def cmd( :raises CoreCommandError: when there is a non-zero exit status or the file to execute is not found """ - logging.debug("command cwd(%s) wait(%s): %s", cwd, wait, args) + logger.debug("command cwd(%s) wait(%s): %s", cwd, wait, args) if shell is False: args = shlex.split(args) try: @@ -230,7 +243,7 @@ def cmd( else: return "" except OSError as e: - logging.error("cmd error: %s", e.strerror) + logger.error("cmd error: %s", e.strerror) raise CoreCommandError(1, args, "", e.strerror) @@ -282,7 +295,7 @@ def file_demunge(pathname: str, header: str) -> None: def expand_corepath( pathname: str, session: "Session" = None, node: "CoreNode" = None -) -> str: +) -> Path: """ Expand a file path given session information. @@ -294,14 +307,12 @@ def expand_corepath( if session is not None: pathname = pathname.replace("~", f"/home/{session.user}") pathname = pathname.replace("%SESSION%", str(session.id)) - pathname = pathname.replace("%SESSION_DIR%", session.session_dir) + pathname = pathname.replace("%SESSION_DIR%", str(session.directory)) pathname = pathname.replace("%SESSION_USER%", session.user) - if node is not None: pathname = pathname.replace("%NODE%", str(node.id)) pathname = pathname.replace("%NODENAME%", node.name) - - return pathname + return Path(pathname) def sysctl_devname(devname: str) -> Optional[str]: @@ -334,10 +345,25 @@ def load_config(file_path: Path, d: Dict[str, str]) -> None: key, value = line.split("=", 1) d[key] = value.strip() except ValueError: - logging.exception("error reading file to dict: %s", file_path) + logger.exception("error reading file to dict: %s", file_path) -def load_classes(path: str, clazz: Generic[T]) -> T: +def load_module(import_statement: str, clazz: Generic[T]) -> List[T]: + classes = [] + try: + module = importlib.import_module(import_statement) + members = inspect.getmembers(module, lambda x: _is_class(module, x, clazz)) + for member in members: + valid_class = member[1] + classes.append(valid_class) + except Exception: + logger.exception( + "unexpected error during import, skipping: %s", import_statement + ) + return classes + + +def load_classes(path: Path, clazz: Generic[T]) -> List[T]: """ Dynamically load classes for use within CORE. @@ -346,50 +372,36 @@ def load_classes(path: str, clazz: Generic[T]) -> T: :return: list of classes loaded """ # validate path exists - logging.debug("attempting to load modules from path: %s", path) - if not os.path.isdir(path): - logging.warning("invalid custom module directory specified" ": %s", path) + logger.debug("attempting to load modules from path: %s", path) + if not path.is_dir(): + logger.warning("invalid custom module directory specified" ": %s", path) # check if path is in sys.path - parent_path = os.path.dirname(path) - if parent_path not in sys.path: - logging.debug("adding parent path to allow imports: %s", parent_path) - sys.path.append(parent_path) - - # retrieve potential service modules, and filter out invalid modules - base_module = os.path.basename(path) - module_names = os.listdir(path) - module_names = filter(lambda x: _valid_module(path, x), module_names) - module_names = map(lambda x: x[:-3], module_names) - + parent = str(path.parent) + if parent not in sys.path: + logger.debug("adding parent path to allow imports: %s", parent) + sys.path.append(parent) # import and add all service modules in the path classes = [] - for module_name in module_names: - import_statement = f"{base_module}.{module_name}" - logging.debug("importing custom module: %s", import_statement) - try: - module = importlib.import_module(import_statement) - members = inspect.getmembers(module, lambda x: _is_class(module, x, clazz)) - for member in members: - valid_class = member[1] - classes.append(valid_class) - except Exception: - logging.exception( - "unexpected error during import, skipping: %s", import_statement - ) - + for p in path.iterdir(): + if not _valid_module(p): + continue + import_statement = f"{path.name}.{p.stem}" + logger.debug("importing custom module: %s", import_statement) + loaded = load_module(import_statement, clazz) + classes.extend(loaded) return classes -def load_logging_config(config_path: str) -> None: +def load_logging_config(config_path: Path) -> None: """ Load CORE logging configuration file. :param config_path: path to logging config file :return: nothing """ - with open(config_path, "r") as log_config_file: - log_config = json.load(log_config_file) - logging.config.dictConfig(log_config) + with config_path.open("r") as f: + log_config = json.load(f) + logging.config.dictConfig(log_config) def threadpool( @@ -415,7 +427,7 @@ def threadpool( result = future.result() results.append(result) except Exception as e: - logging.exception("thread pool exception") + logger.exception("thread pool exception") exceptions.append(e) return results, exceptions diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 8d7b5ea1..647300fc 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -1,4 +1,5 @@ import logging +from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Type, TypeVar from lxml import etree @@ -16,6 +17,8 @@ from core.nodes.lxd import LxcNode from core.nodes.network import CtrlNet, GreTapBridge, WlanNode from core.services.coreservices import CoreService +logger = logging.getLogger(__name__) + if TYPE_CHECKING: from core.emane.emanemodel import EmaneModel from core.emulator.session import Session @@ -25,7 +28,7 @@ T = TypeVar("T") def write_xml_file( - xml_element: etree.Element, file_path: str, doctype: str = None + xml_element: etree.Element, file_path: Path, doctype: str = None ) -> None: xml_data = etree.tostring( xml_element, @@ -34,8 +37,8 @@ def write_xml_file( encoding="UTF-8", doctype=doctype, ) - with open(file_path, "wb") as xml_file: - xml_file.write(xml_data) + with file_path.open("wb") as f: + f.write(xml_data) def get_type(element: etree.Element, name: str, _type: Generic[T]) -> Optional[T]: @@ -77,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.get_configs() - 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", @@ -101,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 @@ -293,13 +282,12 @@ class CoreXmlWriter: self.write_session_metadata() self.write_default_services() - def write(self, file_name: str) -> None: - self.scenario.set("name", file_name) - + def write(self, path: Path) -> None: + self.scenario.set("name", str(path)) # write out generated xml xml_tree = etree.ElementTree(self.scenario) xml_tree.write( - file_name, xml_declaration=True, pretty_print=True, encoding="UTF-8" + str(path), xml_declaration=True, pretty_print=True, encoding="UTF-8" ) def write_session_origin(self) -> None: @@ -374,22 +362,16 @@ 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 in self.session.emane.nodes(): - all_configs = self.session.emane.get_all_configs(node_id) - if not all_configs: - continue + for node_id, model_configs in self.session.emane.node_configs.items(): node_id, iface_id = utils.parse_iface_config_id(node_id) - for model_name in all_configs: - config = all_configs[model_name] - logging.debug( + for model_name, config in model_configs.items(): + logger.debug( "writing emane config node(%s) model(%s)", node_id, model_name ) - model = self.session.emane.models[model_name] + model_class = self.session.emane.get_model(model_name) emane_configuration = create_emane_model_config( - node_id, model, config, iface_id + node_id, model_class, config, iface_id ) emane_configurations.append(emane_configuration) if emane_configurations.getchildren(): @@ -404,7 +386,7 @@ class CoreXmlWriter: for model_name in all_configs: config = all_configs[model_name] - logging.debug( + logger.debug( "writing mobility config node(%s) model(%s)", node_id, model_name ) mobility_configuration = etree.SubElement( @@ -580,8 +562,8 @@ class CoreXmlReader: self.session: "Session" = session self.scenario: Optional[etree.ElementTree] = None - def read(self, file_name: str) -> None: - xml_tree = etree.parse(file_name) + def read(self, file_path: Path) -> None: + xml_tree = etree.parse(str(file_path)) self.scenario = xml_tree.getroot() # read xml session content @@ -593,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() @@ -609,7 +590,7 @@ class CoreXmlReader: services = [] for service in node.iterchildren(): services.append(service.get("name")) - logging.info( + logger.info( "reading default services for nodes(%s): %s", node_type, services ) self.session.services.default_services[node_type] = services @@ -624,7 +605,7 @@ class CoreXmlReader: name = data.get("name") value = data.get("value") configs[name] = value - logging.info("reading session metadata: %s", configs) + logger.info("reading session metadata: %s", configs) self.session.metadata = configs def read_session_options(self) -> None: @@ -636,7 +617,7 @@ class CoreXmlReader: name = configuration.get("name") value = configuration.get("value") xml_config[name] = value - logging.info("reading session options: %s", xml_config) + logger.info("reading session options: %s", xml_config) config = self.session.options.get_configs() config.update(xml_config) @@ -650,7 +631,7 @@ class CoreXmlReader: state = get_int(hook, "state") state = EventTypes(state) data = hook.text - logging.info("reading hook: state(%s) name(%s)", state, name) + logger.info("reading hook: state(%s) name(%s)", state, name) self.session.add_hook(state, name, data) def read_servers(self) -> None: @@ -660,7 +641,7 @@ class CoreXmlReader: for server in servers.iterchildren(): name = server.get("name") address = server.get("address") - logging.info("reading server: name(%s) address(%s)", name, address) + logger.info("reading server: name(%s) address(%s)", name, address) self.session.distributed.add_server(name, address) def read_session_origin(self) -> None: @@ -672,19 +653,19 @@ class CoreXmlReader: lon = get_float(session_origin, "lon") alt = get_float(session_origin, "alt") if all([lat, lon, alt]): - logging.info("reading session reference geo: %s, %s, %s", lat, lon, alt) + logger.info("reading session reference geo: %s, %s, %s", lat, lon, alt) self.session.location.setrefgeo(lat, lon, alt) scale = get_float(session_origin, "scale") if scale: - logging.info("reading session reference scale: %s", scale) + logger.info("reading session reference scale: %s", scale) self.session.location.refscale = scale x = get_float(session_origin, "x") y = get_float(session_origin, "y") z = get_float(session_origin, "z") if all([x, y]): - logging.info("reading session reference xyz: %s, %s, %s", x, y, z) + logger.info("reading session reference xyz: %s, %s, %s", x, y, z) self.session.location.refxyz = (x, y, z) def read_service_configs(self) -> None: @@ -695,7 +676,7 @@ class CoreXmlReader: for service_configuration in service_configurations.iterchildren(): node_id = get_int(service_configuration, "node") service_name = service_configuration.get("name") - logging.info( + logger.info( "reading custom service(%s) for node(%s)", service_name, node_id ) self.session.services.set_service(node_id, service_name) @@ -731,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.set_configs(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") @@ -763,38 +726,39 @@ class CoreXmlReader: node = self.session.nodes.get(node_id) if not node: raise CoreXmlError(f"node for emane config doesn't exist: {node_id}") - model = self.session.emane.models.get(model_name) - if not model: - raise CoreXmlError(f"invalid emane model: {model_name}") + self.session.emane.get_model(model_name) 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 + 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") value = config.get("value") configs[name] = value - logging.info( + logger.info( "reading emane configuration node(%s) model(%s)", node_id, model_name ) node_id = utils.iface_config_id(node_id, iface_id) - self.session.emane.set_model_config(node_id, model_name, configs) + self.session.emane.set_config(node_id, model_name, configs) def read_mobility_configs(self) -> None: mobility_configurations = self.scenario.find("mobility_configurations") @@ -811,7 +775,7 @@ class CoreXmlReader: value = config.get("value") configs[name] = value - logging.info( + logger.info( "reading mobility configuration node(%s) model(%s)", node_id, model_name ) self.session.mobility.set_model_config(node_id, model_name, configs) @@ -868,7 +832,7 @@ class CoreXmlReader: if all([lat, lon, alt]): options.set_location(lat, lon, alt) - logging.info("reading node id(%s) model(%s) name(%s)", node_id, model, name) + logger.info("reading node id(%s) model(%s) name(%s)", node_id, model, name) self.session.add_node(_class, node_id, options) def read_network(self, network_element: etree.Element) -> None: @@ -896,7 +860,7 @@ class CoreXmlReader: if all([lat, lon, alt]): options.set_location(lat, lon, alt) - logging.info( + logger.info( "reading node id(%s) node_type(%s) name(%s)", node_id, node_type, name ) self.session.add_node(_class, node_id, options) @@ -926,7 +890,7 @@ class CoreXmlReader: for template_element in templates_element.iterchildren(): name = template_element.get("name") template = template_element.text - logging.info( + logger.info( "loading xml template(%s): %s", type(template), template ) service.set_template(name, template) @@ -978,12 +942,12 @@ class CoreXmlReader: options.buffer = get_int(options_element, "buffer") if options.unidirectional == 1 and node_set in node_sets: - logging.info("updating link node1(%s) node2(%s)", node1_id, node2_id) + logger.info("updating link node1(%s) node2(%s)", node1_id, node2_id) self.session.update_link( node1_id, node2_id, iface1_data.id, iface2_data.id, options ) else: - logging.info("adding link node1(%s) node2(%s)", node1_id, node2_id) + logger.info("adding link node1(%s) node2(%s)", node1_id, node2_id) self.session.add_link( node1_id, node2_id, iface1_data, iface2_data, options ) diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index c0d5462b..c45259f7 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -1,5 +1,5 @@ import logging -import os +from pathlib import Path from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple @@ -12,11 +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" @@ -47,14 +47,14 @@ def _value_to_params(value: str) -> Optional[Tuple[str]]: return None return values except SyntaxError: - logging.exception("error in value string to param list") + logger.exception("error in value string to param list") return None def create_file( xml_element: etree.Element, doc_name: str, - file_path: str, + file_path: Path, server: DistributedServer = None, ) -> None: """ @@ -71,10 +71,11 @@ def create_file( ) if server: temp = NamedTemporaryFile(delete=False) - corexml.write_xml_file(xml_element, temp.name, doctype=doctype) + temp_path = Path(temp.name) + corexml.write_xml_file(xml_element, temp_path, doctype=doctype) temp.close() - server.remote_put(temp.name, file_path) - os.unlink(temp.name) + server.remote_put(temp_path, file_path) + temp_path.unlink() else: corexml.write_xml_file(xml_element, file_path, doctype=doctype) @@ -92,9 +93,9 @@ def create_node_file( :return: """ if isinstance(node, CoreNode): - file_path = os.path.join(node.nodedir, file_name) + file_path = node.directory / file_name else: - file_path = os.path.join(node.session.session_dir, file_name) + file_path = node.session.directory / file_name create_file(xml_element, doc_name, file_path, node.server) @@ -143,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.get_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: @@ -316,7 +310,7 @@ def create_event_service_xml( group: str, port: str, device: str, - file_directory: str, + file_directory: Path, server: DistributedServer = None, ) -> None: """ @@ -340,8 +334,7 @@ def create_event_service_xml( ): sub_element = etree.SubElement(event_element, name) sub_element.text = value - file_name = "libemaneeventservice.xml" - file_path = os.path.join(file_directory, file_name) + file_path = file_directory / "libemaneeventservice.xml" create_file(event_element, "emaneeventmsgsvc", file_path, server) @@ -394,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/data/logging.conf b/daemon/data/logging.conf index 7f3d496f..a76dc0da 100644 --- a/daemon/data/logging.conf +++ b/daemon/data/logging.conf @@ -13,8 +13,21 @@ "format": "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" } }, - "root": { - "level": "INFO", - "handlers": ["console"] - } + "loggers": { + "": { + "level": "WARNING", + "handlers": ["console"], + "propagate": false + }, + "core": { + "level": "INFO", + "handlers": ["console"], + "propagate": false + }, + "__main__": { + "level": "INFO", + "handlers": ["console"], + "propagate": false + } + } } diff --git a/daemon/examples/docker/daemon.json b/daemon/examples/docker/daemon.json deleted file mode 100644 index 8fefb9ab..00000000 --- a/daemon/examples/docker/daemon.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "bridge": "none", - "iptables": false - -} diff --git a/daemon/examples/grpc/distributed_switch.py b/daemon/examples/grpc/distributed_switch.py index e8ddfb4c..52c327b4 100644 --- a/daemon/examples/grpc/distributed_switch.py +++ b/daemon/examples/grpc/distributed_switch.py @@ -2,7 +2,7 @@ import argparse import logging from core.api.grpc import client -from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState +from core.api.grpc.wrappers import NodeType, Position, Server def log_event(event): @@ -10,62 +10,39 @@ def log_event(event): def main(args): + # helper to create interfaces + interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") + + # create grpc client and connect core = client.CoreGrpcClient() + core.connect() - with core.context_connect(): - # create session - response = core.create_session() - session_id = response.session_id - logging.info("created session: %s", response) + # create session + session = core.create_session() - # add distributed server - server_name = "core2" - response = core.add_session_server(session_id, server_name, args.server) - logging.info("added session server: %s", response) + # add distributed server + server = Server(name="core2", host=args.server) + session.servers.append(server) - # handle events session may broadcast - core.events(session_id, log_event) + # handle events session may broadcast + core.events(session.id, log_event) - # change session state - response = core.set_session_state(session_id, SessionState.CONFIGURATION) - logging.info("set session state: %s", response) + # create switch node + position = Position(x=150, y=100) + switch = session.add_node(1, _type=NodeType.SWITCH, position=position) + position = Position(x=100, y=50) + node1 = session.add_node(2, position=position) + position = Position(x=200, y=50) + node2 = session.add_node(3, position=position, server=server.name) - # create switch node - switch = Node(type=NodeType.SWITCH) - response = core.add_node(session_id, switch) - logging.info("created switch: %s", response) - switch_id = response.node_id + # create links + iface1 = interface_helper.create_iface(node1.id, 0) + session.add_link(node1=node1, node2=switch, iface1=iface1) + iface1 = interface_helper.create_iface(node2.id, 0) + session.add_link(node1=node2, node2=switch, iface1=iface1) - # helper to create interfaces - interface_helper = client.InterfaceHelper(ip4_prefix="10.83.0.0/16") - - # create node one - position = Position(x=100, y=50) - node = Node(position=position) - response = core.add_node(session_id, node) - logging.info("created node one: %s", response) - node1_id = response.node_id - - # create link - interface1 = interface_helper.create_iface(node1_id, 0) - response = core.add_link(session_id, node1_id, switch_id, interface1) - logging.info("created link from node one to switch: %s", response) - - # create node two - position = Position(x=200, y=50) - node = Node(position=position, server=server_name) - response = core.add_node(session_id, node) - logging.info("created node two: %s", response) - node2_id = response.node_id - - # create link - interface1 = interface_helper.create_iface(node2_id, 0) - response = core.add_link(session_id, node2_id, switch_id, interface1) - logging.info("created link from node two to switch: %s", response) - - # change session state - response = core.set_session_state(session_id, SessionState.INSTANTIATION) - logging.info("set session state: %s", response) + # start session + core.start_session(session) if __name__ == "__main__": diff --git a/daemon/examples/grpc/emane80211.py b/daemon/examples/grpc/emane80211.py index c9ced47e..00c5458f 100644 --- a/daemon/examples/grpc/emane80211.py +++ b/daemon/examples/grpc/emane80211.py @@ -1,7 +1,7 @@ # required imports from core.api.grpc import client -from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState -from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.api.grpc.wrappers import NodeType, Position +from core.emane.models.ieee80211abg import EmaneIeee80211abgModel # interface helper iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") @@ -10,45 +10,29 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001 core = client.CoreGrpcClient() core.connect() -# create session and get id -response = core.create_session() -session_id = response.session_id +# add session +session = core.create_session() -# change session state to configuration so that nodes get started when added -core.set_session_state(session_id, SessionState.CONFIGURATION) - -# create emane node +# create nodes position = Position(x=200, y=200) -emane = Node(type=NodeType.EMANE, position=position, emane=EmaneIeee80211abgModel.name) -response = core.add_node(session_id, emane) -emane_id = response.node_id - -# create node one +emane = session.add_node( + 1, _type=NodeType.EMANE, position=position, emane=EmaneIeee80211abgModel.name +) position = Position(x=100, y=100) -n1 = Node(type=NodeType.DEFAULT, position=position, model="mdr") -response = core.add_node(session_id, n1) -n1_id = response.node_id - -# create node two +node1 = session.add_node(2, model="mdr", position=position) position = Position(x=300, y=100) -n2 = Node(type=NodeType.DEFAULT, position=position, model="mdr") -response = core.add_node(session_id, n2) -n2_id = response.node_id +node2 = session.add_node(3, model="mdr", position=position) -# configure general emane settings -core.set_emane_config(session_id, {"eventservicettl": "2"}) +# create links +iface1 = iface_helper.create_iface(node1.id, 0) +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) -# configure emane model settings -# using a dict mapping currently support values as strings -core.set_emane_model_config( - session_id, emane_id, EmaneIeee80211abgModel.name, {"unicastrate": "3"} +# setup emane configurations using a dict mapping currently support values as strings +emane.set_emane_model( + EmaneIeee80211abgModel.name, {"eventservicettl": "2", "unicastrate": "3"} ) -# links nodes to emane -iface1 = iface_helper.create_iface(n1_id, 0) -core.add_link(session_id, n1_id, emane_id, iface1) -iface1 = iface_helper.create_iface(n2_id, 0) -core.add_link(session_id, n2_id, emane_id, iface1) - -# change session state -core.set_session_state(session_id, SessionState.INSTANTIATION) +# start session +core.start_session(session) diff --git a/daemon/examples/grpc/peertopeer.py b/daemon/examples/grpc/peertopeer.py index a5695b4b..d3c72dff 100644 --- a/daemon/examples/grpc/peertopeer.py +++ b/daemon/examples/grpc/peertopeer.py @@ -1,5 +1,5 @@ from core.api.grpc import client -from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState +from core.api.grpc.wrappers import Position # interface helper iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") @@ -8,29 +8,19 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001 core = client.CoreGrpcClient() core.connect() -# create session and get id -response = core.create_session() -session_id = response.session_id +# add session +session = core.create_session() -# change session state to configuration so that nodes get started when added -core.set_session_state(session_id, SessionState.CONFIGURATION) - -# create node one +# create nodes position = Position(x=100, y=100) -n1 = Node(type=NodeType.DEFAULT, position=position, model="PC") -response = core.add_node(session_id, n1) -n1_id = response.node_id - -# create node two +node1 = session.add_node(1, position=position) position = Position(x=300, y=100) -n2 = Node(type=NodeType.DEFAULT, position=position, model="PC") -response = core.add_node(session_id, n2) -n2_id = response.node_id +node2 = session.add_node(2, position=position) -# links nodes together -iface1 = iface_helper.create_iface(n1_id, 0) -iface2 = iface_helper.create_iface(n2_id, 0) -core.add_link(session_id, n1_id, n2_id, iface1, iface2) +# create link +iface1 = iface_helper.create_iface(node1.id, 0) +iface2 = iface_helper.create_iface(node2.id, 0) +session.add_link(node1=node1, node2=node2, iface1=iface1, iface2=iface2) -# change session state -core.set_session_state(session_id, SessionState.INSTANTIATION) +# start session +core.start_session(session) diff --git a/daemon/examples/grpc/switch.py b/daemon/examples/grpc/switch.py index f79f8544..1b693cb6 100644 --- a/daemon/examples/grpc/switch.py +++ b/daemon/examples/grpc/switch.py @@ -1,6 +1,5 @@ -# required imports from core.api.grpc import client -from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState +from core.api.grpc.wrappers import NodeType, Position # interface helper iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") @@ -9,36 +8,22 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001 core = client.CoreGrpcClient() core.connect() -# create session and get id -response = core.create_session() -session_id = response.session_id +# add session +session = core.create_session() -# change session state to configuration so that nodes get started when added -core.set_session_state(session_id, SessionState.CONFIGURATION) - -# create switch node +# create nodes position = Position(x=200, y=200) -switch = Node(type=NodeType.SWITCH, position=position) -response = core.add_node(session_id, switch) -switch_id = response.node_id - -# create node one +switch = session.add_node(1, _type=NodeType.SWITCH, position=position) position = Position(x=100, y=100) -n1 = Node(type=NodeType.DEFAULT, position=position, model="PC") -response = core.add_node(session_id, n1) -n1_id = response.node_id - -# create node two +node1 = session.add_node(2, position=position) position = Position(x=300, y=100) -n2 = Node(type=NodeType.DEFAULT, position=position, model="PC") -response = core.add_node(session_id, n2) -n2_id = response.node_id +node2 = session.add_node(3, position=position) -# links nodes to switch -iface1 = iface_helper.create_iface(n1_id, 0) -core.add_link(session_id, n1_id, switch_id, iface1) -iface1 = iface_helper.create_iface(n2_id, 0) -core.add_link(session_id, n2_id, switch_id, iface1) +# create links +iface1 = iface_helper.create_iface(node1.id, 0) +session.add_link(node1=node1, node2=switch, iface1=iface1) +iface1 = iface_helper.create_iface(node2.id, 0) +session.add_link(node1=node2, node2=switch, iface1=iface1) -# change session state -core.set_session_state(session_id, SessionState.INSTANTIATION) +# start session +core.start_session(session) diff --git a/daemon/examples/grpc/wlan.py b/daemon/examples/grpc/wlan.py index fa8ef9f6..fbffb2c3 100644 --- a/daemon/examples/grpc/wlan.py +++ b/daemon/examples/grpc/wlan.py @@ -1,6 +1,5 @@ -# required imports from core.api.grpc import client -from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState +from core.api.grpc.wrappers import NodeType, Position # interface helper iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") @@ -9,50 +8,34 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001 core = client.CoreGrpcClient() core.connect() -# create session and get id -response = core.create_session() -session_id = response.session_id +# add session +session = core.create_session() -# change session state to configuration so that nodes get started when added -core.set_session_state(session_id, SessionState.CONFIGURATION) - -# create wlan node +# create nodes position = Position(x=200, y=200) -wlan = Node(type=NodeType.WIRELESS_LAN, position=position) -response = core.add_node(session_id, wlan) -wlan_id = response.node_id - -# create node one +wlan = session.add_node(1, _type=NodeType.WIRELESS_LAN, position=position) position = Position(x=100, y=100) -n1 = Node(type=NodeType.DEFAULT, position=position, model="mdr") -response = core.add_node(session_id, n1) -n1_id = response.node_id - -# create node two +node1 = session.add_node(2, model="mdr", position=position) position = Position(x=300, y=100) -n2 = Node(type=NodeType.DEFAULT, position=position, model="mdr") -response = core.add_node(session_id, n2) -n2_id = response.node_id +node2 = session.add_node(3, model="mdr", position=position) -# configure wlan using a dict mapping currently +# create links +iface1 = iface_helper.create_iface(node1.id, 0) +session.add_link(node1=node1, node2=wlan, iface1=iface1) +iface1 = iface_helper.create_iface(node2.id, 0) +session.add_link(node1=node2, node2=wlan, iface1=iface1) + +# set wlan config using a dict mapping currently # support values as strings -core.set_wlan_config( - session_id, - wlan_id, +wlan.set_wlan( { "range": "280", "bandwidth": "55000000", "delay": "6000", "jitter": "5", "error": "5", - }, + } ) -# links nodes to wlan -iface1 = iface_helper.create_iface(n1_id, 0) -core.add_link(session_id, n1_id, wlan_id, iface1) -iface1 = iface_helper.create_iface(n2_id, 0) -core.add_link(session_id, n2_id, wlan_id, iface1) - -# change session state -core.set_session_state(session_id, SessionState.INSTANTIATION) +# start session +core.start_session(session) diff --git a/daemon/examples/myemane/examplemodel.py b/daemon/examples/myemane/examplemodel.py index b9e6e148..c33ac166 100644 --- a/daemon/examples/myemane/examplemodel.py +++ b/daemon/examples/myemane/examplemodel.py @@ -1,6 +1,7 @@ """ Example custom emane model. """ +from pathlib import Path from typing import Dict, List, Optional, Set from core.config import Configuration @@ -39,17 +40,34 @@ class ExampleModel(emanemodel.EmaneModel): name: str = "emane_example" mac_library: str = "rfpipemaclayer" - mac_xml: str = "/usr/share/emane/manifest/rfpipemaclayer.xml" + mac_xml: str = "rfpipemaclayer.xml" mac_defaults: Dict[str, str] = { "pcrcurveuri": "/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" } - mac_config: List[Configuration] = emanemanifest.parse(mac_xml, mac_defaults) + mac_config: List[Configuration] = [] phy_library: Optional[str] = None - phy_xml: str = "/usr/share/emane/manifest/emanephy.xml" + phy_xml: str = "emanephy.xml" phy_defaults: Dict[str, str] = { "subid": "1", "propagationmodel": "2ray", "noisemode": "none", } - phy_config: List[Configuration] = emanemanifest.parse(phy_xml, phy_defaults) + phy_config: List[Configuration] = [] config_ignore: Set[str] = set() + + @classmethod + def load(cls, emane_prefix: Path) -> None: + """ + 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" + # load mac configuration + 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 + cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults) diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py index 4421283f..cdc9cbc3 100644 --- a/daemon/examples/python/distributed_emane.py +++ b/daemon/examples/python/distributed_emane.py @@ -6,7 +6,7 @@ with the GUI. import argparse import logging -from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emane.models.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emulator.coreemu import CoreEmu from core.emulator.data import IpPrefixes, NodeOptions diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py index ae4f194b..0bcc0157 100644 --- a/daemon/examples/python/emane80211.py +++ b/daemon/examples/python/emane80211.py @@ -1,5 +1,5 @@ # required imports -from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emane.models.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emulator.coreemu import CoreEmu from core.emulator.data import IpPrefixes, NodeOptions diff --git a/daemon/poetry.lock b/daemon/poetry.lock index a1fce557..b29e8369 100644 --- a/daemon/poetry.lock +++ b/daemon/poetry.lock @@ -507,11 +507,11 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm [[package]] name = "pyyaml" -version = "5.3.1" +version = "5.4" description = "YAML parser and emitter for Python" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "six" @@ -1095,19 +1095,27 @@ pytest = [ {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, ] pyyaml = [ - {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, - {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, - {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, - {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, - {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, - {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, - {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, + {file = "PyYAML-5.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f7a21e3d99aa3095ef0553e7ceba36fb693998fbb1226f1392ce33681047465f"}, + {file = "PyYAML-5.4-cp27-cp27m-win32.whl", hash = "sha256:52bf0930903818e600ae6c2901f748bc4869c0c406056f679ab9614e5d21a166"}, + {file = "PyYAML-5.4-cp27-cp27m-win_amd64.whl", hash = "sha256:a36a48a51e5471513a5aea920cdad84cbd56d70a5057cca3499a637496ea379c"}, + {file = "PyYAML-5.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5e7ac4e0e79a53451dc2814f6876c2fa6f71452de1498bbe29c0b54b69a986f4"}, + {file = "PyYAML-5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc552b6434b90d9dbed6a4f13339625dc466fd82597119897e9489c953acbc22"}, + {file = "PyYAML-5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0dc9f2eb2e3c97640928dec63fd8dc1dd91e6b6ed236bd5ac00332b99b5c2ff9"}, + {file = "PyYAML-5.4-cp36-cp36m-win32.whl", hash = "sha256:5a3f345acff76cad4aa9cb171ee76c590f37394186325d53d1aa25318b0d4a09"}, + {file = "PyYAML-5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:f3790156c606299ff499ec44db422f66f05a7363b39eb9d5b064f17bd7d7c47b"}, + {file = "PyYAML-5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:124fd7c7bc1e95b1eafc60825f2daf67c73ce7b33f1194731240d24b0d1bf628"}, + {file = "PyYAML-5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8b818b6c5a920cbe4203b5a6b14256f0e5244338244560da89b7b0f1313ea4b6"}, + {file = "PyYAML-5.4-cp37-cp37m-win32.whl", hash = "sha256:737bd70e454a284d456aa1fa71a0b429dd527bcbf52c5c33f7c8eee81ac16b89"}, + {file = "PyYAML-5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:7242790ab6c20316b8e7bb545be48d7ed36e26bbe279fd56f2c4a12510e60b4b"}, + {file = "PyYAML-5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cc547d3ead3754712223abb7b403f0a184e4c3eae18c9bb7fd15adef1597cc4b"}, + {file = "PyYAML-5.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8635d53223b1f561b081ff4adecb828fd484b8efffe542edcfdff471997f7c39"}, + {file = "PyYAML-5.4-cp38-cp38-win32.whl", hash = "sha256:26fcb33776857f4072601502d93e1a619f166c9c00befb52826e7b774efaa9db"}, + {file = "PyYAML-5.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2243dd033fd02c01212ad5c601dafb44fbb293065f430b0d3dbf03f3254d615"}, + {file = "PyYAML-5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:31ba07c54ef4a897758563e3a0fcc60077698df10180abe4b8165d9895c00ebf"}, + {file = "PyYAML-5.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:02c78d77281d8f8d07a255e57abdbf43b02257f59f50cc6b636937d68efa5dd0"}, + {file = "PyYAML-5.4-cp39-cp39-win32.whl", hash = "sha256:fdc6b2cb4b19e431994f25a9160695cc59a4e861710cc6fc97161c5e845fc579"}, + {file = "PyYAML-5.4-cp39-cp39-win_amd64.whl", hash = "sha256:8bf38641b4713d77da19e91f8b5296b832e4db87338d6aeffe422d42f1ca896d"}, + {file = "PyYAML-5.4.tar.gz", hash = "sha256:3c49e39ac034fd64fd576d63bb4db53cda89b362768a67f07749d55f128ac18a"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, diff --git a/daemon/proto/core/api/grpc/configservices.proto b/daemon/proto/core/api/grpc/configservices.proto index f1272df8..189a2892 100644 --- a/daemon/proto/core/api/grpc/configservices.proto +++ b/daemon/proto/core/api/grpc/configservices.proto @@ -39,14 +39,6 @@ message ConfigMode { map config = 2; } -message GetConfigServicesRequest { - int32 session_id = 1; -} - -message GetConfigServicesResponse { - repeated ConfigService services = 1; -} - message GetConfigServiceDefaultsRequest { string name = 1; } @@ -57,14 +49,6 @@ message GetConfigServiceDefaultsResponse { repeated ConfigMode modes = 3; } -message GetNodeConfigServiceConfigsRequest { - int32 session_id = 1; -} - -message GetNodeConfigServiceConfigsResponse { - repeated ConfigServiceConfig configs = 1; -} - message GetNodeConfigServiceRequest { int32 session_id = 1; int32 node_id = 2; @@ -74,23 +58,3 @@ message GetNodeConfigServiceRequest { message GetNodeConfigServiceResponse { map config = 1; } - -message GetNodeConfigServicesRequest { - int32 session_id = 1; - int32 node_id = 2; -} - -message GetNodeConfigServicesResponse { - repeated string services = 1; -} - -message SetNodeConfigServiceRequest { - int32 session_id = 1; - int32 node_id = 2; - string name = 3; - map config = 4; -} - -message SetNodeConfigServiceResponse { - bool result = 1; -} diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index cdc89202..1986dcef 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -25,24 +25,6 @@ service CoreApi { } rpc CheckSession (CheckSessionRequest) returns (CheckSessionResponse) { } - rpc GetSessionOptions (GetSessionOptionsRequest) returns (GetSessionOptionsResponse) { - } - rpc SetSessionOptions (SetSessionOptionsRequest) returns (SetSessionOptionsResponse) { - } - rpc SetSessionMetadata (SetSessionMetadataRequest) returns (SetSessionMetadataResponse) { - } - rpc GetSessionMetadata (GetSessionMetadataRequest) returns (GetSessionMetadataResponse) { - } - rpc GetSessionLocation (GetSessionLocationRequest) returns (GetSessionLocationResponse) { - } - rpc SetSessionLocation (SetSessionLocationRequest) returns (SetSessionLocationResponse) { - } - rpc SetSessionState (SetSessionStateRequest) returns (SetSessionStateResponse) { - } - rpc SetSessionUser (SetSessionUserRequest) returns (SetSessionUserResponse) { - } - rpc AddSessionServer (AddSessionServerRequest) returns (AddSessionServerResponse) { - } rpc SessionAlert (SessionAlertRequest) returns (SessionAlertResponse) { } @@ -67,12 +49,12 @@ service CoreApi { } rpc GetNodeTerminal (GetNodeTerminalRequest) returns (GetNodeTerminalResponse) { } + rpc MoveNode (MoveNodeRequest) returns (MoveNodeResponse) { + } rpc MoveNodes (stream MoveNodesRequest) returns (MoveNodesResponse) { } // link rpc - rpc GetNodeLinks (GetNodeLinksRequest) returns (GetNodeLinksResponse) { - } rpc AddLink (AddLinkRequest) returns (AddLinkResponse) { } rpc EditLink (EditLinkRequest) returns (EditLinkResponse) { @@ -80,15 +62,7 @@ service CoreApi { rpc DeleteLink (DeleteLinkRequest) returns (DeleteLinkResponse) { } - // hook rpc - rpc GetHooks (GetHooksRequest) returns (GetHooksResponse) { - } - rpc AddHook (AddHookRequest) returns (AddHookResponse) { - } - // mobility rpc - rpc GetMobilityConfigs (mobility.GetMobilityConfigsRequest) returns (mobility.GetMobilityConfigsResponse) { - } rpc GetMobilityConfig (mobility.GetMobilityConfigRequest) returns (mobility.GetMobilityConfigResponse) { } rpc SetMobilityConfig (mobility.SetMobilityConfigRequest) returns (mobility.SetMobilityConfigResponse) { @@ -97,42 +71,24 @@ service CoreApi { } // service rpc - rpc GetServices (services.GetServicesRequest) returns (services.GetServicesResponse) { - } rpc GetServiceDefaults (services.GetServiceDefaultsRequest) returns (services.GetServiceDefaultsResponse) { } rpc SetServiceDefaults (services.SetServiceDefaultsRequest) returns (services.SetServiceDefaultsResponse) { } - rpc GetNodeServiceConfigs (services.GetNodeServiceConfigsRequest) returns (services.GetNodeServiceConfigsResponse) { - } rpc GetNodeService (services.GetNodeServiceRequest) returns (services.GetNodeServiceResponse) { } rpc GetNodeServiceFile (services.GetNodeServiceFileRequest) returns (services.GetNodeServiceFileResponse) { } - rpc SetNodeService (services.SetNodeServiceRequest) returns (services.SetNodeServiceResponse) { - } - rpc SetNodeServiceFile (services.SetNodeServiceFileRequest) returns (services.SetNodeServiceFileResponse) { - } rpc ServiceAction (services.ServiceActionRequest) returns (services.ServiceActionResponse) { } // config services - rpc GetConfigServices (configservices.GetConfigServicesRequest) returns (configservices.GetConfigServicesResponse) { - } rpc GetConfigServiceDefaults (configservices.GetConfigServiceDefaultsRequest) returns (configservices.GetConfigServiceDefaultsResponse) { } - rpc GetNodeConfigServiceConfigs (configservices.GetNodeConfigServiceConfigsRequest) returns (configservices.GetNodeConfigServiceConfigsResponse) { - } rpc GetNodeConfigService (configservices.GetNodeConfigServiceRequest) returns (configservices.GetNodeConfigServiceResponse) { } - rpc GetNodeConfigServices (configservices.GetNodeConfigServicesRequest) returns (configservices.GetNodeConfigServicesResponse) { - } - rpc SetNodeConfigService (configservices.SetNodeConfigServiceRequest) returns (configservices.SetNodeConfigServiceResponse) { - } // wlan rpc - rpc GetWlanConfigs (wlan.GetWlanConfigsRequest) returns (wlan.GetWlanConfigsResponse) { - } rpc GetWlanConfig (wlan.GetWlanConfigRequest) returns (wlan.GetWlanConfigResponse) { } rpc SetWlanConfig (wlan.SetWlanConfigRequest) returns (wlan.SetWlanConfigResponse) { @@ -141,22 +97,16 @@ service CoreApi { } // emane rpc - rpc GetEmaneConfig (emane.GetEmaneConfigRequest) returns (emane.GetEmaneConfigResponse) { - } - rpc SetEmaneConfig (emane.SetEmaneConfigRequest) returns (emane.SetEmaneConfigResponse) { - } - rpc GetEmaneModels (emane.GetEmaneModelsRequest) returns (emane.GetEmaneModelsResponse) { - } rpc GetEmaneModelConfig (emane.GetEmaneModelConfigRequest) returns (emane.GetEmaneModelConfigResponse) { } rpc SetEmaneModelConfig (emane.SetEmaneModelConfigRequest) returns (emane.SetEmaneModelConfigResponse) { } - rpc GetEmaneModelConfigs (emane.GetEmaneModelConfigsRequest) returns (emane.GetEmaneModelConfigsResponse) { - } rpc GetEmaneEventChannel (emane.GetEmaneEventChannelRequest) returns (emane.GetEmaneEventChannelResponse) { } rpc EmanePathlosses (stream emane.EmanePathlossesRequest) returns (emane.EmanePathlossesResponse) { } + rpc EmaneLink (emane.EmaneLinkRequest) returns (emane.EmaneLinkResponse) { + } // xml rpc rpc SaveXml (SaveXmlRequest) returns (SaveXmlResponse) { @@ -167,27 +117,28 @@ service CoreApi { // utilities rpc GetInterfaces (GetInterfacesRequest) returns (GetInterfacesResponse) { } - rpc EmaneLink (emane.EmaneLinkRequest) returns (emane.EmaneLinkResponse) { - } rpc ExecuteScript (ExecuteScriptRequest) returns (ExecuteScriptResponse) { } + + // globals + rpc GetConfig (GetConfigRequest) returns (GetConfigResponse) { + } } // rpc request/response messages +message GetConfigRequest { +} + +message GetConfigResponse { + repeated services.Service services = 1; + repeated configservices.ConfigService config_services = 2; + repeated string emane_models = 3; +} + + message StartSessionRequest { - int32 session_id = 1; - repeated Node nodes = 2; - 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; + Session session = 1; + bool definition = 2; } message StartSessionResponse { @@ -208,8 +159,7 @@ message CreateSessionRequest { } message CreateSessionResponse { - int32 session_id = 1; - SessionState.Enum state = 2; + Session session = 1; } message DeleteSessionRequest { @@ -243,85 +193,6 @@ message GetSessionResponse { Session session = 1; } -message GetSessionOptionsRequest { - int32 session_id = 1; -} - -message GetSessionOptionsResponse { - map config = 2; -} - -message SetSessionOptionsRequest { - int32 session_id = 1; - map config = 2; -} - -message SetSessionOptionsResponse { - bool result = 1; -} - -message SetSessionMetadataRequest { - int32 session_id = 1; - map config = 2; -} - -message SetSessionMetadataResponse { - bool result = 1; -} - -message GetSessionMetadataRequest { - int32 session_id = 1; -} - -message GetSessionMetadataResponse { - map config = 1; -} - -message GetSessionLocationRequest { - int32 session_id = 1; -} - -message GetSessionLocationResponse { - SessionLocation location = 1; -} - -message SetSessionLocationRequest { - int32 session_id = 1; - SessionLocation location = 2; -} - -message SetSessionLocationResponse { - bool result = 1; -} - -message SetSessionStateRequest { - int32 session_id = 1; - SessionState.Enum state = 2; -} - -message SetSessionStateResponse { - bool result = 1; -} - -message SetSessionUserRequest { - int32 session_id = 1; - string user = 2; -} - -message SetSessionUserResponse { - bool result = 1; -} - -message AddSessionServerRequest { - int32 session_id = 1; - string name = 2; - string host = 3; -} - -message AddSessionServerResponse { - bool result = 1; -} - message SessionAlertRequest { int32 session_id = 1; ExceptionLevel.Enum level = 2; @@ -454,15 +325,14 @@ message GetNodeRequest { message GetNodeResponse { Node node = 1; repeated Interface ifaces = 2; + repeated Link links = 3; } message EditNodeRequest { int32 session_id = 1; int32 node_id = 2; - Position position = 3; - string icon = 4; - string source = 5; - Geo geo = 6; + string icon = 3; + string source = 4; } message EditNodeResponse { @@ -488,6 +358,21 @@ message GetNodeTerminalResponse { string terminal = 1; } + +message MoveNodeRequest { + int32 session_id = 1; + int32 node_id = 2; + string source = 3; + oneof move_type { + Position position = 4; + Geo geo = 5; + } +} + +message MoveNodeResponse { + bool result = 1; +} + message MoveNodesRequest { int32 session_id = 1; int32 node_id = 2; @@ -514,15 +399,6 @@ message NodeCommandResponse { int32 return_code = 2; } -message GetNodeLinksRequest { - int32 session_id = 1; - int32 node_id = 2; -} - -message GetNodeLinksResponse { - repeated Link links = 1; -} - message AddLinkRequest { int32 session_id = 1; Link link = 2; @@ -562,23 +438,6 @@ message DeleteLinkResponse { bool result = 1; } -message GetHooksRequest { - int32 session_id = 1; -} - -message GetHooksResponse { - repeated Hook hooks = 1; -} - -message AddHookRequest { - int32 session_id = 1; - Hook hook = 2; -} - -message AddHookResponse { - bool result = 1; -} - message SaveXmlRequest { int32 session_id = 1; } @@ -607,6 +466,7 @@ message GetInterfacesResponse { message ExecuteScriptRequest { string script = 1; + string args = 2; } message ExecuteScriptResponse { @@ -718,15 +578,10 @@ message Session { repeated services.ServiceDefaults default_services = 7; SessionLocation location = 8; repeated Hook hooks = 9; - repeated string emane_models = 10; - map emane_config = 11; - repeated emane.GetEmaneModelConfig emane_model_configs = 12; - map wlan_configs = 13; - repeated services.NodeServiceConfig service_configs = 14; - repeated configservices.ConfigServiceConfig config_service_configs = 15; - map mobility_configs = 16; - map metadata = 17; - string file = 18; + map metadata = 10; + string file = 11; + map options = 12; + repeated Server servers = 13; } message SessionSummary { @@ -753,6 +608,11 @@ message Node { string dir = 13; string channel = 14; int32 canvas = 15; + map wlan_config = 16; + map mobility_config = 17; + map service_configs = 18; + map config_service_configs= 19; + repeated emane.NodeEmaneConfig emane_configs = 20; } message Link { @@ -817,3 +677,8 @@ message Geo { float lon = 2; float alt = 3; } + +message Server { + string name = 1; + string host = 2; +} diff --git a/daemon/proto/core/api/grpc/emane.proto b/daemon/proto/core/api/grpc/emane.proto index ad6a22ca..b8579917 100644 --- a/daemon/proto/core/api/grpc/emane.proto +++ b/daemon/proto/core/api/grpc/emane.proto @@ -4,31 +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 GetEmaneModelsRequest { - int32 session_id = 1; -} - -message GetEmaneModelsResponse { - repeated string models = 1; -} - message GetEmaneModelConfigRequest { int32 session_id = 1; int32 node_id = 2; @@ -49,10 +24,6 @@ message SetEmaneModelConfigResponse { bool result = 1; } -message GetEmaneModelConfigsRequest { - int32 session_id = 1; -} - message GetEmaneModelConfig { int32 node_id = 1; string model = 2; @@ -60,12 +31,15 @@ message GetEmaneModelConfig { map config = 4; } -message GetEmaneModelConfigsResponse { - repeated GetEmaneModelConfig configs = 1; +message NodeEmaneConfig { + int32 iface_id = 1; + string model = 2; + map config = 3; } message GetEmaneEventChannelRequest { int32 session_id = 1; + int32 nem_id = 2; } message GetEmaneEventChannelResponse { diff --git a/daemon/proto/core/api/grpc/mobility.proto b/daemon/proto/core/api/grpc/mobility.proto index abfad8ef..6eaf8fc3 100644 --- a/daemon/proto/core/api/grpc/mobility.proto +++ b/daemon/proto/core/api/grpc/mobility.proto @@ -17,14 +17,6 @@ message MobilityConfig { map config = 2; } -message GetMobilityConfigsRequest { - int32 session_id = 1; -} - -message GetMobilityConfigsResponse { - map configs = 1; -} - message GetMobilityConfigRequest { int32 session_id = 1; int32 node_id = 2; diff --git a/daemon/proto/core/api/grpc/services.proto b/daemon/proto/core/api/grpc/services.proto index cf6d9cbf..dc451c40 100644 --- a/daemon/proto/core/api/grpc/services.proto +++ b/daemon/proto/core/api/grpc/services.proto @@ -66,14 +66,6 @@ message NodeServiceConfig { map files = 4; } -message GetServicesRequest { - -} - -message GetServicesResponse { - repeated Service services = 1; -} - message GetServiceDefaultsRequest { int32 session_id = 1; } @@ -91,14 +83,6 @@ message SetServiceDefaultsResponse { bool result = 1; } -message GetNodeServiceConfigsRequest { - int32 session_id = 1; -} - -message GetNodeServiceConfigsResponse { - repeated NodeServiceConfig configs = 1; -} - message GetNodeServiceRequest { int32 session_id = 1; int32 node_id = 2; @@ -120,24 +104,6 @@ message GetNodeServiceFileResponse { string data = 1; } -message SetNodeServiceRequest { - int32 session_id = 1; - ServiceConfig config = 2; -} - -message SetNodeServiceResponse { - bool result = 1; -} - -message SetNodeServiceFileRequest { - int32 session_id = 1; - ServiceFileConfig config = 2; -} - -message SetNodeServiceFileResponse { - bool result = 1; -} - message ServiceActionRequest { int32 session_id = 1; int32 node_id = 2; diff --git a/daemon/proto/core/api/grpc/wlan.proto b/daemon/proto/core/api/grpc/wlan.proto index 9605d633..2d161a04 100644 --- a/daemon/proto/core/api/grpc/wlan.proto +++ b/daemon/proto/core/api/grpc/wlan.proto @@ -9,14 +9,6 @@ message WlanConfig { map config = 2; } -message GetWlanConfigsRequest { - int32 session_id = 1; -} - -message GetWlanConfigsResponse { - map configs = 1; -} - message GetWlanConfigRequest { int32 session_id = 1; int32 node_id = 2; diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index f5ead843..18a4464e 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "core" -version = "7.5.2" +version = "8.0.0" description = "CORE Common Open Research Emulator" authors = ["Boeing Research and Technology"] license = "BSD-2-Clause" @@ -27,7 +27,7 @@ netaddr = "0.7.19" pillow = "8.3.2" protobuf = "3.12.2" pyproj = "2.6.1.post1" -pyyaml = "5.3.1" +pyyaml = "5.4" [tool.poetry.dev-dependencies] black = "==19.3b0" diff --git a/daemon/scripts/core-cleanup b/daemon/scripts/core-cleanup index 8182a917..ced76634 100755 --- a/daemon/scripts/core-cleanup +++ b/daemon/scripts/core-cleanup @@ -61,10 +61,11 @@ 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);} ' -ebtables -L FORWARD | awk ' - /^-.*b\./ {print "removing ebtables " $0; system("ebtables -D FORWARD " $0); print "removing ebtables chain " $4; system("ebtables -X " $4);} +nft list ruleset | awk ' + $3 ~ /^b\./ {print "removing nftables " $3; system("nft delete table bridge " $3);} ' rm -rf /tmp/pycore* diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index 92fe8a3c..fbbe6ede 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -9,25 +9,24 @@ from argparse import ( ) from functools import wraps from pathlib import Path -from typing import Any, Optional, Tuple +from typing import Optional, Tuple import grpc import netaddr -from google.protobuf.json_format import MessageToJson from netaddr import EUI, AddrFormatError, IPNetwork from core.api.grpc.client import CoreGrpcClient -from core.api.grpc.core_pb2 import ( +from core.api.grpc.wrappers import ( Geo, Interface, + Link, LinkOptions, Node, NodeType, Position, - SessionState, ) -NODE_TYPES = [k for k, v in NodeType.Enum.items() if v != NodeType.PEER_TO_PEER] +NODE_TYPES = [x for x in NodeType if x != NodeType.PEER_TO_PEER] def coreclient(func): @@ -105,11 +104,11 @@ def file_type(value: str) -> str: def get_current_session(core: CoreGrpcClient, session_id: Optional[int]) -> int: if session_id: return session_id - response = core.get_sessions() - if not response.sessions: + sessions = core.get_sessions() + if not sessions: print("no current session to interact with") sys.exit(1) - return response.sessions[0].id + return sessions[0].id def create_iface(iface_id: int, mac: str, ip4_net: IPNetwork, ip6_net: IPNetwork) -> Interface: @@ -137,24 +136,16 @@ def print_iface(iface: Interface) -> None: print(f"{iface.id:<3} | {iface.mac:<17} | {iface_ip4:<18} | {iface_ip6}") -def print_json(message: Any) -> None: - json = MessageToJson(message, preserving_proto_field_name=True) - print(json) - - @coreclient def get_wlan_config(core: CoreGrpcClient, args: Namespace) -> None: session_id = get_current_session(core, args.session) - response = core.get_wlan_config(session_id, args.node) - if args.json: - print_json(response) - else: - size = 0 - for option in response.config.values(): - size = max(size, len(option.name)) - print(f"{'Name':<{size}.{size}} | Value") - for option in response.config.values(): - print(f"{option.name:<{size}.{size}} | {option.value}") + config = core.get_wlan_config(session_id, args.node) + size = 0 + for option in config.values(): + size = max(size, len(option.name)) + print(f"{'Name':<{size}.{size}} | Value") + for option in config.values(): + print(f"{option.name:<{size}.{size}} | {option.value}") @coreclient @@ -171,110 +162,87 @@ def set_wlan_config(core: CoreGrpcClient, args: Namespace) -> None: config["jitter"] = str(args.jitter) if args.range: config["range"] = str(args.range) - response = core.set_wlan_config(session_id, args.node, config) - if args.json: - print_json(response) - else: - print(f"set wlan config: {response.result}") + result = core.set_wlan_config(session_id, args.node, config) + print(f"set wlan config: {result}") @coreclient def open_xml(core: CoreGrpcClient, args: Namespace) -> None: - response = core.open_xml(args.file, args.start) - if args.json: - print_json(response) - else: - print(f"opened xml: {response.result}") + result, session_id = core.open_xml(args.file, args.start) + print(f"opened xml: {result},{session_id}") @coreclient def query_sessions(core: CoreGrpcClient, args: Namespace) -> None: - response = core.get_sessions() - if args.json: - print_json(response) - else: - print("Session ID | Session State | Nodes") - for s in response.sessions: - state = SessionState.Enum.Name(s.state) - print(f"{s.id:<10} | {state:<13} | {s.nodes}") + sessions = core.get_sessions() + print("Session ID | Session State | Nodes") + for session in sessions: + print(f"{session.id:<10} | {session.state.name:<13} | {session.nodes}") @coreclient def query_session(core: CoreGrpcClient, args: Namespace) -> None: - response = core.get_session(args.id) - if args.json: - print_json(response) - else: - print("Nodes") - print("Node ID | Node Name | Node Type") - names = {} - for node in response.session.nodes: - names[node.id] = node.name - node_type = NodeType.Enum.Name(node.type) - print(f"{node.id:<7} | {node.name:<9} | {node_type}") - - print("\nLinks") - for link in response.session.links: - n1 = names[link.node1_id] - n2 = names[link.node2_id] - print(f"Node | ", end="") - print_iface_header() - print(f"{n1:<6} | ", end="") - if link.HasField("iface1"): - print_iface(link.iface1) - else: - print() - print(f"{n2:<6} | ", end="") - if link.HasField("iface2"): - print_iface(link.iface2) - else: - print() + session = core.get_session(args.id) + print("Nodes") + print("Node ID | Node Name | Node Type") + for node in session.nodes.values(): + print(f"{node.id:<7} | {node.name:<9} | {node.type.name}") + print("\nLinks") + for link in session.links: + n1 = session.nodes[link.node1_id].name + n2 = session.nodes[link.node2_id].name + print(f"Node | ", end="") + print_iface_header() + print(f"{n1:<6} | ", end="") + if link.iface1: + print_iface(link.iface1) + else: print() + print(f"{n2:<6} | ", end="") + if link.iface2: + print_iface(link.iface2) + else: + print() + print() @coreclient def query_node(core: CoreGrpcClient, args: Namespace) -> None: - names = {} - response = core.get_session(args.id) - for node in response.session.nodes: - names[node.id] = node.name - - response = core.get_node(args.id, args.node) - if args.json: - print_json(response) - else: - node = response.node - node_type = NodeType.Enum.Name(node.type) - print("ID | Name | Type") - print(f"{node.id:<4} | {node.name:<7} | {node_type}") + session = core.get_session(args.id) + node, ifaces, _ = core.get_node(args.id, args.node) + print("ID | Name | Type | XY") + xy_pos = f"{int(node.position.x)},{int(node.position.y)}" + print(f"{node.id:<4} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos}") + if node.geo: + print("Geo") + print(f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}") + if ifaces: print("Interfaces") print("Connected To | ", end="") print_iface_header() - for iface in response.ifaces: + for iface in ifaces: if iface.net_id == node.id: if iface.node_id: - name = names[iface.node_id] + name = session.nodes[iface.node_id].name else: - name = names[iface.net2_id] + name = session.nodes[iface.net2_id].name else: - name = names.get(iface.net_id, "") + net_node = session.nodes.get(iface.net_id) + name = net_node.name if net_node else "" print(f"{name:<12} | ", end="") print_iface(iface) @coreclient def delete_session(core: CoreGrpcClient, args: Namespace) -> None: - response = core.delete_session(args.id) - if args.json: - print_json(response) - else: - print(f"delete session({args.id}): {response.result}") + result = core.delete_session(args.id) + print(f"delete session({args.id}): {result}") @coreclient def add_node(core: CoreGrpcClient, args: Namespace) -> None: session_id = get_current_session(core, args.session) - node_type = NodeType.Enum.Value(args.type) + node_type = NodeType[args.type] pos = None if args.pos: x, y = args.pos @@ -294,15 +262,19 @@ def add_node(core: CoreGrpcClient, args: Namespace) -> None: position=pos, geo=geo, ) - response = core.add_node(session_id, node) - if args.json: - print_json(response) - else: - print(f"created node: {response.node_id}") + node_id = core.add_node(session_id, node) + print(f"created node: {node_id}") @coreclient def edit_node(core: CoreGrpcClient, args: Namespace) -> None: + session_id = get_current_session(core, args.session) + result = core.edit_node(session_id, args.id, args.icon) + print(f"edit node: {result}") + + +@coreclient +def move_node(core: CoreGrpcClient, args: Namespace) -> None: session_id = get_current_session(core, args.session) pos = None if args.pos: @@ -312,21 +284,15 @@ def edit_node(core: CoreGrpcClient, args: Namespace) -> None: if args.geo: lon, lat, alt = args.geo geo = Geo(lon=lon, lat=lat, alt=alt) - response = core.edit_node(session_id, args.id, pos, args.icon, geo) - if args.json: - print_json(response) - else: - print(f"edit node: {response.result}") + result = core.move_node(session_id, args.id, pos, geo) + print(f"move node: {result}") @coreclient def delete_node(core: CoreGrpcClient, args: Namespace) -> None: session_id = get_current_session(core, args.session) - response = core.delete_node(session_id, args.id) - if args.json: - print_json(response) - else: - print(f"deleted node: {response.result}") + result = core.delete_node(session_id, args.id) + print(f"deleted node: {result}") @coreclient @@ -346,11 +312,9 @@ def add_link(core: CoreGrpcClient, args: Namespace) -> None: dup=args.duplicate, unidirectional=args.uni, ) - response = core.add_link(session_id, args.node1, args.node2, iface1, iface2, options) - if args.json: - print_json(response) - else: - print(f"add link: {response.result}") + link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options) + result, _, _ = core.add_link(session_id, link) + print(f"add link: {result}") @coreclient @@ -364,23 +328,21 @@ def edit_link(core: CoreGrpcClient, args: Namespace) -> None: dup=args.duplicate, unidirectional=args.uni, ) - response = core.edit_link( - session_id, args.node1, args.node2, options, args.iface1, args.iface2 - ) - if args.json: - print_json(response) - else: - print(f"edit link: {response.result}") + iface1 = Interface(args.iface1) + iface2 = Interface(args.iface2) + link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options) + result = core.edit_link(session_id, link) + print(f"edit link: {result}") @coreclient def delete_link(core: CoreGrpcClient, args: Namespace) -> None: session_id = get_current_session(core, args.session) - response = core.delete_link(session_id, args.node1, args.node2, args.iface1, args.iface2) - if args.json: - print_json(response) - else: - print(f"delete link: {response.result}") + iface1 = Interface(args.iface1) + iface2 = Interface(args.iface2) + link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2) + result = core.delete_link(session_id, link) + print(f"delete link: {result}") def setup_sessions_parser(parent: _SubParsersAction) -> None: @@ -422,13 +384,18 @@ def setup_node_parser(parent: _SubParsersAction) -> None: edit_parser = subparsers.add_parser("edit", help="edit a node") edit_parser.formatter_class = ArgumentDefaultsHelpFormatter - edit_parser.add_argument("-i", "--id", type=int, help="id to use, optional") - group = edit_parser.add_mutually_exclusive_group(required=True) - group.add_argument("-p", "--pos", type=position_type, help="x,y position") - group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position") + edit_parser.add_argument("-i", "--id", type=int, help="id to use", required=True) edit_parser.add_argument("-ic", "--icon", help="icon to use, optional") edit_parser.set_defaults(func=edit_node) + move_parser = subparsers.add_parser("move", help="move a node") + move_parser.formatter_class = ArgumentDefaultsHelpFormatter + move_parser.add_argument("-i", "--id", type=int, help="id to use, optional", required=True) + group = move_parser.add_mutually_exclusive_group(required=True) + group.add_argument("-p", "--pos", type=position_type, help="x,y position") + group.add_argument("-g", "--geo", type=geo_type, help="lon,lat,alt position") + move_parser.set_defaults(func=move_node) + delete_parser = subparsers.add_parser("delete", help="delete a node") delete_parser.formatter_class = ArgumentDefaultsHelpFormatter delete_parser.add_argument("-i", "--id", type=int, help="node id", required=True) @@ -544,9 +511,6 @@ def setup_wlan_parser(parent: _SubParsersAction) -> None: def main() -> None: parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) - parser.add_argument( - "-js", "--json", action="store_true", help="print responses to terminal as json" - ) subparsers = parser.add_subparsers(help="supported commands") subparsers.required = True subparsers.dest = "command" diff --git a/daemon/scripts/core-daemon b/daemon/scripts/core-daemon index 16b0ac59..0ff4ca77 100755 --- a/daemon/scripts/core-daemon +++ b/daemon/scripts/core-daemon @@ -12,6 +12,7 @@ import sys import threading import time from configparser import ConfigParser +from pathlib import Path from core import constants from core.api.grpc.server import CoreGrpcServer @@ -21,6 +22,8 @@ from core.api.tlv.enumerations import CORE_API_PORT from core.constants import CORE_CONF_DIR, COREDPY_VERSION from core.utils import close_onexec, load_logging_config +logger = logging.getLogger(__name__) + def banner(): """ @@ -28,7 +31,7 @@ def banner(): :return: nothing """ - logging.info("CORE daemon v.%s started %s", constants.COREDPY_VERSION, time.ctime()) + logger.info("CORE daemon v.%s started %s", constants.COREDPY_VERSION, time.ctime()) def start_udp(mainserver, server_address): @@ -61,7 +64,7 @@ def cored(cfg): address = (host, port) server = CoreServer(address, CoreHandler, cfg) except: - logging.exception("error starting main server on: %s:%s", host, port) + logger.exception("error starting main server on: %s:%s", host, port) sys.exit(1) # initialize grpc api @@ -78,7 +81,7 @@ def cored(cfg): # close handlers close_onexec(server.fileno()) - logging.info("CORE TLV API TCP/UDP listening on: %s:%s", host, port) + logger.info("CORE TLV API TCP/UDP listening on: %s:%s", host, port) server.serve_forever() @@ -148,12 +151,13 @@ def main(): :return: nothing """ cfg = get_merged_config(f"{CORE_CONF_DIR}/core.conf") - load_logging_config(cfg["logfile"]) + log_config_path = Path(cfg["logfile"]) + load_logging_config(log_config_path) banner() try: cored(cfg) except KeyboardInterrupt: - logging.info("keyboard interrupt, stopping core daemon") + logger.info("keyboard interrupt, stopping core daemon") if __name__ == "__main__": diff --git a/daemon/scripts/core-pygui b/daemon/scripts/core-gui similarity index 88% rename from daemon/scripts/core-pygui rename to daemon/scripts/core-gui index 7bb44125..ff7795a3 100755 --- a/daemon/scripts/core-pygui +++ b/daemon/scripts/core-gui @@ -6,17 +6,21 @@ from logging.handlers import TimedRotatingFileHandler from core.gui import appconfig, images from core.gui.app import Application -if __name__ == "__main__": + +def main() -> None: # parse flags parser = argparse.ArgumentParser(description=f"CORE Python GUI") parser.add_argument("-l", "--level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], default="INFO", help="logging level") parser.add_argument("-p", "--proxy", action="store_true", help="enable proxy") parser.add_argument("-s", "--session", type=int, help="session id to join") + parser.add_argument("--create-dir", action="store_true", help="create gui directory and exit") args = parser.parse_args() # check home directory exists and create if necessary appconfig.check_directory() + if args.create_dir: + return # setup logging log_format = "%(asctime)s - %(levelname)s - %(module)s:%(funcName)s - %(message)s" @@ -30,3 +34,7 @@ if __name__ == "__main__": images.load_all() app = Application(args.proxy, args.session) app.mainloop() + + +if __name__ == "__main__": + main() diff --git a/daemon/scripts/core-imn-to-xml b/daemon/scripts/core-imn-to-xml index 495093ed..c11533a4 100755 --- a/daemon/scripts/core-imn-to-xml +++ b/daemon/scripts/core-imn-to-xml @@ -61,7 +61,7 @@ if __name__ == "__main__": client = CoreGrpcClient() with client.context_connect(): print(f"saving xml {xml_file.resolve()}") - client.save_xml(session_id, xml_file) + client.save_xml(session_id, str(xml_file)) print(f"deleting session {session_id}") client.delete_session(session_id) diff --git a/daemon/scripts/core-route-monitor b/daemon/scripts/core-route-monitor index d644ae1b..bc61f6fa 100755 --- a/daemon/scripts/core-route-monitor +++ b/daemon/scripts/core-route-monitor @@ -16,7 +16,7 @@ import grpc from core import utils from core.api.grpc.client import CoreGrpcClient -from core.api.grpc.core_pb2 import NodeType +from core.api.grpc.wrappers import NodeType SDT_HOST = "127.0.0.1" SDT_PORT = 50000 @@ -60,15 +60,15 @@ class SdtClient: class RouterMonitor: def __init__( - self, - session: int, - src: str, - dst: str, - pkt: str, - rate: int, - dead: int, - sdt_host: str, - sdt_port: int, + self, + session: int, + src: str, + dst: str, + pkt: str, + rate: int, + dead: int, + sdt_host: str, + sdt_port: int, ) -> None: self.queue = Queue() self.core = CoreGrpcClient() @@ -92,16 +92,15 @@ class RouterMonitor: self.session = self.get_session() print("session: ", self.session) try: - response = self.core.get_session(self.session) - nodes = response.session.nodes + session = self.core.get_session(self.session) node_map = {} - for node in nodes: + for node in session.nodes.values(): if node.type != NodeType.DEFAULT: continue node_map[node.id] = node.channel if self.src_id is None: - response = self.core.get_node(self.session, node.id) - for iface in response.ifaces: + _, ifaces, _ = self.core.get_node(self.session, node.id) + for iface in ifaces: if self.src == iface.ip4: self.src_id = node.id break @@ -117,8 +116,7 @@ class RouterMonitor: return node_map def get_session(self) -> int: - response = self.core.get_sessions() - sessions = response.sessions + sessions = self.core.get_sessions() session = None if sessions: session = sessions[0] diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py index a558fcec..98552540 100644 --- a/daemon/tests/conftest.py +++ b/daemon/tests/conftest.py @@ -60,7 +60,7 @@ def patcher(request): patch_manager.patch_obj( LinuxNetClient, "get_mac", return_value="00:00:00:00:00:00" ) - patch_manager.patch_obj(CoreNode, "nodefile") + patch_manager.patch_obj(CoreNode, "create_file") patch_manager.patch_obj(Session, "write_state") patch_manager.patch_obj(Session, "write_nodes") yield patch_manager @@ -78,6 +78,7 @@ def global_coreemu(patcher): def global_session(request, patcher, global_coreemu): mkdir = not request.config.getoption("mock") session = Session(1000, {"emane_prefix": "/usr"}, mkdir) + session.service_manager = global_coreemu.service_manager yield session session.shutdown() diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py index ccbfb446..5cb14bdc 100644 --- a/daemon/tests/emane/test_emane.py +++ b/daemon/tests/emane/test_emane.py @@ -1,7 +1,7 @@ """ Unit tests for testing CORE EMANE networks. """ -import os +from pathlib import Path from tempfile import TemporaryFile from typing import Type from xml.etree import ElementTree @@ -9,13 +9,13 @@ from xml.etree import ElementTree import pytest from core import utils -from core.emane.bypass import EmaneBypassModel -from core.emane.commeffect import EmaneCommEffectModel from core.emane.emanemodel import EmaneModel -from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emane.models.bypass import EmaneBypassModel +from core.emane.models.commeffect import EmaneCommEffectModel +from core.emane.models.ieee80211abg import EmaneIeee80211abgModel +from core.emane.models.rfpipe import EmaneRfPipeModel +from core.emane.models.tdma import EmaneTdmaModel from core.emane.nodes import EmaneNet -from core.emane.rfpipe import EmaneRfPipeModel -from core.emane.tdma import EmaneTdmaModel from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.session import Session from core.errors import CoreCommandError, CoreError @@ -28,7 +28,8 @@ _EMANE_MODELS = [ EmaneCommEffectModel, EmaneTdmaModel, ] -_DIR = os.path.dirname(os.path.abspath(__file__)) +_DIR: Path = Path(__file__).resolve().parent +_SCHEDULE: Path = _DIR / "../../examples/tdma/schedule.xml" def ping( @@ -99,17 +100,14 @@ class TestEmane: # create emane node for networking the core nodes session.set_location(47.57917, -122.13232, 2.00000, 1.0) - options = NodeOptions() + options = NodeOptions(emane=model.name) options.set_position(80, 50) emane_network = session.add_node(EmaneNet, options=options) - session.emane.set_model(emane_network, model) # configure tdma if model == EmaneTdmaModel: - session.emane.set_model_config( - emane_network.id, - EmaneTdmaModel.name, - {"schedule": os.path.join(_DIR, "../../examples/tdma/schedule.xml")}, + session.emane.set_config( + emane_network.id, EmaneTdmaModel.name, {"schedule": str(_SCHEDULE)} ) # create nodes @@ -143,13 +141,13 @@ class TestEmane: """ # create emane node for networking the core nodes session.set_location(47.57917, -122.13232, 2.00000, 1.0) - options = NodeOptions() + options = NodeOptions(emane=EmaneIeee80211abgModel.name) options.set_position(80, 50) emane_network = session.add_node(EmaneNet, options=options) config_key = "txpower" config_value = "10" - session.emane.set_model( - emane_network, EmaneIeee80211abgModel, {config_key: config_value} + session.emane.set_config( + emane_network.id, EmaneIeee80211abgModel.name, {config_key: config_value} ) # create nodes @@ -175,7 +173,7 @@ class TestEmane: # save xml xml_file = tmpdir.join("session.xml") file_path = xml_file.strpath - session.save_xml(file_path) + session.save_xml(Path(file_path)) # verify xml file was created and can be parsed assert xml_file.isfile() @@ -191,12 +189,11 @@ class TestEmane: assert not session.get_node(node2_id, CoreNode) # load saved xml - session.open_xml(file_path, start=True) + session.open_xml(Path(file_path), start=True) # retrieve configuration we set originally - value = str( - session.emane.get_config(config_key, emane_id, EmaneIeee80211abgModel.name) - ) + config = session.emane.get_config(emane_id, EmaneIeee80211abgModel.name) + value = config[config_key] # verify nodes and configuration were restored assert session.get_node(node1_id, CoreNode) @@ -222,9 +219,9 @@ class TestEmane: session.add_link(node1.id, emane_node.id, iface1_data) session.add_link(node2.id, emane_node.id, iface2_data) - # set node specific conifg + # set node specific config datarate = "101" - session.emane.set_model_config( + session.emane.set_config( node1.id, EmaneRfPipeModel.name, {"datarate": datarate} ) @@ -234,7 +231,7 @@ class TestEmane: # save xml xml_file = tmpdir.join("session.xml") file_path = xml_file.strpath - session.save_xml(file_path) + session.save_xml(Path(file_path)) # verify xml file was created and can be parsed assert xml_file.isfile() @@ -252,7 +249,7 @@ class TestEmane: assert not session.get_node(emane_node.id, EmaneNet) # load saved xml - session.open_xml(file_path, start=True) + session.open_xml(Path(file_path), start=True) # verify nodes have been recreated assert session.get_node(node1.id, CoreNode) @@ -263,7 +260,7 @@ class TestEmane: node = session.nodes[node_id] links += node.links() assert len(links) == 2 - config = session.emane.get_model_config(node1.id, EmaneRfPipeModel.name) + config = session.emane.get_config(node1.id, EmaneRfPipeModel.name) assert config["datarate"] == datarate def test_xml_emane_interface_config( @@ -287,7 +284,7 @@ class TestEmane: # set node specific conifg datarate = "101" config_id = utils.iface_config_id(node1.id, iface1_data.id) - session.emane.set_model_config( + session.emane.set_config( config_id, EmaneRfPipeModel.name, {"datarate": datarate} ) @@ -297,7 +294,7 @@ class TestEmane: # save xml xml_file = tmpdir.join("session.xml") file_path = xml_file.strpath - session.save_xml(file_path) + session.save_xml(Path(file_path)) # verify xml file was created and can be parsed assert xml_file.isfile() @@ -315,7 +312,7 @@ class TestEmane: assert not session.get_node(emane_node.id, EmaneNet) # load saved xml - session.open_xml(file_path, start=True) + session.open_xml(Path(file_path), start=True) # verify nodes have been recreated assert session.get_node(node1.id, CoreNode) @@ -326,5 +323,5 @@ class TestEmane: node = session.nodes[node_id] links += node.links() assert len(links) == 2 - config = session.emane.get_model_config(config_id, EmaneRfPipeModel.name) + config = session.emane.get_config(config_id, EmaneRfPipeModel.name) assert config["datarate"] == datarate diff --git a/daemon/tests/test_conf.py b/daemon/tests/test_conf.py index e90acfbd..2c74841d 100644 --- a/daemon/tests/test_conf.py +++ b/daemon/tests/test_conf.py @@ -1,13 +1,12 @@ import pytest from core.config import ( + ConfigString, ConfigurableManager, ConfigurableOptions, - Configuration, ModelManager, ) -from core.emane.ieee80211abg import EmaneIeee80211abgModel -from core.emulator.enumerations import ConfigDataTypes +from core.emane.models.ieee80211abg import EmaneIeee80211abgModel from core.emulator.session import Session from core.location.mobility import BasicRangeModel from core.nodes.network import WlanNode @@ -16,10 +15,7 @@ from core.nodes.network import WlanNode class TestConfigurableOptions(ConfigurableOptions): name1 = "value1" name2 = "value2" - options = [ - Configuration(_id=name1, _type=ConfigDataTypes.STRING, label=name1), - Configuration(_id=name2, _type=ConfigDataTypes.STRING, label=name2), - ] + options = [ConfigString(id=name1, label=name1), ConfigString(id=name2, label=name2)] class TestConf: diff --git a/daemon/tests/test_config_services.py b/daemon/tests/test_config_services.py index eaba4d47..876b7f32 100644 --- a/daemon/tests/test_config_services.py +++ b/daemon/tests/test_config_services.py @@ -1,14 +1,14 @@ +from pathlib import Path from unittest import mock import pytest -from core.config import Configuration +from core.config import ConfigBool, ConfigString from core.configservice.base import ( ConfigService, ConfigServiceBootError, ConfigServiceMode, ) -from core.emulator.enumerations import ConfigDataTypes from core.errors import CoreCommandError, CoreError TEMPLATE_TEXT = "echo hello" @@ -26,13 +26,10 @@ class MyService(ConfigService): shutdown = [f"pkill {files[0]}"] validation_mode = ConfigServiceMode.BLOCKING default_configs = [ - Configuration(_id="value1", _type=ConfigDataTypes.STRING, label="Text"), - Configuration(_id="value2", _type=ConfigDataTypes.BOOL, label="Boolean"), - Configuration( - _id="value3", - _type=ConfigDataTypes.STRING, - label="Multiple Choice", - options=["value1", "value2", "value3"], + ConfigString(id="value1", label="Text"), + ConfigBool(id="value2", label="Boolean"), + ConfigString( + id="value3", label="Multiple Choice", options=["value1", "value2", "value3"] ), ] modes = { @@ -68,7 +65,8 @@ class TestConfigServices: service.create_dirs() # then - node.privatedir.assert_called_with(MyService.directories[0]) + directory = Path(MyService.directories[0]) + node.create_dir.assert_called_with(directory) def test_create_files_custom(self): # given @@ -81,7 +79,8 @@ class TestConfigServices: service.create_files() # then - node.nodefile.assert_called_with(MyService.files[0], text) + file_path = Path(MyService.files[0]) + node.create_file.assert_called_with(file_path, text) def test_create_files_text(self): # given @@ -92,7 +91,8 @@ class TestConfigServices: service.create_files() # then - node.nodefile.assert_called_with(MyService.files[0], TEMPLATE_TEXT) + file_path = Path(MyService.files[0]) + node.create_file.assert_called_with(file_path, TEMPLATE_TEXT) def test_run_startup(self): # given diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index c4465863..1342861b 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -2,9 +2,9 @@ Unit tests for testing basic CORE networks. """ -import os import threading -from typing import Type +from pathlib import Path +from typing import List, Type import pytest @@ -16,9 +16,9 @@ from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNode, NodeBase from core.nodes.network import HubNode, PtpNet, SwitchNode, WlanNode -_PATH = os.path.abspath(os.path.dirname(__file__)) -_MOBILITY_FILE = os.path.join(_PATH, "mobility.scen") -_WIRED = [PtpNet, HubNode, SwitchNode] +_PATH: Path = Path(__file__).resolve().parent +_MOBILITY_FILE: Path = _PATH / "mobility.scen" +_WIRED: List = [PtpNet, HubNode, SwitchNode] def ping(from_node: CoreNode, to_node: CoreNode, ip_prefixes: IpPrefixes): @@ -195,7 +195,7 @@ class TestCore: # configure mobility script for session config = { - "file": _MOBILITY_FILE, + "file": str(_MOBILITY_FILE), "refresh_ms": "50", "loop": "1", "autostart": "0.0", diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index a4efd6d9..575a502d 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -1,4 +1,5 @@ import time +from pathlib import Path from queue import Queue from tempfile import TemporaryFile from typing import Optional @@ -7,19 +8,35 @@ import grpc import pytest from mock import patch -from core.api.grpc import core_pb2 -from core.api.grpc.client import CoreGrpcClient, InterfaceHelper -from core.api.grpc.emane_pb2 import EmaneModelConfig -from core.api.grpc.mobility_pb2 import MobilityAction, MobilityConfig +from core.api.grpc import core_pb2, wrappers +from core.api.grpc.client import CoreGrpcClient, InterfaceHelper, MoveNodesStreamer from core.api.grpc.server import CoreGrpcServer -from core.api.grpc.services_pb2 import ServiceAction, ServiceConfig, ServiceFileConfig -from core.api.grpc.wlan_pb2 import WlanConfig +from core.api.grpc.wrappers import ( + ConfigOption, + ConfigOptionType, + EmaneModelConfig, + Event, + Geo, + Hook, + Interface, + Link, + LinkOptions, + MobilityAction, + Node, + NodeServiceData, + NodeType, + Position, + ServiceAction, + ServiceValidationMode, + SessionLocation, + SessionState, +) from core.api.tlv.dataconversion import ConfigShim from core.api.tlv.enumerations import ConfigFlags -from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emane.models.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emulator.data import EventData, IpPrefixes, NodeData, NodeOptions -from core.emulator.enumerations import EventTypes, ExceptionLevels, NodeTypes +from core.emulator.enumerations import EventTypes, ExceptionLevels from core.errors import CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNode @@ -28,36 +45,27 @@ from core.xml.corexml import CoreXmlWriter class TestGrpc: - def test_start_session(self, grpc_server: CoreGrpcServer): + @pytest.mark.parametrize("definition", [False, True]) + def test_start_session(self, grpc_server: CoreGrpcServer, definition): # given client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - position = core_pb2.Position(x=50, y=100) - node1 = core_pb2.Node(id=1, position=position, model="PC") - position = core_pb2.Position(x=100, y=100) - node2 = core_pb2.Node(id=2, position=position, model="PC") - position = core_pb2.Position(x=200, y=200) - wlan_node = core_pb2.Node( - id=3, type=NodeTypes.WIRELESS_LAN.value, position=position - ) - nodes = [node1, node2, wlan_node] + with client.context_connect(): + session = client.create_session() + position = Position(x=50, y=100) + node1 = session.add_node(1, position=position) + position = Position(x=100, y=100) + node2 = session.add_node(2, position=position) + position = Position(x=200, y=200) + wlan_node = session.add_node(3, _type=NodeType.WIRELESS_LAN, position=position) iface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16") iface1_id = 0 iface1 = iface_helper.create_iface(node1.id, iface1_id) iface2_id = 0 iface2 = iface_helper.create_iface(node2.id, iface2_id) - link = core_pb2.Link( - type=core_pb2.LinkType.WIRED, - node1_id=node1.id, - node2_id=node2.id, - iface1=iface1, - iface2=iface2, - ) - links = [link] - hook = core_pb2.Hook( - state=core_pb2.SessionState.RUNTIME, file="echo.sh", data="echo hello" - ) - hooks = [hook] + link = Link(node1_id=node1.id, node2_id=node2.id, iface1=iface1, iface2=iface2) + session.links = [link] + hook = Hook(state=SessionState.RUNTIME, file="echo.sh", data="echo hello") + session.hooks = {hook.file: hook} location_x = 5 location_y = 10 location_z = 15 @@ -65,7 +73,7 @@ class TestGrpc: location_lon = 30 location_alt = 40 location_scale = 5 - location = core_pb2.SessionLocation( + session.location = SessionLocation( x=location_x, y=location_y, z=location_z, @@ -74,93 +82,88 @@ class TestGrpc: alt=location_alt, scale=location_scale, ) - emane_config_key = "platform_id_start" - emane_config_value = "2" - emane_config = {emane_config_key: emane_config_value} - model_node_id = 20 - model_config_key = "bandwidth" - model_config_value = "500000" - model_config = EmaneModelConfig( - node_id=model_node_id, - iface_id=-1, - model=EmaneIeee80211abgModel.name, - config={model_config_key: model_config_value}, - ) - model_configs = [model_config] + + # setup wlan config wlan_config_key = "range" wlan_config_value = "333" - wlan_config = WlanConfig( - node_id=wlan_node.id, config={wlan_config_key: wlan_config_value} - ) - wlan_configs = [wlan_config] + wlan_node.set_wlan({wlan_config_key: wlan_config_value}) + + # setup mobility config mobility_config_key = "refresh_ms" mobility_config_value = "60" - mobility_config = MobilityConfig( - node_id=wlan_node.id, config={mobility_config_key: mobility_config_value} + wlan_node.set_mobility({mobility_config_key: mobility_config_value}) + + # setup service config + service_name = "DefaultRoute" + service_validate = ["echo hello"] + node1.service_configs[service_name] = NodeServiceData( + executables=[], + dependencies=[], + dirs=[], + configs=[], + startup=[], + validate=service_validate, + validation_mode=ServiceValidationMode.NON_BLOCKING, + validation_timer=0, + shutdown=[], + meta="", ) - mobility_configs = [mobility_config] - service_config = ServiceConfig( - node_id=node1.id, service="DefaultRoute", validate=["echo hello"] - ) - service_configs = [service_config] - service_file_config = ServiceFileConfig( - node_id=node1.id, - service="DefaultRoute", - file="defaultroute.sh", - data="echo hello", - ) - service_file_configs = [service_file_config] + + # setup service file config + service_file = "defaultroute.sh" + service_file_data = "echo hello" + node1.service_file_configs[service_name] = {service_file: service_file_data} + + # setup session option + option_key = "controlnet" + option_value = "172.16.0.0/24" + session.set_options({option_key: option_value}) # when with patch.object(CoreXmlWriter, "write"): with client.context_connect(): - client.start_session( - session.id, - nodes, - links, - location, - hooks, - emane_config, - model_configs, - wlan_configs, - mobility_configs, - service_configs, - service_file_configs, - ) + client.start_session(session, definition=definition) # then - assert node1.id in session.nodes - assert node2.id in session.nodes - assert wlan_node.id in session.nodes - assert iface1_id in session.nodes[node1.id].ifaces - assert iface2_id in session.nodes[node2.id].ifaces - hook_file, hook_data = session.hooks[EventTypes.RUNTIME_STATE][0] + real_session = grpc_server.coreemu.sessions[session.id] + if definition: + state = EventTypes.DEFINITION_STATE + else: + state = EventTypes.RUNTIME_STATE + assert real_session.state == state + assert node1.id in real_session.nodes + assert node2.id in real_session.nodes + assert wlan_node.id in real_session.nodes + assert iface1_id in real_session.nodes[node1.id].ifaces + assert iface2_id in real_session.nodes[node2.id].ifaces + hook_file, hook_data = real_session.hooks[EventTypes.RUNTIME_STATE][0] assert hook_file == hook.file assert hook_data == hook.data - assert session.location.refxyz == (location_x, location_y, location_z) - assert session.location.refgeo == (location_lat, location_lon, location_alt) - assert session.location.refscale == location_scale - assert session.emane.get_config(emane_config_key) == emane_config_value - set_wlan_config = session.mobility.get_model_config( + assert real_session.location.refxyz == (location_x, location_y, location_z) + assert real_session.location.refgeo == ( + location_lat, + location_lon, + location_alt, + ) + assert real_session.location.refscale == location_scale + set_wlan_config = real_session.mobility.get_model_config( wlan_node.id, BasicRangeModel.name ) assert set_wlan_config[wlan_config_key] == wlan_config_value - set_mobility_config = session.mobility.get_model_config( + set_mobility_config = real_session.mobility.get_model_config( wlan_node.id, Ns2ScriptedMobility.name ) assert set_mobility_config[mobility_config_key] == mobility_config_value - set_model_config = session.emane.get_model_config( - model_node_id, EmaneIeee80211abgModel.name + service = real_session.services.get_service( + node1.id, service_name, default_service=True ) - assert set_model_config[model_config_key] == model_config_value - service = session.services.get_service( - node1.id, service_config.service, default_service=True + assert service.validate == tuple(service_validate) + real_node1 = real_session.get_node(node1.id, CoreNode) + service_file = real_session.services.get_service_file( + real_node1, service_name, service_file ) - assert service.validate == tuple(service_config.validate) - service_file = session.services.get_service_file( - node1, service_file_config.service, service_file_config.file - ) - assert service_file.data == service_file_config.data + assert service_file.data == service_file_data + assert option_value == real_session.options.get_config(option_key) @pytest.mark.parametrize("session_id", [None, 6013]) def test_create_session( @@ -171,16 +174,14 @@ class TestGrpc: # when with client.context_connect(): - response = client.create_session(session_id) + created_session = client.create_session(session_id) # then - assert isinstance(response.session_id, int) - assert isinstance(response.state, int) - session = grpc_server.coreemu.sessions.get(response.session_id) + assert isinstance(created_session, wrappers.Session) + session = grpc_server.coreemu.sessions.get(created_session.id) assert session is not None - assert session.state == EventTypes(response.state) if session_id is not None: - assert response.session_id == session_id + assert created_session.id == session_id assert session.id == session_id @pytest.mark.parametrize("session_id, expected", [(None, True), (6013, False)]) @@ -195,10 +196,10 @@ class TestGrpc: # then with client.context_connect(): - response = client.delete_session(session_id) + result = client.delete_session(session_id) # then - assert response.result is expected + assert result is expected assert grpc_server.coreemu.sessions.get(session_id) is None def test_get_session(self, grpc_server: CoreGrpcServer): @@ -210,12 +211,12 @@ class TestGrpc: # then with client.context_connect(): - response = client.get_session(session.id) + session = client.get_session(session.id) # then - assert response.session.state == core_pb2.SessionState.DEFINITION - assert len(response.session.nodes) == 1 - assert len(response.session.links) == 0 + assert session.state == SessionState.DEFINITION + assert len(session.nodes) == 1 + assert len(session.links) == 0 def test_get_sessions(self, grpc_server: CoreGrpcServer): # given @@ -224,136 +225,17 @@ class TestGrpc: # then with client.context_connect(): - response = client.get_sessions() + sessions = client.get_sessions() # then found_session = None - for current_session in response.sessions: + for current_session in sessions: if current_session.id == session.id: found_session = current_session break - assert len(response.sessions) == 1 + assert len(sessions) == 1 assert found_session is not None - def test_get_session_options(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - - # then - with client.context_connect(): - response = client.get_session_options(session.id) - - # then - assert len(response.config) > 0 - - def test_get_session_location(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - - # then - with client.context_connect(): - response = client.get_session_location(session.id) - - # then - assert response.location.scale == 1.0 - assert response.location.x == 0 - assert response.location.y == 0 - assert response.location.z == 0 - assert response.location.lat == 0 - assert response.location.lon == 0 - assert response.location.alt == 0 - - def test_set_session_location(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - - # then - scale = 2 - xyz = (1, 1, 1) - lat_lon_alt = (1, 1, 1) - with client.context_connect(): - response = client.set_session_location( - session.id, - x=xyz[0], - y=xyz[1], - z=xyz[2], - lat=lat_lon_alt[0], - lon=lat_lon_alt[1], - alt=lat_lon_alt[2], - scale=scale, - ) - - # then - assert response.result is True - assert session.location.refxyz == xyz - assert session.location.refscale == scale - assert session.location.refgeo == lat_lon_alt - - def test_set_session_options(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - - # then - option = "enablerj45" - value = "1" - with client.context_connect(): - response = client.set_session_options(session.id, {option: value}) - - # then - assert response.result is True - assert session.options.get_config(option) == value - config = session.options.get_configs() - assert len(config) > 0 - - def test_set_session_metadata(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - - # then - key = "meta1" - value = "value1" - with client.context_connect(): - response = client.set_session_metadata(session.id, {key: value}) - - # then - assert response.result is True - assert session.metadata[key] == value - - def test_get_session_metadata(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - key = "meta1" - value = "value1" - session.metadata[key] = value - - # then - with client.context_connect(): - response = client.get_session_metadata(session.id) - - # then - assert response.config[key] == value - - def test_set_session_state(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - - # then - with client.context_connect(): - response = client.set_session_state( - session.id, core_pb2.SessionState.DEFINITION - ) - - # then - assert response.result is True - assert session.state == EventTypes.DEFINITION_STATE - def test_add_node(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() @@ -361,12 +243,13 @@ class TestGrpc: # then with client.context_connect(): - node = core_pb2.Node() - response = client.add_node(session.id, node) + position = Position(x=0, y=0) + node = Node(id=1, name="n1", type=NodeType.DEFAULT, position=position) + node_id = client.add_node(session.id, node) # then - assert response.node_id is not None - assert session.get_node(response.node_id, CoreNode) is not None + assert node_id is not None + assert session.get_node(node_id, CoreNode) is not None def test_get_node(self, grpc_server: CoreGrpcServer): # given @@ -376,27 +259,70 @@ class TestGrpc: # then with client.context_connect(): - response = client.get_node(session.id, node.id) + get_node, ifaces, links = client.get_node(session.id, node.id) # then - assert response.node.id == node.id + assert node.id == get_node.id + assert len(ifaces) == 0 + assert len(links) == 0 + + def test_move_node_pos(self, grpc_server: CoreGrpcServer): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + node = session.add_node(CoreNode) + position = Position(x=100.0, y=50.0) + + # then + with client.context_connect(): + result = client.move_node(session.id, node.id, position=position) + + # then + assert result is True + assert node.position.x == position.x + assert node.position.y == position.y + + def test_move_node_geo(self, grpc_server: CoreGrpcServer): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + node = session.add_node(CoreNode) + geo = Geo(lon=0.0, lat=0.0, alt=0.0) + + # then + with client.context_connect(): + result = client.move_node(session.id, node.id, geo=geo) + + # then + assert result is True + assert node.position.lon == geo.lon + assert node.position.lat == geo.lat + assert node.position.alt == geo.alt + + def test_move_node_exception(self, grpc_server: CoreGrpcServer): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + node = session.add_node(CoreNode) + + # then and when + with pytest.raises(CoreError), client.context_connect(): + client.move_node(session.id, node.id) def test_edit_node(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() node = session.add_node(CoreNode) + icon = "test.png" # then - x, y = 10, 10 with client.context_connect(): - position = core_pb2.Position(x=x, y=y) - response = client.edit_node(session.id, node.id, position) + result = client.edit_node(session.id, node.id, icon) # then - assert response.result is True - assert node.position.x == x - assert node.position.y == y + assert result is True + assert node.icon == icon @pytest.mark.parametrize("node_id, expected", [(1, True), (2, False)]) def test_delete_node( @@ -409,10 +335,10 @@ class TestGrpc: # then with client.context_connect(): - response = client.delete_node(session.id, node_id) + result = client.delete_node(session.id, node_id) # then - assert response.result is expected + assert result is expected if expected is True: with pytest.raises(CoreError): assert session.get_node(node.id, CoreNode) @@ -428,15 +354,16 @@ class TestGrpc: options = NodeOptions(model="Host") node = session.add_node(CoreNode, options=options) session.instantiate() - output = "hello world" + expected_output = "hello world" + expected_status = 0 # then - command = f"echo {output}" + command = f"echo {expected_output}" with client.context_connect(): - response = client.node_command(session.id, node.id, command) + output = client.node_command(session.id, node.id, command) # then - assert response.output == output + assert (expected_status, expected_output) == output def test_get_node_terminal(self, grpc_server: CoreGrpcServer): # given @@ -449,45 +376,10 @@ class TestGrpc: # then with client.context_connect(): - response = client.get_node_terminal(session.id, node.id) + terminal = client.get_node_terminal(session.id, node.id) # then - assert response.terminal is not None - - def test_get_hooks(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - file_name = "test" - file_data = "echo hello" - session.add_hook(EventTypes.RUNTIME_STATE, file_name, file_data) - - # then - with client.context_connect(): - response = client.get_hooks(session.id) - - # then - assert len(response.hooks) == 1 - hook = response.hooks[0] - assert hook.state == core_pb2.SessionState.RUNTIME - assert hook.file == file_name - assert hook.data == file_data - - def test_add_hook(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - - # then - file_name = "test" - file_data = "echo hello" - with client.context_connect(): - response = client.add_hook( - session.id, core_pb2.SessionState.RUNTIME, file_name, file_data - ) - - # then - assert response.result is True + assert terminal is not None def test_save_xml(self, grpc_server: CoreGrpcServer, tmpdir: TemporaryFile): # given @@ -506,79 +398,48 @@ class TestGrpc: # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - tmp = tmpdir.join("text.xml") - session.save_xml(str(tmp)) + tmp = Path(tmpdir.join("text.xml")) + session.save_xml(tmp) # then with client.context_connect(): - response = client.open_xml(str(tmp)) + result, session_id = client.open_xml(tmp) # then - assert response.result is True - assert response.session_id is not None + assert result is True + assert session_id is not None - def test_get_node_links(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - switch = session.add_node(SwitchNode) - node = session.add_node(CoreNode) - iface_data = ip_prefixes.create_iface(node) - session.add_link(node.id, switch.id, iface_data) - - # then - with client.context_connect(): - response = client.get_node_links(session.id, switch.id) - - # then - assert len(response.links) == 1 - - def test_get_node_links_exception( - self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes - ): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - switch = session.add_node(SwitchNode) - node = session.add_node(CoreNode) - iface_data = ip_prefixes.create_iface(node) - session.add_link(node.id, switch.id, iface_data) - - # then - with pytest.raises(grpc.RpcError): - with client.context_connect(): - client.get_node_links(session.id, 3) - - def test_add_link(self, grpc_server: CoreGrpcServer, iface_helper: InterfaceHelper): + def test_add_link(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() switch = session.add_node(SwitchNode) node = session.add_node(CoreNode) assert len(switch.links()) == 0 + iface = InterfaceHelper("10.0.0.0/24").create_iface(node.id, 0) + link = Link(node.id, switch.id, iface1=iface) # then - iface = iface_helper.create_iface(node.id, 0) with client.context_connect(): - response = client.add_link(session.id, node.id, switch.id, iface) + result, iface1, _ = client.add_link(session.id, link) # then - assert response.result is True + assert result is True assert len(switch.links()) == 1 + assert iface1.id == iface.id + assert iface1.ip4 == iface.ip4 - def test_add_link_exception( - self, grpc_server: CoreGrpcServer, iface_helper: InterfaceHelper - ): + def test_add_link_exception(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() node = session.add_node(CoreNode) # then - iface = iface_helper.create_iface(node.id, 0) + link = Link(node.id, 3) with pytest.raises(grpc.RpcError): with client.context_connect(): - client.add_link(session.id, 1, 3, iface) + client.add_link(session.id, link) def test_edit_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): # given @@ -588,18 +449,17 @@ class TestGrpc: node = session.add_node(CoreNode) iface = ip_prefixes.create_iface(node) session.add_link(node.id, switch.id, iface) - options = core_pb2.LinkOptions(bandwidth=30000) + options = LinkOptions(bandwidth=30000) link = switch.links()[0] assert options.bandwidth != link.options.bandwidth + link = Link(node.id, switch.id, iface1=Interface(id=iface.id), options=options) # then with client.context_connect(): - response = client.edit_link( - session.id, node.id, switch.id, options, iface1_id=iface.id - ) + result = client.edit_link(session.id, link) # then - assert response.result is True + assert result is True link = switch.links()[0] assert options.bandwidth == link.options.bandwidth @@ -619,15 +479,19 @@ class TestGrpc: link_node = node break assert len(link_node.links()) == 1 + link = Link( + node1.id, + node2.id, + iface1=Interface(id=iface1.id), + iface2=Interface(id=iface2.id), + ) # then with client.context_connect(): - response = client.delete_link( - session.id, node1.id, node2.id, iface1.id, iface2.id - ) + result = client.delete_link(session.id, link) # then - assert response.result is True + assert result is True assert len(link_node.links()) == 0 def test_get_wlan_config(self, grpc_server: CoreGrpcServer): @@ -638,10 +502,10 @@ class TestGrpc: # then with client.context_connect(): - response = client.get_wlan_config(session.id, wlan.id) + config = client.get_wlan_config(session.id, wlan.id) # then - assert len(response.config) > 0 + assert len(config) > 0 def test_set_wlan_config(self, grpc_server: CoreGrpcServer): # given @@ -656,7 +520,7 @@ class TestGrpc: # then with client.context_connect(): - response = client.set_wlan_config( + result = client.set_wlan_config( session.id, wlan.id, { @@ -670,66 +534,11 @@ class TestGrpc: ) # then - assert response.result is True + assert result is True config = session.mobility.get_model_config(wlan.id, BasicRangeModel.name) 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(): - response = client.get_emane_config(session.id) - - # then - assert len(response.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(): - response = client.set_emane_config(session.id, {config_key: config_value}) - - # then - assert response.result is True - config = session.emane.get_configs() - assert len(config) > 1 - assert config[config_key] == config_value - - def test_get_emane_model_configs(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - session.set_location(47.57917, -122.13232, 2.00000, 1.0) - options = NodeOptions(emane=EmaneIeee80211abgModel.name) - emane_network = session.add_node(EmaneNet, options=options) - session.emane.set_model(emane_network, EmaneIeee80211abgModel) - config_key = "platform_id_start" - config_value = "2" - session.emane.set_model_config( - emane_network.id, EmaneIeee80211abgModel.name, {config_key: config_value} - ) - - # then - with client.context_connect(): - response = client.get_emane_model_configs(session.id) - - # then - assert len(response.configs) == 1 - model_config = response.configs[0] - assert emane_network.id == model_config.node_id - assert model_config.model == EmaneIeee80211abgModel.name - assert len(model_config.config) > 0 - assert model_config.iface_id == -1 - def test_set_emane_model_config(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() @@ -737,24 +546,27 @@ class TestGrpc: session.set_location(47.57917, -122.13232, 2.00000, 1.0) options = NodeOptions(emane=EmaneIeee80211abgModel.name) emane_network = session.add_node(EmaneNet, options=options) - session.emane.set_model(emane_network, EmaneIeee80211abgModel) + session.emane.node_models[emane_network.id] = EmaneIeee80211abgModel.name config_key = "bandwidth" config_value = "900000" + option = ConfigOption( + label=config_key, + name=config_key, + value=config_value, + type=ConfigOptionType.INT32, + group="Default", + ) + config = EmaneModelConfig( + emane_network.id, EmaneIeee80211abgModel.name, config={config_key: option} + ) # then with client.context_connect(): - response = client.set_emane_model_config( - session.id, - emane_network.id, - EmaneIeee80211abgModel.name, - {config_key: config_value}, - ) + result = client.set_emane_model_config(session.id, config) # then - assert response.result is True - config = session.emane.get_model_config( - emane_network.id, EmaneIeee80211abgModel.name - ) + assert result is True + config = session.emane.get_config(emane_network.id, EmaneIeee80211abgModel.name) assert config[config_key] == config_value def test_get_emane_model_config(self, grpc_server: CoreGrpcServer): @@ -764,45 +576,16 @@ class TestGrpc: session.set_location(47.57917, -122.13232, 2.00000, 1.0) options = NodeOptions(emane=EmaneIeee80211abgModel.name) emane_network = session.add_node(EmaneNet, options=options) - session.emane.set_model(emane_network, EmaneIeee80211abgModel) + session.emane.node_models[emane_network.id] = EmaneIeee80211abgModel.name # then with client.context_connect(): - response = client.get_emane_model_config( + config = client.get_emane_model_config( session.id, emane_network.id, EmaneIeee80211abgModel.name ) # then - assert len(response.config) > 0 - - def test_get_emane_models(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - - # then - with client.context_connect(): - response = client.get_emane_models(session.id) - - # then - assert len(response.models) > 0 - - def test_get_mobility_configs(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - wlan = session.add_node(WlanNode) - session.mobility.set_model_config(wlan.id, Ns2ScriptedMobility.name, {}) - - # then - with client.context_connect(): - response = client.get_mobility_configs(session.id) - - # then - assert len(response.configs) > 0 - assert wlan.id in response.configs - mapped_config = response.configs[wlan.id] - assert len(mapped_config.config) > 0 + assert len(config) > 0 def test_get_mobility_config(self, grpc_server: CoreGrpcServer): # given @@ -813,10 +596,10 @@ class TestGrpc: # then with client.context_connect(): - response = client.get_mobility_config(session.id, wlan.id) + config = client.get_mobility_config(session.id, wlan.id) # then - assert len(response.config) > 0 + assert len(config) > 0 def test_set_mobility_config(self, grpc_server: CoreGrpcServer): # given @@ -828,12 +611,12 @@ class TestGrpc: # then with client.context_connect(): - response = client.set_mobility_config( + result = client.set_mobility_config( session.id, wlan.id, {config_key: config_value} ) # then - assert response.result is True + assert result is True config = session.mobility.get_model_config(wlan.id, Ns2ScriptedMobility.name) assert config[config_key] == config_value @@ -847,21 +630,10 @@ class TestGrpc: # then with client.context_connect(): - response = client.mobility_action(session.id, wlan.id, MobilityAction.STOP) + result = client.mobility_action(session.id, wlan.id, MobilityAction.STOP) # then - assert response.result is True - - def test_get_services(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - - # then - with client.context_connect(): - response = client.get_services() - - # then - assert len(response.services) > 0 + assert result is True def test_get_service_defaults(self, grpc_server: CoreGrpcServer): # given @@ -870,10 +642,10 @@ class TestGrpc: # then with client.context_connect(): - response = client.get_service_defaults(session.id) + defaults = client.get_service_defaults(session.id) # then - assert len(response.defaults) > 0 + assert len(defaults) > 0 def test_set_service_defaults(self, grpc_server: CoreGrpcServer): # given @@ -884,30 +656,12 @@ class TestGrpc: # then with client.context_connect(): - response = client.set_service_defaults(session.id, {node_type: services}) + result = client.set_service_defaults(session.id, {node_type: services}) # then - assert response.result is True + assert result is True assert session.services.default_services[node_type] == services - def test_get_node_service_configs(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - node = session.add_node(CoreNode) - service_name = "DefaultRoute" - session.services.set_service(node.id, service_name) - - # then - with client.context_connect(): - response = client.get_node_service_configs(session.id) - - # then - assert len(response.configs) == 1 - service_config = response.configs[0] - assert service_config.node_id == node.id - assert service_config.service == service_name - def test_get_node_service(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() @@ -916,10 +670,10 @@ class TestGrpc: # then with client.context_connect(): - response = client.get_node_service(session.id, node.id, "DefaultRoute") + service = client.get_node_service(session.id, node.id, "DefaultRoute") # then - assert len(response.service.configs) > 0 + assert len(service.configs) > 0 def test_get_node_service_file(self, grpc_server: CoreGrpcServer): # given @@ -929,69 +683,29 @@ class TestGrpc: # then with client.context_connect(): - response = client.get_node_service_file( + data = client.get_node_service_file( session.id, node.id, "DefaultRoute", "defaultroute.sh" ) # then - assert response.data is not None - - def test_set_node_service(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - node = session.add_node(CoreNode) - service_name = "DefaultRoute" - validate = ["echo hello"] - - # then - with client.context_connect(): - response = client.set_node_service( - session.id, node.id, service_name, validate=validate - ) - - # then - assert response.result is True - service = session.services.get_service( - node.id, service_name, default_service=True - ) - assert service.validate == tuple(validate) - - def test_set_node_service_file(self, grpc_server: CoreGrpcServer): - # given - client = CoreGrpcClient() - session = grpc_server.coreemu.create_session() - node = session.add_node(CoreNode) - service_name = "DefaultRoute" - file_name = "defaultroute.sh" - file_data = "echo hello" - - # then - with client.context_connect(): - response = client.set_node_service_file( - session.id, node.id, service_name, file_name, file_data - ) - - # then - assert response.result is True - service_file = session.services.get_service_file(node, service_name, file_name) - assert service_file.data == file_data + assert data is not None def test_service_action(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() session = grpc_server.coreemu.create_session() - node = session.add_node(CoreNode) + options = NodeOptions(legacy=True) + node = session.add_node(CoreNode, options=options) service_name = "DefaultRoute" # then with client.context_connect(): - response = client.service_action( + result = client.service_action( session.id, node.id, service_name, ServiceAction.STOP ) # then - assert response.result is True + assert result is True def test_node_events(self, grpc_server: CoreGrpcServer): # given @@ -1003,14 +717,14 @@ class TestGrpc: node.position.alt = 5.0 queue = Queue() - def handle_event(event_data): - assert event_data.session_id == session.id - assert event_data.HasField("node_event") - event_node = event_data.node_event.node + def handle_event(event: Event) -> None: + assert event.session_id == session.id + assert event.node_event is not None + event_node = event.node_event.node assert event_node.geo.lat == node.position.lat assert event_node.geo.lon == node.position.lon assert event_node.geo.alt == node.position.alt - queue.put(event_data) + queue.put(event) # then with client.context_connect(): @@ -1032,10 +746,10 @@ class TestGrpc: link_data = wlan.links()[0] queue = Queue() - def handle_event(event_data): - assert event_data.session_id == session.id - assert event_data.HasField("link_event") - queue.put(event_data) + def handle_event(event: Event) -> None: + assert event.session_id == session.id + assert event.link_event is not None + queue.put(event) # then with client.context_connect(): @@ -1073,19 +787,19 @@ class TestGrpc: session = grpc_server.coreemu.create_session() queue = Queue() - def handle_event(event_data): - assert event_data.session_id == session.id - assert event_data.HasField("session_event") - queue.put(event_data) + def handle_event(event: Event) -> None: + assert event.session_id == session.id + assert event.session_event is not None + queue.put(event) # then with client.context_connect(): client.events(session.id, handle_event) time.sleep(0.1) - event = EventData( + event_data = EventData( event_type=EventTypes.RUNTIME_STATE, time=str(time.monotonic()) ) - session.broadcast_event(event) + session.broadcast_event(event_data) # then queue.get(timeout=5) @@ -1096,10 +810,10 @@ class TestGrpc: session = grpc_server.coreemu.create_session() queue = Queue() - def handle_event(event_data): - assert event_data.session_id == session.id - assert event_data.HasField("config_event") - queue.put(event_data) + def handle_event(event: Event) -> None: + assert event.session_id == session.id + assert event.config_event is not None + queue.put(event) # then with client.context_connect(): @@ -1124,15 +838,15 @@ class TestGrpc: node_id = None text = "exception message" - def handle_event(event_data): - assert event_data.session_id == session.id - assert event_data.HasField("exception_event") - exception_event = event_data.exception_event - assert exception_event.level == exception_level.value + def handle_event(event: Event) -> None: + assert event.session_id == session.id + assert event.exception_event is not None + exception_event = event.exception_event + assert exception_event.level.value == exception_level.value assert exception_event.node_id == 0 assert exception_event.source == source assert exception_event.text == text - queue.put(event_data) + queue.put(event) # then with client.context_connect(): @@ -1150,10 +864,10 @@ class TestGrpc: node = session.add_node(CoreNode) queue = Queue() - def handle_event(event_data): - assert event_data.session_id == session.id - assert event_data.HasField("file_event") - queue.put(event_data) + def handle_event(event: Event) -> None: + assert event.session_id == session.id + assert event.file_event is not None + queue.put(event) # then with client.context_connect(): @@ -1173,17 +887,13 @@ class TestGrpc: session = grpc_server.coreemu.create_session() node = session.add_node(CoreNode) x, y = 10.0, 15.0 - - def move_iter(): - yield core_pb2.MoveNodesRequest( - session_id=session.id, - node_id=node.id, - position=core_pb2.Position(x=x, y=y), - ) + streamer = MoveNodesStreamer(session.id) + streamer.send_position(node.id, x, y) + streamer.stop() # then with client.context_connect(): - client.move_nodes(move_iter()) + client.move_nodes(streamer) # assert assert node.position.x == x @@ -1195,6 +905,9 @@ class TestGrpc: session = grpc_server.coreemu.create_session() node = session.add_node(CoreNode) lon, lat, alt = 10.0, 15.0, 5.0 + streamer = MoveNodesStreamer(session.id) + streamer.send_geo(node.id, lon, lat, alt) + streamer.stop() queue = Queue() def node_handler(node_data: NodeData): @@ -1206,32 +919,26 @@ class TestGrpc: session.node_handlers.append(node_handler) - def move_iter(): - yield core_pb2.MoveNodesRequest( - session_id=session.id, - node_id=node.id, - geo=core_pb2.Geo(lon=lon, lat=lat, alt=alt), - ) - # then with client.context_connect(): - client.move_nodes(move_iter()) + client.move_nodes(streamer) # assert + assert queue.get(timeout=5) assert node.position.lon == lon assert node.position.lat == lat assert node.position.alt == alt - assert queue.get(timeout=5) def test_move_nodes_exception(self, grpc_server: CoreGrpcServer): # given client = CoreGrpcClient() - grpc_server.coreemu.create_session() - - def move_iter(): - yield core_pb2.MoveNodesRequest() + session = grpc_server.coreemu.create_session() + streamer = MoveNodesStreamer(session.id) + request = core_pb2.MoveNodesRequest() + streamer.send(request) + streamer.stop() # then with pytest.raises(grpc.RpcError): with client.context_connect(): - client.move_nodes(move_iter()) + client.move_nodes(streamer) diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py index a0b3bd8a..b14f1fb1 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -1,8 +1,8 @@ """ Tests for testing tlv message handling. """ -import os import time +from pathlib import Path from typing import Optional import mock @@ -22,7 +22,7 @@ from core.api.tlv.enumerations import ( NodeTlvs, SessionTlvs, ) -from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.emane.models.ieee80211abg import EmaneIeee80211abgModel from core.emulator.enumerations import EventTypes, MessageFlags, NodeTypes, RegisterTlvs from core.errors import CoreError from core.location.mobility import BasicRangeModel @@ -425,7 +425,7 @@ class TestGui: assert file_data == service_file.data def test_file_node_file_copy(self, request, coretlv: CoreHandler): - file_name = "/var/log/test/node.log" + file_path = Path("/var/log/test/node.log") node = coretlv.session.add_node(CoreNode) node.makenodedir() file_data = "echo hello" @@ -433,7 +433,7 @@ class TestGui: MessageFlags.ADD.value, [ (FileTlvs.NODE, node.id), - (FileTlvs.NAME, file_name), + (FileTlvs.NAME, str(file_path)), (FileTlvs.DATA, file_data), ], ) @@ -441,10 +441,8 @@ class TestGui: coretlv.handle_message(message) if not request.config.getoption("mock"): - directory, basename = os.path.split(file_name) - created_directory = directory[1:].replace("/", ".") - create_path = os.path.join(node.nodedir, created_directory, basename) - assert os.path.exists(create_path) + expected_path = node.directory / "var.log/test" / file_path.name + assert expected_path.exists() def test_exec_node_tty(self, coretlv: CoreHandler): coretlv.dispatch_replies = mock.MagicMock() @@ -547,20 +545,21 @@ class TestGui: 0, [(EventTlvs.TYPE, EventTypes.FILE_SAVE.value), (EventTlvs.NAME, file_path)], ) - coretlv.handle_message(message) - - assert os.path.exists(file_path) + assert Path(file_path).exists() def test_event_open_xml(self, coretlv: CoreHandler, tmpdir): xml_file = tmpdir.join("coretlv.session.xml") - file_path = xml_file.strpath + file_path = Path(xml_file.strpath) node = coretlv.session.add_node(CoreNode) coretlv.session.save_xml(file_path) coretlv.session.delete_node(node.id) message = coreapi.CoreEventMessage.create( 0, - [(EventTlvs.TYPE, EventTypes.FILE_OPEN.value), (EventTlvs.NAME, file_path)], + [ + (EventTlvs.TYPE, EventTypes.FILE_OPEN.value), + (EventTlvs.NAME, str(file_path)), + ], ) coretlv.handle_message(message) @@ -938,39 +937,5 @@ class TestGui: coretlv.handle_message(message) - config = coretlv.session.emane.get_model_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.get_configs() + config = coretlv.session.emane.get_config(wlan.id, EmaneIeee80211abgModel.name) assert config[config_key] == config_value diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 8ed21f27..3f0fbab1 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -24,19 +24,30 @@ class TestNodes: assert node.alive() assert node.up - def test_node_update(self, session: Session): + def test_node_set_pos(self, session: Session): # given node = session.add_node(CoreNode) - position_value = 100 - update_options = NodeOptions() - update_options.set_position(x=position_value, y=position_value) + x, y = 100.0, 50.0 # when - session.edit_node(node.id, update_options) + session.set_node_pos(node, x, y) # then - assert node.position.x == position_value - assert node.position.y == position_value + assert node.position.x == x + assert node.position.y == y + + def test_node_set_geo(self, session: Session): + # given + node = session.add_node(CoreNode) + lon, lat, alt = 0.0, 0.0, 0.0 + + # when + session.set_node_geo(node, lon, lat, alt) + + # then + assert node.position.lon == lon + assert node.position.lat == lat + assert node.position.alt == alt def test_node_delete(self, session: Session): # given diff --git a/daemon/tests/test_services.py b/daemon/tests/test_services.py index 44776ea2..bbccaaac 100644 --- a/daemon/tests/test_services.py +++ b/daemon/tests/test_services.py @@ -1,5 +1,5 @@ import itertools -import os +from pathlib import Path import pytest from mock import MagicMock @@ -9,8 +9,8 @@ from core.errors import CoreCommandError from core.nodes.base import CoreNode from core.services.coreservices import CoreService, ServiceDependencies, ServiceManager -_PATH = os.path.abspath(os.path.dirname(__file__)) -_SERVICES_PATH = os.path.join(_PATH, "myservices") +_PATH: Path = Path(__file__).resolve().parent +_SERVICES_PATH = _PATH / "myservices" SERVICE_ONE = "MyService" SERVICE_TWO = "MyService2" @@ -64,15 +64,15 @@ class TestServices: ServiceManager.add_services(_SERVICES_PATH) my_service = ServiceManager.get(SERVICE_ONE) node = session.add_node(CoreNode) - file_name = my_service.configs[0] - file_path = node.hostfilename(file_name) + file_path = Path(my_service.configs[0]) + file_path = node.host_path(file_path) # when session.services.create_service_files(node, my_service) # then if not request.config.getoption("mock"): - assert os.path.exists(file_path) + assert file_path.exists() def test_service_validate(self, session: Session): # given diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 8a6e465d..653e77f6 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -1,3 +1,4 @@ +from pathlib import Path from tempfile import TemporaryFile from xml.etree import ElementTree @@ -34,7 +35,7 @@ class TestXml: # save xml xml_file = tmpdir.join("session.xml") - file_path = xml_file.strpath + file_path = Path(xml_file.strpath) session.save_xml(file_path) # verify xml file was created and can be parsed @@ -85,7 +86,7 @@ class TestXml: # save xml xml_file = tmpdir.join("session.xml") - file_path = xml_file.strpath + file_path = Path(xml_file.strpath) session.save_xml(file_path) # verify xml file was created and can be parsed @@ -148,7 +149,7 @@ class TestXml: # save xml xml_file = tmpdir.join("session.xml") - file_path = xml_file.strpath + file_path = Path(xml_file.strpath) session.save_xml(file_path) # verify xml file was created and can be parsed @@ -210,7 +211,7 @@ class TestXml: # save xml xml_file = tmpdir.join("session.xml") - file_path = xml_file.strpath + file_path = Path(xml_file.strpath) session.save_xml(file_path) # verify xml file was created and can be parsed @@ -261,7 +262,7 @@ class TestXml: # save xml xml_file = tmpdir.join("session.xml") - file_path = xml_file.strpath + file_path = Path(xml_file.strpath) session.save_xml(file_path) # verify xml file was created and can be parsed @@ -321,7 +322,7 @@ class TestXml: # save xml xml_file = tmpdir.join("session.xml") - file_path = xml_file.strpath + file_path = Path(xml_file.strpath) session.save_xml(file_path) # verify xml file was created and can be parsed @@ -390,7 +391,7 @@ class TestXml: # save xml xml_file = tmpdir.join("session.xml") - file_path = xml_file.strpath + file_path = Path(xml_file.strpath) session.save_xml(file_path) # verify xml file was created and can be parsed @@ -471,7 +472,7 @@ class TestXml: # save xml xml_file = tmpdir.join("session.xml") - file_path = xml_file.strpath + file_path = Path(xml_file.strpath) session.save_xml(file_path) # verify xml file was created and can be parsed diff --git a/docs/architecture.md b/docs/architecture.md index c262d2be..ceaf7cc2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -51,8 +51,8 @@ filesystem in CORE. CORE combines these namespaces with Linux Ethernet bridging to form networks. Link characteristics are applied using Linux Netem queuing disciplines. -Ebtables is Ethernet frame filtering on Linux bridges. Wireless networks are -emulated by controlling which interfaces can send and receive with ebtables +Nftables provides Ethernet frame filtering on Linux bridges. Wireless networks are +emulated by controlling which interfaces can send and receive with nftables rules. ## Prior Work diff --git a/docs/configservices.md b/docs/configservices.md new file mode 100644 index 00000000..42cf1478 --- /dev/null +++ b/docs/configservices.md @@ -0,0 +1,200 @@ +# CORE Config Services + +* Table of Contents +{:toc} + +## Overview + +Config services are a newer version of services for CORE, that leverage a +templating engine, for more robust service file creation. They also +have the power of configuration key/value pairs that values that can be +defined and displayed within the GUI, to help further tweak a service, +as needed. + +CORE services are a convenience for creating reusable dynamic scripts +to run on nodes, for carrying out specific task(s). + +This boilds down to the following functions: +* generating files the service will use, either directly for commands or for configuration +* command(s) for starting a service +* command(s) for validating a service +* command(s) for stopping a service + +Most CORE nodes will have a default set of services to run, associated with +them. You can however customize the set of services a node will use. Or even +further define a new node type within the GUI, with a set of services, that +will allow quickly dragging and dropping that node type during creation. + +## Available Services + +| Service Group | Services | +|---|---| +|[BIRD](services/bird.md)|BGP, OSPF, RADV, RIP, Static| +|[EMANE](services/emane.md)|Transport Service| +|[FRR](services/frr.md)|BABEL, BGP, OSPFv2, OSPFv3, PIMD, RIP, RIPNG, Zebra| +|[NRL](services/nrl.md)|arouted, MGEN Sink, MGEN Actor, NHDP, OLSR, OLSRORG, OLSRv2, SMF| +|[Quagga](services/quagga.md)|BABEL, BGP, OSPFv2, OSPFv3, OSPFv3 MDR, RIP, RIPNG, XPIMD, Zebra| +|[SDN](services/sdn.md)|OVS, RYU| +|[Security](services/security.md)|Firewall, IPsec, NAT, VPN Client, VPN Server| +|[Utility](services/utility.md)|ATD, Routing Utils, DHCP, FTP, IP Forward, PCAP, RADVD, SSF, UCARP| +|[XORP](services/xorp.md)|BGP, OLSR, OSPFv2, OSPFv3, PIMSM4, PIMSM6, RIP, RIPNG, Router Manager| + +## Node Types and Default Services + +Here are the default node types and their services: + +| Node Type | Services | +|---|---| +| *router* | zebra, OSFPv2, OSPFv3, and IPForward services for IGP link-state routing. | +| *PC* | DefaultRoute service for having a default route when connected directly to a router. | +| *mdr* | zebra, OSPFv3MDR, and IPForward services for wireless-optimized MANET Designated Router routing. | +| *prouter* | a physical router, having the same default services as the *router* node type; for incorporating Linux testbed machines into an emulation. | + +Configuration files can be automatically generated by each service. For +example, CORE automatically generates routing protocol configuration for the +router nodes in order to simplify the creation of virtual networks. + +To change the services associated with a node, double-click on the node to +invoke its configuration dialog and click on the *Services...* button, +or right-click a node a choose *Services...* from the menu. +Services are enabled or disabled by clicking on their names. The button next to +each service name allows you to customize all aspects of this service for this +node. For example, special route redistribution commands could be inserted in +to the Quagga routing configuration associated with the zebra service. + +To change the default services associated with a node type, use the Node Types +dialog available from the *Edit* button at the end of the Layer-3 nodes +toolbar, or choose *Node types...* from the *Session* menu. Note that +any new services selected are not applied to existing nodes if the nodes have +been customized. + +The node types are saved in the GUI config file **~/.coregui/config.yaml**. +Keep this in mind when changing the default services for +existing node types; it may be better to simply create a new node type. It is +recommended that you do not change the default built-in node types. + +## New Services + +Services can save time required to configure nodes, especially if a number +of nodes require similar configuration procedures. New services can be +introduced to automate tasks. + +### Creating New Services + +1. Modify the example service shown below + to do what you want. It could generate config/script files, mount per-node + directories, start processes/scripts, etc. Your file can define one or more + classes to be imported. You can create multiple Python files that will be imported. + +2. Put these files in a directory such as ~/.coregui/custom_services + Note that the last component of this directory name **myservices** should not + be named something like **services** which conflicts with an existing module. + +3. Add a **custom_config_services_dir = ~/.coregui/custom_services** entry to the + /etc/core/core.conf file. + + **NOTE:** + The directory name used in **custom_services_dir** should be unique and + should not correspond to + any existing Python module name. For example, don't use the name **subprocess** + or **services**. + +4. Restart the CORE daemon (core-daemon). Any import errors (Python syntax) + should be displayed in the terminal (or service log, like journalctl). + +5. Start using your custom service on your nodes. You can create a new node + type that uses your service, or change the default services for an existing + node type, or change individual nodes. . + +### Example Custom Service + +Below is the skeleton for a custom service with some documentation. Most +people would likely only setup the required class variables **(name/group)**. +Then define the **files** to generate and implement the +**get_text_template** function to dynamically create the files wanted. Finally, +the **startup** commands would be supplied, which typically tend to be +running the shell files generated. + +```python +from typing import Dict, List + +from core.config import ConfigString, ConfigBool, Configuration +from core.configservice.base import ConfigService, ConfigServiceMode, ShadowDir + +# class that subclasses ConfigService +class ExampleService(ConfigService): + # unique name for your service within CORE + name: str = "Example" + # the group your service is associated with, used for display in GUI + group: str = "ExampleGroup" + # directories that the service should shadow mount, hiding the system directory + directories: List[str] = [ + "/usr/local/core", + ] + # files that this service should generate, defaults to nodes home directory + # or can provide an absolute path to a mounted directory + files: List[str] = [ + "example-start.sh", + "/usr/local/core/file1", + ] + # executables that should exist on path, that this service depends on + executables: List[str] = [] + # other services that this service depends on, can be used to define service start order + dependencies: List[str] = [] + # commands to run to start this service + startup: List[str] = [] + # commands to run to validate this service + validate: List[str] = [] + # commands to run to stop this service + shutdown: List[str] = [] + # validation mode, blocking, non-blocking, and timer + validation_mode: ConfigServiceMode = ConfigServiceMode.BLOCKING + # configurable values that this service can use, for file generation + default_configs: List[Configuration] = [ + ConfigString(id="value1", label="Text"), + ConfigBool(id="value2", label="Boolean"), + ConfigString(id="value3", label="Multiple Choice", options=["value1", "value2", "value3"]), + ] + # sets of values to set for the configuration defined above, can be used to + # provide convenient sets of values to typically use + modes: Dict[str, Dict[str, str]] = { + "mode1": {"value1": "value1", "value2": "0", "value3": "value2"}, + "mode2": {"value1": "value2", "value2": "1", "value3": "value3"}, + "mode3": {"value1": "value3", "value2": "0", "value3": "value1"}, + } + # defines directories that this service can help shadow within a node + shadow_directories: List[ShadowDir] = [ + ShadowDir(path="/user/local/core", src="/opt/core") + ] + + def get_text_template(self, name: str) -> str: + if name == "example-start.sh": + return """ + # sample script 1 + # node id(${node.id}) name(${node.name}) + # config: ${config} + echo hello + """ +``` + +#### Validation Mode + +Validation modes are used to determine if a service has started up successfully. + +* blocking - startup commands are expected to run til completion and return 0 exit code +* non-blocking - startup commands are ran, but do not wait for completion +* timer - startup commands are ran, and an arbitrary amount of time is waited to consider started + +#### Shadow Directories + +Shadow directories provide a convenience for copying a directory and the files within +it to a nodes home directory, to allow a unique set of per node files. + +* `ShadowDir(path="/user/local/core")` - copies files at the given location into the node +* `ShadowDir(path="/user/local/core", src="/opt/core")` - copies files to the given location, + but sourced from the provided location +* `ShadowDir(path="/user/local/core", templates=True)` - copies files and treats them as + templates for generation +* `ShadowDir(path="/user/local/core", has_node_paths=True)` - copies files from the given + location, and looks for unique node names directories within it, using a directory named + default, when not preset diff --git a/docs/devguide.md b/docs/devguide.md index ba34a211..e3b0ad18 100644 --- a/docs/devguide.md +++ b/docs/devguide.md @@ -104,7 +104,7 @@ vcmd -c /tmp/pycore.50160/n1 -- /sbin/ip -4 ro A script named *core-cleanup* is provided to clean up any running CORE emulations. It will attempt to kill any remaining vnoded processes, kill any EMANE processes, remove the :file:`/tmp/pycore.*` session directories, and remove -any bridges or *ebtables* rules. With a *-d* option, it will also kill any running CORE daemon. +any bridges or *nftables* rules. With a *-d* option, it will also kill any running CORE daemon. ### netns command @@ -121,5 +121,5 @@ ip link show type bridge # view the netem rules used for applying link effects tc qdisc show # view the rules that make the wireless LAN work -ebtables -L +nft list ruleset ``` diff --git a/docs/distributed.md b/docs/distributed.md index ad3d61f8..2d46ac96 100644 --- a/docs/distributed.md +++ b/docs/distributed.md @@ -172,7 +172,7 @@ will draw the link with a dashed line. Wireless nodes, i.e. those connected to a WLAN node, can be assigned to different emulation servers and participate in the same wireless network only if an EMANE model is used for the WLAN. The basic range model does -not work across multiple servers due to the Linux bridging and ebtables +not work across multiple servers due to the Linux bridging and nftables rules that are used. **NOTE: The basic range wireless model does not support distributed emulation, diff --git a/daemon/examples/docker/README.md b/docs/docker.md similarity index 59% rename from daemon/examples/docker/README.md rename to docs/docker.md index 17c6cb90..0c730369 100644 --- a/daemon/examples/docker/README.md +++ b/docs/docker.md @@ -1,28 +1,40 @@ -# Docker Support +# Docker Node Support -Information on how Docker can be leveraged and included to create -nodes based on Docker containers and images to interface with -existing CORE nodes, when needed. +## Overview + +Provided below is some information for helping setup and use Docker +nodes within a CORE scenario. ## Installation +### Debian Systems + ```shell sudo apt install docker.io ``` +### RHEL Systems + + ## Configuration Custom configuration required to avoid iptable rules being added and removing the need for the default docker network, since core will be orchestrating connections between nodes. -Place the file below in **/etc/docker/** -* daemon.json +Place the file below in **/etc/docker/docker.json** + +```json +{ + "bridge": "none", + "iptables": false +} +``` ## Group Setup -To use Docker nodes within the python GUI, you will need to make sure the user running the GUI is a member of the -docker group. +To use Docker nodes within the python GUI, you will need to make sure the +user running the GUI is a member of the docker group. ```shell # add group if does not exist @@ -35,18 +47,10 @@ sudo usermod -aG docker $USER newgrp docker ``` -## Tools and Versions Tested With +## Image Requirements -* Docker version 18.09.5, build e8ff056 -* nsenter from util-linux 2.31.1 - -## Examples - -This directory provides a few small examples creating Docker nodes -and linking them to themselves or with standard CORE nodes. - -Images used by nodes need to have networking tools installed for CORE to automate -setup and configuration of the container. +Images used by Docker nodes in CORE need to have networking tools installed for +CORE to automate setup and configuration of the network within the container. Example Dockerfile: ``` @@ -59,3 +63,8 @@ Build image: ```shell sudo docker build -t . ``` + +## Tools and Versions Tested With + +* Docker version 18.09.5, build e8ff056 +* nsenter from util-linux 2.31.1 diff --git a/docs/emane.md b/docs/emane.md index f589f834..9e2e5b0b 100644 --- a/docs/emane.md +++ b/docs/emane.md @@ -120,7 +120,8 @@ Here is an example model with documentation describing functionality: """ Example custom emane model. """ -from typing import Dict, List, Optional, Set +from pathlib import Path +from typing import Dict, Optional, Set, List from core.config import Configuration from core.emane import emanemanifest, emanemodel @@ -162,14 +163,31 @@ class ExampleModel(emanemodel.EmaneModel): mac_defaults: Dict[str, str] = { "pcrcurveuri": "/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" } - mac_config: List[Configuration] = emanemanifest.parse(mac_xml, mac_defaults) + mac_config: List[Configuration] = [] phy_library: Optional[str] = None phy_xml: str = "/usr/share/emane/manifest/emanephy.xml" phy_defaults: Dict[str, str] = { "subid": "1", "propagationmodel": "2ray", "noisemode": "none" } - phy_config: List[Configuration] = emanemanifest.parse(phy_xml, phy_defaults) + phy_config: List[Configuration] = [] config_ignore: Set[str] = set() + + @classmethod + def load(cls, emane_prefix: Path) -> None: + """ + 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" + # load mac configuration + 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 + cls.phy_config = emanemanifest.parse(phy_xml_path, cls.phy_defaults) ``` ## Single PC with EMANE diff --git a/docs/grpc.md b/docs/grpc.md index 998970c5..aef79308 100644 --- a/docs/grpc.md +++ b/docs/grpc.md @@ -53,15 +53,15 @@ and the services they map to. There is an interface helper class that can be leveraged for convenience when creating interface data for nodes. Alternatively one can manually create -a `core.api.grpc.core_pb2.Interface` class instead with appropriate information. +a `core.api.grpc.wrappers.Interface` class instead with appropriate information. -Manually creating gRPC interface data: +Manually creating gRPC client interface: ```python -from core.api.grpc import core_pb2 +from core.api.grpc.wrappers import Interface # id is optional and will set to the next available id # name is optional and will default to eth # mac is optional and will result in a randomly generated mac -iface_data = core_pb2.Interface( +iface = Interface( id=0, name="eth0", ip4="10.0.0.1", @@ -98,16 +98,24 @@ Event types: * file - file events when the legacy gui joins a session ```python -from core.api.grpc import core_pb2 +from core.api.grpc import client +from core.api.grpc.wrappers import EventType def event_listener(event): print(event) +# create grpc client and connect +core = client.CoreGrpcClient() +core.connect() + +# add session +session = core.create_session() + # provide no events to listen to all events -core.events(session_id, event_listener) +core.events(session.id, event_listener) # provide events to listen to specific events -core.events(session_id, event_listener, [core_pb2.EventType.NODE]) +core.events(session.id, event_listener, [EventType.NODE]) ``` ### Configuring Links @@ -122,27 +130,47 @@ Currently supported configuration options: * loss (%) ```python -from core.api.grpc import core_pb2 +from core.api.grpc import client +from core.api.grpc.wrappers import LinkOptions, Position + +# interface helper +iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") + +# create grpc client and connect +core = client.CoreGrpcClient() +core.connect() + +# add session +session = core.create_session() + +# create nodes +position = Position(x=100, y=100) +node1 = session.add_node(1, position=position) +position = Position(x=300, y=100) +node2 = session.add_node(2, position=position) # configuring when creating a link -options = core_pb2.LinkOptions( +options = LinkOptions( bandwidth=54_000_000, delay=5000, dup=5, loss=5.5, jitter=0, ) -core.add_link(session_id, n1_id, n2_id, iface1_data, iface2_data, options) +iface1 = iface_helper.create_iface(node1.id, 0) +iface2 = iface_helper.create_iface(node2.id, 0) +link = session.add_link(node1=node1, node2=node2, iface1=iface1, iface2=iface2) # configuring during runtime -core.edit_link(session_id, n1_id, n2_id, iface1_id, iface2_id, options) +link.options.loss = 10.0 +core.edit_link(session.id, link) ``` ### Peer to Peer Example ```python # required imports from core.api.grpc import client -from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState +from core.api.grpc.core_pb2 import Position # interface helper iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") @@ -151,39 +179,29 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001 core = client.CoreGrpcClient() core.connect() -# create session and get id -response = core.create_session() -session_id = response.session_id +# add session +session = core.create_session() -# change session state to configuration so that nodes get started when added -core.set_session_state(session_id, SessionState.CONFIGURATION) - -# create node one +# create nodes position = Position(x=100, y=100) -n1 = Node(type=NodeType.DEFAULT, position=position, model="PC") -response = core.add_node(session_id, n1) -n1_id = response.node_id - -# create node two +node1 = session.add_node(1, position=position) position = Position(x=300, y=100) -n2 = Node(type=NodeType.DEFAULT, position=position, model="PC") -response = core.add_node(session_id, n2) -n2_id = response.node_id +node2 = session.add_node(2, position=position) -# links nodes together -iface1 = iface_helper.create_iface(n1_id, 0) -iface2 = iface_helper.create_iface(n2_id, 0) -core.add_link(session_id, n1_id, n2_id, iface1, iface2) +# create link +iface1 = iface_helper.create_iface(node1.id, 0) +iface2 = iface_helper.create_iface(node2.id, 0) +session.add_link(node1=node1, node2=node2, iface1=iface1, iface2=iface2) -# change session state -core.set_session_state(session_id, SessionState.INSTANTIATION) +# start session +core.start_session(session) ``` ### Switch/Hub Example ```python # required imports from core.api.grpc import client -from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState +from core.api.grpc.core_pb2 import NodeType, Position # interface helper iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") @@ -192,46 +210,32 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001 core = client.CoreGrpcClient() core.connect() -# create session and get id -response = core.create_session() -session_id = response.session_id +# add session +session = core.create_session() -# change session state to configuration so that nodes get started when added -core.set_session_state(session_id, SessionState.CONFIGURATION) - -# create switch node +# create nodes position = Position(x=200, y=200) -switch = Node(type=NodeType.SWITCH, position=position) -response = core.add_node(session_id, switch) -switch_id = response.node_id - -# create node one +switch = session.add_node(1, _type=NodeType.SWITCH, position=position) position = Position(x=100, y=100) -n1 = Node(type=NodeType.DEFAULT, position=position, model="PC") -response = core.add_node(session_id, n1) -n1_id = response.node_id - -# create node two +node1 = session.add_node(2, position=position) position = Position(x=300, y=100) -n2 = Node(type=NodeType.DEFAULT, position=position, model="PC") -response = core.add_node(session_id, n2) -n2_id = response.node_id +node2 = session.add_node(3, position=position) -# links nodes to switch -iface1 = iface_helper.create_iface(n1_id, 0) -core.add_link(session_id, n1_id, switch_id, iface1) -iface1 = iface_helper.create_iface(n2_id, 0) -core.add_link(session_id, n2_id, switch_id, iface1) +# create links +iface1 = iface_helper.create_iface(node1.id, 0) +session.add_link(node1=node1, node2=switch, iface1=iface1) +iface1 = iface_helper.create_iface(node2.id, 0) +session.add_link(node1=node2, node2=switch, iface1=iface1) -# change session state -core.set_session_state(session_id, SessionState.INSTANTIATION) +# start session +core.start_session(session) ``` ### WLAN Example ```python # required imports from core.api.grpc import client -from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState +from core.api.grpc.core_pb2 import NodeType, Position # interface helper iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") @@ -240,49 +244,37 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001 core = client.CoreGrpcClient() core.connect() -# create session and get id -response = core.create_session() -session_id = response.session_id +# add session +session = core.create_session() -# change session state to configuration so that nodes get started when added -core.set_session_state(session_id, SessionState.CONFIGURATION) - -# create wlan node +# create nodes position = Position(x=200, y=200) -wlan = Node(type=NodeType.WIRELESS_LAN, position=position) -response = core.add_node(session_id, wlan) -wlan_id = response.node_id - -# create node one +wlan = session.add_node(1, _type=NodeType.WIRELESS_LAN, position=position) position = Position(x=100, y=100) -n1 = Node(type=NodeType.DEFAULT, position=position, model="mdr") -response = core.add_node(session_id, n1) -n1_id = response.node_id - -# create node two +node1 = session.add_node(2, model="mdr", position=position) position = Position(x=300, y=100) -n2 = Node(type=NodeType.DEFAULT, position=position, model="mdr") -response = core.add_node(session_id, n2) -n2_id = response.node_id +node2 = session.add_node(3, model="mdr", position=position) -# configure wlan using a dict mapping currently +# create links +iface1 = iface_helper.create_iface(node1.id, 0) +session.add_link(node1=node1, node2=wlan, iface1=iface1) +iface1 = iface_helper.create_iface(node2.id, 0) +session.add_link(node1=node2, node2=wlan, iface1=iface1) + +# set wlan config using a dict mapping currently # support values as strings -core.set_wlan_config(session_id, wlan_id, { - "range": "280", - "bandwidth": "55000000", - "delay": "6000", - "jitter": "5", - "error": "5", -}) +wlan.set_wlan( + { + "range": "280", + "bandwidth": "55000000", + "delay": "6000", + "jitter": "5", + "error": "5", + } +) -# links nodes to wlan -iface1 = iface_helper.create_iface(n1_id, 0) -core.add_link(session_id, n1_id, wlan_id, iface1) -iface1 = iface_helper.create_iface(n2_id, 0) -core.add_link(session_id, n2_id, wlan_id, iface1) - -# change session state -core.set_session_state(session_id, SessionState.INSTANTIATION) +# start session +core.start_session(session) ``` ### EMANE Example @@ -307,8 +299,8 @@ will use the defaults. When no configuration is used, the defaults are used. ```python # required imports from core.api.grpc import client -from core.api.grpc.core_pb2 import Node, NodeType, Position, SessionState -from core.emane.ieee80211abg import EmaneIeee80211abgModel +from core.api.grpc.core_pb2 import NodeType, Position +from core.emane.models.ieee80211abg import EmaneIeee80211abgModel # interface helper iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001::/64") @@ -317,68 +309,46 @@ iface_helper = client.InterfaceHelper(ip4_prefix="10.0.0.0/24", ip6_prefix="2001 core = client.CoreGrpcClient() core.connect() -# create session and get id -response = core.create_session() -session_id = response.session_id +# add session +session = core.create_session() -# change session state to configuration so that nodes get started when added -core.set_session_state(session_id, SessionState.CONFIGURATION) - -# create emane node +# create nodes position = Position(x=200, y=200) -emane = Node(type=NodeType.EMANE, position=position, emane=EmaneIeee80211abgModel.name) -response = core.add_node(session_id, emane) -emane_id = response.node_id - -# create node one +emane = session.add_node( + 1, _type=NodeType.EMANE, position=position, emane=EmaneIeee80211abgModel.name +) position = Position(x=100, y=100) -n1 = Node(type=NodeType.DEFAULT, position=position, model="mdr") -response = core.add_node(session_id, n1) -n1_id = response.node_id - -# create node two +node1 = session.add_node(2, model="mdr", position=position) position = Position(x=300, y=100) -n2 = Node(type=NodeType.DEFAULT, position=position, model="mdr") -response = core.add_node(session_id, n2) -n2_id = response.node_id +node2 = session.add_node(3, model="mdr", position=position) -# configure general emane settings -core.set_emane_config(session_id, { - "eventservicettl": "2" +# create links +iface1 = iface_helper.create_iface(node1.id, 0) +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 emane specific emane model configuration +emane.set_emane_model(EmaneIeee80211abgModel.name, { + "eventservicettl": "2", + "unicastrate": "3", }) -# configure emane model settings -# using a dict mapping currently support values as strings -core.set_emane_model_config(session_id, emane_id, EmaneIeee80211abgModel.name, { - "unicastrate": "3", -}) - -# links nodes to emane -iface1 = iface_helper.create_iface(n1_id, 0) -core.add_link(session_id, n1_id, emane_id, iface1) -iface1 = iface_helper.create_iface(n2_id, 0) -core.add_link(session_id, n2_id, emane_id, iface1) - -# change session state -core.set_session_state(session_id, SessionState.INSTANTIATION) +# start session +core.start_session(session) ``` EMANE Model Configuration: ```python -# emane network specific config -core.set_emane_model_config(session_id, emane_id, EmaneIeee80211abgModel.name, { - "unicastrate": "3", -}) +# emane network specific config, set on an emane node +# this setting applies to all nodes connected +emane.set_emane_model(EmaneIeee80211abgModel.name, {"unicastrate": "3"}) -# node specific config -core.set_emane_model_config(session_id, node_id, EmaneIeee80211abgModel.name, { - "unicastrate": "3", -}) +# node specific config for an individual node connected to an emane network +node.set_emane_model(EmaneIeee80211abgModel.name, {"unicastrate": "3"}) -# node interface specific config -core.set_emane_model_config(session_id, node_id, EmaneIeee80211abgModel.name, { - "unicastrate": "3", -}, iface_id) +# node interface specific config for an individual node connected to an emane network +node.set_emane_model(EmaneIeee80211abgModel.name, {"unicastrate": "3"}, iface_id=0) ``` ## Configuring a Service @@ -398,11 +368,8 @@ The following features can be configured for a service: Editing service properties: ```python # configure a service, for a node, for a given session -core.set_node_service( - session_id, - node_id, - service_name, - files=["file1.sh", "file2.sh"], +node.service_configs[service_name] = NodeServiceData( + configs=["file1.sh", "file2.sh"], directories=["/etc/node"], startup=["bash file1.sh"], validate=[], @@ -417,13 +384,8 @@ Editing a service file: ```python # to edit the contents of a generated file you can specify # the service, the file name, and its contents -core.set_node_service_file( - session_id, - node_id, - service_name, - file_name, - "echo hello", -) +file_configs = node.service_file_configs.setdefault(service_name, {}) +file_configs[file_name] = "echo hello world" ``` ## File Examples diff --git a/docs/gui.md b/docs/gui.md index 85bbb6cd..6a333752 100644 --- a/docs/gui.md +++ b/docs/gui.md @@ -544,7 +544,7 @@ on platform. See the table below for a brief overview of wireless model types. |Model|Type|Supported Platform(s)|Fidelity|Description| |-----|----|---------------------|--------|-----------| -|Basic|on/off|Linux|Low|Ethernet bridging with ebtables| +|Basic|on/off|Linux|Low|Ethernet bridging with nftables| |EMANE|Plug-in|Linux|High|TAP device connected to EMANE emulator with pluggable MAC and PHY radio types| To quickly build a wireless network, you can first place several router nodes diff --git a/docs/index.md b/docs/index.md index 5814e141..0bfe5a26 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,15 +20,16 @@ networking scenarios, security studies, and increasing the size of physical test | Topic | Description| |-------|------------| -|[Architecture](architecture.md)|Overview of the architecture| |[Installation](install.md)|How to install CORE and its requirements| +|[Architecture](architecture.md)|Overview of the architecture| +|[Node Types](nodetypes.md)|Overview of node types supported within CORE| |[GUI](gui.md)|How to use the GUI| |[(BETA) Python GUI](pygui.md)|How to use the BETA python based GUI| |[Python API](python.md)|Covers how to control core directly using python| |[gRPC API](grpc.md)|Covers how control core using gRPC| |[Distributed](distributed.md)|Details for running CORE across multiple servers| -|[Node Types](nodetypes.md)|Overview of node types supported within CORE| -|[CTRLNET](ctrlnet.md)|How to use control networks to communicate with nodes from host| +|[Control Network](ctrlnet.md)|How to use control networks to communicate with nodes from host| +|[Config Services](configservices.md)|Overview of provided config services and creating custom ones| |[Services](services.md)|Overview of provided services and creating custom ones| |[EMANE](emane.md)|Overview of EMANE integration and integrating custom EMANE models| |[Performance](performance.md)|Notes on performance when using CORE| diff --git a/docs/install.md b/docs/install.md index 5c66f420..19f39cd3 100644 --- a/docs/install.md +++ b/docs/install.md @@ -15,20 +15,24 @@ containers, as a general rule you should select a machine having as much RAM and * Linux Kernel v3.3+ * iproute2 4.5+ is a requirement for bridge related commands -* ebtables not backed by nftables +* nftables compatible kernel and nft command line tool ### Supported Linux Distributions Plan is to support recent Ubuntu and CentOS LTS releases. Verified: * Ubuntu - 18.04, 20.04 -* CentOS - 7.8, 8.0* +* CentOS - 7.8, 8.0 -> **NOTE:** Ubuntu 20.04 requires installing legacy ebtables for WLAN -> functionality +> **NOTE:** Ubuntu 20.04 requires installing legacy ebtables for WLAN functionality -> **NOTE:** CentOS 8 does not provide legacy ebtables support, WLAN will not -> function properly +Enabling ebtables legacy: +```shell +sudo apt install ebtables +update-alternatives --set ebtables /usr/sbin/ebtables-legacy +``` + +> **NOTE:** CentOS 8 does not provide legacy ebtables support, WLAN will not function properly > **NOTE:** CentOS 8 does not have the netem kernel mod available by default @@ -97,7 +101,14 @@ After the installation complete it will have installed the following scripts. Please make sure to uninstall any previous installations of CORE cleanly before proceeding to install. -Previous install was built from source: +Clearing out a current install from 7.0.0+, making sure to provide options +used for install (`-l` or `-p`). +```shell +cd +inv uninstall +``` + +Previous install was built from source for CORE release older than 7.0.0: ```shell cd sudo make uninstall @@ -114,16 +125,29 @@ sudo apt remove core ``` ## Automated Install -The automated install will do the following: -* install base tools needed for installation - * python3, pip, pipx, invoke, poetry -* installs system dependencies for building core -* clone/build/install working version of [OPSF MDR](https://github.com/USNavalResearchLaboratory/ospf-mdr) -* installs core into poetry managed virtual environment or locally, if flag is passed -* installs scripts pointing pointing to appropriate python location based on install type -* installs systemd service pointing to appropriate python location based on install type +First we will need to clone and navigate to the CORE repo. +```shell +# clone CORE repo +git clone https://github.com/coreemu/core.git +cd core +``` -After installation has completed you should be able to run `core-daemon` and `core-gui`. +First you can use `setup.sh` as a convenience to install tooling for running invoke tasks: + +> **NOTE:** `setup.sh` will attempt to determine your OS by way of `/etc/os-release`, currently it supports +> attempts to install OSs that are debian/redhat like (yum/apt). + +* python3, pip, venv +* pipx 0.16.4 via pip +* invoke 1.4.1 via pipx +* poetry 1.1.7 via pipx + +Then you can run `inv install `: +* installs system dependencies for building core +* installs core into poetry managed virtual environment or locally, if flag is passed +* installs scripts pointing to appropriate python location based on install type +* installs systemd service pointing to appropriate python location based on install type +* clone/build/install working version of [OPSF MDR](https://github.com/USNavalResearchLaboratory/ospf-mdr) > **NOTE:** installing locally comes with its own risks, it can result it potential > dependency conflicts with system package manager installed python dependencies @@ -131,22 +155,21 @@ After installation has completed you should be able to run `core-daemon` and `co > **NOTE:** provide a prefix that will be found on path when running as sudo, > if the default prefix /usr/local will not be valid -`install.sh` will attempt to determine your OS by way of `/etc/os-release`, currently it supports -attempts to install OSs that are debian/redhat like (yum/apt). ```shell -# make sure pip is the latest version before moving forward -python3 -m pip install -U pip +inv -h install -# clone CORE repo -git clone https://github.com/coreemu/core.git -cd core +Usage: inv[oke] [--core-opts] install [--options] [other tasks here ...] -# script usage: install.sh [-v] [-d] [-l] [-p ] -# -# -v enable verbose install -# -d enable developer install -# -l enable local install, not compatible with developer install -# -p install prefix, defaults to /usr/local +Docstring: + install core, poetry, scripts, service, and ospf mdr + +Options: + -d, --dev install development mode + -i STRING, --install-type=STRING used to force an install type, can be one of the following (redhat, debian) + -l, --local determines if core will install to local system, default is False + -o, --[no-]ospf disable ospf installation + -p STRING, --prefix=STRING prefix where scripts are installed, default is /usr/local + -v, --verbose enable verbose # install core to virtual environment ./install.sh -p @@ -155,6 +178,35 @@ cd core ./install.sh -p -l ``` +After installation has completed you should be able to run `core-daemon` and `core-gui`. + +## Using Invoke Tasks +The invoke tool installed by way of pipx provides conveniences for running +CORE tasks to help ensure usage of the create python virtual environment. + +```shell +inv --list + +Available tasks: + + install install core, poetry, scripts, service, and ospf mdr + install-emane install emane python bindings into the core virtual environment + reinstall run the uninstall task, get latest from specified branch, and run install task + test run core tests + test-emane run core emane tests + test-mock run core tests using mock to avoid running as sudo + uninstall uninstall core, scripts, service, virtual environment, and clean build directory +``` + +### Enabling Service +After installation, the core service is not enabled by default. If you desire to use the +service, run the following commands. + +```shell +sudo systemctl enable core-daemon +sudo systemctl start core-daemon +``` + ### Unsupported Linux Distribution For unsupported OSs you could attempt to do the following to translate an installation to your use case. @@ -163,26 +215,6 @@ an installation to your use case. * make sure you have python3 invoke available to leverage `/tasks.py` ```shell -cd - -# Usage: inv[oke] [--core-opts] install [--options] [other tasks here ...] -# -# Docstring: -# install core, poetry, scripts, service, and ospf mdr -# -# Options: -# -d, --dev install development mode -# -i STRING, --install-type=STRING -# -l, --local determines if core will install to local system, default is False -# -p STRING, --prefix=STRING prefix where scripts are installed, default is /usr/local -# -v, --verbose enable verbose - -# install virtual environment -inv install -p - -# indstall locally -inv install -p -l - # this will print the commands that would be ran for a given installation # type without actually running them, they may help in being used as # the basis for translating to your OS @@ -208,82 +240,14 @@ python3