Merge branch 'develop' into coretk-enhance/execute-xml-script

This commit is contained in:
Huy Pham 2020-02-28 12:52:29 -08:00
commit c1f0e385b4
6 changed files with 194 additions and 295 deletions

View file

@ -27,6 +27,8 @@ from core.api.grpc.configservices_pb2 import (
SetNodeConfigServiceResponse, SetNodeConfigServiceResponse,
) )
from core.api.grpc.core_pb2 import ( from core.api.grpc.core_pb2 import (
ExecuteScriptRequest,
ExecuteScriptResponse,
GetEmaneEventChannelRequest, GetEmaneEventChannelRequest,
GetEmaneEventChannelResponse, GetEmaneEventChannelResponse,
) )
@ -1148,6 +1150,10 @@ class CoreGrpcClient:
request = GetEmaneEventChannelRequest(session_id=session_id) request = GetEmaneEventChannelRequest(session_id=session_id)
return self.stub.GetEmaneEventChannel(request) 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: def connect(self) -> None:
""" """
Open connection to server, must be closed manually. Open connection to server, must be closed manually.

View file

@ -3,6 +3,7 @@ import logging
import os import os
import re import re
import tempfile import tempfile
import threading
import time import time
from concurrent import futures from concurrent import futures
from typing import Type from typing import Type
@ -10,6 +11,7 @@ from typing import Type
import grpc import grpc
from grpc import ServicerContext from grpc import ServicerContext
from core import utils
from core.api.grpc import ( from core.api.grpc import (
common_pb2, common_pb2,
configservices_pb2, configservices_pb2,
@ -33,6 +35,7 @@ from core.api.grpc.configservices_pb2 import (
SetNodeConfigServiceResponse, SetNodeConfigServiceResponse,
) )
from core.api.grpc.core_pb2 import ( from core.api.grpc.core_pb2 import (
ExecuteScriptResponse,
GetEmaneEventChannelRequest, GetEmaneEventChannelRequest,
GetEmaneEventChannelResponse, GetEmaneEventChannelResponse,
) )
@ -1645,3 +1648,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
if session.emane.eventchannel: if session.emane.eventchannel:
group, port, device = session.emane.eventchannel group, port, device = session.emane.eventchannel
return GetEmaneEventChannelResponse(group=group, port=port, device=device) 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)

View file

