From 67da3e5c228e8693037dfdbe1d8cd4930778f43b Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 27 Feb 2020 21:39:18 -0800 Subject: [PATCH 1/2] changes to move sdt calls internal to core interactions, which allows it to work with both guis --- daemon/core/api/tlv/corehandlers.py | 7 - daemon/core/emulator/session.py | 10 +- daemon/core/plugins/sdt.py | 434 ++++++++++------------------ 3 files changed, 156 insertions(+), 295 deletions(-) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index a5dbb882..1f3b24e9 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -526,11 +526,6 @@ class CoreHandler(socketserver.BaseRequestHandler): logging.debug( "%s handling message:\n%s", threading.currentThread().getName(), message ) - - # provide to sdt, if enabled - if self.session and self.session.sdt.is_enabled(): - self.session.sdt.handle_distributed(message) - if message.message_type not in self.message_handlers: logging.error("no handler for message type: %s", message.type_str()) return @@ -2042,7 +2037,6 @@ class CoreUdpHandler(CoreHandler): logging.debug("session handling message: %s", session.session_id) self.session = session self.handle_message(message) - self.session.sdt.handle_distributed(message) self.broadcast(message) else: logging.error( @@ -2067,7 +2061,6 @@ class CoreUdpHandler(CoreHandler): if session or message.message_type == MessageTypes.REGISTER.value: self.session = session self.handle_message(message) - self.session.sdt.handle_distributed(message) self.broadcast(message) else: logging.error( diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 2d91bab3..d112eb9c 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -432,6 +432,7 @@ class Session: if node_two: node_two.lock.release() + self.sdt.add_link(node_one_id, node_two_id, is_wireless=False) return node_one_interface, node_two_interface def delete_link( @@ -540,6 +541,8 @@ class Session: if node_two: node_two.lock.release() + self.sdt.delete_link(node_one_id, node_two_id) + def update_link( self, node_one_id: int, @@ -757,6 +760,7 @@ class Session: self.add_remove_control_interface(node=node, remove=False) self.services.boot_services(node) + self.sdt.add_node(node) return node def edit_node(self, node_id: int, options: NodeOptions) -> None: @@ -765,7 +769,7 @@ class Session: :param node_id: id of node to update :param options: data to update node with - :return: True if node updated, False otherwise + :return: nothing :raises core.CoreError: when node to update does not exist """ # get node to update @@ -778,6 +782,8 @@ class Session: node.canvas = options.canvas node.icon = options.icon + self.sdt.edit_node(node) + def set_node_position(self, node: NodeBase, options: NodeOptions) -> None: """ Set position for a node, use lat/lon/alt if needed. @@ -1402,6 +1408,7 @@ class Session: if node: node.shutdown() self.check_shutdown() + self.sdt.delete_node(_id) return node is not None @@ -1413,6 +1420,7 @@ class Session: funcs = [] while self.nodes: _, node = self.nodes.popitem() + self.sdt.delete_node(node.id) funcs.append((node.shutdown, [], {})) utils.threadpool(funcs) self.node_id_gen.id = 0 diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index e5a5a545..aca349ac 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -4,22 +4,15 @@ sdt.py: Scripted Display Tool (SDT3D) helper import logging import socket -from typing import TYPE_CHECKING, Any, Optional +import threading +from typing import TYPE_CHECKING, Optional, Tuple from urllib.parse import urlparse from core import constants -from core.api.tlv.coreapi import CoreLinkMessage, CoreMessage, CoreNodeMessage from core.constants import CORE_DATA_DIR from core.emane.nodes import EmaneNet from core.emulator.data import LinkData, NodeData -from core.emulator.enumerations import ( - EventTypes, - LinkTlvs, - LinkTypes, - MessageFlags, - NodeTlvs, - NodeTypes, -) +from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags from core.errors import CoreError from core.nodes.base import CoreNetworkBase, NodeBase from core.nodes.network import WlanNode @@ -28,19 +21,11 @@ if TYPE_CHECKING: from core.emulator.session import Session -# TODO: A named tuple may be more appropriate, than abusing a class dict like this -class Bunch: - """ - Helper class for recording a collection of attributes. - """ - - def __init__(self, **kwargs: Any) -> None: - """ - Create a Bunch instance. - - :param kwargs: keyword arguments - """ - self.__dict__.update(kwargs) +def link_data_params(link_data: LinkData) -> Tuple[int, int, bool]: + node_one = link_data.node1_id + node_two = link_data.node2_id + is_wireless = link_data.link_type == LinkTypes.WIRELESS.value + return node_one, node_two, is_wireless class Sdt: @@ -74,53 +59,16 @@ class Sdt: :param session: session this manager is tied to """ self.session = session + self.lock = threading.Lock() self.sock = None self.connected = False self.showerror = True self.url = self.DEFAULT_SDT_URL - # node information for remote nodes not in session._objs - # local nodes also appear here since their obj may not exist yet - self.remotes = {} - - # add handler for node updates + self.address = None + self.protocol = None self.session.node_handlers.append(self.handle_node_update) - - # add handler for link updates self.session.link_handlers.append(self.handle_link_update) - def handle_node_update(self, node_data: NodeData) -> None: - """ - Handler for node updates, specifically for updating their location. - - :param node_data: node data being updated - :return: nothing - """ - x = node_data.x_position - y = node_data.y_position - lat = node_data.latitude - lon = node_data.longitude - alt = node_data.altitude - if all([lat is not None, lon is not None, alt is not None]): - self.updatenodegeo(node_data.id, lat, lon, alt) - elif node_data.message_type == 0: - # TODO: z is not currently supported by node messages - self.updatenode(node_data.id, 0, x, y, 0) - - def handle_link_update(self, link_data: LinkData) -> None: - """ - Handler for link updates, checking for wireless link/unlink messages. - - :param link_data: link data being updated - :return: nothing - """ - if link_data.link_type == LinkTypes.WIRELESS.value: - self.updatelink( - link_data.node1_id, - link_data.node2_id, - link_data.message_type, - wireless=True, - ) - def is_enabled(self) -> bool: """ Check for "enablesdt" session option. Return False by default if @@ -137,9 +85,7 @@ class Sdt: :return: nothing """ - url = self.session.options.get_config("stdurl") - if not url: - url = self.DEFAULT_SDT_URL + url = self.session.options.get_config("stdurl", default=self.DEFAULT_SDT_URL) self.url = urlparse(url) self.address = (self.url.hostname, self.url.port) self.protocol = self.url.scheme @@ -178,7 +124,6 @@ class Sdt: # refresh all objects in SDT3D when connecting after session start if not flags & MessageFlags.ADD.value and not self.sendobjs(): return False - return True def initialize(self) -> bool: @@ -234,8 +179,10 @@ class Sdt: """ if self.sock is None: return False + try: cmd = f"{cmdstr}\n".encode() + logging.debug("sdt cmd: %s", cmd) self.sock.sendall(cmd) return True except IOError: @@ -244,91 +191,6 @@ class Sdt: self.connected = False return False - def updatenode( - self, - nodenum: int, - flags: int, - x: Optional[float], - y: Optional[float], - z: Optional[float], - name: str = None, - node_type: str = None, - icon: str = None, - ) -> None: - """ - Node is updated from a Node Message or mobility script. - - :param nodenum: node id to update - :param flags: update flags - :param x: x position - :param y: y position - :param z: z position - :param name: node name - :param node_type: node type - :param icon: node icon - :return: nothing - """ - if not self.connect(): - return - if flags & MessageFlags.DELETE.value: - self.cmd(f"delete node,{nodenum}") - return - if x is None or y is None: - return - lat, lon, alt = self.session.location.getgeo(x, y, z) - pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" - if flags & MessageFlags.ADD.value: - if icon is not None: - node_type = name - icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR) - icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR) - self.cmd(f"sprite {node_type} image {icon}") - self.cmd(f'node {nodenum} type {node_type} label on,"{name}" {pos}') - else: - self.cmd(f"node {nodenum} {pos}") - - def updatenodegeo(self, nodenum: int, lat: float, lon: float, alt: float) -> None: - """ - Node is updated upon receiving an EMANE Location Event. - - :param nodenum: node id to update geospatial for - :param lat: latitude - :param lon: longitude - :param alt: altitude - :return: nothing - """ - - # TODO: received Node Message with lat/long/alt. - if not self.connect(): - return - pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" - self.cmd(f"node {nodenum} {pos}") - - def updatelink( - self, node1num: int, node2num: int, flags: int, wireless: bool = False - ) -> None: - """ - Link is updated from a Link Message or by a wireless model. - - :param node1num: node one id - :param node2num: node two id - :param flags: link flags - :param wireless: flag to check if wireless or not - :return: nothing - """ - if node1num is None or node2num is None: - return - if not self.connect(): - return - if flags & MessageFlags.DELETE.value: - self.cmd(f"delete link,{node1num},{node2num}") - elif flags & MessageFlags.ADD.value: - if wireless: - attr = " line green,2" - else: - attr = " line red,2" - self.cmd(f"link {node1num},{node2num}{attr}") - def sendobjs(self) -> None: """ Session has already started, and the SDT3D GUI later connects. @@ -345,171 +207,169 @@ class Sdt: nets.append(node) if not isinstance(node, NodeBase): continue - (x, y, z) = node.getposition() - if x is None or y is None: - continue - self.updatenode( - node.id, - MessageFlags.ADD.value, - x, - y, - z, - node.name, - node.type, - node.icon, - ) - for nodenum in sorted(self.remotes.keys()): - r = self.remotes[nodenum] - x, y, z = r.pos - self.updatenode( - nodenum, MessageFlags.ADD.value, x, y, z, r.name, r.type, r.icon - ) + self.add_node(node) for net in nets: all_links = net.all_link_data(flags=MessageFlags.ADD.value) for link_data in all_links: is_wireless = isinstance(net, (WlanNode, EmaneNet)) - wireless_link = link_data.message_type == LinkTypes.WIRELESS.value if is_wireless and link_data.node1_id == net.id: continue + params = link_data_params(link_data) + self.add_link(*params) - self.updatelink( - link_data.node1_id, - link_data.node2_id, - MessageFlags.ADD.value, - wireless_link, - ) - - for n1num in sorted(self.remotes.keys()): - r = self.remotes[n1num] - for n2num, wireless_link in r.links: - self.updatelink(n1num, n2num, MessageFlags.ADD.value, wireless_link) - - def handle_distributed(self, message: CoreMessage) -> None: + def get_node_position(self, node: NodeBase) -> Optional[str]: """ - Broker handler for processing CORE API messages as they are - received. This is used to snoop the Node messages and update - node positions. + Convenience to generate an SDT position string, given a node. - :param message: message to handle + :param node: + :return: + """ + x, y, z = node.position.get() + if x is None or y is None: + return None + lat, lon, alt = self.session.location.getgeo(x, y, z) + return f"pos {lon:.6f},{lat:.6f},{alt:.6f}" + + def add_node(self, node: NodeBase) -> None: + """ + Handle adding a node in SDT. + + :param node: node to add :return: nothing """ - if isinstance(message, CoreLinkMessage): - self.handlelinkmsg(message) - elif isinstance(message, CoreNodeMessage): - self.handlenodemsg(message) + logging.debug("sdt add node: %s - %s", node.id, node.name) + if not self.connect(): + return + pos = self.get_node_position(node) + if not pos: + return + node_type = node.type + if node_type is None: + node_type = type(node).type + icon = node.icon + if icon: + node_type = node.name + icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR) + icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR) + self.cmd(f"sprite {node_type} image {icon}") + self.cmd(f'node {node.id} type {node_type} label on,"{node.name}" {pos}') - def handlenodemsg(self, msg: CoreNodeMessage) -> None: + def edit_node(self, node: NodeBase) -> None: """ - Process a Node Message to add/delete or move a node on - the SDT display. Node properties are found in a session or - self.remotes for remote nodes (or those not yet instantiated). + Handle updating a node in SDT. - :param msg: node message to handle + :param node: node to update :return: nothing """ - # for distributed sessions to work properly, the SDT option should be - # enabled prior to starting the session - if not self.is_enabled(): + logging.debug("sdt update node: %s - %s", node.id, node.name) + if not self.connect(): return - # node.(_id, type, icon, name) are used. - nodenum = msg.get_tlv(NodeTlvs.NUMBER.value) - if not nodenum: + pos = self.get_node_position(node) + if not pos: return - x = msg.get_tlv(NodeTlvs.X_POSITION.value) - y = msg.get_tlv(NodeTlvs.Y_POSITION.value) - z = None - name = msg.get_tlv(NodeTlvs.NAME.value) + self.cmd(f"node {node.id} {pos}") - nodetype = msg.get_tlv(NodeTlvs.TYPE.value) - model = msg.get_tlv(NodeTlvs.MODEL.value) - icon = msg.get_tlv(NodeTlvs.ICON.value) + def delete_node(self, node_id: int) -> None: + """ + Handle deleting a node in SDT. - net = False - if nodetype == NodeTypes.DEFAULT.value or nodetype == NodeTypes.PHYSICAL.value: - if model is None: - model = "router" - nodetype = model - elif nodetype is not None: - nodetype = NodeTypes(nodetype) - nodetype = self.session.get_node_class(nodetype).type - net = True + :param node_id: node id to delete + :return: nothing + """ + logging.debug("sdt delete node: %s", node_id) + if not self.connect(): + return + self.cmd(f"delete node,{node_id}") + + def handle_node_update(self, node_data: NodeData) -> None: + """ + Handler for node updates, specifically for updating their location. + + :param node_data: node data being updated + :return: nothing + """ + logging.debug("sdt handle node update: %s - %s", node_data.id, node_data.name) + if not self.connect(): + return + + # delete node + if node_data.message_type == MessageFlags.DELETE.value: + self.cmd(f"delete node,{node_data.id}") else: - nodetype = None + x = node_data.x_position + y = node_data.y_position + lat = node_data.latitude + lon = node_data.longitude + alt = node_data.altitude + if all([lat is not None, lon is not None, alt is not None]): + pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" + self.cmd(f"node {node_data.id} {pos}") + elif node_data.message_type == 0: + lat, lon, alt = self.session.location.getgeo(x, y, 0) + pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}" + self.cmd(f"node {node_data.id} {pos}") + def wireless_net_check(self, node_id: int) -> bool: + """ + Determines if a node is either a wireless node type. + + :param node_id: node id to check + :return: True is a wireless node type, False otherwise + """ + result = False try: - node = self.session.get_node(nodenum) + node = self.session.get_node(node_id) + result = isinstance(node, (WlanNode, EmaneNet)) except CoreError: - node = None - if node: - self.updatenode( - node.id, msg.flags, x, y, z, node.name, node.type, node.icon - ) - else: - if nodenum in self.remotes: - remote = self.remotes[nodenum] - if name is None: - name = remote.name - if nodetype is None: - nodetype = remote.type - if icon is None: - icon = remote.icon - else: - remote = Bunch( - _id=nodenum, - type=nodetype, - icon=icon, - name=name, - net=net, - links=set(), - ) - self.remotes[nodenum] = remote - remote.pos = (x, y, z) - self.updatenode(nodenum, msg.flags, x, y, z, name, nodetype, icon) + pass + return result - def handlelinkmsg(self, msg: CoreLinkMessage) -> None: + def add_link(self, node_one: int, node_two: int, is_wireless: bool) -> None: """ - Process a Link Message to add/remove links on the SDT display. - Links are recorded in the remotes[nodenum1].links set for updating - the SDT display at a later time. + Handle adding a link in SDT. - :param msg: link message to handle + :param node_one: node one id + :param node_two: node two id + :param is_wireless: True if link is wireless, False otherwise :return: nothing """ - if not self.is_enabled(): + logging.debug("sdt add link: %s, %s, %s", node_one, node_two, is_wireless) + if not self.connect(): return - nodenum1 = msg.get_tlv(LinkTlvs.N1_NUMBER.value) - nodenum2 = msg.get_tlv(LinkTlvs.N2_NUMBER.value) - link_msg_type = msg.get_tlv(LinkTlvs.TYPE.value) - # this filters out links to WLAN and EMANE nodes which are not drawn - if self.wlancheck(nodenum1): + if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): return - wl = link_msg_type == LinkTypes.WIRELESS.value - if nodenum1 in self.remotes: - r = self.remotes[nodenum1] - if msg.flags & MessageFlags.DELETE.value: - if (nodenum2, wl) in r.links: - r.links.remove((nodenum2, wl)) - else: - r.links.add((nodenum2, wl)) - self.updatelink(nodenum1, nodenum2, msg.flags, wireless=wl) - - def wlancheck(self, nodenum: int) -> bool: - """ - Helper returns True if a node number corresponds to a WLAN or EMANE node. - - :param nodenum: node id to check - :return: True if node is wlan or emane, False otherwise - """ - if nodenum in self.remotes: - node_type = self.remotes[nodenum].type - if node_type in ("wlan", "emane"): - return True + if is_wireless: + attr = "green,2" else: - try: - n = self.session.get_node(nodenum) - except CoreError: - return False - if isinstance(n, (WlanNode, EmaneNet)): - return True - return False + attr = "red,2" + self.cmd(f"link {node_one},{node_two} line {attr}") + + def delete_link(self, node_one: int, node_two: int) -> None: + """ + Handle deleting a node in SDT. + + :param node_one: node one id + :param node_two: node two id + :return: nothing + """ + logging.debug("sdt delete link: %s, %s", node_one, node_two) + if not self.connect(): + return + if self.wireless_net_check(node_one) or self.wireless_net_check(node_two): + return + self.cmd(f"delete link,{node_one},{node_two}") + + def handle_link_update(self, link_data: LinkData) -> None: + """ + Handle link broadcast messages and push changes to SDT. + + :param link_data: link data to handle + :return: nothing + """ + if link_data.message_type == MessageFlags.ADD.value: + params = link_data_params(link_data) + self.add_link(*params) + elif link_data.message_type == MessageFlags.DELETE.value: + params = link_data_params(link_data) + self.delete_link(*params[:2]) From 9535d40b700ab64b657b516e7ed59ed79f6d0923 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 28 Feb 2020 12:28:41 -0800 Subject: [PATCH 2/2] added grpc call to execute python script, to replicate prior gui functionality --- daemon/core/api/grpc/client.py | 6 ++++++ daemon/core/api/grpc/server.py | 22 ++++++++++++++++++++++ daemon/proto/core/api/grpc/core.proto | 10 ++++++++++ 3 files changed, 38 insertions(+) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index f0808713..15122e67 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -27,6 +27,8 @@ from core.api.grpc.configservices_pb2 import ( SetNodeConfigServiceResponse, ) from core.api.grpc.core_pb2 import ( + ExecuteScriptRequest, + ExecuteScriptResponse, GetEmaneEventChannelRequest, GetEmaneEventChannelResponse, ) @@ -1148,6 +1150,10 @@ class CoreGrpcClient: request = GetEmaneEventChannelRequest(session_id=session_id) return self.stub.GetEmaneEventChannel(request) + def execute_script(self, script: str) -> ExecuteScriptResponse: + request = ExecuteScriptRequest(script=script) + return self.stub.ExecuteScript(request) + def connect(self) -> None: """ Open connection to server, must be closed manually. diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index f155867d..ca992481 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -3,6 +3,7 @@ import logging import os import re import tempfile +import threading import time from concurrent import futures from typing import Type @@ -10,6 +11,7 @@ from typing import Type import grpc from grpc import ServicerContext +from core import utils from core.api.grpc import ( common_pb2, configservices_pb2, @@ -33,6 +35,7 @@ from core.api.grpc.configservices_pb2 import ( SetNodeConfigServiceResponse, ) from core.api.grpc.core_pb2 import ( + ExecuteScriptResponse, GetEmaneEventChannelRequest, GetEmaneEventChannelResponse, ) @@ -1645,3 +1648,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if session.emane.eventchannel: group, port, device = session.emane.eventchannel return GetEmaneEventChannelResponse(group=group, port=port, device=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() + current_sessions = set(self.coreemu.sessions.keys()) + new_sessions = list(current_sessions.difference(existing_sessions)) + new_session = -1 + if new_sessions: + new_session = new_sessions[0] + return ExecuteScriptResponse(session_id=new_session) diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index e515ab2e..b89e5fb1 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -154,6 +154,8 @@ service CoreApi { } rpc EmaneLink (EmaneLinkRequest) returns (EmaneLinkResponse) { } + rpc ExecuteScript (ExecuteScriptRequest) returns (ExecuteScriptResponse) { + } } // rpc request/response messages @@ -759,6 +761,14 @@ message EmaneLinkResponse { bool result = 1; } +message ExecuteScriptRequest { + string script = 1; +} + +message ExecuteScriptResponse { + int32 session_id = 1; +} + // data structures for messages below message WlanConfig { int32 node_id = 1;