quick base to try and help flesh out documentation under core.*

This commit is contained in:
Blake J. Harnden 2017-05-03 09:30:49 -07:00
parent 8f45e5c4da
commit 4ae7958a63
15 changed files with 1956 additions and 292 deletions

View file

@ -6,19 +6,12 @@ core
Top-level Python package containing CORE components. Top-level Python package containing CORE components.
See http://www.nrl.navy.mil/itd/ncs/products/core and See http://www.nrl.navy.mil/itd/ncs/products/core for more information on CORE.
http://code.google.com/p/coreemu/ for more information on CORE.
Pieces can be imported individually, for example Pieces can be imported individually, for example
import core.netns.vnode from core.netns import vnode
or everything listed in __all__ can be imported using
from core import *
""" """
__all__ = []
# Automatically import all add-ons listed in addons.__all__ # Automatically import all add-ons listed in addons.__all__
from addons import * from addons import *

View file

@ -1,6 +1,6 @@
"""Optional add-ons
Add on files can be put in this directory. Everything listed in
__all__ is automatically loaded by the main core module.
""" """
Optional add ons can be put in this directory. Everything listed in __all__ is automatically
loaded by the main core module.
"""
__all__ = [] __all__ = []

View file

@ -0,0 +1,3 @@
"""
Contains code specific to the legacy TCP API for interacting with the TCL based GUI.
"""

View file