@ -526,11 +526,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
logging.debug( logging.debug(
"%s handling message:\n%s", threading.currentThread().getName(), message "%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: if message.message_type not in self.message_handlers:
logging.error("no handler for message type: %s", message.type_str()) logging.error("no handler for message type: %s", message.type_str())
return return
@ -2042,7 +2037,6 @@ class CoreUdpHandler(CoreHandler):
logging.debug("session handling message: %s", session.session_id) logging.debug("session handling message: %s", session.session_id)
self.session = session self.session = session
self.handle_message(message) self.handle_message(message)
self.session.sdt.handle_distributed(message)
self.broadcast(message) self.broadcast(message)
else: else:
logging.error( logging.error(
@ -2067,7 +2061,6 @@ class CoreUdpHandler(CoreHandler):
if session or message.message_type == MessageTypes.REGISTER.value: if session or message.message_type == MessageTypes.REGISTER.value:
self.session = session self.session = session
self.handle_message(message) self.handle_message(message)
self.session.sdt.handle_distributed(message)
self.broadcast(message) self.broadcast(message)
else: else:
logging.error( logging.error(

View file

@ -432,6 +432,7 @@ class Session:
if node_two: if node_two:
node_two.lock.release() node_two.lock.release()
self.sdt.add_link(node_one_id, node_two_id, is_wireless=False)
return node_one_interface, node_two_interface return node_one_interface, node_two_interface
def delete_link( def delete_link(
@ -540,6 +541,8 @@ class Session:
if node_two: if node_two:
node_two.lock.release() node_two.lock.release()
self.sdt.delete_link(node_one_id, node_two_id)
def update_link( def update_link(
self, self,
node_one_id: int, node_one_id: int,
@ -757,6 +760,7 @@ class Session:
self.add_remove_control_interface(node=node, remove=False) self.add_remove_control_interface(node=node, remove=False)
self.services.boot_services(node) self.services.boot_services(node)
self.sdt.add_node(node)
return node return node
def edit_node(self, node_id: int, options: NodeOptions) -> None: 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 node_id: id of node to update
:param options: data to update node with :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 :raises core.CoreError: when node to update does not exist
""" """
# get node to update # get node to update
@ -778,6 +782,8 @@ class Session:
node.canvas = options.canvas node.canvas = options.canvas
node.icon = options.icon node.icon = options.icon
self.sdt.edit_node(node)
def set_node_position(self, node: NodeBase, options: NodeOptions) -> None: def set_node_position(self, node: NodeBase, options: NodeOptions) -> None:
""" """
Set position for a node, use lat/lon/alt if needed. Set position for a node, use lat/lon/alt if needed.
@ -1402,6 +1408,7 @@ class Session:
if node: if node:
node.shutdown() node.shutdown()
self.check_shutdown() self.check_shutdown()
self.sdt.delete_node(_id)
return node is not None return node is not None
@ -1413,6 +1420,7 @@ class Session:
funcs = [] funcs = []
while self.nodes: while self.nodes:
_, node = self.nodes.popitem() _, node = self.nodes.popitem()
self.sdt.delete_node(node.id)
funcs.append((node.shutdown, [], {})) funcs.append((node.shutdown, [], {}))
utils.threadpool(funcs) utils.threadpool(funcs)
self.node_id_gen.id = 0 self.node_id_gen.id = 0

View file

@ -4,22 +4,15 @@ sdt.py: Scripted Display Tool (SDT3D) helper
import logging import logging
import socket import socket
from typing import TYPE_CHECKING, Any, Optional import threading
from typing import TYPE_CHECKING, Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
from core import constants from core import constants
from core.api.tlv.coreapi import CoreLinkMessage, CoreMessage, CoreNodeMessage
from core.constants import CORE_DATA_DIR from core.constants import CORE_DATA_DIR
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.data import LinkData, NodeData from core.emulator.data import LinkData, NodeData
from core.emulator.enumerations import ( from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags
EventTypes,
LinkTlvs,
LinkTypes,
MessageFlags,
NodeTlvs,
NodeTypes,
)
from core.errors import CoreError from core.errors import CoreError
from core.nodes.base import CoreNetworkBase, NodeBase from core.nodes.base import CoreNetworkBase, NodeBase
from core.nodes.network import WlanNode from core.nodes.network import WlanNode
@ -28,19 +21,11 @@ if TYPE_CHECKING:
from core.emulator.session import Session from core.emulator.session import Session
# TODO: A named tuple may be more appropriate, than abusing a class dict like this def link_data_params(link_data: LinkData) -> Tuple[int, int, bool]:
class Bunch: node_one = link_data.node1_id
""" node_two = link_data.node2_id
Helper class for recording a collection of attributes. is_wireless = link_data.link_type == LinkTypes.WIRELESS.value
""" return node_one, node_two, is_wireless
def __init__(self, **kwargs: Any) -> None:
"""
Create a Bunch instance.
:param kwargs: keyword arguments
"""
self.__dict__.update(kwargs)
class Sdt: class Sdt:
@ -74,53 +59,16 @@ class Sdt:
:param session: session this manager is tied to :param session: session this manager is tied to
""" """
self.session = session self.session = session
self.lock = threading.Lock()
self.sock = None self.sock = None
self.connected = False self.connected = False
self.showerror = True self.showerror = True
self.url = self.DEFAULT_SDT_URL self.url = self.DEFAULT_SDT_URL
# node information for remote nodes not in session._objs self.address = None
# local nodes also appear here since their obj may not exist yet self.protocol = None
self.remotes = {}
# add handler for node updates
self.session.node_handlers.append(self.handle_node_update) self.session.node_handlers.append(self.handle_node_update)
# add handler for link updates
self.session.link_handlers.append(self.handle_link_update) 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: def is_enabled(self) -> bool:
""" """
Check for "enablesdt" session option. Return False by default if Check for "enablesdt" session option. Return False by default if
@ -137,9 +85,7 @@ class Sdt:
:return: nothing :return: nothing
""" """
url = self.session.options.get_config("stdurl") url = self.session.options.get_config("stdurl", default=self.DEFAULT_SDT_URL)
if not url:
url = self.DEFAULT_SDT_URL
self.url = urlparse(url) self.url = urlparse(url)
self.address = (self.url.hostname, self.url.port) self.address = (self.url.hostname, self.url.port)
self.protocol = self.url.scheme self.protocol = self.url.scheme
@ -178,7 +124,6 @@ class Sdt:
# refresh all objects in SDT3D when connecting after session start # refresh all objects in SDT3D when connecting after session start
if not flags & MessageFlags.ADD.value and not self.sendobjs(): if not flags & MessageFlags.ADD.value and not self.sendobjs():
return False return False
return True return True
def initialize(self) -> bool: def initialize(self) -> bool:
@ -234,8 +179,10 @@ class Sdt:
""" """
if self.sock is None: if self.sock is None:
return False return False
try: try:
cmd = f"{cmdstr}\n".encode() cmd = f"{cmdstr}\n".encode()
logging.debug("sdt cmd: %s", cmd)
self.sock.sendall(cmd) self.sock.sendall(cmd)
return True return True
except IOError: except IOError:
@ -244,91 +191,6 @@ class Sdt:
self.connected = False self.connected = False
return 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: def sendobjs(self) -> None:
""" """
Session has already started, and the SDT3D GUI later connects. Session has already started, and the SDT3D GUI later connects.
@ -345,171 +207,169 @@ class Sdt:
nets.append(node) nets.append(node)
if not isinstance(node, NodeBase): if not isinstance(node, NodeBase):
continue continue
(x, y, z) = node.getposition() self.add_node(node)
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
)
for net in nets: for net in nets:
all_links = net.all_link_data(flags=MessageFlags.ADD.value) all_links = net.all_link_data(flags=MessageFlags.ADD.value)
for link_data in all_links: for link_data in all_links:
is_wireless = isinstance(net, (WlanNode, EmaneNet)) 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: if is_wireless and link_data.node1_id == net.id:
continue continue
params = link_data_params(link_data)
self.add_link(*params)
self.updatelink( def get_node_position(self, node: NodeBase) -> Optional[str]:
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:
""" """
Broker handler for processing CORE API messages as they are Convenience to generate an SDT position string, given a node.
received. This is used to snoop the Node messages and update
node positions.
: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 :return: nothing
""" """
if isinstance(message, CoreLinkMessage): logging.debug("sdt add node: %s - %s", node.id, node.name)
self.handlelinkmsg(message) if not self.connect():
elif isinstance(message, CoreNodeMessage): return
self.handlenodemsg(message) 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 Handle updating a node in SDT.
the SDT display. Node properties are found in a session or
self.remotes for remote nodes (or those not yet instantiated).
:param msg: node message to handle :param node: node to update
:return: nothing :return: nothing
""" """
# for distributed sessions to work properly, the SDT option should be logging.debug("sdt update node: %s - %s", node.id, node.name)
# enabled prior to starting the session if not self.connect():
if not self.is_enabled():
return return
# node.(_id, type, icon, name) are used. pos = self.get_node_position(node)
nodenum = msg.get_tlv(NodeTlvs.NUMBER.value) if not pos:
if not nodenum:
return return
x = msg.get_tlv(NodeTlvs.X_POSITION.value) self.cmd(f"node {node.id} {pos}")
y = msg.get_tlv(NodeTlvs.Y_POSITION.value)
z = None
name = msg.get_tlv(NodeTlvs.NAME.value)
nodetype = msg.get_tlv(NodeTlvs.TYPE.value) def delete_node(self, node_id: int) -> None:
model = msg.get_tlv(NodeTlvs.MODEL.value) """
icon = msg.get_tlv(NodeTlvs.ICON.value) Handle deleting a node in SDT.
net = False :param node_id: node id to delete
if nodetype == NodeTypes.DEFAULT.value or nodetype == NodeTypes.PHYSICAL.value: :return: nothing
if model is None: """
model = "router" logging.debug("sdt delete node: %s", node_id)
nodetype = model if not self.connect():
elif nodetype is not None: return
nodetype = NodeTypes(nodetype) self.cmd(f"delete node,{node_id}")
nodetype = self.session.get_node_class(nodetype).type
net = True 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: 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: try:
node = self.session.get_node(nodenum) node = self.session.get_node(node_id)
result = isinstance(node, (WlanNode, EmaneNet))
except CoreError: except CoreError:
node = None pass
if node: return result
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)
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. Handle adding a link in SDT.
Links are recorded in the remotes[nodenum1].links set for updating
the SDT display at a later time.
: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 :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 return
nodenum1 = msg.get_tlv(LinkTlvs.N1_NUMBER.value) if self.wireless_net_check(node_one) or self.wireless_net_check(node_two):
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):
return return
wl = link_msg_type == LinkTypes.WIRELESS.value if is_wireless:
if nodenum1 in self.remotes: attr = "green,2"
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
else: else:
try: attr = "red,2"
n = self.session.get_node(nodenum) self.cmd(f"link {node_one},{node_two} line {attr}")
except CoreError:
return False def delete_link(self, node_one: int, node_two: int) -> None:
if isinstance(n, (WlanNode, EmaneNet)): """
return True Handle deleting a node in SDT.
return False
: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])

View file

@ -154,6 +154,8 @@ service CoreApi {
} }
rpc EmaneLink (EmaneLinkRequest) returns (EmaneLinkResponse) { rpc EmaneLink (EmaneLinkRequest) returns (EmaneLinkResponse) {
} }
rpc ExecuteScript (ExecuteScriptRequest) returns (ExecuteScriptResponse) {
}
} }
// rpc request/response messages // rpc request/response messages
@ -759,6 +761,14 @@ message EmaneLinkResponse {
bool result = 1; bool result = 1;
} }
message ExecuteScriptRequest {
string script = 1;
}
message ExecuteScriptResponse {
int32 session_id = 1;
}
// data structures for messages below // data structures for messages below
message WlanConfig { message WlanConfig {
int32 node_id = 1; int32 node_id = 1;