core-extra/daemon/core/sdt.py

388 lines
14 KiB
Python
Raw Normal View History

"""
sdt.py: Scripted Display Tool (SDT3D) helper
"""
import socket
from urlparse import urlparse
from core import constants
from core.api import coreapi
from core.coreobj import PyCoreNet
from core.coreobj import PyCoreObj
from core.enumerations import EventTypes
from core.enumerations import LinkTlvs
from core.enumerations import LinkTypes
from core.enumerations import MessageFlags
from core.enumerations import MessageTypes
from core.enumerations import NodeTlvs
from core.enumerations import NodeTypes
from core.misc import log
from core.misc import nodeutils
logger = log.get_logger(__name__)
class Bunch:
"""
Helper class for recording a collection of attributes.
"""
def __init__(self, **kwds):
self.__dict__.update(kwds)
class Sdt(object):
"""
Helper class for exporting session objects to NRL's SDT3D.
The connect() method initializes the display, and can be invoked
when a node position or link has changed.
"""
DEFAULT_SDT_URL = "tcp://127.0.0.1:50000/"
# default altitude (in meters) for flyto view
DEFAULT_ALT = 2500
# TODO: read in user's nodes.conf here; below are default node types from the GUI
DEFAULT_SPRITES = [
('router', 'router.gif'), ('host', 'host.gif'),
('PC', 'pc.gif'), ('mdr', 'mdr.gif'),
('prouter', 'router_green.gif'), ('xen', 'xen.gif'),
('hub', 'hub.gif'), ('lanswitch', 'lanswitch.gif'),
('wlan', 'wlan.gif'), ('rj45', 'rj45.gif'),
('tunnel', 'tunnel.gif'),
]
def __init__(self, session):
"""
Creates a Sdt instance.
:param core.session.Session session: session this manager is tied to
:return: nothing
"""
self.session = session
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 = {}
session.broker.handlers.add(self.handledistributed)
def is_enabled(self):
"""
Check for 'enablesdt' session option. Return False by default if
the option is missing.
"""
if not hasattr(self.session.options, 'enablesdt'):
return False
enabled = self.session.options.enablesdt
if enabled in ('1', 'true', 1, True):
return True
return False
def seturl(self):
"""
Read 'sdturl' from session options, or use the default value.
Set self.url, self.address, self.protocol
"""
url = None
if hasattr(self.session.options, 'sdturl'):
if self.session.options.sdturl != "":
url = self.session.options.sdturl
if url is None or url == "":
url = self.DEFAULT_SDT_URL
self.url = urlparse(url)
self.address = (self.url.hostname, self.url.port)
self.protocol = self.url.scheme
def connect(self, flags=0):
"""
Connect to the SDT address/port if enabled.
"""
if not self.is_enabled():
return False
if self.connected:
return True
if self.session.state == EventTypes.SHUTDOWN_STATE.value:
return False
self.seturl()
logger.info("connecting to SDT at %s://%s" % (self.protocol, self.address))
if self.sock is None:
try:
if self.protocol.lower() == 'udp':
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.connect(self.address)
else:
# Default to tcp
self.sock = socket.create_connection(self.address, 5)
except IOError:
logger.exception("SDT socket connect error")
return False
if not self.initialize():
return False
self.connected = True
# 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):
"""
Load icon sprites, and fly to the reference point location on
the virtual globe.
"""
if not self.cmd('path "%s/icons/normal"' % constants.CORE_DATA_DIR):
return False
# send node type to icon mappings
for type, icon in self.DEFAULT_SPRITES:
if not self.cmd('sprite %s image %s' % (type, icon)):
return False
(lat, long) = self.session.location.refgeo[:2]
return self.cmd('flyto %.6f,%.6f,%d' % (long, lat, self.DEFAULT_ALT))
def disconnect(self):
if self.sock:
try:
self.sock.close()
except IOError:
logger.error("error closing socket")
finally:
self.sock = None
self.connected = False
def shutdown(self):
"""
Invoked from Session.shutdown() and Session.checkshutdown().
"""
self.cmd('clear all')
self.disconnect()
self.showerror = True
def cmd(self, cmdstr):
"""
Send an SDT command over a UDP socket. socket.sendall() is used
as opposed to socket.sendto() because an exception is raised when
there is no socket listener.
"""
if self.sock is None:
return False
try:
logger.info("sdt: %s" % cmdstr)
self.sock.sendall("%s\n" % cmdstr)
return True
except IOError:
logger.exception("SDT connection error")
self.sock = None
self.connected = False
return False
def updatenode(self, nodenum, flags, x, y, z, name=None, type=None, icon=None):
"""
Node is updated from a Node Message or mobility script.
"""
if not self.connect():
return
if flags & MessageFlags.DELETE.value:
self.cmd('delete node,%d' % nodenum)
return
if x is None or y is None:
return
(lat, long, alt) = self.session.location.getgeo(x, y, z)
pos = "pos %.6f,%.6f,%.6f" % (long, lat, alt)
if flags & MessageFlags.ADD.value:
if icon is not None:
type = name
icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR)
icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR)
self.cmd('sprite %s image %s' % (type, icon))
self.cmd('node %d type %s label on,"%s" %s' % (nodenum, type, name, pos))
else:
self.cmd('node %d %s' % (nodenum, pos))
def updatenodegeo(self, nodenum, lat, long, alt):
"""
Node is updated upon receiving an EMANE Location Event.
TODO: received Node Message with lat/long/alt.
"""
if not self.connect():
return
pos = "pos %.6f,%.6f,%.6f" % (long, lat, alt)
self.cmd('node %d %s' % (nodenum, pos))
def updatelink(self, node1num, node2num, flags, wireless=False):
"""
Link is updated from a Link Message or by a wireless model.
"""
if node1num is None or node2num is None:
return
if not self.connect():
return
if flags & MessageFlags.DELETE.value:
self.cmd('delete link,%s,%s' % (node1num, node2num))
elif flags & MessageFlags.ADD.value:
attr = ""
if wireless:
attr = " line green,2"
else:
attr = " line red,2"
self.cmd('link %s,%s%s' % (node1num, node2num, attr))
def sendobjs(self):
"""
Session has already started, and the SDT3D GUI later connects.
Send all node and link objects for display. Otherwise, nodes and
links will only be drawn when they have been updated (e.g. moved).
"""
nets = []
with self.session._objects_lock:
for obj in self.session.objects.itervalues():
if isinstance(obj, PyCoreNet):
nets.append(obj)
if not isinstance(obj, PyCoreObj):
continue
(x, y, z) = obj.getposition()
if x is None or y is None:
continue
self.updatenode(obj.objid, MessageFlags.ADD.value, x, y, z,
obj.name, obj.type, obj.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:
# use tolinkmsgs() to handle various types of links
messages = net.all_link_data(flags=MessageFlags.ADD.value)
for message in messages:
msghdr = message[:coreapi.CoreMessage.header_len]
flags = coreapi.CoreMessage.unpack_header(msghdr)[1]
m = coreapi.CoreLinkMessage(flags, msghdr, message[coreapi.CoreMessage.header_len:])
n1num = m.get_tlv(LinkTlvs.N1_NUMBER.value)
n2num = m.get_tlv(LinkTlvs.N2_NUMBER.value)
link_msg_type = m.get_tlv(LinkTlvs.TYPE.value)
if nodeutils.is_node(net, (NodeTypes.WIRELESS_LAN, NodeTypes.EMANE)):
if n1num == net.objid:
continue
wl = link_msg_type == LinkTypes.WIRELESS.value
self.updatelink(n1num, n2num, MessageFlags.ADD.value, wl)
for n1num in sorted(self.remotes.keys()):
r = self.remotes[n1num]
for n2num, wl in r.links:
self.updatelink(n1num, n2num, MessageFlags.ADD.value, wl)
def handledistributed(self, message):
"""
Broker handler for processing CORE API messages as they are
received. This is used to snoop the Node messages and update
node positions.
"""
if message.message_type == MessageTypes.LINK.value:
return self.handlelinkmsg(message)
elif message.message_type == MessageTypes.NODE.value:
return self.handlenodemsg(message)
def handlenodemsg(self, msg):
"""
Process a Node Message to add/delete or move a node on
the SDT display. Node properties are found in session._objs or
self.remotes for remote nodes (or those not yet instantiated).
"""
# for distributed sessions to work properly, the SDT option should be
# enabled prior to starting the session
if not self.is_enabled():
return False
# node.(objid, type, icon, name) are used.
nodenum = msg.get_tlv(NodeTlvs.NUMBER.value)
if not nodenum:
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)
nodetype = msg.get_tlv(NodeTlvs.TYPE.value)
model = msg.get_tlv(NodeTlvs.MODEL.value)
icon = msg.get_tlv(NodeTlvs.ICON.value)
net = False
if nodetype == NodeTypes.DEFAULT.value or \
nodetype == NodeTypes.PHYSICAL.value or \
nodetype == NodeTypes.XEN.value:
if model is None:
model = "router"
type = model
elif nodetype is not None:
type = nodeutils.get_node_class(NodeTypes(nodetype)).type
net = True
else:
type = None
try:
node = self.session.get_object(nodenum)
except KeyError:
node = None
if node:
self.updatenode(node.objid, 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 type is None:
type = remote.type
if icon is None:
icon = remote.icon
else:
remote = Bunch(objid=nodenum, type=type, 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, type, icon)
def handlelinkmsg(self, msg):
"""
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.
"""
if not self.is_enabled():
return False
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):
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):
"""
Helper returns True if a node number corresponds to a WlanNode
or EmaneNode.
"""
if nodenum in self.remotes:
type = self.remotes[nodenum].type
if type in ("wlan", "emane"):
return True
else:
try:
n = self.session.get_object(nodenum)
except KeyError:
return False
if nodeutils.is_node(n, (NodeTypes.WIRELESS_LAN, NodeTypes.EMANE)):
return True
return False