@ -44,20 +44,46 @@ class CoreTlvData(object):
@classmethod @classmethod
def pack(cls, value): def pack(cls, value):
"""
Convenience method for packing data using the struct module.
:param value: value to pack
:return: length of data and the packed data itself
:rtype: tuple
"""
data = struct.pack(cls.data_format, value) data = struct.pack(cls.data_format, value)
length = len(data) - cls.pad_len length = len(data) - cls.pad_len
return length, data return length, data
@classmethod @classmethod
def unpack(cls, data): def unpack(cls, data):
"""
Convenience method for unpacking data using the struct module.
:param data: data to unpack
:return: the value of the unpacked data
"""
return struct.unpack(cls.data_format, data)[0] return struct.unpack(cls.data_format, data)[0]
@classmethod @classmethod
def pack_string(cls, value): def pack_string(cls, value):
"""
Convenience method for packing data from a string representation.
:param str value: value to pack
:return: length of data and the packed data itself
:rtype: tuple
"""
return cls.pack(cls.from_string(value)) return cls.pack(cls.from_string(value))
@classmethod @classmethod
def from_string(cls, value): def from_string(cls, value):
"""
Retrieve the value type from a string representation.
:param str value: value to get a data type from
:return: value parse from string representation
"""
return cls.data_type(value) return cls.data_type(value)
@ -68,20 +94,45 @@ class CoreTlvDataObj(CoreTlvData):
@classmethod @classmethod
def pack(cls, obj): def pack(cls, obj):
data = struct.pack(cls.data_format, cls.get_value(obj)) """
length = len(data) - cls.pad_len Convenience method for packing custom object data.
return length, data
:param obj: custom object to pack
:return: length of data and the packed data itself
:rtype: tuple
"""
value = cls.get_value(obj)
return super(CoreTlvDataObj, cls).pack(value)
@classmethod @classmethod
def unpack(cls, data): def unpack(cls, data):
return cls.new_obj(struct.unpack(cls.data_format, data)[0]) """
Convenience method for unpacking custom object data.
:param data: data to unpack custom object from
:return: unpacked custom object
"""
data = cls.new_obj(data)
return super(CoreTlvDataObj, cls).unpack(data)
@staticmethod @staticmethod
def get_value(obj): def get_value(obj):
"""
Method that will be used to retrieve the data to pack from a custom object.
:param obj: custom object to get data to pack
:return: data value to pack
"""
raise NotImplementedError raise NotImplementedError
@staticmethod @staticmethod
def new_obj(obj): def new_obj(obj):
"""
Method for retrieving data to unpack from an object.
:param obj: object to get unpack data from
:return: value of unpacked data
"""
raise NotImplementedError raise NotImplementedError
@ -120,6 +171,13 @@ class CoreTlvDataString(CoreTlvData):
@classmethod @classmethod
def pack(cls, value): def pack(cls, value):
"""
Convenience method for packing string data.
:param str value: string to pack
:return: length of data packed and the packed data
:rtype: tuple
"""
if not isinstance(value, str): if not isinstance(value, str):
raise ValueError("value not a string: %s" % value) raise ValueError("value not a string: %s" % value)
@ -129,11 +187,17 @@ class CoreTlvDataString(CoreTlvData):
header_len = CoreTlv.long_header_len header_len = CoreTlv.long_header_len
pad_len = -(header_len + len(value)) % 4 pad_len = -(header_len + len(value)) % 4
return len(value), value + '\0' * pad_len return len(value), value + "\0" * pad_len
@classmethod @classmethod
def unpack(cls, data): def unpack(cls, data):
return data.rstrip('\0') """
Convenience method for unpacking string data.
:param str data: unpack string data
:return: unpacked string data
"""
return data.rstrip("\0")
class CoreTlvDataUint16List(CoreTlvData): class CoreTlvDataUint16List(CoreTlvData):
@ -145,6 +209,13 @@ class CoreTlvDataUint16List(CoreTlvData):
@classmethod @classmethod
def pack(cls, values): def pack(cls, values):
"""
Convenience method for packing a uint 16 list.
:param list values: unint 16 list to pack
:return: length of data packed and the packed data
:rtype: tuple
"""
if not isinstance(values, tuple): if not isinstance(values, tuple):
raise ValueError("value not a tuple: %s" % values) raise ValueError("value not a tuple: %s" % values)
@ -157,59 +228,123 @@ class CoreTlvDataUint16List(CoreTlvData):
@classmethod @classmethod
def unpack(cls, data): def unpack(cls, data):
"""
Convenience method for unpacking a uint 16 list.
:param data: data to unpack
:return: unpacked data
"""
data_format = "!%dH" % (len(data) / 2) data_format = "!%dH" % (len(data) / 2)
return struct.unpack(data_format, data) return struct.unpack(data_format, data)
@classmethod @classmethod
def from_string(cls, value): def from_string(cls, value):
"""
Retrieves a unint 16 list from a string
:param str value: string representation of a uint 16 list
:return: unint 16 list
:rtype: list
"""
return tuple(map(lambda (x): int(x), value.split())) return tuple(map(lambda (x): int(x), value.split()))
class CoreTlvDataIpv4Addr(CoreTlvDataObj): class CoreTlvDataIpv4Addr(CoreTlvDataObj):
"""
Utility class for packing/unpacking Ipv4 addresses.
"""
data_type = IpAddress.from_string data_type = IpAddress.from_string
data_format = "!2x4s" data_format = "!2x4s"
pad_len = 2 pad_len = 2
@staticmethod @staticmethod
def get_value(obj): def get_value(obj):
"""
Retrieve Ipv4 address value from object.
:param core.misc.ipaddress.IpAddress obj: ip address to get value from
:return:
"""
return obj.addr return obj.addr
@staticmethod @staticmethod
def new_obj(value): def new_obj(value):
"""
Retrieve Ipv4 address from a string representation.
:param str value: value to get Ipv4 address from
:return: Ipv4 address
:rtype: core.misc.ipaddress.IpAddress
"""
return IpAddress(af=socket.AF_INET, address=value) return IpAddress(af=socket.AF_INET, address=value)
class CoreTlvDataIPv6Addr(CoreTlvDataObj): class CoreTlvDataIPv6Addr(CoreTlvDataObj):
"""
Utility class for packing/unpacking Ipv6 addresses.
"""
data_format = "!16s2x" data_format = "!16s2x"
data_type = IpAddress.from_string data_type = IpAddress.from_string
pad_len = 2 pad_len = 2
@staticmethod @staticmethod
def get_value(obj): def get_value(obj):
"""
Retrieve Ipv6 address value from object.
:param core.misc.ipaddress.IpAddress obj: ip address to get value from
:return:
"""
return obj.addr return obj.addr
@staticmethod @staticmethod
def new_obj(value): def new_obj(value):
"""
Retrieve Ipv6 address from a string representation.
:param str value: value to get Ipv4 address from
:return: Ipv4 address
:rtype: core.misc.ipaddress.IpAddress
"""
return IpAddress(af=socket.AF_INET6, address=value) return IpAddress(af=socket.AF_INET6, address=value)
class CoreTlvDataMacAddr(CoreTlvDataObj): class CoreTlvDataMacAddr(CoreTlvDataObj):
"""
Utility class for packing/unpacking mac addresses.
"""
data_format = "!2x8s" data_format = "!2x8s"
data_type = MacAddress.from_string data_type = MacAddress.from_string
pad_len = 2 pad_len = 2
@staticmethod @staticmethod
def get_value(obj): def get_value(obj):
"""
Retrieve Ipv6 address value from object.
:param core.misc.ipaddress.MacAddress obj: mac address to get value from
:return:
"""
# extend to 64 bits # extend to 64 bits
return '\0\0' + obj.addr return "\0\0" + obj.addr
@staticmethod @staticmethod
def new_obj(value): def new_obj(value):
"""
Retrieve mac address from a string representation.
:param str value: value to get Ipv4 address from
:return: Ipv4 address
:rtype: core.misc.ipaddress.MacAddress
"""
# only use 48 bits # only use 48 bits
return MacAddress(address=value[2:]) return MacAddress(address=value[2:])
class CoreTlv(object): class CoreTlv(object):
"""
Base class for representing CORE TLVs.
"""
header_format = "!BB" header_format = "!BB"
header_len = struct.calcsize(header_format) header_len = struct.calcsize(header_format)
@ -220,6 +355,13 @@ class CoreTlv(object):
tlv_data_class_map = {} tlv_data_class_map = {}
def __init__(self, tlv_type, tlv_data): def __init__(self, tlv_type, tlv_data):
"""
Create a CoreTlv instance.
:param int tlv_type: tlv type
:param tlv_data: data to unpack
:return: unpacked data
"""
self.tlv_type = tlv_type self.tlv_type = tlv_type
if tlv_data: if tlv_data:
try: try:
@ -232,7 +374,10 @@ class CoreTlv(object):
@classmethod @classmethod
def unpack(cls, data): def unpack(cls, data):
""" """
parse data and return unpacked class. Parse data and return unpacked class.
:param data: data to unpack
:return: unpacked data class
""" """
tlv_type, tlv_len = struct.unpack(cls.header_format, data[:cls.header_len]) tlv_type, tlv_len = struct.unpack(cls.header_format, data[:cls.header_len])
header_len = cls.header_len header_len = cls.header_len
@ -240,11 +385,19 @@ class CoreTlv(object):
tlv_type, zero, tlv_len = struct.unpack(cls.long_header_format, data[:cls.long_header_len]) tlv_type, zero, tlv_len = struct.unpack(cls.long_header_format, data[:cls.long_header_len])
header_len = cls.long_header_len header_len = cls.long_header_len
tlv_size = header_len + tlv_len tlv_size = header_len + tlv_len
tlv_size += -tlv_size % 4 # for 32-bit alignment # for 32-bit alignment
tlv_size += -tlv_size % 4
return cls(tlv_type, data[header_len:tlv_size]), data[tlv_size:] return cls(tlv_type, data[header_len:tlv_size]), data[tlv_size:]
@classmethod @classmethod
def pack(cls, tlv_type, value): def pack(cls, tlv_type, value):
"""
Pack a TLV value, based on type.
:param int tlv_type: type of data to pack
:param value: data to pack
:return: header and packed data
"""
tlv_len, tlv_data = cls.tlv_data_class_map[tlv_type].pack(value) tlv_len, tlv_data = cls.tlv_data_class_map[tlv_type].pack(value)
if tlv_len < 256: if tlv_len < 256:
@ -256,19 +409,42 @@ class CoreTlv(object):
@classmethod @classmethod
def pack_string(cls, tlv_type, value): def pack_string(cls, tlv_type, value):
"""
Pack data type from a string representation
:param int tlv_type: type of data to pack
:param str value: string representation of data
:return: header and packed data
"""
return cls.pack(tlv_type, cls.tlv_data_class_map[tlv_type].from_string(value)) return cls.pack(tlv_type, cls.tlv_data_class_map[tlv_type].from_string(value))
def type_str(self): def type_str(self):
"""
Retrieve type string for this data type.
:return: data type name
:rtype: str
"""
try: try:
return self.tlv_type_map(self.tlv_type).name return self.tlv_type_map(self.tlv_type).name
except ValueError: except ValueError:
return "unknown tlv type: %s" % str(self.tlv_type) return "unknown tlv type: %s" % str(self.tlv_type)
def __str__(self): def __str__(self):
"""
String representation of this data type.
:return: string representation
:rtype: str
"""
return "%s <tlvtype = %s, value = %s>" % (self.__class__.__name__, self.type_str(), self.value) return "%s <tlvtype = %s, value = %s>" % (self.__class__.__name__, self.type_str(), self.value)
class CoreNodeTlv(CoreTlv): class CoreNodeTlv(CoreTlv):
"""
Class for representing CORE Node TLVs.
"""
tlv_type_map = NodeTlvs tlv_type_map = NodeTlvs
tlv_data_class_map = { tlv_data_class_map = {
NodeTlvs.NUMBER.value: CoreTlvDataUint32, NodeTlvs.NUMBER.value: CoreTlvDataUint32,
@ -295,6 +471,10 @@ class CoreNodeTlv(CoreTlv):
class CoreLinkTlv(CoreTlv): class CoreLinkTlv(CoreTlv):
"""
Class for representing CORE link TLVs.
"""
tlv_type_map = LinkTlvs tlv_type_map = LinkTlvs
tlv_data_class_map = { tlv_data_class_map = {
LinkTlvs.N1_NUMBER.value: CoreTlvDataUint32, LinkTlvs.N1_NUMBER.value: CoreTlvDataUint32,
@ -333,6 +513,10 @@ class CoreLinkTlv(CoreTlv):
class CoreExecuteTlv(CoreTlv): class CoreExecuteTlv(CoreTlv):
"""
Class for representing CORE execute TLVs.
"""
tlv_type_map = ExecuteTlvs tlv_type_map = ExecuteTlvs
tlv_data_class_map = { tlv_data_class_map = {
ExecuteTlvs.NODE.value: CoreTlvDataUint32, ExecuteTlvs.NODE.value: CoreTlvDataUint32,
@ -346,6 +530,10 @@ class CoreExecuteTlv(CoreTlv):
class CoreRegisterTlv(CoreTlv): class CoreRegisterTlv(CoreTlv):
"""
Class for representing CORE register TLVs.
"""
tlv_type_map = RegisterTlvs tlv_type_map = RegisterTlvs
tlv_data_class_map = { tlv_data_class_map = {
RegisterTlvs.WIRELESS.value: CoreTlvDataString, RegisterTlvs.WIRELESS.value: CoreTlvDataString,
@ -359,6 +547,10 @@ class CoreRegisterTlv(CoreTlv):
class CoreConfigTlv(CoreTlv): class CoreConfigTlv(CoreTlv):
"""
Class for representing CORE configuration TLVs.
"""
tlv_type_map = ConfigTlvs tlv_type_map = ConfigTlvs
tlv_data_class_map = { tlv_data_class_map = {
ConfigTlvs.NODE.value: CoreTlvDataUint32, ConfigTlvs.NODE.value: CoreTlvDataUint32,
@ -378,6 +570,10 @@ class CoreConfigTlv(CoreTlv):
class CoreFileTlv(CoreTlv): class CoreFileTlv(CoreTlv):
"""
Class for representing CORE file TLVs.
"""
tlv_type_map = FileTlvs tlv_type_map = FileTlvs
tlv_data_class_map = { tlv_data_class_map = {
FileTlvs.NODE.value: CoreTlvDataUint32, FileTlvs.NODE.value: CoreTlvDataUint32,
@ -393,6 +589,10 @@ class CoreFileTlv(CoreTlv):
class CoreInterfaceTlv(CoreTlv): class CoreInterfaceTlv(CoreTlv):
"""
Class for representing CORE interface TLVs.
"""
tlv_type_map = InterfaceTlvs tlv_type_map = InterfaceTlvs
tlv_data_class_map = { tlv_data_class_map = {
InterfaceTlvs.NODE.value: CoreTlvDataUint32, InterfaceTlvs.NODE.value: CoreTlvDataUint32,
@ -412,6 +612,10 @@ class CoreInterfaceTlv(CoreTlv):
class CoreEventTlv(CoreTlv): class CoreEventTlv(CoreTlv):
"""
Class for representing CORE event TLVs.
"""
tlv_type_map = EventTlvs tlv_type_map = EventTlvs
tlv_data_class_map = { tlv_data_class_map = {
EventTlvs.NODE.value: CoreTlvDataUint32, EventTlvs.NODE.value: CoreTlvDataUint32,
@ -424,6 +628,10 @@ class CoreEventTlv(CoreTlv):
class CoreSessionTlv(CoreTlv): class CoreSessionTlv(CoreTlv):
"""
Class for representing CORE session TLVs.
"""
tlv_type_map = SessionTlvs tlv_type_map = SessionTlvs
tlv_data_class_map = { tlv_data_class_map = {
SessionTlvs.NUMBER.value: CoreTlvDataString, SessionTlvs.NUMBER.value: CoreTlvDataString,
@ -438,6 +646,10 @@ class CoreSessionTlv(CoreTlv):
class CoreExceptionTlv(CoreTlv): class CoreExceptionTlv(CoreTlv):
"""
Class for representing CORE exception TLVs.
"""
tlv_type_map = ExceptionTlvs tlv_type_map = ExceptionTlvs
tlv_data_class_map = { tlv_data_class_map = {
ExceptionTlvs.NODE.value: CoreTlvDataUint32, ExceptionTlvs.NODE.value: CoreTlvDataUint32,
@ -451,6 +663,10 @@ class CoreExceptionTlv(CoreTlv):
class CoreMessage(object): class CoreMessage(object):
"""
Base class for representing CORE messages.
"""
header_format = "!BBH" header_format = "!BBH"
header_len = struct.calcsize(header_format) header_len = struct.calcsize(header_format)
message_type = None message_type = None
@ -477,19 +693,45 @@ class CoreMessage(object):
@classmethod @classmethod
def pack(cls, message_flags, tlv_data): def pack(cls, message_flags, tlv_data):
"""
Pack CORE message data.
:param message_flags: message flags to pack with data
:param tlv_data: data to get length from for packing
:return: combined header and tlv data
"""
header = struct.pack(cls.header_format, cls.message_type, message_flags, len(tlv_data)) header = struct.pack(cls.header_format, cls.message_type, message_flags, len(tlv_data))
return header + tlv_data return header + tlv_data
def add_tlv_data(self, key, value): def add_tlv_data(self, key, value):
"""
Add TLV data into the data map.
:param int key: key to store TLV data
:param value: data to associate with key
:return: nothing
"""
if key in self.tlv_data: if key in self.tlv_data:
raise KeyError("key already exists: %s (val=%s)" % (key, value)) raise KeyError("key already exists: %s (val=%s)" % (key, value))
self.tlv_data[key] = value self.tlv_data[key] = value
def get_tlv(self, tlv_type): def get_tlv(self, tlv_type):
"""
Retrieve TLV data from data map.
:param int tlv_type: type of data to retrieve
:return: TLV type data
"""
return self.tlv_data.get(tlv_type) return self.tlv_data.get(tlv_type)
def parse_data(self, data): def parse_data(self, data):
"""
Parse data while possible and adding TLV data to the data map.
:param data: data to parse for TLV data
:return: nothing
"""
while data: while data:
tlv, data = self.tlv_class.unpack(data) tlv, data = self.tlv_class.unpack(data)
self.add_tlv_data(tlv.tlv_type, tlv.value) self.add_tlv_data(tlv.tlv_type, tlv.value)
@ -522,12 +764,24 @@ class CoreMessage(object):
self.raw_message = self.pack(self.flags, tlv_data) self.raw_message = self.pack(self.flags, tlv_data)
def type_str(self): def type_str(self):
"""
Retrieve data of the message type.
:return: name of message type
:rtype: str
"""
try: try:
return MessageTypes(self.message_type).name return MessageTypes(self.message_type).name
except ValueError: except ValueError:
return "unknown message type: %s" % str(self.message_type) return "unknown message type: %s" % str(self.message_type)
def flag_str(self): def flag_str(self):
"""
Retrieve message flag string.
:return: message flag string
:rtype: str
"""
message_flags = [] message_flags = []
flag = 1L flag = 1L
@ -544,6 +798,12 @@ class CoreMessage(object):
return "0x%x <%s>" % (self.flags, " | ".join(message_flags)) return "0x%x <%s>" % (self.flags, " | ".join(message_flags))
def __str__(self): def __str__(self):
"""
Retrieve string representation of the message.
:return: string representation
:rtype: str
"""
result = "%s <msgtype = %s, flags = %s>" % (self.__class__.__name__, self.type_str(), self.flag_str()) result = "%s <msgtype = %s, flags = %s>" % (self.__class__.__name__, self.type_str(), self.flag_str())
for key, value in self.tlv_data.iteritems(): for key, value in self.tlv_data.iteritems():
@ -612,55 +872,86 @@ class CoreMessage(object):
class CoreNodeMessage(CoreMessage): class CoreNodeMessage(CoreMessage):
"""
CORE node message class.
"""
message_type = MessageTypes.NODE.value message_type = MessageTypes.NODE.value
tlv_class = CoreNodeTlv tlv_class = CoreNodeTlv
class CoreLinkMessage(CoreMessage): class CoreLinkMessage(CoreMessage):
"""
CORE link message class.
"""
message_type = MessageTypes.LINK.value message_type = MessageTypes.LINK.value
tlv_class = CoreLinkTlv tlv_class = CoreLinkTlv
class CoreExecMessage(CoreMessage): class CoreExecMessage(CoreMessage):
"""
CORE execute message class.
"""
message_type = MessageTypes.EXECUTE.value message_type = MessageTypes.EXECUTE.value
tlv_class = CoreExecuteTlv tlv_class = CoreExecuteTlv
class CoreRegMessage(CoreMessage): class CoreRegMessage(CoreMessage):
"""
CORE register message class.
"""
message_type = MessageTypes.REGISTER.value message_type = MessageTypes.REGISTER.value
tlv_class = CoreRegisterTlv tlv_class = CoreRegisterTlv
class CoreConfMessage(CoreMessage): class CoreConfMessage(CoreMessage):
"""
CORE configuration message class.
"""
message_type = MessageTypes.CONFIG.value message_type = MessageTypes.CONFIG.value
tlv_class = CoreConfigTlv tlv_class = CoreConfigTlv
class CoreFileMessage(CoreMessage): class CoreFileMessage(CoreMessage):
"""
CORE file message class.
"""
message_type = MessageTypes.FILE.value message_type = MessageTypes.FILE.value
tlv_class = CoreFileTlv tlv_class = CoreFileTlv
class CoreIfaceMessage(CoreMessage): class CoreIfaceMessage(CoreMessage):
"""
CORE interface message class.
"""
message_type = MessageTypes.INTERFACE.value message_type = MessageTypes.INTERFACE.value
tlv_class = CoreInterfaceTlv tlv_class = CoreInterfaceTlv
class CoreEventMessage(CoreMessage): class CoreEventMessage(CoreMessage):
"""
CORE event message class.
"""
message_type = MessageTypes.EVENT.value message_type = MessageTypes.EVENT.value
tlv_class = CoreEventTlv tlv_class = CoreEventTlv
class CoreSessionMessage(CoreMessage): class CoreSessionMessage(CoreMessage):
"""
CORE session message class.
"""
message_type = MessageTypes.SESSION.value message_type = MessageTypes.SESSION.value
tlv_class = CoreSessionTlv tlv_class = CoreSessionTlv
class CoreExceptionMessage(CoreMessage): class CoreExceptionMessage(CoreMessage):
"""
CORE exception message class.
"""
message_type = MessageTypes.EXCEPTION.value message_type = MessageTypes.EXCEPTION.value
tlv_class = CoreExceptionTlv tlv_class = CoreExceptionTlv
# map used to translate enumerated message type values to message class objects
CLASS_MAP = { CLASS_MAP = {
MessageTypes.NODE.value: CoreNodeMessage, MessageTypes.NODE.value: CoreNodeMessage,
MessageTypes.LINK.value: CoreLinkMessage, MessageTypes.LINK.value: CoreLinkMessage,

View file

@ -1,6 +1,7 @@
""" """
Converts CORE data objects into legacy API messages. Converts CORE data objects into legacy API messages.
""" """
from core.api import coreapi from core.api import coreapi
from core.enumerations import NodeTlvs from core.enumerations import NodeTlvs
from core.misc import log from core.misc import log

View file

@ -1,9 +1,7 @@
""" """
broker.py: definition of CoreBroker class that is part of the Broker class that is part of the session object. Handles distributing parts of the emulation out to
pycore session object. Handles distributing parts of the emulation out to other emulation servers. The broker is consulted when handling messages to determine if messages
other emulation servers. The broker is consulted during the should be handled locally or forwarded on to another emulation server.
CoreRequestHandler.handlemsg() loop to determine if messages should be handled
locally or forwarded on to another emulation server.
""" """
import os import os
@ -40,7 +38,17 @@ logger = log.get_logger(__name__)
# TODO: name conflict with main core server, probably should rename # TODO: name conflict with main core server, probably should rename
class CoreServer(object): class CoreServer(object):
"""
Reptesents CORE daemon servers for communication.
"""
def __init__(self, name, host, port): def __init__(self, name, host, port):
"""
Creates a CoreServer instance.
:param str name: name of the CORE server
:param str host: server address
:param int port: server port
"""
self.name = name self.name = name
self.host = host self.host = host
self.port = port self.port = port
@ -48,17 +56,28 @@ class CoreServer(object):
self.instantiation_complete = False self.instantiation_complete = False
def connect(self): def connect(self):
"""
Connect to CORE server and save connection.
:return: nothing
"""
assert self.sock is None assert self.sock is None
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try: try:
sock.connect((self.host, self.port)) sock.connect((self.host, self.port))
except: except IOError as e:
sock.close() sock.close()
raise raise e
self.sock = sock self.sock = sock
def close(self): def close(self):
"""
Close connection with CORE server.
:return: nothing
"""
if self.sock is not None: if self.sock is not None:
self.sock.close() self.sock.close()
self.sock = None self.sock = None
@ -66,9 +85,13 @@ class CoreServer(object):
class CoreBroker(ConfigurableManager): class CoreBroker(ConfigurableManager):
""" """
Member of pycore session class for handling global emulation server data. Helps with brokering messages between CORE daemon servers.
""" """
# configurable manager name
name = "broker" name = "broker"
# configurable manager type
config_type = RegisterTlvs.UTILITY.value config_type = RegisterTlvs.UTILITY.value
def __init__(self, session): def __init__(self, session):
@ -104,6 +127,7 @@ class CoreBroker(ConfigurableManager):
self.tunnels = {} self.tunnels = {}
self.dorecvloop = False self.dorecvloop = False
self.recvthread = None self.recvthread = None
self.bootcount = 0
def startup(self): def startup(self):
""" """
@ -151,7 +175,7 @@ class CoreBroker(ConfigurableManager):
def startrecvloop(self): def startrecvloop(self):
""" """
Spawn the recvloop() thread if it hasn"t been already started. Spawn the receive loop for receiving messages.
""" """
if self.recvthread is not None: if self.recvthread is not None:
if self.recvthread.isAlive(): if self.recvthread.isAlive():
@ -166,7 +190,7 @@ class CoreBroker(ConfigurableManager):
def recvloop(self): def recvloop(self):
""" """
Thread target that receives messages from server sockets. Receive loop for receiving messages from server sockets.
""" """
self.dorecvloop = True self.dorecvloop = True
# note: this loop continues after emulation is stopped, # note: this loop continues after emulation is stopped,
@ -250,9 +274,13 @@ class CoreBroker(ConfigurableManager):
def addserver(self, name, host, port): def addserver(self, name, host, port):
""" """
Add a new server, and try to connect to it. If we"re already Add a new server, and try to connect to it. If we"re already connected to this
connected to this (host, port), then leave it alone. When host,port (host, port), then leave it alone. When host,port is None, do not try to connect.
is None, do not try to connect.
:param str name: name of server
:param str host: server address
:param int port: server port
:return: nothing
""" """
with self.servers_lock: with self.servers_lock:
server = self.servers.get(name) server = self.servers.get(name)
@ -282,14 +310,15 @@ class CoreBroker(ConfigurableManager):
Remove a server and hang up any connection. Remove a server and hang up any connection.
:param CoreServer server: server to delete :param CoreServer server: server to delete
:return: :return: nothing
""" """
with self.servers_lock: with self.servers_lock:
try: try:
s = self.servers.pop(server.name) s = self.servers.pop(server.name)
assert s == server assert s == server
except KeyError: except KeyError:
pass logger.exception("error deleting server")
if server.sock is not None: if server.sock is not None:
logger.info("closing connection with %s @ %s:%s" % (server.name, server.host, server.port)) logger.info("closing connection with %s @ %s:%s" % (server.name, server.host, server.port))
server.close() server.close()
@ -322,6 +351,9 @@ class CoreBroker(ConfigurableManager):
def getservers(self): def getservers(self):
""" """
Return a list of servers sorted by name. Return a list of servers sorted by name.
:return: sorted server list
:rtype: list
""" """
with self.servers_lock: with self.servers_lock:
return sorted(self.servers.values(), key=lambda x: x.name) return sorted(self.servers.values(), key=lambda x: x.name)
@ -329,6 +361,9 @@ class CoreBroker(ConfigurableManager):
def getservernames(self): def getservernames(self):
""" """
Return a sorted list of server names (keys from self.servers). Return a sorted list of server names (keys from self.servers).
:return: sorted server names
:rtype: list
""" """
with self.servers_lock: with self.servers_lock:
return sorted(self.servers.keys()) return sorted(self.servers.keys())
@ -338,6 +373,11 @@ class CoreBroker(ConfigurableManager):
Compute a 32-bit key used to uniquely identify a GRE tunnel. Compute a 32-bit key used to uniquely identify a GRE tunnel.
The hash(n1num), hash(n2num) values are used, so node numbers may be The hash(n1num), hash(n2num) values are used, so node numbers may be
None or string values (used for e.g. "ctrlnet"). None or string values (used for e.g. "ctrlnet").
:param int n1num: node one id
:param int n2num: node two id
:return: tunnel key for the node pair
:rtype: int
""" """
sid = self.session_id_master sid = self.session_id_master
if sid is None: if sid is None:
@ -349,7 +389,13 @@ class CoreBroker(ConfigurableManager):
def addtunnel(self, remoteip, n1num, n2num, localnum): def addtunnel(self, remoteip, n1num, n2num, localnum):
""" """
Add a new GreTapBridge between nodes on two different machines. Adds a new GreTapBridge between nodes on two different machines.
:param str remoteip: remote address for tunnel
:param int n1num: node one id
:param int n2num: node two id
:param int localnum: local id
:return: nothing
""" """
key = self.tunnelkey(n1num, n2num) key = self.tunnelkey(n1num, n2num)
if localnum == n2num: if localnum == n2num:
@ -380,27 +426,37 @@ class CoreBroker(ConfigurableManager):
for n in self.network_nodes: for n in self.network_nodes:
self.addnettunnel(n) self.addnettunnel(n)
def addnettunnel(self, n): def addnettunnel(self, node):
"""
Add network tunnel between node and broker.
:param node: node to add network tunnel to
:return: list of grep taps
:rtype: list
"""
try: try:
net = self.session.get_object(n) net = self.session.get_object(node)
except KeyError: except KeyError:
raise KeyError("network node %s not found" % n) raise KeyError("network node %s not found" % node)
# add other nets here that do not require tunnels # add other nets here that do not require tunnels
if nodeutils.is_node(net, NodeTypes.EMANE_NET): if nodeutils.is_node(net, NodeTypes.EMANE_NET):
return None return None
if nodeutils.is_node(net, NodeTypes.CONTROL_NET):
if hasattr(net, "serverintf"): server_interface = getattr(net, "serverintf", None)
if net.serverintf is not None: if nodeutils.is_node(net, NodeTypes.CONTROL_NET) and server_interface is not None:
return None return None
servers = self.getserversbynode(n) servers = self.getserversbynode(node)
if len(servers) < 2: if len(servers) < 2:
return None return None
hosts = [] hosts = []
for server in servers: for server in servers:
if server.host is None: if server.host is None:
continue continue
hosts.append(server.host) hosts.append(server.host)
if len(hosts) == 0 and self.session_handler.client_address != "": if len(hosts) == 0 and self.session_handler.client_address != "":
# get IP address from API message sender (master) # get IP address from API message sender (master)
hosts.append(self.session_handler.client_address[0]) hosts.append(self.session_handler.client_address[0])
@ -413,21 +469,26 @@ class CoreBroker(ConfigurableManager):
else: else:
# we are the session master # we are the session master
myip = host myip = host
key = self.tunnelkey(n, IpAddress.to_int(myip)) key = self.tunnelkey(node, IpAddress.to_int(myip))
if key in self.tunnels.keys(): if key in self.tunnels.keys():
continue continue
logger.info("Adding tunnel for net %s to %s with key %s" % (n, host, key)) logger.info("Adding tunnel for net %s to %s with key %s" % (node, host, key))
gt = GreTap(node=None, name=None, session=self.session, remoteip=host, key=key) gt = GreTap(node=None, name=None, session=self.session, remoteip=host, key=key)
self.tunnels[key] = gt self.tunnels[key] = gt
r.append(gt) r.append(gt)
# attaching to net will later allow gt to be destroyed # attaching to net will later allow gt to be destroyed
# during net.shutdown() # during net.shutdown()
net.attach(gt) net.attach(gt)
return r return r
def deltunnel(self, n1num, n2num): def deltunnel(self, n1num, n2num):
""" """
Cleanup of the GreTapBridge. Delete tunnel between nodes.
:param int n1num: node one id
:param int n2num: node two id
:return: nothing
""" """
key = self.tunnelkey(n1num, n2num) key = self.tunnelkey(n1num, n2num)
try: try:
@ -441,6 +502,10 @@ class CoreBroker(ConfigurableManager):
def gettunnel(self, n1num, n2num): def gettunnel(self, n1num, n2num):
""" """
Return the GreTap between two nodes if it exists. Return the GreTap between two nodes if it exists.
:param int n1num: node one id
:param int n2num: node two id
:return: gre tap between nodes or none
""" """
key = self.tunnelkey(n1num, n2num) key = self.tunnelkey(n1num, n2num)
if key in self.tunnels.keys(): if key in self.tunnels.keys():
@ -451,6 +516,10 @@ class CoreBroker(ConfigurableManager):
def addnodemap(self, server, nodenum): def addnodemap(self, server, nodenum):
""" """
Record a node number to emulation server mapping. Record a node number to emulation server mapping.
:param CoreServer server: core server to associate node with
:param int nodenum: node id
:return: nothing
""" """
with self.nodemap_lock: with self.nodemap_lock:
if nodenum in self.nodemap: if nodenum in self.nodemap:
@ -459,6 +528,7 @@ class CoreBroker(ConfigurableManager):
self.nodemap[nodenum].add(server) self.nodemap[nodenum].add(server)
else: else:
self.nodemap[nodenum] = {server} self.nodemap[nodenum] = {server}
if server in self.nodecounts: if server in self.nodecounts:
self.nodecounts[server] += 1 self.nodecounts[server] += 1
else: else:
@ -468,21 +538,32 @@ class CoreBroker(ConfigurableManager):
""" """
Remove a node number to emulation server mapping. Remove a node number to emulation server mapping.
Return the number of nodes left on this server. Return the number of nodes left on this server.
:param CoreServer server: server to remove from node map
:param int nodenum: node id
:return: number of nodes left on server
:rtype: int
""" """
count = None count = None
with self.nodemap_lock: with self.nodemap_lock:
if nodenum not in self.nodemap: if nodenum not in self.nodemap:
return count return count
self.nodemap[nodenum].remove(server) self.nodemap[nodenum].remove(server)
if server in self.nodecounts: if server in self.nodecounts:
count = self.nodecounts[server] count = self.nodecounts[server]
count -= 1 count -= 1
self.nodecounts[server] = count self.nodecounts[server] = count
return count return count
def getserversbynode(self, nodenum): def getserversbynode(self, nodenum):
""" """
Retrieve a set of emulation servers given a node number. Retrieve a set of emulation servers given a node number.
:param int nodenum: node id
:return: core server associated with node
:rtype: set
""" """
with self.nodemap_lock: with self.nodemap_lock:
if nodenum not in self.nodemap: if nodenum not in self.nodemap:
@ -492,12 +573,18 @@ class CoreBroker(ConfigurableManager):
def addnet(self, nodenum): def addnet(self, nodenum):
""" """
Add a node number to the list of link-layer nodes. Add a node number to the list of link-layer nodes.
:param int nodenum: node id to add
:return: nothing
""" """
self.network_nodes.add(nodenum) self.network_nodes.add(nodenum)
def addphys(self, nodenum): def addphys(self, nodenum):
""" """
Add a node number to the list of physical nodes. Add a node number to the list of physical nodes.
:param int nodenum: node id to add
:return: nothing
""" """
self.physical_nodes.add(nodenum) self.physical_nodes.add(nodenum)
@ -507,7 +594,7 @@ class CoreBroker(ConfigurableManager):
arrive and require the use of nodecounts. arrive and require the use of nodecounts.
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration :param core.conf.ConfigData config_data: configuration data for carrying out a configuration
:return: None :return: nothing
""" """
return None return None
@ -517,7 +604,7 @@ class CoreBroker(ConfigurableManager):
combinations that we"ll need to connect with. combinations that we"ll need to connect with.
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration :param core.conf.ConfigData config_data: configuration data for carrying out a configuration
:return: None :return: nothing
""" """
values = config_data.data_values values = config_data.data_values
session_id = config_data.session session_id = config_data.session
@ -614,8 +701,10 @@ class CoreBroker(ConfigurableManager):
def setupserver(self, servername): def setupserver(self, servername):
""" """
Send the appropriate API messages for configuring the specified Send the appropriate API messages for configuring the specified emulation server.
emulation server.
:param str servername: name of server to configure
:return: nothing
""" """
server = self.getserverbyname(servername) server = self.getserverbyname(servername)
if server is None: if server is None:
@ -647,6 +736,11 @@ class CoreBroker(ConfigurableManager):
""" """
When an interactive TTY request comes from the GUI, snoop the reply When an interactive TTY request comes from the GUI, snoop the reply
and add an SSH command to the appropriate remote server. and add an SSH command to the appropriate remote server.
:param msghdr: message header
:param msgdata: message data
:param str host: host address
:return: packed core execute tlv data
""" """
msgtype, msgflags, msglen = coreapi.CoreMessage.unpack_header(msghdr) msgtype, msgflags, msglen = coreapi.CoreMessage.unpack_header(msghdr)
msgcls = coreapi.CLASS_MAP[msgtype] msgcls = coreapi.CLASS_MAP[msgtype]
@ -674,13 +768,16 @@ class CoreBroker(ConfigurableManager):
nodes to servers. nodes to servers.
:param core.api.coreapi.CoreMessage message: message to handle :param core.api.coreapi.CoreMessage message: message to handle
:return: :return: boolean for handling locally and set of servers
:rtype: tuple
""" """
servers = set() servers = set()
handle_locally = False handle_locally = False
serverfiletxt = None serverfiletxt = None
# snoop Node Message for emulation server TLV and record mapping # snoop Node Message for emulation server TLV and record mapping
n = message.tlv_data[NodeTlvs.NUMBER.value] n = message.tlv_data[NodeTlvs.NUMBER.value]
# replicate link-layer nodes on all servers # replicate link-layer nodes on all servers
nodetype = message.get_tlv(NodeTlvs.TYPE.value) nodetype = message.get_tlv(NodeTlvs.TYPE.value)
if nodetype is not None: if nodetype is not None:
@ -720,9 +817,11 @@ class CoreBroker(ConfigurableManager):
servers.add(server) servers.add(server)
if serverfiletxt and self.session.master: if serverfiletxt and self.session.master:
self.writenodeserver(serverfiletxt, server) self.writenodeserver(serverfiletxt, server)
# hook to update coordinates of physical nodes # hook to update coordinates of physical nodes
if n in self.physical_nodes: if n in self.physical_nodes:
self.session.mobility.physnodeupdateposition(message) self.session.mobility.physnodeupdateposition(message)
return handle_locally, servers return handle_locally, servers
def handlelinkmsg(self, message): def handlelinkmsg(self, message):
@ -732,7 +831,8 @@ class CoreBroker(ConfigurableManager):
opaque data to the link message before forwarding. opaque data to the link message before forwarding.
:param core.api.coreapi.CoreMessage message: message to handle :param core.api.coreapi.CoreMessage message: message to handle
:return: :return: boolean to handle locally, a set of server, and message
:rtype: tuple
""" """
servers = set() servers = set()
handle_locally = False handle_locally = False
@ -799,7 +899,8 @@ class CoreBroker(ConfigurableManager):
:param core.api.coreapi.CoreMessage message: message to link end points :param core.api.coreapi.CoreMessage message: message to link end points
:param servers1: :param servers1:
:param servers2: :param servers2:
:return: :return: core link message
:rtype: coreapi.CoreLinkMessage
""" """
ip1 = "" ip1 = ""
for server in servers1: for server in servers1:
@ -823,6 +924,11 @@ class CoreBroker(ConfigurableManager):
and we need to determine the tunnel endpoint. First look for and we need to determine the tunnel endpoint. First look for
opaque data in the link message, otherwise use the IP of the message opaque data in the link message, otherwise use the IP of the message
sender (the master server). sender (the master server).
:param coreapi.CoreLinkMessage msg:
:param bool first_is_local: is first local
:return: host address
:rtype: str
""" """
host = None host = None
opaque = msg.get_tlv(LinkTlvs.OPAQUE.value) opaque = msg.get_tlv(LinkTlvs.OPAQUE.value)
@ -840,7 +946,11 @@ class CoreBroker(ConfigurableManager):
def handlerawmsg(self, msg): def handlerawmsg(self, msg):
""" """
Helper to invoke handlemsg() using raw (packed) message bytes. Helper to invoke message handler, using raw (packed) message bytes.
:param msg: raw message butes
:return: should handle locally or not
:rtype: bool
""" """
hdr = msg[:coreapi.CoreMessage.header_len] hdr = msg[:coreapi.CoreMessage.header_len]
msgtype, flags, msglen = coreapi.CoreMessage.unpack_header(hdr) msgtype, flags, msglen = coreapi.CoreMessage.unpack_header(hdr)
@ -856,7 +966,8 @@ class CoreBroker(ConfigurableManager):
:param core.api.coreapi.CoreMessage message: message to forward :param core.api.coreapi.CoreMessage message: message to forward
:param list servers: server to forward message to :param list servers: server to forward message to
:return: :return: handle locally value
:rtype: bool
""" """
handle_locally = len(servers) == 0 handle_locally = len(servers) == 0
for server in servers: for server in servers:
@ -874,6 +985,8 @@ class CoreBroker(ConfigurableManager):
""" """
Write the server list to a text file in the session directory upon Write the server list to a text file in the session directory upon
startup: /tmp/pycore.nnnnn/servers startup: /tmp/pycore.nnnnn/servers
:return: nothing
""" """
servers = self.getservers() servers = self.getservers()
filename = os.path.join(self.session.session_dir, "servers") filename = os.path.join(self.session.session_dir, "servers")
@ -900,6 +1013,10 @@ class CoreBroker(ConfigurableManager):
and server info. This may be used by scripts for accessing nodes on and server info. This may be used by scripts for accessing nodes on
other machines, much like local nodes may be accessed via the other machines, much like local nodes may be accessed via the
VnodeClient class. VnodeClient class.
:param str nodestr: node string
:param CoreServer server: core server
:return: nothing
""" """
serverstr = "%s %s %s" % (server.name, server.host, server.port) serverstr = "%s %s %s" % (server.name, server.host, server.port)
name = nodestr.split()[1] name = nodestr.split()[1]
@ -920,6 +1037,8 @@ class CoreBroker(ConfigurableManager):
def local_instantiation_complete(self): def local_instantiation_complete(self):
""" """
Set the local server"s instantiation-complete status to True. Set the local server"s instantiation-complete status to True.
:return: nothing
""" """
# TODO: do we really want to allow a localhost to not exist? # TODO: do we really want to allow a localhost to not exist?
with self.servers_lock: with self.servers_lock:
@ -937,6 +1056,9 @@ class CoreBroker(ConfigurableManager):
""" """
Return True if all servers have completed instantiation, False Return True if all servers have completed instantiation, False
otherwise. otherwise.
:return: have all server completed instantiation
:rtype: bool
""" """
with self.servers_lock: with self.servers_lock:
for server in self.servers.itervalues(): for server in self.servers.itervalues():

View file

@ -1,5 +1,5 @@
""" """
conf.py: common support for configurable objects Common support for configurable CORE objects.
""" """
import string import string
@ -34,6 +34,9 @@ class ConfigurableManager(object):
# configurable key=values, indexed by node number # configurable key=values, indexed by node number
self.configs = {} self.configs = {}
# TODO: fix the need for this and isolate to the mobility class that wants it
self._modelclsmap = {}
def configure(self, session, config_data): def configure(self, session, config_data):
""" """
Handle configure messages. The configuration message sent to a Handle configure messages. The configuration message sent to a
@ -45,6 +48,7 @@ class ConfigurableManager(object):
Returns any reply messages. Returns any reply messages.
:param core.session.Session session: CORE session object
:param ConfigData config_data: configuration data for carrying out a configuration :param ConfigData config_data: configuration data for carrying out a configuration
:return: response messages :return: response messages
""" """
@ -89,10 +93,10 @@ class ConfigurableManager(object):
'key=value' strings from a values field. The key name must be 'key=value' strings from a values field. The key name must be
in the keys list, and target.key=value is set. in the keys list, and target.key=value is set.
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration :param ConfigData config_data: configuration data for carrying out a configuration
:param target: target to set attribute values on :param target: target to set attribute values on
:param keys: list of keys to verify validity :param keys: list of keys to verify validity
:return: None :return: nothing
""" """
values = config_data.data_values values = config_data.data_values
@ -117,13 +121,23 @@ class ConfigurableManager(object):
return None return None
def reset(self): def reset(self):
"""
Reset functionality for the configurable class.
:return: nothing
"""
return None return None
def setconfig(self, nodenum, conftype, values): def setconfig(self, nodenum, conftype, values):
""" """
add configuration values for a node to a dictionary; values are Add configuration values for a node to a dictionary; values are
usually received from a Configuration Message, and may refer to a usually received from a Configuration Message, and may refer to a
node for which no object exists yet node for which no object exists yet
:param int nodenum: node id
:param conftype: configuration types
:param values: configuration values
:return: nothing
""" """
logger.info("setting config for node(%s): %s - %s", nodenum, conftype, values) logger.info("setting config for node(%s): %s - %s", nodenum, conftype, values)
conflist = [] conflist = []
@ -145,8 +159,14 @@ class ConfigurableManager(object):
def getconfig(self, nodenum, conftype, defaultvalues): def getconfig(self, nodenum, conftype, defaultvalues):
""" """
get configuration values for a node; if the values don't exist in Get configuration values for a node; if the values don't exist in
our dictionary then return the default values supplied our dictionary then return the default values supplied
:param int nodenum: node id
:param conftype: configuration type
:param defaultvalues: default values
:return: configuration type and default values
:type: tuple
""" """
logger.info("getting config for node(%s): %s - default(%s)", logger.info("getting config for node(%s): %s - default(%s)",
nodenum, conftype, defaultvalues) nodenum, conftype, defaultvalues)
@ -163,6 +183,10 @@ class ConfigurableManager(object):
""" """
Return (nodenum, conftype, values) tuples for all stored configs. Return (nodenum, conftype, values) tuples for all stored configs.
Used when reconnecting to a session. Used when reconnecting to a session.
:param bool use_clsmap: should a class map be used, default to True
:return: list of all configurations
:rtype: list
""" """
r = [] r = []
for nodenum in self.configs: for nodenum in self.configs:
@ -176,6 +200,9 @@ class ConfigurableManager(object):
""" """
remove configuration values for the specified node; remove configuration values for the specified node;
when nodenum is None, remove all configuration values when nodenum is None, remove all configuration values
:param int nodenum: node id
:return: nothing
""" """
if nodenum is None: if nodenum is None:
self.configs = {} self.configs = {}
@ -185,7 +212,12 @@ class ConfigurableManager(object):
def setconfig_keyvalues(self, nodenum, conftype, keyvalues): def setconfig_keyvalues(self, nodenum, conftype, keyvalues):
""" """
keyvalues list of tuples Key values list of tuples for a node.
:param int nodenum: node id
:param conftype: configuration type
:param keyvalues: key valyes
:return: nothing
""" """
if conftype not in self._modelclsmap: if conftype not in self._modelclsmap:
logger.warn("Unknown model type '%s'" % conftype) logger.warn("Unknown model type '%s'" % conftype)
@ -209,6 +241,10 @@ class ConfigurableManager(object):
configured. This is invoked when exporting a session to XML. configured. This is invoked when exporting a session to XML.
This assumes self.configs contains an iterable of (model-names, values) This assumes self.configs contains an iterable of (model-names, values)
and a self._modelclsmapdict exists. and a self._modelclsmapdict exists.
:param n: network node to get models for
:return: list of model and values tuples for the network node
:rtype: list
""" """
r = [] r = []
if n.objid in self.configs: if n.objid in self.configs:
@ -239,29 +275,57 @@ class Configurable(object):
:param core.session.Session session: session for this configurable :param core.session.Session session: session for this configurable
:param object_id: :param object_id:
:return:
""" """
self.session = session self.session = session
self.object_id = object_id self.object_id = object_id
def reset(self): def reset(self):
"""
Reset method.
:return: nothing
"""
pass pass
def register(self): def register(self):
"""
Register method.
:return: nothing
"""
pass pass
@classmethod @classmethod
def getdefaultvalues(cls): def getdefaultvalues(cls):
"""
Retrieve default values from configuration matrix.
:return: tuple of default values
:rtype: tuple
"""
# TODO: why the need for a tuple?
return tuple(map(lambda x: x[2], cls.config_matrix)) return tuple(map(lambda x: x[2], cls.config_matrix))
@classmethod @classmethod
def getnames(cls): def getnames(cls):
"""
Retrieve name values from configuration matrix.
:return: tuple of name values
:rtype: tuple
"""
# TODO: why the need for a tuple?
return tuple(map(lambda x: x[0], cls.config_matrix)) return tuple(map(lambda x: x[0], cls.config_matrix))
@classmethod @classmethod
def configure(cls, manager, config_data): def configure(cls, manager, config_data):
""" """
Handle configuration messages for this object. Handle configuration messages for this object.
:param ConfigurableManager manager: configuration manager
:param config_data: configuration data
:return: configuration data object
:rtype: ConfigData
""" """
reply = None reply = None
node_id = config_data.node node_id = config_data.node
@ -330,6 +394,13 @@ class Configurable(object):
Convert this class to a Config API message. Some TLVs are defined Convert this class to a Config API message. Some TLVs are defined
by the class, but node number, conf type flags, and values must by the class, but node number, conf type flags, and values must
be passed in. be passed in.
:param flags: message flags
:param int node_id: node id
:param type_flags: type flags
:param values: values
:return: configuration data object
:rtype: ConfigData
""" """
keys = cls.getnames() keys = cls.getnames()
keyvalues = map(lambda a, b: "%s=%s" % (a, b), keys, values) keyvalues = map(lambda a, b: "%s=%s" % (a, b), keys, values)
@ -355,6 +426,10 @@ class Configurable(object):
def booltooffon(value): def booltooffon(value):
""" """
Convenience helper turns bool into on (True) or off (False) string. Convenience helper turns bool into on (True) or off (False) string.
:param str value: value to retrieve on/off value for
:return: on or off string
:rtype: str
""" """
if value == "1" or value == "true" or value == "on": if value == "1" or value == "true" or value == "on":
return "on" return "on"
@ -363,6 +438,13 @@ class Configurable(object):
@staticmethod @staticmethod
def offontobool(value): def offontobool(value):
"""
Convenience helper for converting an on/off string to a integer.
:param str value: on/off string
:return: on/off integer value
:rtype: int
"""
if type(value) == str: if type(value) == str:
if value.lower() == "on": if value.lower() == "on":
return 1 return 1
@ -375,6 +457,10 @@ class Configurable(object):
""" """
Helper to return a value by the name defined in confmatrix. Helper to return a value by the name defined in confmatrix.
Checks if it is boolean Checks if it is boolean
:param str name: name to get value of
:param values: values to get value from
:return: value for name
""" """
i = cls.getnames().index(name) i = cls.getnames().index(name)
if cls.config_matrix[i][1] == ConfigDataTypes.BOOL.value and values[i] != "": if cls.config_matrix[i][1] == ConfigDataTypes.BOOL.value and values[i] != "":
@ -387,6 +473,10 @@ class Configurable(object):
""" """
Helper to check for list of key=value pairs versus a plain old Helper to check for list of key=value pairs versus a plain old
list of values. Returns True if all elements are "key=value". list of values. Returns True if all elements are "key=value".
:param values: items to check for key/value pairs
:return: True if all values are key/value pairs, False otherwise
:rtype: bool
""" """
if len(values) == 0: if len(values) == 0:
return False return False
@ -398,10 +488,16 @@ class Configurable(object):
def getkeyvaluelist(self): def getkeyvaluelist(self):
""" """
Helper to return a list of (key, value) tuples. Keys come from Helper to return a list of (key, value) tuples. Keys come from
self._confmatrix and values are instance attributes. configuration matrix and values are instance attributes.
:return: tuples of key value pairs
:rtype: list
""" """
r = [] key_values = []
for k in self.getnames():
if hasattr(self, k): for name in self.getnames():
r.append((k, getattr(self, k))) if hasattr(self, name):
return r value = getattr(self, name)
key_values.append((name, value))
return key_values

View file

@ -57,6 +57,14 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
""" """
def __init__(self, request, client_address, server): def __init__(self, request, client_address, server):
"""
Create a CoreRequestHandler instance.
:param request: request object
:param str client_address: client address
:param CoreServer server: core server instance
:return:
"""
self.done = False self.done = False
self.message_handlers = { self.message_handlers = {
MessageTypes.NODE.value: self.handle_node_message, MessageTypes.NODE.value: self.handle_node_message,
@ -93,6 +101,8 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def setup(self): def setup(self):
""" """
Client has connected, set up a new connection. Client has connected, set up a new connection.
:return: nothing
""" """
logger.info("new TCP connection: %s", self.client_address) logger.info("new TCP connection: %s", self.client_address)
# self.register() # self.register()
@ -101,6 +111,8 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
""" """
Client has disconnected, end this request handler and disconnect Client has disconnected, end this request handler and disconnect
from the session. Shutdown sessions that are not running. from the session. Shutdown sessions that are not running.
:return: nothing
""" """
logger.info("finishing request handler") logger.info("finishing request handler")
self.done = True self.done = True
@ -322,6 +334,8 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def register(self): def register(self):
""" """
Return a Register Message Return a Register Message
:return: register message data
""" """
logger.info("GUI has connected to session %d at %s", self.session.session_id, time.ctime()) logger.info("GUI has connected to session %d at %s", self.session.session_id, time.ctime())
@ -341,14 +355,19 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
""" """
Send raw data to the other end of this TCP connection Send raw data to the other end of this TCP connection
using socket"s sendall(). using socket"s sendall().
:param data: data to send over request socket
:return: data sent
""" """
return self.request.sendall(data) return self.request.sendall(data)
def receive_message(self): def receive_message(self):
""" """
Receive data and return a CORE API message object. Receive data and return a CORE API message object.
"""
:return: received message
:rtype: coreapi.CoreMessage
"""
try: try:
header = self.request.recv(coreapi.CoreMessage.header_len) header = self.request.recv(coreapi.CoreMessage.header_len)
if len(header) > 0: if len(header) > 0:
@ -389,6 +408,9 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def queue_message(self, message): def queue_message(self, message):
""" """
Queue an API message for later processing. Queue an API message for later processing.
:param message: message to queue
:return: nothing
""" """
logger.info("queueing msg (queuedtimes = %s): type %s", logger.info("queueing msg (queuedtimes = %s): type %s",
message.queuedtimes, MessageTypes(message.message_type)) message.queuedtimes, MessageTypes(message.message_type))
@ -399,6 +421,8 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
CORE API message handling loop that is spawned for each server CORE API message handling loop that is spawned for each server
thread; get CORE API messages from the incoming message queue, thread; get CORE API messages from the incoming message queue,
and call handlemsg() for processing. and call handlemsg() for processing.
:return: nothing
""" """
while not self.done: while not self.done:
try: try:
@ -411,6 +435,8 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
""" """
Handle an incoming message; dispatch based on message type, Handle an incoming message; dispatch based on message type,
optionally sending replies. optionally sending replies.
:return: nothing
""" """
if self.session and self.session.broker.handle_message(message): if self.session and self.session.broker.handle_message(message):
logger.info("%s forwarding message:\n%s", threading.currentThread().getName(), message) logger.info("%s forwarding message:\n%s", threading.currentThread().getName(), message)
@ -436,6 +462,10 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def dispatch_replies(self, replies, message): def dispatch_replies(self, replies, message):
""" """
Dispatch replies by CORE to message msg previously received from the client. Dispatch replies by CORE to message msg previously received from the client.
:param replies: reply messages to dispatch
:param message: message for replies
:return: nothing
""" """
for reply in replies: for reply in replies:
message_type, message_flags, message_length = coreapi.CoreMessage.unpack_header(reply) message_type, message_flags, message_length = coreapi.CoreMessage.unpack_header(reply)
@ -462,6 +492,8 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
Handle a new connection request from a client. Dispatch to the Handle a new connection request from a client. Dispatch to the
recvmsg() method for receiving data into CORE API messages, and recvmsg() method for receiving data into CORE API messages, and
add them to an incoming message queue. add them to an incoming message queue.
:return: nothing
""" """
# use port as session id # use port as session id
port = self.request.getpeername()[1] port = self.request.getpeername()[1]
@ -505,6 +537,9 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def handle_node_message(self, message): def handle_node_message(self, message):
""" """
Node Message handler Node Message handler
:param coreapi.CoreNodeMessage message: node message
:return: replies to node message
""" """
replies = [] replies = []
if message.flags & MessageFlags.ADD.value and message.flags & MessageFlags.DELETE.value: if message.flags & MessageFlags.ADD.value and message.flags & MessageFlags.DELETE.value:
@ -634,6 +669,9 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def handle_link_message(self, message): def handle_link_message(self, message):
""" """
Link Message handler Link Message handler
:param coreapi.CoreLinkMessage message: link message to handle
:return: link message replies
""" """
# get node classes # get node classes
ptp_class = nodeutils.get_node_class(NodeTypes.PEER_TO_PEER) ptp_class = nodeutils.get_node_class(NodeTypes.PEER_TO_PEER)
@ -988,6 +1026,9 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def handle_execute_message(self, message): def handle_execute_message(self, message):
""" """
Execute Message handler Execute Message handler
:param coreapi.CoreExecMessage message: execute message to handle
:return: reply messages
""" """
node_num = message.get_tlv(ExecuteTlvs.NODE.value) node_num = message.get_tlv(ExecuteTlvs.NODE.value)
execute_num = message.get_tlv(ExecuteTlvs.NUMBER.value) execute_num = message.get_tlv(ExecuteTlvs.NUMBER.value)
@ -1060,6 +1101,9 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def handle_register_message(self, message): def handle_register_message(self, message):
""" """
Register Message Handler Register Message Handler
:param coreapi.CoreRegMessage message: register message to handle
:return: reply messages
""" """
replies = [] replies = []
@ -1093,7 +1137,8 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
) )
thread.daemon = True thread.daemon = True
thread.start() thread.start()
time.sleep(0.25) # allow time for session creation # allow time for session creation
time.sleep(0.25)
if message.flags & MessageFlags.STRING.value: if message.flags & MessageFlags.STRING.value:
new_session_ids = set(server.get_session_ids()) new_session_ids = set(server.get_session_ids())
new_sid = new_session_ids.difference(old_session_ids) new_sid = new_session_ids.difference(old_session_ids)
@ -1145,8 +1190,10 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def handle_config_message(self, message): def handle_config_message(self, message):
""" """
Configuration Message handler Configuration Message handler
"""
:param coreapi.CoreConfMessage message: configuration message to handle
:return: reply messages
"""
# convert config message to standard config data object # convert config message to standard config data object
config_data = ConfigData( config_data = ConfigData(
node=message.get_tlv(ConfigTlvs.NODE.value), node=message.get_tlv(ConfigTlvs.NODE.value),
@ -1174,6 +1221,9 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def handle_file_message(self, message): def handle_file_message(self, message):
""" """
File Message handler File Message handler
:param coreapi.CoreFileMessage message: file message to handle
:return: reply messages
""" """
if message.flags & MessageFlags.ADD.value: if message.flags & MessageFlags.ADD.value:
node_num = message.get_tlv(NodeTlvs.NUMBER.value) node_num = message.get_tlv(NodeTlvs.NUMBER.value)
@ -1231,6 +1281,9 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def handle_interface_message(self, message): def handle_interface_message(self, message):
""" """
Interface Message handler Interface Message handler
:param message: interface message to handle
:return: reply messages
""" """
logger.info("ignoring Interface message") logger.info("ignoring Interface message")
return () return ()
@ -1238,6 +1291,9 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def handle_event_message(self, message): def handle_event_message(self, message):
""" """
Event Message handler Event Message handler
:param coreapi.CoreEventMessage message: event message to handle
:return: reply messages
""" """
event_data = EventData( event_data = EventData(
node=message.get_tlv(EventTlvs.NODE.value), node=message.get_tlv(EventTlvs.NODE.value),
@ -1350,6 +1406,9 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def handle_session_message(self, message): def handle_session_message(self, message):
""" """
Session Message handler Session Message handler
:param coreapi.CoreSessionMessage message: session message to handle
:return: reply messages
""" """
session_id_str = message.get_tlv(SessionTlvs.NUMBER.value) session_id_str = message.get_tlv(SessionTlvs.NUMBER.value)
name_str = message.get_tlv(SessionTlvs.NAME.value) name_str = message.get_tlv(SessionTlvs.NAME.value)
@ -1436,10 +1495,11 @@ class CoreRequestHandler(SocketServer.BaseRequestHandler):
def send_node_emulation_id(self, node_id): def send_node_emulation_id(self, node_id):
""" """
Send back node messages to the GUI for node messages that had Node emulation id to send.
the status request flag.
"""
:param int node_id: node id to send
:return: nothing
"""
if node_id in self.node_status_request: if node_id in self.node_status_request:
tlv_data = "" tlv_data = ""
tlv_data += coreapi.CoreNodeTlv.pack(NodeTlvs.NUMBER.value, node_id) tlv_data += coreapi.CoreNodeTlv.pack(NodeTlvs.NUMBER.value, node_id)
@ -1462,6 +1522,13 @@ class CoreDatagramRequestHandler(CoreRequestHandler):
""" """
def __init__(self, request, client_address, server): def __init__(self, request, client_address, server):
"""
Create a CoreDatagramRequestHandler instance.
:param request: request object
:param str client_address: client address
:param CoreServer server: core server instance
"""
# TODO: decide which messages cannot be handled with connectionless UDP # TODO: decide which messages cannot be handled with connectionless UDP
self.message_handlers = { self.message_handlers = {
MessageTypes.NODE.value: self.handle_node_message, MessageTypes.NODE.value: self.handle_node_message,
@ -1482,19 +1549,33 @@ class CoreDatagramRequestHandler(CoreRequestHandler):
def setup(self): def setup(self):
""" """
Client has connected, set up a new connection. Client has connected, set up a new connection.
:return: nothing
""" """
logger.info("new UDP connection: %s:%s" % self.client_address) logger.info("new UDP connection: %s:%s" % self.client_address)
def handle(self): def handle(self):
"""
Receive a message.
:return: nothing
"""
self.receive_message() self.receive_message()
def finish(self): def finish(self):
"""
Handle the finish state of a client.
:return: nothing
"""
return SocketServer.BaseRequestHandler.finish(self) return SocketServer.BaseRequestHandler.finish(self)
def receive_message(self): def receive_message(self):
""" """
Receive data, parse a CoreMessage and queue it onto an existing Receive data, parse a CoreMessage and queue it onto an existing
session handler"s queue, if available. session handler"s queue, if available.
:return: nothing
""" """
data = self.request[0] data = self.request[0]
sock = self.request[1] sock = self.request[1]
@ -1550,12 +1631,16 @@ class CoreDatagramRequestHandler(CoreRequestHandler):
def queue_message(self, message): def queue_message(self, message):
""" """
UDP handlers are short-lived and do not have message queues. UDP handlers are short-lived and do not have message queues.
:return: nothing
""" """
raise Exception("Unable to queue %s message for later processing using UDP!" % message.type_str()) raise Exception("Unable to queue %s message for later processing using UDP!" % message.type_str())
def sendall(self, data): def sendall(self, data):
""" """
Use sendto() on the connectionless UDP socket. Use sendto() on the connectionless UDP socket.
:return: nothing
""" """
self.request[1].sendto(data, self.client_address) self.request[1].sendto(data, self.client_address)
@ -1568,6 +1653,13 @@ class BaseAuxRequestHandler(CoreRequestHandler):
""" """
def __init__(self, request, client_address, server): def __init__(self, request, client_address, server):
"""
Create a BaseAuxRequestHandler instance.
:param request: request client
:param str client_address: client address
:param CoreServer server: core server instance
"""
self.message_handlers = { self.message_handlers = {
MessageTypes.NODE.value: self.handle_node_message, MessageTypes.NODE.value: self.handle_node_message,
MessageTypes.LINK.value: self.handle_link_message, MessageTypes.LINK.value: self.handle_link_message,
@ -1588,12 +1680,16 @@ class BaseAuxRequestHandler(CoreRequestHandler):
def setup(self): def setup(self):
""" """
New client has connected to the auxiliary server. New client has connected to the auxiliary server.
:return: nothing
""" """
logger.info("new auxiliary server client: %s:%s" % self.client_address) logger.info("new auxiliary server client: %s:%s" % self.client_address)
def handle(self): def handle(self):
""" """
The handler main loop The handler main loop
:return: nothing
""" """
port = self.request.getpeername()[1] port = self.request.getpeername()[1]
self.session = self.server.mainserver.create_session(session_id=port) self.session = self.server.mainserver.create_session(session_id=port)
@ -1616,6 +1712,8 @@ class BaseAuxRequestHandler(CoreRequestHandler):
def finish(self): def finish(self):
""" """
Disconnect the client Disconnect the client
:return: nothing
""" """
if self.session: if self.session:
self.session.event_handlers.remove(self.handle_broadcast_event) self.session.event_handlers.remove(self.handle_broadcast_event)
@ -1639,6 +1737,7 @@ class BaseAuxRequestHandler(CoreRequestHandler):
EXAMPLE: EXAMPLE:
return self.handler.request.recv(siz) return self.handler.request.recv(siz)
:return: nothing
""" """
raise NotImplemented raise NotImplemented
@ -1662,6 +1761,7 @@ class BaseAuxRequestHandler(CoreRequestHandler):
logger.info("-"*60) logger.info("-"*60)
raise e raise e
:return: nothing
""" """
raise NotImplemented raise NotImplemented
@ -1683,5 +1783,7 @@ class BaseAuxRequestHandler(CoreRequestHandler):
traceback.print_exc(file=sys.stdout) traceback.print_exc(file=sys.stdout)
logger.info("-"*60) logger.info("-"*60)
raise e raise e
:return: nothing
""" """
raise NotImplemented raise NotImplemented

View file

@ -1,6 +1,6 @@
""" """
coreobj.py: defines the basic objects for emulation: the PyCoreObj base class, Defines the basic objects for CORE emulation: the PyCoreObj base class, along with PyCoreNode,
along with PyCoreNode, PyCoreNet, and PyCoreNetIf PyCoreNet, and PyCoreNetIf.
""" """
import os import os
@ -23,14 +23,27 @@ class Position(object):
""" """
def __init__(self, x=None, y=None, z=None): def __init__(self, x=None, y=None, z=None):
self.x = None """
self.y = None Creates a Position instance.
self.z = None
self.set(x, y, z) :param x: x position
:param y: y position
:param z: z position
:return:
"""
self.x = x
self.y = y
self.z = z
def set(self, x=None, y=None, z=None): def set(self, x=None, y=None, z=None):
""" """
Returns True if the position has actually changed. Returns True if the position has actually changed.
:param x: x position
:param y: y position
:param z: z position
:return: True if position changed, False otherwise
:rtype: bool
""" """
if self.x == x and self.y == y and self.z == z: if self.x == x and self.y == y and self.z == z:
return False return False
@ -41,18 +54,32 @@ class Position(object):
def get(self): def get(self):
""" """
Fetch the (x,y,z) position tuple. Retrieve x,y,z position.
:return: x,y,z position tuple
:rtype: tuple
""" """
return self.x, self.y, self.z return self.x, self.y, self.z
class PyCoreObj(object): class PyCoreObj(object):
""" """
Base class for pycore objects (nodes and nets) Base class for CORE objects (nodes and networks)
""" """
apitype = None apitype = None
# TODO: appears start has no usage, verify and remove
def __init__(self, session, objid=None, name=None, start=True): def __init__(self, session, objid=None, name=None, start=True):
"""
Creates a PyCoreObj instance.
:param core.session.Session session: CORE session object
:param int objid: object id
:param str name: object name
:param bool start: start value
:return:
"""
self.session = session self.session = session
if objid is None: if objid is None:
objid = session.get_object_id() objid = session.get_object_id()
@ -71,33 +98,57 @@ class PyCoreObj(object):
def startup(self): def startup(self):
""" """
Each object implements its own startup method. Each object implements its own startup method.
:return: nothing
""" """
raise NotImplementedError raise NotImplementedError
def shutdown(self): def shutdown(self):
""" """
Each object implements its own shutdown method. Each object implements its own shutdown method.
:return: nothing
""" """
raise NotImplementedError raise NotImplementedError
def setposition(self, x=None, y=None, z=None): def setposition(self, x=None, y=None, z=None):
""" """
Set the (x,y,z) position of the object. Set the (x,y,z) position of the object.
:param x: x position
:param y: y position
:param z: z position
:return: True if position changed, False otherwise
:rtype: bool
""" """
return self.position.set(x=x, y=y, z=z) return self.position.set(x=x, y=y, z=z)
def getposition(self): def getposition(self):
""" """
Return an (x,y,z) tuple representing this object's position. Return an (x,y,z) tuple representing this object's position.
:return: x,y,z position tuple
:rtype: tuple
""" """
return self.position.get() return self.position.get()
def ifname(self, ifindex): def ifname(self, ifindex):
return self.netif(ifindex).name """
Retrieve interface name for index.
:param int ifindex: interface index
:return: interface name
:rtype: str
"""
return self._netif[ifindex].name
def netifs(self, sort=False): def netifs(self, sort=False):
""" """
Iterate over attached network interfaces. Retrieve network interfaces, sorted if desired.
:param bool sort: boolean used to determine if interfaces should be sorted
:return: network interfaces
:rtype: list
""" """
if sort: if sort:
return map(lambda k: self._netif[k], sorted(self._netif.keys())) return map(lambda k: self._netif[k], sorted(self._netif.keys()))
@ -107,16 +158,34 @@ class PyCoreObj(object):
def numnetif(self): def numnetif(self):
""" """
Return the attached interface count. Return the attached interface count.
:return: number of network interfaces
:rtype: int
""" """
return len(self._netif) return len(self._netif)
def getifindex(self, netif): def getifindex(self, netif):
"""
Retrieve index for an interface.
:param PyCoreNetIf netif: interface to get index for
:return: interface index if found, -1 otherwise
:rtype: int
"""
for ifindex in self._netif: for ifindex in self._netif:
if self._netif[ifindex] is netif: if self._netif[ifindex] is netif:
return ifindex return ifindex
return -1 return -1
def newifindex(self): def newifindex(self):
"""
Create a new interface index.
:return: interface index
:rtype: int
"""
while self.ifindex in self._netif: while self.ifindex in self._netif:
self.ifindex += 1 self.ifindex += 1
ifindex = self.ifindex ifindex = self.ifindex
@ -171,85 +240,161 @@ class PyCoreObj(object):
def all_link_data(self, flags): def all_link_data(self, flags):
""" """
Build CORE API Link Messages for this object. There is no default Build CORE Link data for this object. There is no default
method for PyCoreObjs as PyCoreNodes do not implement this but method for PyCoreObjs as PyCoreNodes do not implement this but
PyCoreNets do. PyCoreNets do.
:param flags: message flags
:return: list of link data
:rtype: link
""" """
return [] return []
class PyCoreNode(PyCoreObj): class PyCoreNode(PyCoreObj):
""" """
Base class for nodes Base class for CORE nodes.
""" """
# TODO: start seems like it should go away
def __init__(self, session, objid=None, name=None, start=True): def __init__(self, session, objid=None, name=None, start=True):
""" """
Initialization for node objects. Create a PyCoreNode instance.
:param core.session.Session session: CORE session object
:param int objid: object id
:param str name: object name
:param bool start: boolean for starting
""" """
PyCoreObj.__init__(self, session, objid, name, start=start) PyCoreObj.__init__(self, session, objid, name, start=start)
self.services = [] self.services = []
if not hasattr(self, "type"): if not hasattr(self, "type"):
self.type = None self.type = None
self.nodedir = None self.nodedir = None
self.tmpnodedir = False
# TODO: getter method that should not be needed
def nodeid(self): def nodeid(self):
"""
Retrieve node id.
:return: node id
:rtype: int
"""
return self.objid return self.objid
def addservice(self, service): def addservice(self, service):
"""
Add a services to the service list.
:param core.service.CoreService service: service to add
:return: nothing
"""
if service is not None: if service is not None:
self.services.append(service) self.services.append(service)
def makenodedir(self): def makenodedir(self):
"""
Create the node directory.
:return: nothing
"""
if self.nodedir is None: if self.nodedir is None:
self.nodedir = \ self.nodedir = os.path.join(self.session.session_dir, self.name + ".conf")
os.path.join(self.session.session_dir, self.name + ".conf")
os.makedirs(self.nodedir) os.makedirs(self.nodedir)
self.tmpnodedir = True self.tmpnodedir = True
else: else:
self.tmpnodedir = False self.tmpnodedir = False
def rmnodedir(self): def rmnodedir(self):
if hasattr(self.session.options, 'preservedir'): """
if self.session.options.preservedir == '1': Remove the node directory, unless preserve directory has been set.
:return: nothing
"""
preserve = getattr(self.session.options, "preservedir", None)
if preserve == "1":
return return
if self.tmpnodedir: if self.tmpnodedir:
shutil.rmtree(self.nodedir, ignore_errors=True) shutil.rmtree(self.nodedir, ignore_errors=True)
def addnetif(self, netif, ifindex): def addnetif(self, netif, ifindex):
"""
Add network interface to node and set the network interface index if successful.
:param PyCoreNetIf netif: network interface to add
:param int ifindex: interface index
:return: nothing
"""
if ifindex in self._netif: if ifindex in self._netif:
raise ValueError, "ifindex %s already exists" % ifindex raise ValueError("ifindex %s already exists" % ifindex)
self._netif[ifindex] = netif self._netif[ifindex] = netif
# TODO: this hould have probably been set ahead, seems bad to me, check for failure and fix
netif.netindex = ifindex netif.netindex = ifindex
def delnetif(self, ifindex): def delnetif(self, ifindex):
"""
Delete a network interface
:param int ifindex: interface index to delete
:return: nothing
"""
if ifindex not in self._netif: if ifindex not in self._netif:
raise ValueError, "ifindex %s does not exist" % ifindex raise ValueError("ifindex %s does not exist" % ifindex)
netif = self._netif.pop(ifindex) netif = self._netif.pop(ifindex)
netif.shutdown() netif.shutdown()
del netif del netif
# TODO: net parameter is not used, remove
def netif(self, ifindex, net=None): def netif(self, ifindex, net=None):
"""
Retrieve network interface.
:param int ifindex: index of interface to retrieve
:param PyCoreNetIf net: network node
:return: network interface, or None if not found
:rtype: PyCoreNetIf
"""
if ifindex in self._netif: if ifindex in self._netif:
return self._netif[ifindex] return self._netif[ifindex]
else: else:
return None return None
def attachnet(self, ifindex, net): def attachnet(self, ifindex, net):
"""
Attach a network.
:param int ifindex: interface of index to attach
:param PyCoreNetIf net: network to attach
:return:
"""
if ifindex not in self._netif: if ifindex not in self._netif:
raise ValueError, "ifindex %s does not exist" % ifindex raise ValueError("ifindex %s does not exist" % ifindex)
self._netif[ifindex].attachnet(net) self._netif[ifindex].attachnet(net)
def detachnet(self, ifindex): def detachnet(self, ifindex):
"""
Detach network interface.
:param int ifindex: interface index to detach
:return: nothing
"""
if ifindex not in self._netif: if ifindex not in self._netif:
raise ValueError, "ifindex %s does not exist" % ifindex raise ValueError("ifindex %s does not exist" % ifindex)
self._netif[ifindex].detachnet() self._netif[ifindex].detachnet()
def setposition(self, x=None, y=None, z=None): def setposition(self, x=None, y=None, z=None):
changed = PyCoreObj.setposition(self, x=x, y=y, z=z) """
if not changed: Set position.
# save extra interface range calculations
return :param x: x position
:param y: y position
:param z: z position
:return: nothing
"""
changed = super(PyCoreNode, self).setposition(x, y, z)
if changed:
for netif in self.netifs(sort=True): for netif in self.netifs(sort=True):
netif.setposition(x, y, z) netif.setposition(x, y, z)
@ -258,15 +403,21 @@ class PyCoreNode(PyCoreObj):
Given another node or net object, return common networks between Given another node or net object, return common networks between
this node and that object. A list of tuples is returned, with each tuple this node and that object. A list of tuples is returned, with each tuple
consisting of (network, interface1, interface2). consisting of (network, interface1, interface2).
:param obj: object to get common network with
:param want_ctrl: flag set to determine if control network are wanted
:return: tuples of common networks
:rtype: list
""" """
r = [] common = []
for netif1 in self.netifs(): for netif1 in self.netifs():
if not want_ctrl and hasattr(netif1, 'control'): if not want_ctrl and hasattr(netif1, "control"):
continue continue
for netif2 in obj.netifs(): for netif2 in obj.netifs():
if netif1.net == netif2.net: if netif1.net == netif2.net:
r += (netif1.net, netif1, netif2), common.append((netif1.net, netif1, netif2))
return r
return common
class PyCoreNet(PyCoreObj): class PyCoreNet(PyCoreObj):
@ -275,15 +426,27 @@ class PyCoreNet(PyCoreObj):
""" """
linktype = LinkTypes.WIRED.value linktype = LinkTypes.WIRED.value
# TODO: remove start if appropriate
def __init__(self, session, objid, name, start=True): def __init__(self, session, objid, name, start=True):
""" """
Initialization for network objects. Create a PyCoreNet instance.
:param core.session.Session session: CORE session object
:param int objid: object id
:param str name: object name
:param bool start: should object start
""" """
PyCoreObj.__init__(self, session, objid, name, start=start) PyCoreObj.__init__(self, session, objid, name, start=start)
self._linked = {} self._linked = {}
self._linked_lock = threading.Lock() self._linked_lock = threading.Lock()
def attach(self, netif): def attach(self, netif):
"""
Attach network interface.
:param PyCoreNetIf netif: network interface to attach
:return: nothing
"""
i = self.newifindex() i = self.newifindex()
self._netif[i] = netif self._netif[i] = netif
netif.netifi = i netif.netifi = i
@ -291,22 +454,31 @@ class PyCoreNet(PyCoreObj):
self._linked[netif] = {} self._linked[netif] = {}
def detach(self, netif): def detach(self, netif):
"""
Detach network interface.
:param PyCoreNetIf netif: network interface to detach
:return: nothing
"""
del self._netif[netif.netifi] del self._netif[netif.netifi]
netif.netifi = None netif.netifi = None
with self._linked_lock: with self._linked_lock:
del self._linked[netif] del self._linked[netif]
# TODO: needs to be abstracted out, seems like it may be ok to remove
def netifparamstolink(self, netif): def netifparamstolink(self, netif):
""" """
Helper for tolinkmsgs() to build TLVs having link parameters Helper for tolinkmsgs() to build TLVs having link parameters from interface parameters.
from interface parameters.
:param PyCoreNetIf netif: network interface to retrieve params from
:return: tlv data
""" """
delay = netif.getparam('delay') delay = netif.getparam("delay")
bw = netif.getparam('bw') bw = netif.getparam("bw")
loss = netif.getparam('loss') loss = netif.getparam("loss")
duplicate = netif.getparam('duplicate') duplicate = netif.getparam("duplicate")
jitter = netif.getparam('jitter') jitter = netif.getparam("jitter")
tlvdata = "" tlvdata = ""
if delay is not None: if delay is not None:
@ -319,12 +491,13 @@ class PyCoreNet(PyCoreObj):
tlvdata += coreapi.CoreLinkTlv.pack(LinkTlvs.DUP.value, str(duplicate)) tlvdata += coreapi.CoreLinkTlv.pack(LinkTlvs.DUP.value, str(duplicate))
if jitter is not None: if jitter is not None:
tlvdata += coreapi.CoreLinkTlv.pack(LinkTlvs.JITTER.value, jitter) tlvdata += coreapi.CoreLinkTlv.pack(LinkTlvs.JITTER.value, jitter)
return tlvdata return tlvdata
def all_link_data(self, flags): def all_link_data(self, flags):
""" """
Build CORE API Link Messages for this network. Each link message Build link data objects for this network. Each link object describes a link
describes a link between this network and a node. between this network and a node.
""" """
all_links = [] all_links = []
@ -415,10 +588,18 @@ class PyCoreNet(PyCoreObj):
class PyCoreNetIf(object): class PyCoreNetIf(object):
""" """
Base class for interfaces. Base class for network interfaces.
""" """
def __init__(self, node, name, mtu): def __init__(self, node, name, mtu):
"""
Creates a PyCoreNetIf instance.
:param node: node for interface
:param str name: interface name
:param mtu: mtu value
"""
self.node = node self.node = node
self.name = name self.name = name
if not isinstance(mtu, (int, long)): if not isinstance(mtu, (int, long)):
@ -437,68 +618,114 @@ class PyCoreNetIf(object):
self.flow_id = None self.flow_id = None
def startup(self): def startup(self):
"""
Startup method for the interface.
:return: nothing
"""
pass pass
def shutdown(self): def shutdown(self):
"""
Shutdown method for the interface.
:return: nothing
"""
pass pass
def attachnet(self, net): def attachnet(self, net):
"""
Attach network.
:param PyCoreNet net: network to attach to
:return:nothing
"""
if self.net: if self.net:
self.detachnet() self.detachnet()
self.net = None self.net = None
net.attach(self) net.attach(self)
self.net = net self.net = net
def detachnet(self): def detachnet(self):
"""
Detach from a network.
:return: nothing
"""
if self.net is not None: if self.net is not None:
self.net.detach(self) self.net.detach(self)
def addaddr(self, addr): def addaddr(self, addr):
"""
Add address.
:param str addr: address to add
:return: nothing
"""
self.addrlist.append(addr) self.addrlist.append(addr)
def deladdr(self, addr): def deladdr(self, addr):
"""
Delete address.
:param str addr: address to delete
:return: nothing
"""
self.addrlist.remove(addr) self.addrlist.remove(addr)
def sethwaddr(self, addr): def sethwaddr(self, addr):
"""
Set hardware address.
:param str addr: hardware address to set to.
:return: nothing
"""
self.hwaddr = addr self.hwaddr = addr
def getparam(self, key): def getparam(self, key):
""" """
Retrieve a parameter from the _params dict, Retrieve a parameter from the, or None if the parameter does not exist.
or None if the parameter does not exist.
:param key: parameter to get value for
:return: parameter value
""" """
if key not in self._params: return self._params.get(key)
return None
return self._params[key]
def getparams(self): def getparams(self):
""" """
Return (key, value) pairs from the _params dict. Return (key, value) pairs for parameters.
""" """
r = [] parameters = []
for k in sorted(self._params.keys()): for k in sorted(self._params.keys()):
r.append((k, self._params[k])) parameters.append((k, self._params[k]))
return r return parameters
def setparam(self, key, value): def setparam(self, key, value):
""" """
Set a parameter in the _params dict. Set a parameter value, returns True if the parameter has changed.
Returns True if the parameter has changed.
:param key: parameter name to set
:param value: parameter value
:return: True if parameter changed, False otherwise
""" """
if key in self._params:
if self._params[key] == value:
return False
elif self._params[key] <= 0 and value <= 0:
# treat None and 0 as unchanged values # treat None and 0 as unchanged values
current_value = self._params.get(key)
if current_value == value or current_value <= 0 and value <= 0:
return False return False
self._params[key] = value self._params[key] = value
return True return True
def swapparams(self, name): def swapparams(self, name):
""" """
Swap out the _params dict for name. If name does not exist, Swap out parameters dict for name. If name does not exist,
intialize it. This is for supporting separate upstream/downstream intialize it. This is for supporting separate upstream/downstream
parameters when two layer-2 nodes are linked together. parameters when two layer-2 nodes are linked together.
:param str name: name of parameter to swap
:return: nothing
""" """
tmp = self._params tmp = self._params
if not hasattr(self, name): if not hasattr(self, name):
@ -508,7 +735,12 @@ class PyCoreNetIf(object):
def setposition(self, x, y, z): def setposition(self, x, y, z):
""" """
Dispatch to any position hook (self.poshook) handler. Dispatch position hook handler.
:param x: x position
:param y: y position
:param z: z position
:return: nothing
""" """
if self.poshook is not None: if self.poshook is not None:
self.poshook(self, x, y, z) self.poshook(self, x, y, z)

View file

@ -1,5 +1,6 @@
""" """
Defines server classes and request handlers for TCP and UDP. Also defined here is a TCP based auxiliary server class for supporting externally defined handlers. Defines server classes and request handlers for TCP and UDP. Also defined here is a TCP based
auxiliary server class for supporting externally defined handlers.
""" """
import SocketServer import SocketServer
@ -48,6 +49,7 @@ class CoreServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
@classmethod @classmethod
def add_server(cls, server): def add_server(cls, server):
""" """
Add a core server to the known servers set.
:param CoreServer server: server to add :param CoreServer server: server to add
:return: nothing :return: nothing
@ -57,9 +59,10 @@ class CoreServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
@classmethod @classmethod
def remove_server(cls, server): def remove_server(cls, server):
""" """
Remove a core server from the known servers set.
:param CoreServer server: server to remove :param CoreServer server: server to remove
:return: :return: nothing
""" """
try: try:
cls.servers.remove(server) cls.servers.remove(server)
@ -67,6 +70,11 @@ class CoreServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
logger.exception("error removing server: %s", server) logger.exception("error removing server: %s", server)
def shutdown(self): def shutdown(self):
"""
Shutdown the server, all known sessions, and remove server from known servers set.
:return: nothing
"""
# shutdown all known sessions # shutdown all known sessions
for session in self.sessions.values(): for session in self.sessions.values():
session.shutdown() session.shutdown()
@ -103,6 +111,7 @@ class CoreServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
:param core.session.Session session: session to remove :param core.session.Session session: session to remove
:return: removed session :return: removed session
:rtype: core.session.Session
""" """
with self._sessions_lock: with self._sessions_lock:
if session.session_id not in self.sessions: if session.session_id not in self.sessions:
@ -115,6 +124,9 @@ class CoreServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
def get_session_ids(self): def get_session_ids(self):
""" """
Return a list of active session numbers. Return a list of active session numbers.
:return: known session ids
:rtype: list
""" """
with self._sessions_lock: with self._sessions_lock:
session_ids = self.sessions.keys() session_ids = self.sessions.keys()
@ -178,6 +190,9 @@ class CoreServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
def to_session_message(self, flags=0): def to_session_message(self, flags=0):
""" """
Build CORE API Sessions message based on current session info. Build CORE API Sessions message based on current session info.
:param int flags: message flags
:return: session message
""" """
id_list = [] id_list = []
name_list = [] name_list = []
@ -242,7 +257,7 @@ class CoreServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
def dump_sessions(self): def dump_sessions(self):
""" """
Debug print all session info. Log currently known session information.
""" """
logger.info("sessions:") logger.info("sessions:")
with self._sessions_lock: with self._sessions_lock:
@ -277,6 +292,10 @@ class CoreUdpServer(SocketServer.ThreadingMixIn, SocketServer.UDPServer):
""" """
Server class initialization takes configuration data and calls Server class initialization takes configuration data and calls
the SocketServer constructor the SocketServer constructor
:param str server_address: server address
:param class handler_class: class for handling requests
:param main_server: main server to associate with
""" """
self.mainserver = main_server self.mainserver = main_server
SocketServer.UDPServer.__init__(self, server_address, handler_class) SocketServer.UDPServer.__init__(self, server_address, handler_class)
@ -284,6 +303,8 @@ class CoreUdpServer(SocketServer.ThreadingMixIn, SocketServer.UDPServer):
def start(self): def start(self):
""" """
Thread target to run concurrently with the TCP server. Thread target to run concurrently with the TCP server.
:return: nothing
""" """
self.serve_forever() self.serve_forever()
@ -296,18 +317,49 @@ class CoreAuxServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
allow_reuse_address = True allow_reuse_address = True
def __init__(self, server_address, handler_class, main_server): def __init__(self, server_address, handler_class, main_server):
"""
Create a CoreAuxServer instance.
:param str server_address: server address
:param class handler_class: class for handling requests
:param main_server: main server to associate with
"""
self.mainserver = main_server self.mainserver = main_server
logger.info("auxiliary server started, listening on: %s", server_address) logger.info("auxiliary server started, listening on: %s", server_address)
SocketServer.TCPServer.__init__(self, server_address, handler_class) SocketServer.TCPServer.__init__(self, server_address, handler_class)
def start(self): def start(self):
"""
Start the core auxiliary server.
:return: nothing
"""
self.serve_forever() self.serve_forever()
def set_session_master(self, handler): def set_session_master(self, handler):
"""
Set the session master handler.
:param func handler: session master handler
:return:
"""
return self.mainserver.set_session_master(handler) return self.mainserver.set_session_master(handler)
def get_session(self, session_id=None): def get_session(self, session_id=None):
"""
Retrieve a session.
:param int session_id: id of session to retrieve
:return: core.session.Session
"""
return self.mainserver.get_session(session_id) return self.mainserver.get_session(session_id)
def to_session_message(self, flags=0): def to_session_message(self, flags=0):
"""
Retrieve a session message.
:param flags: message flags
:return: session message
"""
return self.mainserver.to_session_message(flags) return self.mainserver.to_session_message(flags)

View file

@ -29,12 +29,15 @@ class CoreLocation(ConfigurableManager):
""" """
Creates a MobilityManager instance. Creates a MobilityManager instance.
:param core.session.Session session: session this manager is tied to
:return: nothing :return: nothing
""" """
ConfigurableManager.__init__(self) ConfigurableManager.__init__(self)
self.reset() self.reset()
self.zonemap = {} self.zonemap = {}
self.refxyz = (0.0, 0.0, 0.0)
self.refscale = 1.0
self.zoneshifts = {}
self.refgeo = (0.0, 0.0, 0.0)
for n, l in utm.ZONE_LETTERS: for n, l in utm.ZONE_LETTERS:
self.zonemap[l] = n self.zonemap[l] = n
@ -85,6 +88,9 @@ class CoreLocation(ConfigurableManager):
Convert the specified value in pixels to meters using the Convert the specified value in pixels to meters using the
configured scale. The scale is given as s, where configured scale. The scale is given as s, where
100 pixels = s meters. 100 pixels = s meters.
:param val: value to use in converting to meters
:return: value converted to meters
""" """
return (val / 100.0) * self.refscale return (val / 100.0) * self.refscale
@ -93,6 +99,9 @@ class CoreLocation(ConfigurableManager):
Convert the specified value in meters to pixels using the Convert the specified value in meters to pixels using the
configured scale. The scale is given as s, where configured scale. The scale is given as s, where
100 pixels = s meters. 100 pixels = s meters.
:param val: value to convert to pixels
:return: value converted to pixels
""" """
if self.refscale == 0.0: if self.refscale == 0.0:
return 0.0 return 0.0
@ -102,10 +111,15 @@ class CoreLocation(ConfigurableManager):
""" """
Record the geographical reference point decimal (lat, lon, alt) Record the geographical reference point decimal (lat, lon, alt)
and convert and store its UTM equivalent for later use. and convert and store its UTM equivalent for later use.
:param lat: latitude
:param lon: longitude
:param alt: altitude
:return: nothing
""" """
self.refgeo = (lat, lon, alt) self.refgeo = (lat, lon, alt)
# easting, northing, zone # easting, northing, zone
(e, n, zonen, zonel) = utm.from_latlon(lat, lon) e, n, zonen, zonel = utm.from_latlon(lat, lon)
self.refutm = ((zonen, zonel), e, n, alt) self.refutm = ((zonen, zonel), e, n, alt)
def getgeo(self, x, y, z): def getgeo(self, x, y, z):
@ -113,9 +127,15 @@ class CoreLocation(ConfigurableManager):
Given (x, y, z) Cartesian coordinates, convert them to latitude, Given (x, y, z) Cartesian coordinates, convert them to latitude,
longitude, and altitude based on the configured reference point longitude, and altitude based on the configured reference point
and scale. and scale.
:param x: x value
:param y: y value
:param z: z value
:return: lat, lon, alt values for provided coordinates
:rtype: tuple
""" """
# shift (x,y,z) over to reference point (x,y,z) # shift (x,y,z) over to reference point (x,y,z)
x = x - self.refxyz[0] x -= self.refxyz[0]
y = -(y - self.refxyz[1]) y = -(y - self.refxyz[1])
if z is None: if z is None:
z = self.refxyz[2] z = self.refxyz[2]
@ -124,7 +144,7 @@ class CoreLocation(ConfigurableManager):
# use UTM coordinates since unit is meters # use UTM coordinates since unit is meters
zone = self.refutm[0] zone = self.refutm[0]
if zone == "": if zone == "":
raise ValueError, "reference point not configured" raise ValueError("reference point not configured")
e = self.refutm[1] + self.px2m(x) e = self.refutm[1] + self.px2m(x)
n = self.refutm[2] + self.px2m(y) n = self.refutm[2] + self.px2m(y)
alt = self.refutm[3] + self.px2m(z) alt = self.refutm[3] + self.px2m(z)
@ -133,7 +153,7 @@ class CoreLocation(ConfigurableManager):
lat, lon = utm.to_latlon(e, n, zone[0], zone[1]) lat, lon = utm.to_latlon(e, n, zone[0], zone[1])
except utm.OutOfRangeError: except utm.OutOfRangeError:
logger.exception("UTM out of range error for n=%s zone=%s xyz=(%s,%s,%s)", n, zone, x, y, z) logger.exception("UTM out of range error for n=%s zone=%s xyz=(%s,%s,%s)", n, zone, x, y, z)
(lat, lon) = self.refgeo[:2] lat, lon = self.refgeo[:2]
# self.info("getgeo(%s,%s,%s) e=%s n=%s zone=%s lat,lon,alt=" \ # self.info("getgeo(%s,%s,%s) e=%s n=%s zone=%s lat,lon,alt=" \
# "%.3f,%.3f,%.3f" % (x, y, z, e, n, zone, lat, lon, alt)) # "%.3f,%.3f,%.3f" % (x, y, z, e, n, zone, lat, lon, alt))
return lat, lon, alt return lat, lon, alt
@ -145,6 +165,12 @@ class CoreLocation(ConfigurableManager):
reference point and scale. Lat/lon is converted to UTM meter reference point and scale. Lat/lon is converted to UTM meter
coordinates, UTM zones are accounted for, and the scale turns coordinates, UTM zones are accounted for, and the scale turns
meters to pixels. meters to pixels.
:param lat: latitude
:param lon: longitude
:param alt: altitude
:return: converted x, y, z coordinates
:rtype: tuple
""" """
# convert lat/lon to UTM coordinates in meters # convert lat/lon to UTM coordinates in meters
e, n, zonen, zonel = utm.from_latlon(lat, lon) e, n, zonen, zonel = utm.from_latlon(lat, lon)
@ -175,17 +201,25 @@ class CoreLocation(ConfigurableManager):
This picks a reference point in the same longitudinal band This picks a reference point in the same longitudinal band
(UTM zone number) as the provided zone, to calculate the shift in (UTM zone number) as the provided zone, to calculate the shift in
meters for the x coordinate. meters for the x coordinate.
:param zonen: zonen
:param zonel: zone1
:return: the x shift value
""" """
rzonen = int(self.refutm[0][0]) rzonen = int(self.refutm[0][0])
# same zone number, no x shift required
if zonen == rzonen: if zonen == rzonen:
return None # same zone number, no x shift required return None
z = (zonen, zonel) z = (zonen, zonel)
# x shift already calculated, cached
if z in self.zoneshifts and self.zoneshifts[z][0] is not None: if z in self.zoneshifts and self.zoneshifts[z][0] is not None:
return self.zoneshifts[z][0] # x shift already calculated, cached return self.zoneshifts[z][0]
rlat, rlon, ralt = self.refgeo rlat, rlon, ralt = self.refgeo
lon2 = rlon + 6 * (zonen - rzonen) # ea. zone is 6deg band # ea. zone is 6deg band
e2, n2, zonen2, zonel2 = utm.from_latlon(rlat, lon2) # ignore northing lon2 = rlon + 6 * (zonen - rzonen)
# ignore northing
e2, n2, zonen2, zonel2 = utm.from_latlon(rlat, lon2)
# NOTE: great circle distance used here, not reference ellipsoid! # NOTE: great circle distance used here, not reference ellipsoid!
xshift = utm.haversine(rlon, rlat, lon2, rlat) - e2 xshift = utm.haversine(rlon, rlat, lon2, rlat) - e2
# cache the return value # cache the return value
@ -203,18 +237,25 @@ class CoreLocation(ConfigurableManager):
This picks a reference point in the same latitude band (UTM zone letter) This picks a reference point in the same latitude band (UTM zone letter)
as the provided zone, to calculate the shift in meters for the as the provided zone, to calculate the shift in meters for the
y coordinate. y coordinate.
:param zonen: zonen
:param zonel: zone1
:return: calculated y shift
""" """
rzonel = self.refutm[0][1] rzonel = self.refutm[0][1]
# same zone letter, no y shift required
if zonel == rzonel: if zonel == rzonel:
return None # same zone letter, no y shift required return None
z = (zonen, zonel) z = (zonen, zonel)
# y shift already calculated, cached
if z in self.zoneshifts and self.zoneshifts[z][1] is not None: if z in self.zoneshifts and self.zoneshifts[z][1] is not None:
return self.zoneshifts[z][1] # y shift already calculated, cached return self.zoneshifts[z][1]
rlat, rlon, ralt = self.refgeo rlat, rlon, ralt = self.refgeo
# zonemap is used to calculate degrees difference between zone letters # zonemap is used to calculate degrees difference between zone letters
latshift = self.zonemap[zonel] - self.zonemap[rzonel] latshift = self.zonemap[zonel] - self.zonemap[rzonel]
lat2 = rlat + latshift # ea. latitude band is 8deg high # ea. latitude band is 8deg high
lat2 = rlat + latshift
e2, n2, zonen2, zonel2 = utm.from_latlon(lat2, rlon) e2, n2, zonen2, zonel2 = utm.from_latlon(lat2, rlon)
# NOTE: great circle distance used here, not reference ellipsoid # NOTE: great circle distance used here, not reference ellipsoid
yshift = -(utm.haversine(rlon, rlat, rlon, lat2) + n2) yshift = -(utm.haversine(rlon, rlat, rlon, lat2) + n2)
@ -231,6 +272,11 @@ class CoreLocation(ConfigurableManager):
the reference point's zone boundary. Return the UTM coordinates in a the reference point's zone boundary. Return the UTM coordinates in a
different zone and the new zone if they do. Zone lettering is only different zone and the new zone if they do. Zone lettering is only
changed when the reference point is in the opposite hemisphere. changed when the reference point is in the opposite hemisphere.
:param e: easting value
:param n: northing value
:return: modified easting, northing, and zone values
:rtype: tuple
""" """
zone = self.refutm[0] zone = self.refutm[0]
rlat, rlon, ralt = self.refgeo rlat, rlon, ralt = self.refgeo

View file

@ -39,7 +39,6 @@ class MobilityManager(ConfigurableManager):
Creates a MobilityManager instance. Creates a MobilityManager instance.
:param core.session.Session session: session this manager is tied to :param core.session.Session session: session this manager is tied to
:return: nothing
""" """
ConfigurableManager.__init__(self) ConfigurableManager.__init__(self)
self.session = session self.session = session
@ -59,6 +58,9 @@ class MobilityManager(ConfigurableManager):
""" """
Session is transitioning from instantiation to runtime state. Session is transitioning from instantiation to runtime state.
Instantiate any mobility models that have been configured for a WLAN. Instantiate any mobility models that have been configured for a WLAN.
:param list node_ids: node ids to startup
:return: nothing
""" """
if node_ids is None: if node_ids is None:
node_ids = self.configs.keys() node_ids = self.configs.keys()
@ -96,12 +98,19 @@ class MobilityManager(ConfigurableManager):
def reset(self): def reset(self):
""" """
Reset all configs. Reset all configs.
:return: nothing
""" """
self.clearconfig(nodenum=None) self.clearconfig(nodenum=None)
def setconfig(self, node_id, config_type, values): def setconfig(self, node_id, config_type, values):
""" """
Normal setconfig() with check for run-time updates for WLANs. Normal setconfig() with check for run-time updates for WLANs.
:param int node_id: node id
:param config_type: configuration type
:param values: configuration value
:return: nothing
""" """
super(MobilityManager, self).setconfig(node_id, config_type, values) super(MobilityManager, self).setconfig(node_id, config_type, values)
if self.session is None: if self.session is None:
@ -117,6 +126,9 @@ class MobilityManager(ConfigurableManager):
""" """
Handle an Event Message used to start, stop, or pause Handle an Event Message used to start, stop, or pause
mobility scripts for a given WlanNode. mobility scripts for a given WlanNode.
:param EventData event_data: event data to handle
:return: nothing
""" """
event_type = event_data.event_type event_type = event_data.event_type
node_id = event_data.node node_id = event_data.node
@ -161,6 +173,9 @@ class MobilityManager(ConfigurableManager):
""" """
Send an event message on behalf of a mobility model. Send an event message on behalf of a mobility model.
This communicates the current and end (max) times to the GUI. This communicates the current and end (max) times to the GUI.
:param WayPointMobility model: mobility model to send event for
:return: nothing
""" """
event_type = EventTypes.NONE.value event_type = EventTypes.NONE.value
if model.state == model.STATE_STOPPED: if model.state == model.STATE_STOPPED:
@ -174,7 +189,7 @@ class MobilityManager(ConfigurableManager):
data += " end=%d" % int(model.endtime) data += " end=%d" % int(model.endtime)
event_data = EventData( event_data = EventData(
node=model.objid, node=model.object_id,
event_type=event_type, event_type=event_type,
name="mobility:%s" % model.name, name="mobility:%s" % model.name,
data=data, data=data,
@ -188,11 +203,16 @@ class MobilityManager(ConfigurableManager):
A mobility script has caused nodes in the 'moved' list to move. A mobility script has caused nodes in the 'moved' list to move.
Update every WlanNode. This saves range calculations if the model Update every WlanNode. This saves range calculations if the model
were to recalculate for each individual node movement. were to recalculate for each individual node movement.
:param list moved: moved nodes
:param list moved_netifs: moved network interfaces
:return: nothing
""" """
for nodenum in self.configs: for nodenum in self.configs:
try: try:
n = self.session.get_object(nodenum) n = self.session.get_object(nodenum)
except KeyError: except KeyError:
logger.exception("error getting session object")
continue continue
if n.model: if n.model:
n.model.update(moved, moved_netifs) n.model.update(moved, moved_netifs)
@ -200,6 +220,10 @@ class MobilityManager(ConfigurableManager):
def addphys(self, netnum, node): def addphys(self, netnum, node):
""" """
Keep track of PhysicalNodes and which network they belong to. Keep track of PhysicalNodes and which network they belong to.
:param int netnum: network number
:param core.coreobj.PyCoreNode node: node to add physical network to
:return: nothing
""" """
nodenum = node.objid nodenum = node.objid
self.phys[nodenum] = node self.phys[nodenum] = node
@ -208,12 +232,16 @@ class MobilityManager(ConfigurableManager):
else: else:
self.physnets[netnum].append(nodenum) self.physnets[netnum].append(nodenum)
# TODO: remove need for handling old style message
def physnodehandlelink(self, message): def physnodehandlelink(self, message):
""" """
Broker handler. Snoop Link add messages to get Broker handler. Snoop Link add messages to get
node numbers of PhyiscalNodes and their nets. node numbers of PhyiscalNodes and their nets.
Physical nodes exist only on other servers, but a shadow object is Physical nodes exist only on other servers, but a shadow object is
created here for tracking node position. created here for tracking node position.
:param message: link message to handle
:return: nothing
""" """
if message.message_type == MessageTypes.LINK.value and message.flags & MessageFlags.ADD.value: if message.message_type == MessageTypes.LINK.value and message.flags & MessageFlags.ADD.value:
nn = message.node_numbers() nn = message.node_numbers()
@ -226,10 +254,14 @@ class MobilityManager(ConfigurableManager):
name="n%d" % nn[1], start=False) name="n%d" % nn[1], start=False)
self.addphys(nn[0], dummy) self.addphys(nn[0], dummy)
# TODO: remove need to handling old style messages
def physnodeupdateposition(self, message): def physnodeupdateposition(self, message):
""" """
Snoop node messages belonging to physical nodes. The dummy object Snoop node messages belonging to physical nodes. The dummy object
in self.phys[] records the node position. in self.phys[] records the node position.
:param message: message to handle
:return: nothing
""" """
nodenum = message.node_numbers()[0] nodenum = message.node_numbers()[0]
try: try:
@ -245,6 +277,9 @@ class MobilityManager(ConfigurableManager):
After installing a mobility model on a net, include any physical After installing a mobility model on a net, include any physical
nodes that we have recorded. Use the GreTap tunnel to the physical node nodes that we have recorded. Use the GreTap tunnel to the physical node
as the node's interface. as the node's interface.
:param net: network to install
:return: nothing
""" """
try: try:
nodenums = self.physnets[net.objid] nodenums = self.physnets[net.objid]
@ -274,6 +309,13 @@ class WirelessModel(Configurable):
position_callback = None position_callback = None
def __init__(self, session, object_id, values=None): def __init__(self, session, object_id, values=None):
"""
Create a WirelessModel instance.
:param core.session.Session session: core session we are tied to
:param int object_id: object id
:param values: values
"""
Configurable.__init__(self, session, object_id) Configurable.__init__(self, session, object_id)
# 'values' can be retrieved from a ConfigurableManager, or used here # 'values' can be retrieved from a ConfigurableManager, or used here
# during initialization, depending on the model. # during initialization, depending on the model.
@ -282,17 +324,31 @@ class WirelessModel(Configurable):
""" """
May be used if the model can populate the GUI with wireless (green) May be used if the model can populate the GUI with wireless (green)
link lines. link lines.
:param flags: link data flags
:return: link data
:rtype: list
""" """
return [] return []
def update(self, moved, moved_netifs): def update(self, moved, moved_netifs):
"""
Update this wireless model.
:param bool moved: flag is it was moved
:param list moved_netifs: moved network interfaces
:return: nothing
"""
raise NotImplementedError raise NotImplementedError
def updateconfig(self, values): def updateconfig(self, values):
""" """
For run-time updates of model config. For run-time updates of model config. Returns True when position callback and set link
Returns True when self._positioncallback() and self.setlinkparams() parameters should be invoked.
should be invoked.
:param values: value to update
:return: False
:rtype: bool
""" """
return False return False
@ -325,7 +381,11 @@ class BasicRangeModel(WirelessModel):
def __init__(self, session, object_id, values=None): def __init__(self, session, object_id, values=None):
""" """
Range model is only instantiated during runtime. Create a BasicRangeModel instance.
:param core.session.Session session: related core session
:param int object_id: object id
:param values: values
""" """
super(BasicRangeModel, self).__init__(session=session, object_id=object_id) super(BasicRangeModel, self).__init__(session=session, object_id=object_id)
self.wlan = session.get_object(object_id) self.wlan = session.get_object(object_id)
@ -337,7 +397,19 @@ class BasicRangeModel(WirelessModel):
logger.info("Basic range model configured for WLAN %d using range %d", object_id, self.range) logger.info("Basic range model configured for WLAN %d using range %d", object_id, self.range)
self.valuestolinkparams(values) self.valuestolinkparams(values)
# link parameters
self.bw = None
self.delay = None
self.loss = None
self.jitter = None
def valuestolinkparams(self, values): def valuestolinkparams(self, values):
"""
Values to convert to link parameters.
:param values: values to convert
:return: nothing
"""
self.bw = int(self.valueof("bandwidth", values)) self.bw = int(self.valueof("bandwidth", values))
if self.bw == 0.0: if self.bw == 0.0:
self.bw = None self.bw = None
@ -357,8 +429,10 @@ class BasicRangeModel(WirelessModel):
Handle configuration messages for setting up a model. Handle configuration messages for setting up a model.
Pass the MobilityManager object as the manager object. Pass the MobilityManager object as the manager object.
:param session: current session calling function :param core.session.Session session: current session calling function
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration :param core.conf.ConfigData config_data: configuration data for carrying out a configuration
:return: configuration data
:rtype: core.data.ConfigData
""" """
return cls.configure(session.mobility, config_data) return cls.configure(session.mobility, config_data)
@ -374,6 +448,12 @@ class BasicRangeModel(WirelessModel):
jitter=self.jitter) jitter=self.jitter)
def get_position(self, netif): def get_position(self, netif):
"""
Retrieve network interface position.
:param netif: network interface position to retrieve
:return: network interface position
"""
with self._netifslock: with self._netifslock:
return self._netifs[netif] return self._netifs[netif]
@ -382,6 +462,12 @@ class BasicRangeModel(WirelessModel):
A node has moved; given an interface, a new (x,y,z) position has A node has moved; given an interface, a new (x,y,z) position has
been set; calculate the new distance between other nodes and link or been set; calculate the new distance between other nodes and link or
unlink node pairs based on the configured range. unlink node pairs based on the configured range.
:param netif: network interface to set position for
:param x: x position
:param y: y position
:param z: z position
:return: nothing
""" """
# print "set_position(%s, x=%s, y=%s, z=%s)" % (netif.localname, x, y, z) # print "set_position(%s, x=%s, y=%s, z=%s)" % (netif.localname, x, y, z)
self._netifslock.acquire() self._netifslock.acquire()
@ -401,6 +487,10 @@ class BasicRangeModel(WirelessModel):
node.position, then re-calculate links for those that have moved. node.position, then re-calculate links for those that have moved.
Assumes bidirectional links, with one calculation per node pair, where Assumes bidirectional links, with one calculation per node pair, where
one of the nodes has moved. one of the nodes has moved.
:param bool moved: flag is it was moved
:param list moved_netifs: moved network interfaces
:return: nothing
""" """
with self._netifslock: with self._netifslock:
while len(moved_netifs): while len(moved_netifs):
@ -419,6 +509,10 @@ class BasicRangeModel(WirelessModel):
calculate distance between two interfaces and perform calculate distance between two interfaces and perform
linking/unlinking. Sends link/unlink messages and updates the linking/unlinking. Sends link/unlink messages and updates the
WlanNode's linked dict. WlanNode's linked dict.
:param netif: interface one
:param netif2: interface two
:return: nothing
""" """
if netif == netif2: if netif == netif2:
return return
@ -455,6 +549,11 @@ class BasicRangeModel(WirelessModel):
def calcdistance(p1, p2): def calcdistance(p1, p2):
""" """
Calculate the distance between two three-dimensional points. Calculate the distance between two three-dimensional points.
:param tuple p1: point one
:param tuple p2: point two
:return: distance petween the points
:rtype: float
""" """
a = p1[0] - p2[0] a = p1[0] - p2[0]
b = p1[1] - p2[1] b = p1[1] - p2[1]
@ -468,6 +567,10 @@ class BasicRangeModel(WirelessModel):
Configuration has changed during runtime. Configuration has changed during runtime.
MobilityManager.setconfig() -> WlanNode.updatemodel() -> MobilityManager.setconfig() -> WlanNode.updatemodel() ->
WirelessModel.updateconfig() WirelessModel.updateconfig()
:param values: values to update configuration
:return: was update successful
:rtype: bool
""" """
self.valuestolinkparams(values) self.valuestolinkparams(values)
self.range = float(self.valueof("range", values)) self.range = float(self.valueof("range", values))
@ -476,6 +579,12 @@ class BasicRangeModel(WirelessModel):
def create_link_data(self, interface1, interface2, message_type): def create_link_data(self, interface1, interface2, message_type):
""" """
Create a wireless link/unlink data message. Create a wireless link/unlink data message.
:param core.coreobj.PyCoreNetIf interface1: interface one
:param core.coreobj.PyCoreNetIf interface2: interface two
:param message_type: link message type
:return: link data
:rtype: LinkData
""" """
return LinkData( return LinkData(
@ -489,6 +598,11 @@ class BasicRangeModel(WirelessModel):
def sendlinkmsg(self, netif, netif2, unlink=False): def sendlinkmsg(self, netif, netif2, unlink=False):
""" """
Send a wireless link/unlink API message to the GUI. Send a wireless link/unlink API message to the GUI.
:param core.coreobj.PyCoreNetIf netif: interface one
:param core.coreobj.PyCoreNetIf netif2: interface two
:param bool unlink: unlink or not
:return: nothing
""" """
if unlink: if unlink:
message_type = MessageFlags.DELETE.value message_type = MessageFlags.DELETE.value
@ -504,6 +618,10 @@ class BasicRangeModel(WirelessModel):
def all_link_data(self, flags): def all_link_data(self, flags):
""" """
Return a list of wireless link messages for when the GUI reconnects. Return a list of wireless link messages for when the GUI reconnects.
:param flags: link flags
:return: all link data
:rtype: list
""" """
all_links = [] all_links = []
with self.wlan._linked_lock: with self.wlan._linked_lock:
@ -514,6 +632,39 @@ class BasicRangeModel(WirelessModel):
return all_links return all_links
class WayPoint(object):
"""
Maintains information regarding waypoints.
"""
def __init__(self, time, nodenum, coords, speed):
"""
Creates a WayPoint instance.
:param time: waypoint time
:param int nodenum: node id
:param coords: waypoint coordinates
:param speed: waypoint speed
"""
self.time = time
self.nodenum = nodenum
self.coords = coords
self.speed = speed
def __cmp__(self, other):
"""
Custom comparison method for waypoints.
:param WayPoint other: waypoint to compare to
:return: the comparison result against the other waypoint
:rtype: int
"""
tmp = cmp(self.time, other.time)
if tmp == 0:
tmp = cmp(self.nodenum, other.nodenum)
return tmp
class WayPointMobility(WirelessModel): class WayPointMobility(WirelessModel):
""" """
Abstract class for mobility models that set node waypoints. Abstract class for mobility models that set node waypoints.
@ -525,20 +676,15 @@ class WayPointMobility(WirelessModel):
STATE_RUNNING = 1 STATE_RUNNING = 1
STATE_PAUSED = 2 STATE_PAUSED = 2
class WayPoint(object):
def __init__(self, time, nodenum, coords, speed):
self.time = time
self.nodenum = nodenum
self.coords = coords
self.speed = speed
def __cmp__(self, other):
tmp = cmp(self.time, other.time)
if tmp == 0:
tmp = cmp(self.nodenum, other.nodenum)
return tmp
def __init__(self, session, object_id, values=None): def __init__(self, session, object_id, values=None):
"""
Create a WayPointMobility instance.
:param core.session.Session session: CORE session instance
:param int object_id: object id
:param values: values for this model
:return:
"""
super(WayPointMobility, self).__init__(session=session, object_id=object_id, values=values) super(WayPointMobility, self).__init__(session=session, object_id=object_id, values=values)
self.state = self.STATE_STOPPED self.state = self.STATE_STOPPED
self.queue = [] self.queue = []
@ -558,6 +704,8 @@ class WayPointMobility(WirelessModel):
def runround(self): def runround(self):
""" """
Advance script time and move nodes. Advance script time and move nodes.
:return: nothing
""" """
if self.state != self.STATE_RUNNING: if self.state != self.STATE_RUNNING:
return return
@ -608,6 +756,11 @@ class WayPointMobility(WirelessModel):
self.session.evq.add_event(0.001 * self.refresh_ms, self.runround) self.session.evq.add_event(0.001 * self.refresh_ms, self.runround)
def run(self): def run(self):
"""
Run the waypoint mobility scenario.
:return: nothing
"""
self.timezero = time.time() self.timezero = time.time()
self.lasttime = self.timezero - (0.001 * self.refresh_ms) self.lasttime = self.timezero - (0.001 * self.refresh_ms)
self.movenodesinitial() self.movenodesinitial()
@ -618,6 +771,11 @@ class WayPointMobility(WirelessModel):
""" """
Calculate next node location and update its coordinates. Calculate next node location and update its coordinates.
Returns True if the node's position has changed. Returns True if the node's position has changed.
:param core.netns.nodes.CoreNode node: node to move
:param dt: move factor
:return: True if node was moved, False otherwise
:rtype: bool
""" """
if node.objid not in self.points: if node.objid not in self.points:
return False return False
@ -666,6 +824,8 @@ class WayPointMobility(WirelessModel):
def movenodesinitial(self): def movenodesinitial(self):
""" """
Move nodes to their initial positions. Then calculate the ranges. Move nodes to their initial positions. Then calculate the ranges.
:return: nothing
""" """
moved = [] moved = []
moved_netifs = [] moved_netifs = []
@ -683,21 +843,38 @@ class WayPointMobility(WirelessModel):
def addwaypoint(self, time, nodenum, x, y, z, speed): def addwaypoint(self, time, nodenum, x, y, z, speed):
""" """
Waypoints are pushed to a heapq, sorted by time. Waypoints are pushed to a heapq, sorted by time.
:param time: waypoint time
:param int nodenum: node id
:param x: x position
:param y: y position
:param z: z position
:param speed: speed
:return: nothing
""" """
# print "addwaypoint: %s %s %s,%s,%s %s" % (time, nodenum, x, y, z, speed) # print "addwaypoint: %s %s %s,%s,%s %s" % (time, nodenum, x, y, z, speed)
wp = self.WayPoint(time, nodenum, coords=(x, y, z), speed=speed) wp = WayPoint(time, nodenum, coords=(x, y, z), speed=speed)
heapq.heappush(self.queue, wp) heapq.heappush(self.queue, wp)
def addinitial(self, nodenum, x, y, z): def addinitial(self, nodenum, x, y, z):
""" """
Record initial position in a dict. Record initial position in a dict.
:param int nodenum: node id
:param x: x position
:param y: y position
:param z: z position
:return: nothing
""" """
wp = self.WayPoint(0, nodenum, coords=(x, y, z), speed=0) wp = WayPoint(0, nodenum, coords=(x, y, z), speed=0)
self.initial[nodenum] = wp self.initial[nodenum] = wp
def updatepoints(self, now): def updatepoints(self, now):
""" """
Move items from self.queue to self.points when their time has come. Move items from self.queue to self.points when their time has come.
:param int now: current timestamp
:return: nothing
""" """
while len(self.queue): while len(self.queue):
if self.queue[0].time > now: if self.queue[0].time > now:
@ -708,12 +885,16 @@ class WayPointMobility(WirelessModel):
def copywaypoints(self): def copywaypoints(self):
""" """
Store backup copy of waypoints for looping and stopping. Store backup copy of waypoints for looping and stopping.
:return: nothing
""" """
self.queue_copy = list(self.queue) self.queue_copy = list(self.queue)
def loopwaypoints(self): def loopwaypoints(self):
""" """
Restore backup copy of waypoints when looping. Restore backup copy of waypoints when looping.
:return: nothing
""" """
self.queue = list(self.queue_copy) self.queue = list(self.queue_copy)
return self.loop return self.loop
@ -723,6 +904,12 @@ class WayPointMobility(WirelessModel):
Helper to move a node, notify any GUI (connected session handlers), Helper to move a node, notify any GUI (connected session handlers),
without invoking the interface poshook callback that may perform without invoking the interface poshook callback that may perform
range calculation. range calculation.
:param core.netns.nodes.CoreNode node: node to set position for
:param x: x position
:param y: y position
:param z: z position
:return: nothing
""" """
# this would cause PyCoreNetIf.poshook() callback (range calculation) # this would cause PyCoreNetIf.poshook() callback (range calculation)
# node.setposition(x, y, z) # node.setposition(x, y, z)
@ -739,6 +926,8 @@ class WayPointMobility(WirelessModel):
waypoints. This is just an estimate. The endtime will later be waypoints. This is just an estimate. The endtime will later be
adjusted, after one round of the script has run, to be the time adjusted, after one round of the script has run, to be the time
that the last moving node has reached its final waypoint. that the last moving node has reached its final waypoint.
:return: nothing
""" """
try: try:
self.endtime = self.queue[-1].time self.endtime = self.queue[-1].time
@ -749,6 +938,8 @@ class WayPointMobility(WirelessModel):
""" """
Run the script from the beginning or unpause from where it Run the script from the beginning or unpause from where it
was before. was before.
:return: nothing
""" """
laststate = self.state laststate = self.state
self.state = self.STATE_RUNNING self.state = self.STATE_RUNNING
@ -766,6 +957,9 @@ class WayPointMobility(WirelessModel):
def stop(self, move_initial=True): def stop(self, move_initial=True):
""" """
Stop the script and move nodes to initial positions. Stop the script and move nodes to initial positions.
:param bool move_initial: flag to check if we should move nodes to initial position
:return: nothing
""" """
self.state = self.STATE_STOPPED self.state = self.STATE_STOPPED
self.loopwaypoints() self.loopwaypoints()
@ -778,6 +972,8 @@ class WayPointMobility(WirelessModel):
def pause(self): def pause(self):
""" """
Pause the script; pause time is stored to self.lasttime. Pause the script; pause time is stored to self.lasttime.
:return: nothing
""" """
self.state = self.STATE_PAUSED self.state = self.STATE_PAUSED
self.lasttime = time.time() self.lasttime = time.time()
@ -811,6 +1007,13 @@ class Ns2ScriptedMobility(WayPointMobility):
config_groups = "ns-2 Mobility Script Parameters:1-%d" % len(config_matrix) config_groups = "ns-2 Mobility Script Parameters:1-%d" % len(config_matrix)
def __init__(self, session, object_id, values=None): def __init__(self, session, object_id, values=None):
"""
Creates a Ns2ScriptedMobility instance.
:param core.session.Session session: CORE session instance
:param int object_id: object id
:param values: values
"""
super(Ns2ScriptedMobility, self).__init__(session=session, object_id=object_id, values=values) super(Ns2ScriptedMobility, self).__init__(session=session, object_id=object_id, values=values)
self._netifs = {} self._netifs = {}
self._netifslock = threading.Lock() self._netifslock = threading.Lock()
@ -835,7 +1038,7 @@ class Ns2ScriptedMobility(WayPointMobility):
Handle configuration messages for setting up a model. Handle configuration messages for setting up a model.
Pass the MobilityManager object as the manager object. Pass the MobilityManager object as the manager object.
:param session: current session calling function :param core.session.Session session: current session calling function
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration :param core.conf.ConfigData config_data: configuration data for carrying out a configuration
""" """
return cls.configure(session.mobility, config_data) return cls.configure(session.mobility, config_data)
@ -845,6 +1048,8 @@ class Ns2ScriptedMobility(WayPointMobility):
Read in mobility script from a file. This adds waypoints to a Read in mobility script from a file. This adds waypoints to a
priority queue, sorted by waypoint time. Initial waypoints are priority queue, sorted by waypoint time. Initial waypoints are
stored in a separate dict. stored in a separate dict.
:return: nothing
""" """
filename = self.findfile(self.file) filename = self.findfile(self.file)
try: try:
@ -901,51 +1106,66 @@ class Ns2ScriptedMobility(WayPointMobility):
if ix is not None and iy is not None: if ix is not None and iy is not None:
self.addinitial(self.map(inodenum), ix, iy, iz) self.addinitial(self.map(inodenum), ix, iy, iz)
def findfile(self, fn): def findfile(self, file_name):
""" """
Locate a script file. If the specified file doesn't exist, look in the Locate a script file. If the specified file doesn't exist, look in the
same directory as the scenario file (session.filename), or in the default same directory as the scenario file (session.filename), or in the default
configs directory (~/.core/configs). This allows for sample files without configs directory (~/.core/configs). This allows for sample files without
absolute pathnames. absolute pathnames.
:param str file_name: file name to find
:return: absolute path to the file
:rtype: str
""" """
if os.path.exists(fn): if os.path.exists(file_name):
return fn return file_name
if self.session.filename is not None: if self.session.filename is not None:
d = os.path.dirname(self.session.filename) d = os.path.dirname(self.session.filename)
sessfn = os.path.join(d, fn) sessfn = os.path.join(d, file_name)
if os.path.exists(sessfn): if os.path.exists(sessfn):
return sessfn return sessfn
if self.session.user is not None: if self.session.user is not None:
userfn = os.path.join('/home', self.session.user, '.core', 'configs', fn) userfn = os.path.join('/home', self.session.user, '.core', 'configs', file_name)
if os.path.exists(userfn): if os.path.exists(userfn):
return userfn return userfn
return fn
return file_name
def parsemap(self, mapstr): def parsemap(self, mapstr):
""" """
Parse a node mapping string, given as a configuration parameter. Parse a node mapping string, given as a configuration parameter.
:param str mapstr: mapping string to parse
:return: nothing
""" """
self.nodemap = {} self.nodemap = {}
if mapstr.strip() == '': if mapstr.strip() == "":
return return
for pair in mapstr.split(','):
parts = pair.split(':') for pair in mapstr.split(","):
parts = pair.split(":")
try: try:
if len(parts) != 2: if len(parts) != 2:
raise ValueError raise ValueError
self.nodemap[int(parts[0])] = int(parts[1]) self.nodemap[int(parts[0])] = int(parts[1])
except ValueError: except ValueError:
logger.warn("ns-2 mobility node map error") logger.exception("ns-2 mobility node map error")
return
def map(self, nodenum): def map(self, nodenum):
""" """
Map one node number (from a script file) to another. Map one node number (from a script file) to another.
:param str nodenum: node id to map
:return: mapped value or the node id itself
:rtype: int
""" """
nodenum = int(nodenum) nodenum = int(nodenum)
try: try:
return self.nodemap[nodenum] return self.nodemap[nodenum]
except KeyError: except KeyError:
logger.exception("error find value in node map")
return nodenum return nodenum
def startup(self): def startup(self):
@ -954,6 +1174,8 @@ class Ns2ScriptedMobility(WayPointMobility):
Move node to initial positions when any autostart time is specified. Move node to initial positions when any autostart time is specified.
Ignore the script if autostart is an empty string (can still be Ignore the script if autostart is an empty string (can still be
started via GUI controls). started via GUI controls).
:return: nothing
""" """
if self.autostart == '': if self.autostart == '':
logger.info("not auto-starting ns-2 script for %s" % self.wlan.name) logger.info("not auto-starting ns-2 script for %s" % self.wlan.name)
@ -971,6 +1193,8 @@ class Ns2ScriptedMobility(WayPointMobility):
def start(self): def start(self):
""" """
Handle the case when un-paused. Handle the case when un-paused.
:return: nothing
""" """
laststate = self.state laststate = self.state
super(Ns2ScriptedMobility, self).start() super(Ns2ScriptedMobility, self).start()
@ -980,19 +1204,38 @@ class Ns2ScriptedMobility(WayPointMobility):
def run(self): def run(self):
""" """
Start is pressed or autostart is triggered. Start is pressed or autostart is triggered.
:return: nothing
""" """
super(Ns2ScriptedMobility, self).run() super(Ns2ScriptedMobility, self).run()
self.statescript("run") self.statescript("run")
def pause(self): def pause(self):
"""
Pause the mobility script.
:return: nothing
"""
super(Ns2ScriptedMobility, self).pause() super(Ns2ScriptedMobility, self).pause()
self.statescript("pause") self.statescript("pause")
def stop(self, move_initial=True): def stop(self, move_initial=True):
"""
Stop the mobility script.
:param bool move_initial: flag to check if we should move node to initial position
:return: nothing
"""
super(Ns2ScriptedMobility, self).stop(move_initial=move_initial) super(Ns2ScriptedMobility, self).stop(move_initial=move_initial)
self.statescript("stop") self.statescript("stop")
def statescript(self, typestr): def statescript(self, typestr):
"""
State of the mobility script.
:param str typestr: state type string
:return: nothing
"""
filename = None filename = None
if typestr == "run" or typestr == "unpause": if typestr == "run" or typestr == "unpause":
filename = self.script_start filename = self.script_start

View file

@ -22,32 +22,39 @@ from core.misc import nodeutils
logger = log.get_logger(__name__) logger = log.get_logger(__name__)
class Bunch: # TODO: A named tuple may be more appropriate, than abusing a class dict like this
class Bunch(object):
""" """
Helper class for recording a collection of attributes. Helper class for recording a collection of attributes.
""" """
def __init__(self, **kwds): def __init__(self, **kwds):
"""
Create a Bunch instance.
:param dict kwds: keyword arguments
:return:
"""
self.__dict__.update(kwds) self.__dict__.update(kwds)
class Sdt(object): class Sdt(object):
""" """
Helper class for exporting session objects to NRL's SDT3D. Helper class for exporting session objects to NRL"s SDT3D.
The connect() method initializes the display, and can be invoked The connect() method initializes the display, and can be invoked
when a node position or link has changed. when a node position or link has changed.
""" """
DEFAULT_SDT_URL = "tcp://127.0.0.1:50000/" DEFAULT_SDT_URL = "tcp://127.0.0.1:50000/"
# default altitude (in meters) for flyto view # default altitude (in meters) for flyto view
DEFAULT_ALT = 2500 DEFAULT_ALT = 2500
# TODO: read in user's nodes.conf here; below are default node types from the GUI # TODO: read in user"s nodes.conf here; below are default node types from the GUI
DEFAULT_SPRITES = [ DEFAULT_SPRITES = [
('router', 'router.gif'), ('host', 'host.gif'), ("router", "router.gif"), ("host", "host.gif"),
('PC', 'pc.gif'), ('mdr', 'mdr.gif'), ("PC", "pc.gif"), ("mdr", "mdr.gif"),
('prouter', 'router_green.gif'), ('xen', 'xen.gif'), ("prouter", "router_green.gif"), ("xen", "xen.gif"),
('hub', 'hub.gif'), ('lanswitch', 'lanswitch.gif'), ("hub", "hub.gif"), ("lanswitch", "lanswitch.gif"),
('wlan', 'wlan.gif'), ('rj45', 'rj45.gif'), ("wlan", "wlan.gif"), ("rj45", "rj45.gif"),
('tunnel', 'tunnel.gif'), ("tunnel", "tunnel.gif"),
] ]
def __init__(self, session): def __init__(self, session):
@ -55,7 +62,6 @@ class Sdt(object):
Creates a Sdt instance. Creates a Sdt instance.
:param core.session.Session session: session this manager is tied to :param core.session.Session session: session this manager is tied to
:return: nothing
""" """
self.session = session self.session = session
self.sock = None self.sock = None
@ -69,23 +75,28 @@ class Sdt(object):
def is_enabled(self): def is_enabled(self):
""" """
Check for 'enablesdt' session option. Return False by default if Check for "enablesdt" session option. Return False by default if
the option is missing. the option is missing.
:return: True if enabled, False otherwise
:rtype: bool
""" """
if not hasattr(self.session.options, 'enablesdt'): if not hasattr(self.session.options, "enablesdt"):
return False return False
enabled = self.session.options.enablesdt enabled = self.session.options.enablesdt
if enabled in ('1', 'true', 1, True): if enabled in ("1", "true", 1, True):
return True return True
return False return False
def seturl(self): def seturl(self):
""" """
Read 'sdturl' from session options, or use the default value. Read "sdturl" from session options, or use the default value.
Set self.url, self.address, self.protocol Set self.url, self.address, self.protocol
:return: nothing
""" """
url = None url = None
if hasattr(self.session.options, 'sdturl'): if hasattr(self.session.options, "sdturl"):
if self.session.options.sdturl != "": if self.session.options.sdturl != "":
url = self.session.options.sdturl url = self.session.options.sdturl
if url is None or url == "": if url is None or url == "":
@ -97,6 +108,9 @@ class Sdt(object):
def connect(self, flags=0): def connect(self, flags=0):
""" """
Connect to the SDT address/port if enabled. Connect to the SDT address/port if enabled.
:return: True if connected, False otherwise
:rtype: bool
""" """
if not self.is_enabled(): if not self.is_enabled():
return False return False
@ -109,7 +123,7 @@ class Sdt(object):
logger.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: if self.sock is None:
try: try:
if self.protocol.lower() == 'udp': if self.protocol.lower() == "udp":
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.connect(self.address) self.sock.connect(self.address)
else: else:
@ -133,17 +147,25 @@ class Sdt(object):
""" """
Load icon sprites, and fly to the reference point location on Load icon sprites, and fly to the reference point location on
the virtual globe. the virtual globe.
:return: initialize command status
:rtype: bool
""" """
if not self.cmd('path "%s/icons/normal"' % constants.CORE_DATA_DIR): if not self.cmd("path \"%s/icons/normal\"" % constants.CORE_DATA_DIR):
return False return False
# send node type to icon mappings # send node type to icon mappings
for type, icon in self.DEFAULT_SPRITES: for type, icon in self.DEFAULT_SPRITES:
if not self.cmd('sprite %s image %s' % (type, icon)): if not self.cmd("sprite %s image %s" % (type, icon)):
return False return False
(lat, long) = self.session.location.refgeo[:2] (lat, long) = self.session.location.refgeo[:2]
return self.cmd('flyto %.6f,%.6f,%d' % (long, lat, self.DEFAULT_ALT)) return self.cmd("flyto %.6f,%.6f,%d" % (long, lat, self.DEFAULT_ALT))
def disconnect(self): def disconnect(self):
"""
Disconnect from SDT.
:return: nothing
"""
if self.sock: if self.sock:
try: try:
self.sock.close() self.sock.close()
@ -157,8 +179,10 @@ class Sdt(object):
def shutdown(self): def shutdown(self):
""" """
Invoked from Session.shutdown() and Session.checkshutdown(). Invoked from Session.shutdown() and Session.checkshutdown().
:return: nothing
""" """
self.cmd('clear all') self.cmd("clear all")
self.disconnect() self.disconnect()
self.showerror = True self.showerror = True
@ -167,6 +191,10 @@ class Sdt(object):
Send an SDT command over a UDP socket. socket.sendall() is used Send an SDT command over a UDP socket. socket.sendall() is used
as opposed to socket.sendto() because an exception is raised when as opposed to socket.sendto() because an exception is raised when
there is no socket listener. there is no socket listener.
:param str cmdstr: command to send
:return: True if command was successful, False otherwise
:rtype: bool
""" """
if self.sock is None: if self.sock is None:
return False return False
@ -183,59 +211,84 @@ class Sdt(object):
def updatenode(self, nodenum, flags, x, y, z, name=None, type=None, icon=None): def updatenode(self, nodenum, flags, x, y, z, name=None, type=None, icon=None):
""" """
Node is updated from a Node Message or mobility script. Node is updated from a Node Message or mobility script.
:param int nodenum: node id to update
:param flags: update flags
:param x: x position
:param y: y position
:param z: z position
:param str name: node name
:param type: node type
:param icon: node icon
:return: nothing
""" """
if not self.connect(): if not self.connect():
return return
if flags & MessageFlags.DELETE.value: if flags & MessageFlags.DELETE.value:
self.cmd('delete node,%d' % nodenum) self.cmd("delete node,%d" % nodenum)
return return
if x is None or y is None: if x is None or y is None:
return return
(lat, long, alt) = self.session.location.getgeo(x, y, z) lat, long, alt = self.session.location.getgeo(x, y, z)
pos = "pos %.6f,%.6f,%.6f" % (long, lat, alt) pos = "pos %.6f,%.6f,%.6f" % (long, lat, alt)
if flags & MessageFlags.ADD.value: if flags & MessageFlags.ADD.value:
if icon is not None: if icon is not None:
type = name type = name
icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR) icon = icon.replace("$CORE_DATA_DIR", constants.CORE_DATA_DIR)
icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR) icon = icon.replace("$CORE_CONF_DIR", constants.CORE_CONF_DIR)
self.cmd('sprite %s image %s' % (type, icon)) self.cmd("sprite %s image %s" % (type, icon))
self.cmd('node %d type %s label on,"%s" %s' % (nodenum, type, name, pos)) self.cmd("node %d type %s label on,\"%s\" %s" % (nodenum, type, name, pos))
else: else:
self.cmd('node %d %s' % (nodenum, pos)) self.cmd("node %d %s" % (nodenum, pos))
def updatenodegeo(self, nodenum, lat, long, alt): def updatenodegeo(self, nodenum, lat, long, alt):
""" """
Node is updated upon receiving an EMANE Location Event. Node is updated upon receiving an EMANE Location Event.
TODO: received Node Message with lat/long/alt.
:param int nodenum: node id to update geospatial for
:param lat: latitude
:param long: longitude
:param alt: altitude
:return: nothing
""" """
# TODO: received Node Message with lat/long/alt.
if not self.connect(): if not self.connect():
return return
pos = "pos %.6f,%.6f,%.6f" % (long, lat, alt) pos = "pos %.6f,%.6f,%.6f" % (long, lat, alt)
self.cmd('node %d %s' % (nodenum, pos)) self.cmd("node %d %s" % (nodenum, pos))
def updatelink(self, node1num, node2num, flags, wireless=False): def updatelink(self, node1num, node2num, flags, wireless=False):
""" """
Link is updated from a Link Message or by a wireless model. Link is updated from a Link Message or by a wireless model.
:param int node1num: node one id
:param int node2num: node two id
:param flags: link flags
:param bool wireless: flag to check if wireless or not
:return: nothing
""" """
if node1num is None or node2num is None: if node1num is None or node2num is None:
return return
if not self.connect(): if not self.connect():
return return
if flags & MessageFlags.DELETE.value: if flags & MessageFlags.DELETE.value:
self.cmd('delete link,%s,%s' % (node1num, node2num)) self.cmd("delete link,%s,%s" % (node1num, node2num))
elif flags & MessageFlags.ADD.value: elif flags & MessageFlags.ADD.value:
attr = "" attr = ""
if wireless: if wireless:
attr = " line green,2" attr = " line green,2"
else: else:
attr = " line red,2" attr = " line red,2"
self.cmd('link %s,%s%s' % (node1num, node2num, attr)) self.cmd("link %s,%s%s" % (node1num, node2num, attr))
def sendobjs(self): def sendobjs(self):
""" """
Session has already started, and the SDT3D GUI later connects. Session has already started, and the SDT3D GUI later connects.
Send all node and link objects for display. Otherwise, nodes and Send all node and link objects for display. Otherwise, nodes and
links will only be drawn when they have been updated (e.g. moved). links will only be drawn when they have been updated (e.g. moved).
:return: nothing
""" """
nets = [] nets = []
with self.session._objects_lock: with self.session._objects_lock:
@ -251,7 +304,7 @@ class Sdt(object):
obj.name, obj.type, obj.icon) obj.name, obj.type, obj.icon)
for nodenum in sorted(self.remotes.keys()): for nodenum in sorted(self.remotes.keys()):
r = self.remotes[nodenum] r = self.remotes[nodenum]
(x, y, z) = r.pos x, y, z = r.pos
self.updatenode(nodenum, MessageFlags.ADD.value, x, y, z, self.updatenode(nodenum, MessageFlags.ADD.value, x, y, z,
r.name, r.type, r.icon) r.name, r.type, r.icon)
@ -276,22 +329,30 @@ class Sdt(object):
for n2num, wl in r.links: for n2num, wl in r.links:
self.updatelink(n1num, n2num, MessageFlags.ADD.value, wl) self.updatelink(n1num, n2num, MessageFlags.ADD.value, wl)
# TODO: remove the need for this
def handledistributed(self, message): def handledistributed(self, message):
""" """
Broker handler for processing CORE API messages as they are Broker handler for processing CORE API messages as they are
received. This is used to snoop the Node messages and update received. This is used to snoop the Node messages and update
node positions. node positions.
:param message: message to handle
:return: replies
""" """
if message.message_type == MessageTypes.LINK.value: if message.message_type == MessageTypes.LINK.value:
return self.handlelinkmsg(message) return self.handlelinkmsg(message)
elif message.message_type == MessageTypes.NODE.value: elif message.message_type == MessageTypes.NODE.value:
return self.handlenodemsg(message) return self.handlenodemsg(message)
# TODO: remove the need for this
def handlenodemsg(self, msg): def handlenodemsg(self, msg):
""" """
Process a Node Message to add/delete or move a node on Process a Node Message to add/delete or move a node on
the SDT display. Node properties are found in session._objs or the SDT display. Node properties are found in session._objs or
self.remotes for remote nodes (or those not yet instantiated). self.remotes for remote nodes (or those not yet instantiated).
:param msg: node message to handle
:return: nothing
""" """
# for distributed sessions to work properly, the SDT option should be # for distributed sessions to work properly, the SDT option should be
# enabled prior to starting the session # enabled prior to starting the session
@ -344,11 +405,15 @@ class Sdt(object):
remote.pos = (x, y, z) remote.pos = (x, y, z)
self.updatenode(nodenum, msg.flags, x, y, z, name, type, icon) self.updatenode(nodenum, msg.flags, x, y, z, name, type, icon)
# TODO: remove the need for this
def handlelinkmsg(self, msg): def handlelinkmsg(self, msg):
""" """
Process a Link Message to add/remove links on the SDT display. Process a Link Message to add/remove links on the SDT display.
Links are recorded in the remotes[nodenum1].links set for updating Links are recorded in the remotes[nodenum1].links set for updating
the SDT display at a later time. the SDT display at a later time.
:param msg: link message to handle
:return: nothing
""" """
if not self.is_enabled(): if not self.is_enabled():
return False return False
@ -370,8 +435,11 @@ class Sdt(object):
def wlancheck(self, nodenum): def wlancheck(self, nodenum):
""" """
Helper returns True if a node number corresponds to a WlanNode Helper returns True if a node number corresponds to a WlanNode or EmaneNode.
or EmaneNode.
:param int nodenum: node id to check
:return: True if node is wlan or emane, False otherwise
:rtype: bool
""" """
if nodenum in self.remotes: if nodenum in self.remotes:
type = self.remotes[nodenum].type type = self.remotes[nodenum].type

View file

@ -1,8 +1,8 @@
""" """
service.py: definition of CoreService class that is subclassed to define Definition of CoreService class that is subclassed to define
startup services and routing for nodes. A service is typically a daemon startup services and routing for nodes. A service is typically a daemon
program launched when a node starts that provides some sort of program launched when a node starts that provides some sort of service.
service. The CoreServices class handles configuration messages for sending The CoreServices class handles configuration messages for sending
a list of available services to the GUI and for configuring individual a list of available services to the GUI and for configuring individual
services. services.
""" """
@ -30,10 +30,19 @@ logger = log.get_logger(__name__)
class ServiceManager(object): class ServiceManager(object):
"""
Manages services available for CORE nodes to use.
"""
services = [] services = []
@classmethod @classmethod
def add(cls, service): def add(cls, service):
"""
Add a service to manager.
:param CoreService service: service to add
:return: nothing
"""
insert = 0 insert = 0
for index, known_service in enumerate(cls.services): for index, known_service in enumerate(cls.services):
if known_service._group == service._group: if known_service._group == service._group:
@ -45,6 +54,13 @@ class ServiceManager(object):
@classmethod @classmethod
def get(cls, name): def get(cls, name):
"""
Retrieve a service from the manager.
:param str name: name of the service to retrieve
:return: service if it exists, None otherwise
:rtype: CoreService
"""
for service in cls.services: for service in cls.services:
if service._name == name: if service._name == name:
return service return service
@ -93,9 +109,13 @@ class CoreServices(ConfigurableManager):
def importcustom(self, path): def importcustom(self, path):
""" """
Import services from a myservices directory. Import services from a myservices directory.
:param str path: path to import custom services from
:return: nothing
""" """
if not path or len(path) == 0: if not path or len(path) == 0:
return return
if not os.path.isdir(path): if not os.path.isdir(path):
logger.warn("invalid custom service directory specified" ": %s" % path) logger.warn("invalid custom service directory specified" ": %s" % path)
return return
@ -124,6 +144,10 @@ class CoreServices(ConfigurableManager):
""" """
Get the list of default services that should be enabled for a Get the list of default services that should be enabled for a
node for the given node type. node for the given node type.
:param service_type: service type to get default services for
:return: default services
:rtype: list
""" """
logger.debug("getting default services for type: %s", service_type) logger.debug("getting default services for type: %s", service_type)
results = [] results = []
@ -138,22 +162,31 @@ class CoreServices(ConfigurableManager):
results.append(service) results.append(service)
return results return results
def getcustomservice(self, objid, service): def getcustomservice(self, object_id, service):
""" """
Get any custom service configured for the given node that Get any custom service configured for the given node that matches the specified service name.
matches the specified service name. If no custom service If no custom service is found, return the specified service.
is found, return the specified service.
:param int object_id: object id to get service from
:param CoreService service: custom service to retrieve
:return: custom service from the node
:rtype: CoreService
""" """
if objid in self.customservices: if object_id in self.customservices:
for s in self.customservices[objid]: for s in self.customservices[object_id]:
if s._name == service._name: if s._name == service._name:
return s return s
return service return service
def setcustomservice(self, objid, service, values): def setcustomservice(self, object_id, service, values):
""" """
Store service customizations in an instantiated service object Store service customizations in an instantiated service object
using a list of values that came from a config message. using a list of values that came from a config message.
:param int object_id: object id to set custom service for
:param class service: service to set
:param list values: values to
:return:
""" """
if service._custom: if service._custom:
s = service s = service
@ -174,19 +207,24 @@ class CoreServices(ConfigurableManager):
if service._custom: if service._custom:
return return
# add the custom service to dict # add the custom service to dict
if objid in self.customservices: if object_id in self.customservices:
self.customservices[objid] += (s,) self.customservices[object_id] += (s,)
else: else:
self.customservices[objid] = (s,) self.customservices[object_id] = (s,)
def addservicestonode(self, node, nodetype, services_str): def addservicestonode(self, node, nodetype, services_str):
""" """
Populate the node.service list using (1) the list of services Populate the node.service list using (1) the list of services
requested from the services TLV, (2) using any custom service requested from the services TLV, (2) using any custom service
configuration, or (3) using the default services for this node type. configuration, or (3) using the default services for this node type.
:param core.coreobj.PyCoreNode node: node to add services to
:param str nodetype: node type to add services to
:param str services_str: string formatted service list
:return: nothing
""" """
if services_str is not None: if services_str is not None:
services = services_str.split('|') services = services_str.split("|")
for name in services: for name in services:
s = ServiceManager.get(name) s = ServiceManager.get(name)
if s is None: if s is None:
@ -202,35 +240,48 @@ class CoreServices(ConfigurableManager):
s = self.getcustomservice(node.objid, s) s = self.getcustomservice(node.objid, s)
node.addservice(s) node.addservice(s)
def getallconfigs(self): def getallconfigs(self, use_clsmap=True):
""" """
Return (nodenum, service) tuples for all stored configs. Return (nodenum, service) tuples for all stored configs. Used when reconnecting to a
Used when reconnecting to a session or opening XML. session or opening XML.
:param bool use_clsmap: should a class map be used, default to True
:return: list of tuples of node ids and services
:rtype: list
""" """
r = [] configs = []
for nodenum in self.customservices: for nodenum in self.customservices:
for s in self.customservices[nodenum]: for service in self.customservices[nodenum]:
r.append((nodenum, s)) configs.append((nodenum, service))
return r return configs
def getallfiles(self, service): def getallfiles(self, service):
""" """
Return all customized files stored with a service. Return all customized files stored with a service.
Used when reconnecting to a session or opening XML. Used when reconnecting to a session or opening XML.
:param CoreService service: service to get files for
:return:
""" """
r = [] files = []
if not service._custom: if not service._custom:
return r return files
for filename in service._configs: for filename in service._configs:
data = self.getservicefiledata(service, filename) data = self.getservicefiledata(service, filename)
if data is None: if data is None:
continue continue
r.append((filename, data)) files.append((filename, data))
return r
return files
def bootnodeservices(self, node): def bootnodeservices(self, node):
""" """
Start all services on a node. Start all services on a node.
:param core.netns.nodes.CoreNode node: node to start services on
:return:
""" """
services = sorted(node.services, key=lambda service: service._startindex) services = sorted(node.services, key=lambda service: service._startindex)
use_startup_service = any(map(self.is_startup_service, services)) use_startup_service = any(map(self.is_startup_service, services))
@ -246,49 +297,66 @@ class CoreServices(ConfigurableManager):
logger.exception("error converting start time to float") logger.exception("error converting start time to float")
self.bootnodeservice(node, s, services, use_startup_service) self.bootnodeservice(node, s, services, use_startup_service)
def bootnodeservice(self, node, s, services, use_startup_service): def bootnodeservice(self, node, service, services, use_startup_service):
""" """
Start a service on a node. Create private dirs, generate config Start a service on a node. Create private dirs, generate config
files, and execute startup commands. files, and execute startup commands.
:param core.netns.nodes.CoreNode node: node to boot services on
:param CoreService service: service to start
:param list services: service list
:param bool use_startup_service: flag to use startup services or not
:return: nothing
""" """
if s._custom: if service._custom:
self.bootnodecustomservice(node, s, services, use_startup_service) self.bootnodecustomservice(node, service, services, use_startup_service)
return return
logger.info("starting service %s (%s)" % (s._name, s._startindex))
for d in s._dirs: logger.info("starting service %s (%s)" % (service._name, service._startindex))
for directory in service._dirs:
try: try:
node.privatedir(d) node.privatedir(directory)
except: except:
logger.exception("Error making node %s dir %s", node.name, d) logger.exception("Error making node %s dir %s", node.name, directory)
for filename in s.getconfigfilenames(node.objid, services):
cfg = s.generateconfig(node, filename, services) for filename in service.getconfigfilenames(node.objid, services):
cfg = service.generateconfig(node, filename, services)
node.nodefile(filename, cfg) node.nodefile(filename, cfg)
if use_startup_service and not self.is_startup_service(s):
if use_startup_service and not self.is_startup_service(service):
return return
for cmd in s.getstartup(node, services):
for cmd in service.getstartup(node, services):
try: try:
# NOTE: this wait=False can be problematic! # NOTE: this wait=False can be problematic!
node.cmd(shlex.split(cmd), wait=False) node.cmd(shlex.split(cmd), wait=False)
except: except:
logger.exception("error starting command %s", cmd) logger.exception("error starting command %s", cmd)
def bootnodecustomservice(self, node, s, services, use_startup_service): def bootnodecustomservice(self, node, service, services, use_startup_service):
""" """
Start a custom service on a node. Create private dirs, use supplied Start a custom service on a node. Create private dirs, use supplied
config files, and execute supplied startup commands. config files, and execute supplied startup commands.
:param core.netns.nodes.CoreNode node: node to boot services on
:param CoreService service: service to start
:param list services: service list
:param bool use_startup_service: flag to use startup services or not
:return: nothing
""" """
logger.info("starting service %s (%s)(custom)" % (s._name, s._startindex)) logger.info("starting service %s (%s)(custom)" % (service._name, service._startindex))
for d in s._dirs: for directory in service._dirs:
try: try:
node.privatedir(d) node.privatedir(directory)
except: except:
logger.exception("Error making node %s dir %s", node.name, d) logger.exception("Error making node %s dir %s", node.name, directory)
for i, filename in enumerate(s._configs):
for i, filename in enumerate(service._configs):
if len(filename) == 0: if len(filename) == 0:
continue continue
cfg = self.getservicefiledata(s, filename) cfg = self.getservicefiledata(service, filename)
if cfg is None: if cfg is None:
cfg = s.generateconfig(node, filename, services) cfg = service.generateconfig(node, filename, services)
# cfg may have a file:/// url for copying from a file # cfg may have a file:/// url for copying from a file
try: try:
if self.copyservicefile(node, filename, cfg): if self.copyservicefile(node, filename, cfg):
@ -298,10 +366,10 @@ class CoreServices(ConfigurableManager):
continue continue
node.nodefile(filename, cfg) node.nodefile(filename, cfg)
if use_startup_service and not self.is_startup_service(s): if use_startup_service and not self.is_startup_service(service):
return return
for cmd in s._startup: for cmd in service._startup:
try: try:
# NOTE: this wait=False can be problematic! # NOTE: this wait=False can be problematic!
node.cmd(shlex.split(cmd), wait=False) node.cmd(shlex.split(cmd), wait=False)
@ -313,6 +381,12 @@ class CoreServices(ConfigurableManager):
Given a configured service filename and config, determine if the Given a configured service filename and config, determine if the
config references an existing file that should be copied. config references an existing file that should be copied.
Returns True for local files, False for generated. Returns True for local files, False for generated.
:param core.netns.nodes.CoreNode node: node to copy service for
:param str filename: file name for a configured service
:param str cfg: configuration string
:return: True if successful, False otherwise
:rtype: bool
""" """
if cfg[:7] == 'file://': if cfg[:7] == 'file://':
src = cfg[7:] src = cfg[7:]
@ -326,6 +400,9 @@ class CoreServices(ConfigurableManager):
def validatenodeservices(self, node): def validatenodeservices(self, node):
""" """
Run validation commands for all services on a node. Run validation commands for all services on a node.
:param core.netns.nodes.CoreNode node: node to validate services for
:return: nothing
""" """
services = sorted(node.services, key=lambda service: service._startindex) services = sorted(node.services, key=lambda service: service._startindex)
for s in services: for s in services:
@ -334,6 +411,12 @@ class CoreServices(ConfigurableManager):
def validatenodeservice(self, node, service, services): def validatenodeservice(self, node, service, services):
""" """
Run the validation command(s) for a service. Run the validation command(s) for a service.
:param core.netns.nodes.CoreNode node: node to validate service for
:param CoreService service: service to validate
:param list services: services for node
:return: service validation status
:rtype: int
""" """
logger.info("validating service for node (%s - %s): %s (%s)", logger.info("validating service for node (%s - %s): %s (%s)",
node.objid, node.name, service._name, service._startindex) node.objid, node.name, service._name, service._startindex)
@ -360,21 +443,29 @@ class CoreServices(ConfigurableManager):
def stopnodeservices(self, node): def stopnodeservices(self, node):
""" """
Stop all services on a node. Stop all services on a node.
:param core.netns.nodes.CoreNode node: node to stop services on
:return: nothing
""" """
services = sorted(node.services, key=lambda service: service._startindex) services = sorted(node.services, key=lambda service: service._startindex)
for s in services: for s in services:
self.stopnodeservice(node, s) self.stopnodeservice(node, s)
def stopnodeservice(self, node, s): def stopnodeservice(self, node, service):
""" """
Stop a service on a node. Stop a service on a node.
:param core.netns.nodes.CoreNode node: node to stop a service on
:param CoreService service: service to stop
:return: status for stopping the services
:rtype: str
""" """
status = "" status = ""
if len(s._shutdown) == 0: if len(service._shutdown) == 0:
# doesn't have a shutdown command # doesn't have a shutdown command
status += "0" status += "0"
else: else:
for cmd in s._shutdown: for cmd in service._shutdown:
try: try:
tmp = node.cmd(shlex.split(cmd), wait=True) tmp = node.cmd(shlex.split(cmd), wait=True)
status += "%s" % tmp status += "%s" % tmp
@ -392,6 +483,7 @@ class CoreServices(ConfigurableManager):
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration :param core.conf.ConfigData config_data: configuration data for carrying out a configuration
:return: response messages :return: response messages
:rtype: ConfigData
""" """
node_id = config_data.node node_id = config_data.node
session_id = config_data.session session_id = config_data.session
@ -525,9 +617,14 @@ class CoreServices(ConfigurableManager):
return None return None
def servicesfromopaque(self, opaque, objid): def servicesfromopaque(self, opaque, object_id):
""" """
Build a list of services from an opaque data string. Build a list of services from an opaque data string.
:param str opaque: opaque data string
:param int object_id: object id
:return: services and unknown services lists tuple
:rtype: tuple
""" """
services = [] services = []
unknown = [] unknown = []
@ -537,7 +634,7 @@ class CoreServices(ConfigurableManager):
servicenames = servicesstring[1].split(',') servicenames = servicesstring[1].split(',')
for name in servicenames: for name in servicenames:
s = ServiceManager.get(name) s = ServiceManager.get(name)
s = self.getcustomservice(objid, s) s = self.getcustomservice(object_id, s)
if s is None: if s is None:
unknown.append(name) unknown.append(name)
else: else:
@ -551,6 +648,10 @@ class CoreServices(ConfigurableManager):
"title1:1-5|title2:6-9|10-12", where title is an optional group title "title1:1-5|title2:6-9|10-12", where title is an optional group title
and i-j is a numeric range of value indices; groups are and i-j is a numeric range of value indices; groups are
separated by commas. separated by commas.
:param list servicelist: service list to build group string from
:return: groups string
:rtype: str
""" """
i = 0 i = 0
r = "" r = ""
@ -573,10 +674,16 @@ class CoreServices(ConfigurableManager):
r += "-%d" % i r += "-%d" % i
return r return r
# TODO: need to remove depenency on old message structure below
def getservicefile(self, services, node, filename): def getservicefile(self, services, node, filename):
""" """
Send a File Message when the GUI has requested a service file. Send a File Message when the GUI has requested a service file.
The file data is either auto-generated or comes from an existing config. The file data is either auto-generated or comes from an existing config.
:param list services: service list
:param core.netns.nodes.CoreNode node: node to get service file from
:param str filename: file name to retrieve
:return: file message for node
""" """
svc = services[0] svc = services[0]
# get the filename and determine the config file index # get the filename and determine the config file index
@ -609,6 +716,10 @@ class CoreServices(ConfigurableManager):
""" """
Get the customized file data associated with a service. Return None Get the customized file data associated with a service. Return None
for invalid filenames or missing file data. for invalid filenames or missing file data.
:param CoreService service: service to get file data from
:param str filename: file name to get data from
:return: file data
""" """
try: try:
i = service._configs.index(filename) i = service._configs.index(filename)
@ -623,6 +734,13 @@ class CoreServices(ConfigurableManager):
Receive a File Message from the GUI and store the customized file Receive a File Message from the GUI and store the customized file
in the service config. The filename must match one from the list of in the service config. The filename must match one from the list of
config files in the service. config files in the service.
:param int nodenum: node id to set service file
:param str type: file type to set
:param str filename: file name to set
:param str srcname: source name of file to set
:param data: data for file to set
:return: nothing
""" """
if len(type.split(':')) < 2: if len(type.split(':')) < 2:
logger.warn("Received file type did not contain service info.") logger.warn("Received file type did not contain service info.")
@ -653,6 +771,9 @@ class CoreServices(ConfigurableManager):
""" """
Handle an Event Message used to start, stop, restart, or validate Handle an Event Message used to start, stop, restart, or validate
a service on a given node. a service on a given node.
:param EventData event_data: event data to handle
:return: nothing
""" """
event_type = event_data.event_type event_type = event_data.event_type
node_id = event_data.node node_id = event_data.node
@ -670,7 +791,7 @@ class CoreServices(ConfigurableManager):
if event_type == EventTypes.STOP.value or event_type == EventTypes.RESTART.value: if event_type == EventTypes.STOP.value or event_type == EventTypes.RESTART.value:
status = self.stopnodeservice(node, s) status = self.stopnodeservice(node, s)
if status != "0": if status != "0":
fail += "Stop %s," % (s._name) fail += "Stop %s," % s._name
if event_type == EventTypes.START.value or event_type == EventTypes.RESTART.value: if event_type == EventTypes.START.value or event_type == EventTypes.RESTART.value:
if s._custom: if s._custom:
cmds = s._startup cmds = s._startup
@ -779,6 +900,11 @@ class CoreService(object):
Return the tuple of configuration file filenames. This default method Return the tuple of configuration file filenames. This default method
returns the cls._configs tuple, but this method may be overriden to returns the cls._configs tuple, but this method may be overriden to
provide node-specific filenames that may be based on other services. provide node-specific filenames that may be based on other services.
:param int nodenum: node id to get config file names for
:param list services: node services
:return: class configuration files
:rtype: tuple
""" """
return cls._configs return cls._configs
@ -790,6 +916,11 @@ class CoreService(object):
provided to allow interdependencies (e.g. zebra and OSPF). provided to allow interdependencies (e.g. zebra and OSPF).
Return the configuration string to be written to a file or sent Return the configuration string to be written to a file or sent
to the GUI for customization. to the GUI for customization.
:param core.netns.nodes.CoreNode node: node to generate config for
:param str filename: file name to generate config for
:param list services: services for node
:return: nothing
""" """
raise NotImplementedError raise NotImplementedError
@ -798,8 +929,13 @@ class CoreService(object):
""" """
Return the tuple of startup commands. This default method Return the tuple of startup commands. This default method
returns the cls._startup tuple, but this method may be returns the cls._startup tuple, but this method may be
overriden to provide node-specific commands that may be overridden to provide node-specific commands that may be
based on other services. based on other services.
:param core.netns.nodes.CoreNode node: node to get startup for
:param list services: services for node
:return: startup commands
:rtype: tuple
""" """
return cls._startup return cls._startup
@ -810,6 +946,11 @@ class CoreService(object):
returns the cls._validate tuple, but this method may be returns the cls._validate tuple, but this method may be
overriden to provide node-specific commands that may be overriden to provide node-specific commands that may be
based on other services. based on other services.
:param core.netns.nodes.CoreNode node: node to validate
:param list services: services for node
:return: validation commands
:rtype: tuple
""" """
return cls._validate return cls._validate
@ -818,6 +959,11 @@ class CoreService(object):
""" """
Convert service properties into a string list of key=value pairs, Convert service properties into a string list of key=value pairs,
separated by "|". separated by "|".
:param core.netns.nodes.CoreNode node: node to get value list for
:param list services: services for node
:return: value list string
:rtype: str
""" """
valmap = [cls._dirs, cls._configs, cls._startindex, cls._startup, valmap = [cls._dirs, cls._configs, cls._startindex, cls._startup,
cls._shutdown, cls._validate, cls._meta, cls._starttime] cls._shutdown, cls._validate, cls._meta, cls._starttime]
@ -834,6 +980,9 @@ class CoreService(object):
""" """
Convert list of values into properties for this instantiated Convert list of values into properties for this instantiated
(customized) service. (customized) service.
:param list values: value list to set properties from
:return: nothing
""" """
# TODO: support empty value? e.g. override default meta with '' # TODO: support empty value? e.g. override default meta with ''
for key in self.keys: for key in self.keys:
@ -844,6 +993,13 @@ class CoreService(object):
logger.exception("error indexing into key") logger.exception("error indexing into key")
def setvalue(self, key, value): def setvalue(self, key, value):
"""
Set values for this service.
:param str key: key to set value for
:param value: value of key to set
:return: nothing
"""
if key not in self.keys: if key not in self.keys:
raise ValueError raise ValueError
# this handles data conversion to int, string, and tuples # this handles data conversion to int, string, and tuples

View file

@ -53,11 +53,29 @@ logger = log.get_logger(__name__)
class HookManager(object): class HookManager(object):
"""
Manages hooks that can be ran as part of state changes.
"""
def __init__(self, hook_dir): def __init__(self, hook_dir):
"""
Creates a HookManager instance.
:param str hook_dir: the hook directory
"""
self.hook_dir = hook_dir self.hook_dir = hook_dir
self.hooks = {} self.hooks = {}
def add(self, hook_type, file_name, source_name, data): def add(self, hook_type, file_name, source_name, data):
"""
Adds a hook to the manager.
:param str hook_type: hook type
:param str file_name: file name for hook
:param str source_name: source name for hook
:param data: data for hook
:return: nothing
"""
logger.info("setting state hook: %s - %s from %s", hook_type, file_name, source_name) logger.info("setting state hook: %s - %s from %s", hook_type, file_name, source_name)
hook_id, state = hook_type.split(":")[:2] hook_id, state = hook_type.split(":")[:2]
@ -73,9 +91,21 @@ class HookManager(object):
state_hooks.append(hook) state_hooks.append(hook)
def clear(self): def clear(self):
"""
Clear all known hooks.
:return: nothing
"""
self.hooks.clear() self.hooks.clear()
def state_change(self, state, environment): def state_change(self, state, environment):
"""
Mark a state change to notify related hooks to run with the provided environment.
:param int state: the new state after a change
:param dict environment: environment to run hook in
:return: nothing
"""
# retrieve all state hooks # retrieve all state hooks
hooks = self.hooks.get(state, []) hooks = self.hooks.get(state, [])
@ -87,6 +117,13 @@ class HookManager(object):
self.run(hook, environment) self.run(hook, environment)
def run(self, hook, environment): def run(self, hook, environment):
"""
Run a hook, with a provided environment.
:param tuple hook: hook to run
:param dict environment: environment to run hook with
:return:
"""
file_name, data = hook file_name, data = hook
logger.info("running hook: %s", file_name) logger.info("running hook: %s", file_name)
hook_file_name = os.path.join(self.hook_dir, file_name) hook_file_name = os.path.join(self.hook_dir, file_name)
@ -119,17 +156,32 @@ class HookManager(object):
class SessionManager(object): class SessionManager(object):
"""
Manages currently known sessions.
"""
sessions = set() sessions = set()
session_lock = threading.Lock() session_lock = threading.Lock()
@classmethod @classmethod
def add(cls, session): def add(cls, session):
"""
Add a session to the manager.
:param Session session: session to add
:return: nothing
"""
with cls.session_lock: with cls.session_lock:
logger.info("adding session to manager: %s", session.session_id) logger.info("adding session to manager: %s", session.session_id)
cls.sessions.add(session) cls.sessions.add(session)
@classmethod @classmethod
def remove(cls, session): def remove(cls, session):
"""
Remove session from the manager.
:param Session session: session to remove
:return: nothing
"""
with cls.session_lock: with cls.session_lock:
logger.info("removing session from manager: %s", session.session_id) logger.info("removing session from manager: %s", session.session_id)
if session in cls.sessions: if session in cls.sessions:
@ -139,6 +191,11 @@ class SessionManager(object):
@classmethod @classmethod
def on_exit(cls): def on_exit(cls):
"""
Method used to shutdown all currently known sessions, in case of unexpected exit.
:return: nothing
"""
logger.info("caught program exit, shutting down all known sessions") logger.info("caught program exit, shutting down all known sessions")
while cls.sessions: while cls.sessions:
with cls.session_lock: with cls.session_lock:
@ -154,6 +211,15 @@ class Session(object):
""" """
def __init__(self, session_id, config=None, server=None, persistent=False, mkdir=True): def __init__(self, session_id, config=None, server=None, persistent=False, mkdir=True):
"""
Create a Session instance.
:param int session_id: session id
:param dict config: session configuration
:param core.coreserver.CoreServer server: core server object
:param bool persistent: flag is session is considered persistent
:param bool mkdir: flag to determine if a directory should be made
"""
self.session_id = session_id self.session_id = session_id
# dict of configuration items from /etc/core/core.conf config file # dict of configuration items from /etc/core/core.conf config file
@ -346,7 +412,7 @@ class Session(object):
:param EventTypes state: state to set to :param EventTypes state: state to set to
:param send_event: if true, generate core API event messages :param send_event: if true, generate core API event messages
:return: :return: nothing
""" """
state_name = coreapi.state_name(state) state_name = coreapi.state_name(state)
@ -389,6 +455,9 @@ class Session(object):
def write_state(self, state): def write_state(self, state):
""" """
Write the current state to a state file in the session dir. Write the current state to a state file in the session dir.
:param int state: state to write to file
:return: nothing
""" """
try: try:
state_file = open(self._state_file, "w") state_file = open(self._state_file, "w")
@ -401,7 +470,7 @@ class Session(object):
""" """
Run hook scripts upon changing states. If hooks is not specified, run all hooks in the given state. Run hook scripts upon changing states. If hooks is not specified, run all hooks in the given state.
:param state: state to run hooks for :param int state: state to run hooks for
:return: nothing :return: nothing
""" """
@ -421,6 +490,12 @@ class Session(object):
def set_hook(self, hook_type, file_name, source_name, data): def set_hook(self, hook_type, file_name, source_name, data):
""" """
Store a hook from a received file message. Store a hook from a received file message.
:param str hook_type: hook type
:param str file_name: file name for hook
:param str source_name: source name
:param data: hook data
:return: nothing
""" """
logger.info("setting state hook: %s - %s from %s", hook_type, file_name, source_name) logger.info("setting state hook: %s - %s from %s", hook_type, file_name, source_name)
@ -449,6 +524,12 @@ class Session(object):
self._hooks.clear() self._hooks.clear()
def run_hook(self, hook): def run_hook(self, hook):
"""
Run a hook.
:param tuple hook: hook to run
:return: nothing
"""
file_name, data = hook file_name, data = hook
logger.info("running hook %s", file_name) logger.info("running hook %s", file_name)
@ -478,6 +559,12 @@ class Session(object):
logger.exception("error running hook '%s'", file_name) logger.exception("error running hook '%s'", file_name)
def run_state_hooks(self, state): def run_state_hooks(self, state):
"""
Run state hooks.
:param int state: state to run hooks for
:return: nothing
"""
for hook in self._state_hooks.get(state, []): for hook in self._state_hooks.get(state, []):
try: try:
hook(state) hook(state)
@ -492,6 +579,13 @@ class Session(object):
) )
def add_state_hook(self, state, hook): def add_state_hook(self, state, hook):
"""
Add a state hook.
:param int state: state to add hook for
:param func hook: hook callback for the state
:return: nothing
"""
hooks = self._state_hooks.setdefault(state, []) hooks = self._state_hooks.setdefault(state, [])
assert hook not in hooks assert hook not in hooks
hooks.append(hook) hooks.append(hook)
@ -500,10 +594,23 @@ class Session(object):
hook(state) hook(state)
def del_state_hook(self, state, hook): def del_state_hook(self, state, hook):
"""
Delete a state hook.
:param int state: state to delete hook for
:param func hook: hook to delete
:return:
"""
hooks = self._state_hooks.setdefault(state, []) hooks = self._state_hooks.setdefault(state, [])
hooks.remove(hook) hooks.remove(hook)
def runtime_state_hook(self, state): def runtime_state_hook(self, state):
"""
Runtime state hook check.
:param int state: state to check
:return: nothing
"""
if state == EventTypes.RUNTIME_STATE.value: if state == EventTypes.RUNTIME_STATE.value:
self.emane.poststartup() self.emane.poststartup()
xml_file_version = self.get_config_item("xmlfilever") xml_file_version = self.get_config_item("xmlfilever")
@ -516,6 +623,9 @@ class Session(object):
Get an environment suitable for a subprocess.Popen call. Get an environment suitable for a subprocess.Popen call.
This is the current process environment with some session-specific This is the current process environment with some session-specific
variables. variables.
:param bool state: flag to determine if session state should be included
:return:
""" """
env = os.environ.copy() env = os.environ.copy()
env["SESSION"] = "%s" % self.session_id env["SESSION"] = "%s" % self.session_id
@ -550,6 +660,9 @@ class Session(object):
def set_thumbnail(self, thumb_file): def set_thumbnail(self, thumb_file):
""" """
Set the thumbnail filename. Move files from /tmp to session dir. Set the thumbnail filename. Move files from /tmp to session dir.
:param str thumb_file: tumbnail file to set for session
:return: nothing
""" """
if not os.path.exists(thumb_file): if not os.path.exists(thumb_file):
logger.error("thumbnail file to set does not exist: %s", thumb_file) logger.error("thumbnail file to set does not exist: %s", thumb_file)
@ -565,7 +678,7 @@ class Session(object):
Set the username for this session. Update the permissions of the Set the username for this session. Update the permissions of the
session dir to allow the user write access. session dir to allow the user write access.
:param user: user to give write permissions to for the session directory :param str user: user to give write permissions to for the session directory
:return: nothing :return: nothing
""" """
if user: if user:
@ -595,6 +708,11 @@ class Session(object):
def add_object(self, cls, *clsargs, **clskwds): def add_object(self, cls, *clsargs, **clskwds):
""" """
Add an emulation object. Add an emulation object.
:param class cls: object class to add
:param list clsargs: list of arguments for the class to create
:param dict clskwds: dictionary of arguments for the class to create
:return: the created class instance
""" """
obj = cls(self, *clsargs, **clskwds) obj = cls(self, *clsargs, **clskwds)
@ -611,6 +729,9 @@ class Session(object):
def get_object(self, object_id): def get_object(self, object_id):
""" """
Get an emulation object. Get an emulation object.
:param int object_id: object id to retrieve
:return: object for the given id
""" """
if object_id not in self.objects: if object_id not in self.objects:
raise KeyError("unknown object id %s" % object_id) raise KeyError("unknown object id %s" % object_id)
@ -619,6 +740,9 @@ class Session(object):
def get_object_by_name(self, name): def get_object_by_name(self, name):
""" """
Get an emulation object using its name attribute. Get an emulation object using its name attribute.
:param str name: name of object to retrieve
:return: object for the name given
""" """
with self._objects_lock: with self._objects_lock:
for obj in self.objects.itervalues(): for obj in self.objects.itervalues():
@ -630,7 +754,7 @@ class Session(object):
""" """
Remove an emulation object. Remove an emulation object.
:param object_id: object id to remove :param int object_id: object id to remove
:return: nothing :return: nothing
""" """
with self._objects_lock: with self._objects_lock:
@ -642,7 +766,7 @@ class Session(object):
def delete_objects(self): def delete_objects(self):
""" """
Clear the _objs dictionary, and call each obj.shutdown() routine. Clear the objects dictionary, and call shutdown for each object.
""" """
with self._objects_lock: with self._objects_lock:
while self.objects: while self.objects:
@ -652,8 +776,7 @@ class Session(object):
def write_objects(self): def write_objects(self):
""" """
Write objects to a 'nodes' file in the session dir. Write objects to a 'nodes' file in the session dir.
The 'nodes' file lists: The 'nodes' file lists: number, name, api-type, class-type
number, name, api-type, class-type
""" """
try: try:
nodes_file = open(os.path.join(self.session_dir, "nodes"), "w") nodes_file = open(os.path.join(self.session_dir, "nodes"), "w")
@ -670,6 +793,11 @@ class Session(object):
Objects can register configuration objects that are included in Objects can register configuration objects that are included in
the Register Message and may be configured via the Configure the Register Message and may be configured via the Configure
Message. The callback is invoked when receiving a Configure Message. Message. The callback is invoked when receiving a Configure Message.
:param str name: name of configuration object to add
:param int object_type: register tlv type
:param func callback: callback function for object
:return: nothing
""" """
register_tlv = RegisterTlvs(object_type) register_tlv = RegisterTlvs(object_type)
logger.info("adding config object callback: %s - %s", name, register_tlv) logger.info("adding config object callback: %s - %s", name, register_tlv)
@ -678,8 +806,12 @@ class Session(object):
def config_object(self, config_data): def config_object(self, config_data):
""" """
Invoke the callback for an object upon receipt of a Configure Invoke the callback for an object upon receipt of configuration data for that object.
Message for that object. A no-op if the object doesn't exist. A no-op if the object doesn't exist.
:param core.data.ConfigData config_data: configuration data to execute against
:return: responses to the configuration data
:rtype: list
""" """
name = config_data.object name = config_data.object
logger.info("session(%s): handling config message(%s): \n%s", logger.info("session(%s): handling config message(%s): \n%s",
@ -715,7 +847,7 @@ class Session(object):
def dump_session(self): def dump_session(self):
""" """
Debug print this session. Log information about the session in its current state.
""" """
logger.info("session id=%s name=%s state=%s", self.session_id, self.name, self.state) logger.info("session id=%s name=%s state=%s", self.session_id, self.name, self.state)
logger.info("file=%s thumbnail=%s node_count=%s/%s", logger.info("file=%s thumbnail=%s node_count=%s/%s",
@ -723,7 +855,13 @@ class Session(object):
def exception(self, level, source, object_id, text): def exception(self, level, source, object_id, text):
""" """
Generate an Exception Message Generate and broadcast an exception event.
:param str level: exception level
:param str source: source name
:param int object_id: object id
:param str text: exception message
:return: nothing
""" """
exception_data = ExceptionData( exception_data = ExceptionData(
@ -751,6 +889,11 @@ class Session(object):
""" """
Return a boolean entry from the configuration dictionary, may Return a boolean entry from the configuration dictionary, may
return None if undefined. return None if undefined.
:param str name: configuration item name
:param default: default value to return if not found
:return: boolean value of the configuration item
:rtype: bool
""" """
item = self.get_config_item(name) item = self.get_config_item(name)
if item is None: if item is None:
@ -761,6 +904,11 @@ class Session(object):
""" """
Return an integer entry from the configuration dictionary, may Return an integer entry from the configuration dictionary, may
return None if undefined. return None if undefined.
:param str name: configuration item name
:param default: default value to return if not found
:return: integer value of the configuration item
:rtype: int
""" """
item = self.get_config_item(name) item = self.get_config_item(name)
if item is None: if item is None:
@ -856,6 +1004,7 @@ class Session(object):
# stop node services # stop node services
with self._objects_lock: with self._objects_lock:
for obj in self.objects.itervalues(): for obj in self.objects.itervalues():
# TODO: determine if checking for CoreNode alone is ok
if isinstance(obj, nodes.PyCoreNode): if isinstance(obj, nodes.PyCoreNode):
self.services.stopnodeservices(obj) self.services.stopnodeservices(obj)
@ -901,6 +1050,7 @@ class Session(object):
""" """
with self._objects_lock: with self._objects_lock:
for obj in self.objects.itervalues(): for obj in self.objects.itervalues():
# TODO: determine instance type we need to check, due to method issue below
if isinstance(obj, nodes.PyCoreNode) and not nodeutils.is_node(obj, NodeTypes.RJ45): if isinstance(obj, nodes.PyCoreNode) and not nodeutils.is_node(obj, NodeTypes.RJ45):
# add a control interface if configured # add a control interface if configured
self.add_remove_control_interface(node=obj, remove=False) self.add_remove_control_interface(node=obj, remove=False)
@ -909,9 +1059,14 @@ class Session(object):
self.update_control_interface_hosts() self.update_control_interface_hosts()
def validate_nodes(self): def validate_nodes(self):
"""
Validate all nodes that are known by the session.
:return: nothing
"""
with self._objects_lock: with self._objects_lock:
for obj in self.objects.itervalues(): for obj in self.objects.itervalues():
# TODO: this can be extended to validate everything # TODO: this can be extended to validate everything, bad node check here as well
# such as vnoded process, bridges, etc. # such as vnoded process, bridges, etc.
if not isinstance(obj, nodes.PyCoreNode): if not isinstance(obj, nodes.PyCoreNode):
continue continue
@ -922,6 +1077,12 @@ class Session(object):
obj.validate() obj.validate()
def get_control_lnet_prefixes(self): def get_control_lnet_prefixes(self):
"""
Retrieve control net prefixes.
:return: control net prefix list
:rtype: list
"""
p = getattr(self.options, "controlnet", self.config.get("controlnet")) p = getattr(self.options, "controlnet", self.config.get("controlnet"))
p0 = getattr(self.options, "controlnet0", self.config.get("controlnet0")) p0 = getattr(self.options, "controlnet0", self.config.get("controlnet0"))
p1 = getattr(self.options, "controlnet1", self.config.get("controlnet1")) p1 = getattr(self.options, "controlnet1", self.config.get("controlnet1"))
@ -934,6 +1095,12 @@ class Session(object):
return [p0, p1, p2, p3] return [p0, p1, p2, p3]
def get_control_net_server_interfaces(self): def get_control_net_server_interfaces(self):
"""
Retrieve control net server interfaces.
:return: list of control net server interfaces
:rtype: list
"""
d0 = self.config.get("controlnetif0") d0 = self.config.get("controlnetif0")
if d0: if d0:
logger.error("controlnet0 cannot be assigned with a host interface") logger.error("controlnet0 cannot be assigned with a host interface")
@ -943,6 +1110,13 @@ class Session(object):
return [None, d1, d2, d3] return [None, d1, d2, d3]
def get_control_net_index(self, dev): def get_control_net_index(self, dev):
"""
Retrieve control net index.
:param str dev: device to get control net index for
:return: control net index, -1 otherwise
:rtype: int
"""
if dev[0:4] == "ctrl" and int(dev[4]) in [0, 1, 2, 3]: if dev[0:4] == "ctrl" and int(dev[4]) in [0, 1, 2, 3]:
index = int(dev[4]) index = int(dev[4])
if index == 0: if index == 0:
@ -952,6 +1126,7 @@ class Session(object):
return -1 return -1
def get_control_net_object(self, net_index): def get_control_net_object(self, net_index):
# TODO: all nodes use an integer id and now this wants to use a string =(
object_id = "ctrl%dnet" % net_index object_id = "ctrl%dnet" % net_index
return self.get_object(object_id) return self.get_object(object_id)
@ -961,12 +1136,19 @@ class Session(object):
When the remove flag is True, remove the bridge that connects control When the remove flag is True, remove the bridge that connects control
interfaces. The conf_reqd flag, when False, causes a control network interfaces. The conf_reqd flag, when False, causes a control network
bridge to be added even if one has not been configured. bridge to be added even if one has not been configured.
:param int net_index: network index
:param bool remove: flag to check if it should be removed
:param bool conf_required: flag to check if conf is required
:return: control net object
:rtype: core.netns.nodes.CtrlNet
""" """
prefix_spec_list = self.get_control_lnet_prefixes() prefix_spec_list = self.get_control_lnet_prefixes()
prefix_spec = prefix_spec_list[net_index] prefix_spec = prefix_spec_list[net_index]
if not prefix_spec: if not prefix_spec:
if conf_required: if conf_required:
return None # no controlnet needed # no controlnet needed
return None
else: else:
control_net_class = nodeutils.get_node_class(NodeTypes.CONTROL_NET) control_net_class = nodeutils.get_node_class(NodeTypes.CONTROL_NET)
prefix_spec = control_net_class.DEFAULT_PREFIX_LIST[net_index] prefix_spec = control_net_class.DEFAULT_PREFIX_LIST[net_index]
@ -1041,7 +1223,8 @@ class Session(object):
prefix = prefixes[0].split(':', 1)[1] prefix = prefixes[0].split(':', 1)[1]
except IndexError: except IndexError:
prefix = prefixes[0] prefix = prefixes[0]
else: # len(prefixes) == 1 # len(prefixes) == 1
else:
# TODO: can we get the server name from the servers.conf or from the node assignments? # TODO: can we get the server name from the servers.conf or from the node assignments?
# with one prefix, only master gets a ctrlnet address # with one prefix, only master gets a ctrlnet address
assign_address = self.master assign_address = self.master
@ -1053,6 +1236,7 @@ class Session(object):
updown_script=updown_script, serverintf=server_interface) updown_script=updown_script, serverintf=server_interface)
# tunnels between controlnets will be built with Broker.addnettunnels() # tunnels between controlnets will be built with Broker.addnettunnels()
# TODO: potentialy remove documentation saying object ids are ints
self.broker.addnet(object_id) self.broker.addnet(object_id)
for server in self.broker.getservers(): for server in self.broker.getservers():
self.broker.addnodemap(server, object_id) self.broker.addnodemap(server, object_id)
@ -1066,6 +1250,12 @@ class Session(object):
addremovectrlnet() to build or remove the control bridge. addremovectrlnet() to build or remove the control bridge.
If conf_reqd is False, the control network may be built even If conf_reqd is False, the control network may be built even
when the user has not configured one (e.g. for EMANE.) when the user has not configured one (e.g. for EMANE.)
:param core.netns.nodes.CoreNode node: node to add or remove control interface
:param int net_index: network index
:param bool remove: flag to check if it should be removed
:param bool conf_required: flag to check if conf is required
:return: nothing
""" """
control_net = self.add_remove_control_net(net_index, remove, conf_required) control_net = self.add_remove_control_net(net_index, remove, conf_required)
if not control_net: if not control_net:
@ -1074,8 +1264,9 @@ class Session(object):
if not node: if not node:
return return
# ctrl# already exists
if node.netif(control_net.CTRLIF_IDX_BASE + net_index): if node.netif(control_net.CTRLIF_IDX_BASE + net_index):
return # ctrl# already exists return
control_ip = node.objid control_ip = node.objid
@ -1097,6 +1288,10 @@ class Session(object):
def update_control_interface_hosts(self, net_index=0, remove=False): def update_control_interface_hosts(self, net_index=0, remove=False):
""" """
Add the IP addresses of control interfaces to the /etc/hosts file. Add the IP addresses of control interfaces to the /etc/hosts file.
:param int net_index: network index to update
:param bool remove: flag to check if it should be removed
:return: nothing
""" """
if not self.get_config_item_bool("update_etc_hosts", False): if not self.get_config_item_bool("update_etc_hosts", False):
return return
@ -1137,6 +1332,12 @@ class Session(object):
""" """
Add an event to the event queue, with a start time relative to the Add an event to the event queue, with a start time relative to the
start of the runtime state. start of the runtime state.
:param event_time: event time
:param core.netns.nodes.CoreNode node: node to add event for
:param str name: name of event
:param data: data for event
:return: nothing
""" """
event_time = float(event_time) event_time = float(event_time)
current_time = self.runtime() current_time = self.runtime()
@ -1156,6 +1357,11 @@ class Session(object):
def run_event(self, node_id=None, name=None, data=None): def run_event(self, node_id=None, name=None, data=None):
""" """
Run a scheduled event, executing commands in the data string. Run a scheduled event, executing commands in the data string.
:param int node_id: node id to run event
:param str name: event name
:param data: event data
:return: nothing
""" """
now = self.runtime() now = self.runtime()
if not name: if not name:
@ -1266,6 +1472,9 @@ class Session(object):
class SessionConfig(ConfigurableManager, Configurable): class SessionConfig(ConfigurableManager, Configurable):
"""
Session configuration object.
"""
name = "session" name = "session"
config_type = RegisterTlvs.UTILITY.value config_type = RegisterTlvs.UTILITY.value
config_matrix = [ config_matrix = [
@ -1291,6 +1500,11 @@ class SessionConfig(ConfigurableManager, Configurable):
self.reset() self.reset()
def reset(self): def reset(self):
"""
Reset the session configuration.
:return: nothing
"""
defaults = self.getdefaultvalues() defaults = self.getdefaultvalues()
for key in self.getnames(): for key in self.getnames():
# value may come from config file # value may come from config file
@ -1302,6 +1516,8 @@ class SessionConfig(ConfigurableManager, Configurable):
def configure_values(self, config_data): def configure_values(self, config_data):
""" """
Handle configuration values.
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration :param core.conf.ConfigData config_data: configuration data for carrying out a configuration
:return: None :return: None
""" """
@ -1309,6 +1525,7 @@ class SessionConfig(ConfigurableManager, Configurable):
def configure_request(self, config_data, type_flags=ConfigFlags.NONE.value): def configure_request(self, config_data, type_flags=ConfigFlags.NONE.value):
""" """
Handle a configuration request.
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration :param core.conf.ConfigData config_data: configuration data for carrying out a configuration
:param type_flags: :param type_flags:
@ -1325,11 +1542,15 @@ class SessionConfig(ConfigurableManager, Configurable):
return self.config_data(0, node_id, type_flags, values) return self.config_data(0, node_id, type_flags, values)
# TODO: update logic to not be tied to old style messages
def handle_distributed(self, message): def handle_distributed(self, message):
""" """
Handle the session options config message as it has reached the Handle the session options config message as it has reached the
broker. Options requiring modification for distributed operation should broker. Options requiring modification for distributed operation should
be handled here. be handled here.
:param message: message to handle
:return: nothing
""" """
if not self.session.master: if not self.session.master:
return return
@ -1350,11 +1571,17 @@ class SessionConfig(ConfigurableManager, Configurable):
if key == "controlnet": if key == "controlnet":
self.handle_distributed_control_net(message, value_strings, value_strings.index(value_string)) self.handle_distributed_control_net(message, value_strings, value_strings.index(value_string))
# TODO: update logic to not be tied to old style messages
def handle_distributed_control_net(self, message, values, index): def handle_distributed_control_net(self, message, values, index):
""" """
Modify Config Message if multiple control network prefixes are Modify Config Message if multiple control network prefixes are
defined. Map server names to prefixes and repack the message before defined. Map server names to prefixes and repack the message before
it is forwarded to slave servers. it is forwarded to slave servers.
:param message: message to handle
:param list values: values to handle
:param int index: index ti get key value from
:return: nothing
""" """
key_value = values[index] key_value = values[index]
key, value = key_value.split('=', 1) key, value = key_value.split('=', 1)
@ -1391,8 +1618,10 @@ class SessionMetaData(ConfigurableManager):
def configure_values(self, config_data): def configure_values(self, config_data):
""" """
Handle configuration values.
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration :param core.conf.ConfigData config_data: configuration data for carrying out a configuration
:return: :return: None
""" """
values = config_data.data_values values = config_data.data_values
if values is None: if values is None:
@ -1411,17 +1640,28 @@ class SessionMetaData(ConfigurableManager):
def configure_request(self, config_data, type_flags=ConfigFlags.NONE.value): def configure_request(self, config_data, type_flags=ConfigFlags.NONE.value):
""" """
Handle a configuration request.
:param core.conf.ConfigData config_data: configuration data for carrying out a configuration :param core.conf.ConfigData config_data: configuration data for carrying out a configuration
:param type_flags: :param int type_flags: configuration request flag value
:return: :return: configuration data
:rtype: ConfigData
""" """
node_number = config_data.node node_number = config_data.node
values_str = "|".join(map(lambda item: "%s=%s" % item, self.items())) values_str = "|".join(map(lambda item: "%s=%s" % item, self.items()))
return self.config_data(0, node_number, type_flags, values_str) return self.config_data(0, node_number, type_flags, values_str)
def config_data(self, flags, node_id, type_flags, values_str): def config_data(self, flags, node_id, type_flags, values_str):
"""
Retrieve configuration data object, leveraging provided data.
:param flags: configuration data flags
:param int node_id: node id
:param type_flags: type flags
:param values_str: values string
:return: configuration data
:rtype: ConfigData
"""
data_types = tuple(map(lambda (k, v): ConfigDataTypes.STRING.value, self.items())) data_types = tuple(map(lambda (k, v): ConfigDataTypes.STRING.value, self.items()))
return ConfigData( return ConfigData(
@ -1434,9 +1674,22 @@ class SessionMetaData(ConfigurableManager):
) )
def add_item(self, key, value): def add_item(self, key, value):
"""
Add configuration key/value pair.
:param key: configuration key
:param value: configuration value
:return: nothing
"""
self.configs[key] = value self.configs[key] = value
def get_item(self, key): def get_item(self, key):
"""
Retrieve configuration value.
:param key: key for configuration value to retrieve
:return: configuration value
"""
try: try:
return self.configs[key] return self.configs[key]
except KeyError: except KeyError:
@ -1445,7 +1698,13 @@ class SessionMetaData(ConfigurableManager):
return None return None
def items(self): def items(self):
"""
Retrieve configuration items.
:return: configuration items iterator
"""
return self.configs.iteritems() return self.configs.iteritems()
# configure the program exit function to run
atexit.register(SessionManager.on_exit) atexit.register(SessionManager.on_exit)