updates to layout core module packages in a more logical way, including renaming methods from objects to nodes and nodes.objid to nodes.id

This commit is contained in:
bharnden 2019-04-29 23:31:47 -07:00
parent 9517740704
commit 66e603906a
100 changed files with 10283 additions and 3489 deletions

View file

1158
daemon/core/nodes/base.py Normal file

File diff suppressed because it is too large Load diff

293
daemon/core/nodes/client.py Normal file
View file

@ -0,0 +1,293 @@
"""
client.py: implementation of the VnodeClient class for issuing commands
over a control channel to the vnoded process running in a network namespace.
The control channel can be accessed via calls to the vcmd Python module or
by invoking the vcmd shell command.
"""
import logging
import os
import vcmd
from core import CoreCommandError, utils
from core import constants
class VnodeClient(object):
"""
Provides client functionality for interacting with a virtual node.
"""
def __init__(self, name, ctrlchnlname):
"""
Create a VnodeClient instance.
:param str name: name for client
:param str ctrlchnlname: control channel name
"""
self.name = name
self.ctrlchnlname = ctrlchnlname
self.cmdchnl = vcmd.VCmd(self.ctrlchnlname)
self._addr = {}
def _verify_connection(self):
"""
Checks that the vcmd client is properly connected.
:return: nothing
:raises IOError: when not connected
"""
if not self.connected():
raise IOError("vcmd not connected")
def connected(self):
"""
Check if node is connected or not.
:return: True if connected, False otherwise
:rtype: bool
"""
return self.cmdchnl.connected()
def close(self):
"""
Close the client connection.
:return: nothing
"""
self.cmdchnl.close()
def cmd(self, args, wait=True):
"""
Execute a command on a node and return the status (return code).
:param list[str]|str args: command arguments
:param bool wait: wait for command to end or not
:return: command status
:rtype: int
"""
self._verify_connection()
args = utils.split_args(args)
# run command, return process when not waiting
p = self.cmdchnl.qcmd(args)
if not wait:
return 0
# wait for and return exit status
return p.wait()
def cmd_output(self, args):
"""
Execute a command on a node and return a tuple containing the
exit status and result string. stderr output
is folded into the stdout result string.
:param list[str]|str args: command to run
:return: command status and combined stdout and stderr output
:rtype: tuple[int, str]
"""
p, stdin, stdout, stderr = self.popen(args)
stdin.close()
output = stdout.read() + stderr.read()
stdout.close()
stderr.close()
status = p.wait()
return status, output.strip()
def check_cmd(self, args):
"""
Run command and return exit status and combined stdout and stderr.
:param list[str]|str args: command to run
:return: combined stdout and stderr
:rtype: str
:raises core.CoreCommandError: when there is a non-zero exit status
"""
status, output = self.cmd_output(args)
if status != 0:
raise CoreCommandError(status, args, output)
return output.strip()
def popen(self, args):
"""
Execute a popen command against the node.
:param list[str]|str args: command arguments
:return: popen object, stdin, stdout, and stderr
:rtype: tuple
"""
self._verify_connection()
args = utils.split_args(args)
return self.cmdchnl.popen(args)
def icmd(self, args):
"""
Execute an icmd against a node.
:param list[str]|str args: command arguments
:return: command result
:rtype: int
"""
args = utils.split_args(args)
return os.spawnlp(os.P_WAIT, constants.VCMD_BIN, constants.VCMD_BIN, "-c", self.ctrlchnlname, "--", *args)
def redircmd(self, infd, outfd, errfd, args, wait=True):
"""
Execute a command on a node with standard input, output, and
error redirected according to the given file descriptors.
:param infd: stdin file descriptor
:param outfd: stdout file descriptor
:param errfd: stderr file descriptor
:param list[str]|str args: command arguments
:param bool wait: wait flag
:return: command status
:rtype: int
"""
self._verify_connection()
# run command, return process when not waiting
args = utils.split_args(args)
p = self.cmdchnl.redircmd(infd, outfd, errfd, args)
if not wait:
return p
# wait for and return exit status
status = p.wait()
if status:
logging.warn("cmd exited with status %s: %s", status, args)
return status
def term(self, sh="/bin/sh"):
"""
Open a terminal on a node.
:param str sh: shell to open terminal with
:return: terminal command result
:rtype: int
"""
args = ("xterm", "-ut", "-title", self.name, "-e", constants.VCMD_BIN, "-c", self.ctrlchnlname, "--", sh)
if "SUDO_USER" in os.environ:
args = ("su", "-s", "/bin/sh", "-c",
"exec " + " ".join(map(lambda x: "'%s'" % x, args)),
os.environ["SUDO_USER"])
return os.spawnvp(os.P_NOWAIT, args[0], args)
def termcmdstring(self, sh="/bin/sh"):
"""
Create a terminal command string.
:param str sh: shell to execute command in
:return: str
"""
return "%s -c %s -- %s" % (constants.VCMD_BIN, self.ctrlchnlname, sh)
def shcmd(self, cmd, sh="/bin/sh"):
"""
Execute a shell command.
:param str cmd: command string
:param str sh: shell to run command in
:return: command result
:rtype: int
"""
return self.cmd([sh, "-c", cmd])
def shcmd_result(self, cmd, sh="/bin/sh"):
"""
Execute a shell command and return the exist status and combined output.
:param str cmd: shell command to run
:param str sh: shell to run command in
:return: exist status and combined output
:rtype: tuple[int, str]
"""
return self.cmd_output([sh, "-c", cmd])
def getaddr(self, ifname, rescan=False):
"""
Get address for interface on node.
:param str ifname: interface name to get address for
:param bool rescan: rescan flag
:return: interface information
:rtype: dict
"""
if ifname in self._addr and not rescan:
return self._addr[ifname]
interface = {"ether": [], "inet": [], "inet6": [], "inet6link": []}
args = [constants.IP_BIN, "addr", "show", "dev", ifname]
p, stdin, stdout, stderr = self.popen(args)
stdin.close()
for line in stdout:
line = line.strip().split()
if line[0] == "link/ether":
interface["ether"].append(line[1])
elif line[0] == "inet":
interface["inet"].append(line[1])
elif line[0] == "inet6":
if line[3] == "global":
interface["inet6"].append(line[1])
elif line[3] == "link":
interface["inet6link"].append(line[1])
else:
logging.warn("unknown scope: %s" % line[3])
err = stderr.read()
stdout.close()
stderr.close()
status = p.wait()
if status:
logging.warn("nonzero exist status (%s) for cmd: %s", status, args)
if err:
logging.warn("error output: %s", err)
self._addr[ifname] = interface
return interface
def netifstats(self, ifname=None):
"""
Retrieve network interface state.
:param str ifname: name of interface to get state for
:return: interface state information
:rtype: dict
"""
stats = {}
args = ["cat", "/proc/net/dev"]
p, stdin, stdout, stderr = self.popen(args)
stdin.close()
# ignore first line
stdout.readline()
# second line has count names
tmp = stdout.readline().strip().split("|")
rxkeys = tmp[1].split()
txkeys = tmp[2].split()
for line in stdout:
line = line.strip().split()
devname, tmp = line[0].split(":")
if tmp:
line.insert(1, tmp)
stats[devname] = {"rx": {}, "tx": {}}
field = 1
for count in rxkeys:
stats[devname]["rx"][count] = int(line[field])
field += 1
for count in txkeys:
stats[devname]["tx"][count] = int(line[field])
field += 1
err = stderr.read()
stdout.close()
stderr.close()
status = p.wait()
if status:
logging.warn("nonzero exist status (%s) for cmd: %s", status, args)
if err:
logging.warn("error output: %s", err)
if ifname is not None:
return stats[ifname]
else:
return stats

View file

@ -0,0 +1,482 @@
"""
virtual ethernet classes that implement the interfaces available under Linux.
"""
import logging
import time
from core import CoreCommandError, utils
from core import constants
from core.emulator.enumerations import NodeTypes
from core.nodes import nodeutils
utils.check_executables([constants.IP_BIN])
class CoreInterface(object):
"""
Base class for network interfaces.
"""
def __init__(self, node, name, mtu):
"""
Creates a PyCoreNetIf instance.
:param core.coreobj.PyCoreNode node: node for interface
:param str name: interface name
:param mtu: mtu value
"""
self.node = node
self.name = name
if not isinstance(mtu, (int, long)):
raise ValueError
self.mtu = mtu
self.net = None
self._params = {}
self.addrlist = []
self.hwaddr = None
# placeholder position hook
self.poshook = lambda a, b, c, d: None
# used with EMANE
self.transport_type = None
# interface index on the network
self.netindex = None
# index used to find flow data
self.flow_id = None
def startup(self):
"""
Startup method for the interface.
:return: nothing
"""
pass
def shutdown(self):
"""
Shutdown method for the interface.
:return: nothing
"""
pass
def attachnet(self, net):
"""
Attach network.
:param core.coreobj.PyCoreNet net: network to attach
:return: nothing
"""
if self.net:
self.detachnet()
self.net = None
net.attach(self)
self.net = net
def detachnet(self):
"""
Detach from a network.
:return: nothing
"""
if self.net is not None:
self.net.detach(self)
def addaddr(self, addr):
"""
Add address.
:param str addr: address to add
:return: nothing
"""
self.addrlist.append(addr)
def deladdr(self, addr):
"""
Delete address.
:param str addr: address to delete
:return: nothing
"""
self.addrlist.remove(addr)
def sethwaddr(self, addr):
"""
Set hardware address.
:param core.misc.ipaddress.MacAddress addr: hardware address to set to.
:return: nothing
"""
self.hwaddr = addr
def getparam(self, key):
"""
Retrieve a parameter from the, or None if the parameter does not exist.
:param key: parameter to get value for
:return: parameter value
"""
return self._params.get(key)
def getparams(self):
"""
Return (key, value) pairs for parameters.
"""
parameters = []
for k in sorted(self._params.keys()):
parameters.append((k, self._params[k]))
return parameters
def setparam(self, key, value):
"""
Set a parameter value, returns True if the parameter has changed.
:param key: parameter name to set
:param value: parameter value
:return: True if parameter changed, False otherwise
"""
# 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
self._params[key] = value
return True
def swapparams(self, name):
"""
Swap out parameters dict for name. If name does not exist,
intialize it. This is for supporting separate upstream/downstream
parameters when two layer-2 nodes are linked together.
:param str name: name of parameter to swap
:return: nothing
"""
tmp = self._params
if not hasattr(self, name):
setattr(self, name, {})
self._params = getattr(self, name)
setattr(self, name, tmp)
def setposition(self, x, y, z):
"""
Dispatch position hook handler.
:param x: x position
:param y: y position
:param z: z position
:return: nothing
"""
self.poshook(self, x, y, z)
class Veth(CoreInterface):
"""
Provides virtual ethernet functionality for core nodes.
"""
# TODO: network is not used, why was it needed?
def __init__(self, node, name, localname, mtu=1500, net=None, start=True):
"""
Creates a VEth instance.
:param core.netns.vnode.SimpleLxcNode node: related core node
:param str name: interface name
:param str localname: interface local name
:param mtu: interface mtu
:param net: network
:param bool start: start flag
:raises CoreCommandError: when there is a command exception
"""
# note that net arg is ignored
CoreInterface.__init__(self, node=node, name=name, mtu=mtu)
self.localname = localname
self.up = False
if start:
self.startup()
def startup(self):
"""
Interface startup logic.
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
utils.check_cmd([constants.IP_BIN, "link", "add", "name", self.localname,
"type", "veth", "peer", "name", self.name])
utils.check_cmd([constants.IP_BIN, "link", "set", self.localname, "up"])
self.up = True
def shutdown(self):
"""
Interface shutdown logic.
:return: nothing
"""
if not self.up:
return
if self.node:
try:
self.node.check_cmd([constants.IP_BIN, "-6", "addr", "flush", "dev", self.name])
except CoreCommandError:
logging.exception("error shutting down interface")
if self.localname:
try:
utils.check_cmd([constants.IP_BIN, "link", "delete", self.localname])
except CoreCommandError:
logging.exception("error deleting link")
self.up = False
class TunTap(CoreInterface):
"""
TUN/TAP virtual device in TAP mode
"""
# TODO: network is not used, why was it needed?
def __init__(self, node, name, localname, mtu=1500, net=None, start=True):
"""
Create a TunTap instance.
:param core.netns.vnode.SimpleLxcNode node: related core node
:param str name: interface name
:param str localname: local interface name
:param mtu: interface mtu
:param net: related network
:param bool start: start flag
"""
CoreInterface.__init__(self, node=node, name=name, mtu=mtu)
self.localname = localname
self.up = False
self.transport_type = "virtual"
if start:
self.startup()
def startup(self):
"""
Startup logic for a tunnel tap.
:return: nothing
"""
# TODO: more sophisticated TAP creation here
# Debian does not support -p (tap) option, RedHat does.
# For now, this is disabled to allow the TAP to be created by another
# system (e.g. EMANE"s emanetransportd)
# check_call(["tunctl", "-t", self.name])
# self.install()
self.up = True
def shutdown(self):
"""
Shutdown functionality for a tunnel tap.
:return: nothing
"""
if not self.up:
return
try:
self.node.check_cmd([constants.IP_BIN, "-6", "addr", "flush", "dev", self.name])
except CoreCommandError:
logging.exception("error shutting down tunnel tap")
self.up = False
def waitfor(self, func, attempts=10, maxretrydelay=0.25):
"""
Wait for func() to return zero with exponential backoff.
:param func: function to wait for a result of zero
:param int attempts: number of attempts to wait for a zero result
:param float maxretrydelay: maximum retry delay
:return: True if wait succeeded, False otherwise
:rtype: bool
"""
delay = 0.01
result = False
for i in xrange(1, attempts + 1):
r = func()
if r == 0:
result = True
break
msg = "attempt %s failed with nonzero exit status %s" % (i, r)
if i < attempts + 1:
msg += ", retrying..."
logging.info(msg)
time.sleep(delay)
delay += delay
if delay > maxretrydelay:
delay = maxretrydelay
else:
msg += ", giving up"
logging.info(msg)
return result
def waitfordevicelocal(self):
"""
Check for presence of a local device - tap device may not
appear right away waits
:return: wait for device local response
:rtype: int
"""
logging.debug("waiting for device local: %s", self.localname)
def localdevexists():
args = [constants.IP_BIN, "link", "show", self.localname]
return utils.cmd(args)
self.waitfor(localdevexists)
def waitfordevicenode(self):
"""
Check for presence of a node device - tap device may not appear right away waits.
:return: nothing
"""
logging.debug("waiting for device node: %s", self.name)
def nodedevexists():
args = [constants.IP_BIN, "link", "show", self.name]
return self.node.cmd(args)
count = 0
while True:
result = self.waitfor(nodedevexists)
if result:
break
# check if this is an EMANE interface; if so, continue
# waiting if EMANE is still running
# TODO: remove emane code
should_retry = count < 5
is_emane_node = nodeutils.is_node(self.net, NodeTypes.EMANE)
is_emane_running = self.node.session.emane.emanerunning(self.node)
if all([should_retry, is_emane_node, is_emane_running]):
count += 1
else:
raise RuntimeError("node device failed to exist")
def install(self):
"""
Install this TAP into its namespace. This is not done from the
startup() method but called at a later time when a userspace
program (running on the host) has had a chance to open the socket
end of the TAP.
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
self.waitfordevicelocal()
netns = str(self.node.pid)
utils.check_cmd([constants.IP_BIN, "link", "set", self.localname, "netns", netns])
self.node.check_cmd([constants.IP_BIN, "link", "set", self.localname, "name", self.name])
self.node.check_cmd([constants.IP_BIN, "link", "set", self.name, "up"])
def setaddrs(self):
"""
Set interface addresses based on self.addrlist.
:return: nothing
"""
self.waitfordevicenode()
for addr in self.addrlist:
self.node.check_cmd([constants.IP_BIN, "addr", "add", str(addr), "dev", self.name])
class GreTap(CoreInterface):
"""
GRE TAP device for tunneling between emulation servers.
Uses the "gretap" tunnel device type from Linux which is a GRE device
having a MAC address. The MAC address is required for bridging.
"""
def __init__(self, node=None, name=None, session=None, mtu=1458,
remoteip=None, _id=None, localip=None, ttl=255,
key=None, start=True):
"""
Creates a GreTap instance.
:param core.netns.vnode.SimpleLxcNode node: related core node
:param str name: interface name
:param core.session.Session session: core session instance
:param mtu: interface mtu
:param str remoteip: remote address
:param int _id: object id
:param str localip: local address
:param ttl: ttl value
:param key: gre tap key
:param bool start: start flag
:raises CoreCommandError: when there is a command exception
"""
CoreInterface.__init__(self, node=node, name=name, mtu=mtu)
self.session = session
if _id is None:
# from PyCoreObj
_id = ((id(self) >> 16) ^ (id(self) & 0xffff)) & 0xffff
self.id = _id
sessionid = self.session.short_session_id()
# interface name on the local host machine
self.localname = "gt.%s.%s" % (self.id, sessionid)
self.transport_type = "raw"
if not start:
self.up = False
return
if remoteip is None:
raise ValueError, "missing remote IP required for GRE TAP device"
args = [constants.IP_BIN, "link", "add", self.localname, "type", "gretap",
"remote", str(remoteip)]
if localip:
args += ["local", str(localip)]
if ttl:
args += ["ttl", str(ttl)]
if key:
args += ["key", str(key)]
utils.check_cmd(args)
args = [constants.IP_BIN, "link", "set", self.localname, "up"]
utils.check_cmd(args)
self.up = True
def shutdown(self):
"""
Shutdown logic for a GreTap.
:return: nothing
"""
if self.localname:
try:
args = [constants.IP_BIN, "link", "set", self.localname, "down"]
utils.check_cmd(args)
args = [constants.IP_BIN, "link", "del", self.localname]
utils.check_cmd(args)
except CoreCommandError:
logging.exception("error during shutdown")
self.localname = None
def data(self, message_type):
"""
Data for a gre tap.
:param message_type: message type for data
:return: None
"""
return None
def all_link_data(self, flags):
"""
Retrieve link data.
:param flags: link flags
:return: link data
:rtype: list[core.data.LinkData]
"""
return []

View file

@ -0,0 +1,446 @@
"""
Helper objects for dealing with IPv4/v6 addresses.
"""
import logging
import random
import socket
import struct
from socket import AF_INET
from socket import AF_INET6
class MacAddress(object):
"""
Provides mac address utilities for use within core.
"""
def __init__(self, address):
"""
Creates a MacAddress instance.
:param str address: mac address
"""
self.addr = address
def __str__(self):
"""
Create a string representation of a MacAddress.
:return: string representation
:rtype: str
"""
return ":".join("%02x" % ord(x) for x in self.addr)
def to_link_local(self):
"""
Convert the MAC address to a IPv6 link-local address, using EUI 48
to EUI 64 conversion process per RFC 5342.
:return: ip address object
:rtype: IpAddress
"""
if not self.addr:
return IpAddress.from_string("::")
tmp = struct.unpack("!Q", "\x00\x00" + self.addr)[0]
nic = long(tmp) & 0x000000FFFFFFL
oui = long(tmp) & 0xFFFFFF000000L
# toggle U/L bit
oui ^= 0x020000000000L
# append EUI-48 octets
oui = (oui << 16) | 0xFFFE000000L
return IpAddress(AF_INET6, struct.pack("!QQ", 0xfe80 << 48, oui | nic))
@classmethod
def from_string(cls, s):
"""
Create a mac address object from a string.
:param s: string representation of a mac address
:return: mac address class
:rtype: MacAddress
"""
addr = "".join(chr(int(x, 16)) for x in s.split(":"))
return cls(addr)
@classmethod
def random(cls):
"""
Create a random mac address.
:return: random mac address
:rtype: MacAddress
"""
tmp = random.randint(0, 0xFFFFFF)
# use the Xen OID 00:16:3E
tmp |= 0x00163E << 24
tmpbytes = struct.pack("!Q", tmp)
return cls(tmpbytes[2:])
class IpAddress(object):
"""
Provides ip utilities and functionality for use within core.
"""
def __init__(self, af, address):
"""
Create a IpAddress instance.
:param int af: address family
:param str address: ip address
:return:
"""
# check if (af, addr) is valid
if not socket.inet_ntop(af, address):
raise ValueError("invalid af/addr")
self.af = af
self.addr = address
def is_ipv4(self):
"""
Checks if this is an ipv4 address.
:return: True if ipv4 address, False otherwise
:rtype: bool
"""
return self.af == AF_INET
def is_ipv6(self):
"""
Checks if this is an ipv6 address.
:return: True if ipv6 address, False otherwise
:rtype: bool
"""
return self.af == AF_INET6
def __str__(self):
"""
Create a string representation of this address.
:return: string representation of address
:rtype: str
"""
return socket.inet_ntop(self.af, self.addr)
def __eq__(self, other):
"""
Checks for equality with another ip address.
:param IpAddress other: other ip address to check equality with
:return: True is the other IpAddress is equal, False otherwise
:rtype: bool
"""
if not isinstance(other, IpAddress):
return False
elif self is other:
return True
else:
return other.af == self.af and other.addr == self.addr
def __add__(self, other):
"""
Add value to ip addresses.
:param int other: value to add to ip address
:return: added together ip address instance
:rtype: IpAddress
"""
try:
carry = int(other)
except ValueError:
logging.exception("error during addition")
return NotImplemented
tmp = [ord(x) for x in self.addr]
for i in xrange(len(tmp) - 1, -1, -1):
x = tmp[i] + carry
tmp[i] = x & 0xff
carry = x >> 8
if carry == 0:
break
addr = "".join(chr(x) for x in tmp)
return self.__class__(self.af, addr)
def __sub__(self, other):
"""
Subtract value from ip address.
:param int other: value to subtract from ip address
:return:
"""
try:
tmp = -int(other)
except ValueError:
logging.exception("error during subtraction")
return NotImplemented
return self.__add__(tmp)
@classmethod
def from_string(cls, s):
"""
Create a ip address from a string representation.
:param s: string representation to create ip address from
:return: ip address instance
:rtype: IpAddress
"""
for af in AF_INET, AF_INET6:
return cls(af, socket.inet_pton(af, s))
@staticmethod
def to_int(s):
"""
Convert IPv4 string to integer
:param s: string to convert to 32-bit integer
:return: integer value
:rtype: int
"""
value = socket.inet_pton(AF_INET, s)
return struct.unpack("!I", value)[0]
class IpPrefix(object):
"""
Provides ip address generation and prefix utilities.
"""
def __init__(self, af, prefixstr):
"""
Create a IpPrefix instance.
:param int af: address family for ip prefix
:param prefixstr: ip prefix string
"""
# prefixstr format: address/prefixlen
tmp = prefixstr.split("/")
if len(tmp) > 2:
raise ValueError("invalid prefix: %s" % prefixstr)
self.af = af
if self.af == AF_INET:
self.addrlen = 32
elif self.af == AF_INET6:
self.addrlen = 128
else:
raise ValueError("invalid address family: %s" % self.af)
if len(tmp) == 2:
self.prefixlen = int(tmp[1])
else:
self.prefixlen = self.addrlen
self.prefix = socket.inet_pton(self.af, tmp[0])
if self.addrlen > self.prefixlen:
addrbits = self.addrlen - self.prefixlen
netmask = ((1L << self.prefixlen) - 1) << addrbits
prefix = ""
for i in xrange(-1, -(addrbits >> 3) - 2, -1):
prefix = chr(ord(self.prefix[i]) & (netmask & 0xff)) + prefix
netmask >>= 8
self.prefix = self.prefix[:i] + prefix
def __str__(self):
"""
String representation of an ip prefix.
:return: string representation
:rtype: str
"""
return "%s/%s" % (socket.inet_ntop(self.af, self.prefix), self.prefixlen)
def __eq__(self, other):
"""
Compare equality with another ip prefix.
:param IpPrefix other: other ip prefix to compare with
:return: True is equal, False otherwise
:rtype: bool
"""
if not isinstance(other, IpPrefix):
return False
elif self is other:
return True
else:
return other.af == self.af and other.prefixlen == self.prefixlen and other.prefix == self.prefix
def __add__(self, other):
"""
Add a value to this ip prefix.
:param int other: value to add
:return: added ip prefix instance
:rtype: IpPrefix
"""
try:
tmp = int(other)
except ValueError:
logging.exception("error during addition")
return NotImplemented
a = IpAddress(self.af, self.prefix) + (tmp << (self.addrlen - self.prefixlen))
prefixstr = "%s/%s" % (a, self.prefixlen)
if self.__class__ == IpPrefix:
return self.__class__(self.af, prefixstr)
else:
return self.__class__(prefixstr)
def __sub__(self, other):
"""
Subtract value from this ip prefix.
:param int other: value to subtract
:return: subtracted ip prefix instance
:rtype: IpPrefix
"""
try:
tmp = -int(other)
except ValueError:
logging.exception("error during subtraction")
return NotImplemented
return self.__add__(tmp)
def addr(self, hostid):
"""
Create an ip address for a given host id.
:param hostid: host id for an ip address
:return: ip address
:rtype: IpAddress
"""
tmp = int(hostid)
if tmp in [-1, 0, 1] and self.addrlen == self.prefixlen:
return IpAddress(self.af, self.prefix)
if tmp == 0 or tmp > (1 << (self.addrlen - self.prefixlen)) - 1 or (
self.af == AF_INET and tmp == (1 << (self.addrlen - self.prefixlen)) - 1):
raise ValueError("invalid hostid for prefix %s: %s" % (self, hostid))
addr = ""
prefix_endpoint = -1
for i in xrange(-1, -(self.addrlen >> 3) - 1, -1):
prefix_endpoint = i
addr = chr(ord(self.prefix[i]) | (tmp & 0xff)) + addr
tmp >>= 8
if not tmp:
break
addr = self.prefix[:prefix_endpoint] + addr
return IpAddress(self.af, addr)
def min_addr(self):
"""
Return the minimum ip address for this prefix.
:return: minimum ip address
:rtype: IpAddress
"""
return self.addr(1)
def max_addr(self):
"""
Return the maximum ip address for this prefix.
:return: maximum ip address
:rtype: IpAddress
"""
if self.af == AF_INET:
return self.addr((1 << (self.addrlen - self.prefixlen)) - 2)
else:
return self.addr((1 << (self.addrlen - self.prefixlen)) - 1)
def num_addr(self):
"""
Retrieve the number of ip addresses for this prefix.
:return: maximum number of ip addresses
:rtype: int
"""
return max(0, (1 << (self.addrlen - self.prefixlen)) - 2)
def prefix_str(self):
"""
Retrieve the prefix string for this ip address.
:return: prefix string
:rtype: str
"""
return "%s" % socket.inet_ntop(self.af, self.prefix)
def netmask_str(self):
"""
Retrieve the netmask string for this ip address.
:return: netmask string
:rtype: str
"""
addrbits = self.addrlen - self.prefixlen
netmask = ((1L << self.prefixlen) - 1) << addrbits
netmaskbytes = struct.pack("!L", netmask)
return IpAddress(af=AF_INET, address=netmaskbytes).__str__()
class Ipv4Prefix(IpPrefix):
"""
Provides an ipv4 specific class for ip prefixes.
"""
def __init__(self, prefixstr):
"""
Create a Ipv4Prefix instance.
:param str prefixstr: ip prefix
"""
IpPrefix.__init__(self, AF_INET, prefixstr)
class Ipv6Prefix(IpPrefix):
"""
Provides an ipv6 specific class for ip prefixes.
"""
def __init__(self, prefixstr):
"""
Create a Ipv6Prefix instance.
:param str prefixstr: ip prefix
"""
IpPrefix.__init__(self, AF_INET6, prefixstr)
def is_ip_address(af, addrstr):
"""
Check if ip address string is a valid ip address.
:param int af: address family
:param str addrstr: ip address string
:return: True if a valid ip address, False otherwise
:rtype: bool
"""
try:
socket.inet_pton(af, addrstr)
return True
except IOError:
return False
def is_ipv4_address(addrstr):
"""
Check if ipv4 address string is a valid ipv4 address.
:param str addrstr: ipv4 address string
:return: True if a valid ipv4 address, False otherwise
:rtype: bool
"""
return is_ip_address(AF_INET, addrstr)
def is_ipv6_address(addrstr):
"""
Check if ipv6 address string is a valid ipv6 address.
:param str addrstr: ipv6 address string
:return: True if a valid ipv6 address, False otherwise
:rtype: bool
"""
return is_ip_address(AF_INET6, addrstr)

1069
daemon/core/nodes/network.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
"""
Provides default node maps that can be used to run core with.
"""
import core.nodes.base
import core.nodes.network
import core.nodes.physical
from core.emane.nodes import EmaneNet
from core.emane.nodes import EmaneNode
from core.emulator.enumerations import NodeTypes
from core.nodes.network import GreTapBridge
from core.nodes import physical
# legacy core nodes, that leverage linux bridges
NODES = {
NodeTypes.DEFAULT: core.nodes.base.CoreNode,
NodeTypes.PHYSICAL: physical.PhysicalNode,
NodeTypes.TBD: None,
NodeTypes.SWITCH: core.nodes.network.SwitchNode,
NodeTypes.HUB: core.nodes.network.HubNode,
NodeTypes.WIRELESS_LAN: core.nodes.network.WlanNode,
NodeTypes.RJ45: core.nodes.physical.Rj45Node,
NodeTypes.TUNNEL: core.nodes.network.TunnelNode,
NodeTypes.KTUNNEL: None,
NodeTypes.EMANE: EmaneNode,
NodeTypes.EMANE_NET: EmaneNet,
NodeTypes.TAP_BRIDGE: GreTapBridge,
NodeTypes.PEER_TO_PEER: core.nodes.network.PtpNet,
NodeTypes.CONTROL_NET: core.nodes.network.CtrlNet
}

View file

@ -0,0 +1,97 @@
"""
Serves as a global point for storing and retrieving node types needed during simulation.
"""
import logging
_NODE_MAP = None
def _log_map():
global _NODE_MAP
for key, value in _NODE_MAP.iteritems():
name = None
if value:
name = value.__name__
logging.debug("node type (%s) - class (%s)", key.name, name)
def _convert_map(x, y):
"""
Convenience method to create a human readable version of the node map to log.
:param dict x: dictionary to reduce node items into
:param tuple y: current node item
:return:
"""
x[y[0].name] = y[1]
return x
def update_node_map(node_map):
"""
Update the current node map with the provided node map values.
:param dict node_map: node map to update with
"""
global _NODE_MAP
_NODE_MAP.update(node_map)
_log_map()
def set_node_map(node_map):
"""
Set the global node map that proides a consistent way to retrieve differently configured nodes.
:param dict node_map: node map to set to
:return: nothing
"""
global _NODE_MAP
_NODE_MAP = node_map
_log_map()
def get_node_class(node_type):
"""
Retrieve the node class for a given node type.
:param int node_type: node type to retrieve class for
:return: node class
"""
global _NODE_MAP
return _NODE_MAP[node_type]
def get_node_type(node_class):
"""
Retrieve the node type given a node class.
:param class node_class: node class to get type for
:return: node type
:rtype: core.enumerations.NodeTypes
"""
global _NODE_MAP
node_type_map = {v: k for k, v in _NODE_MAP.iteritems()}
return node_type_map.get(node_class)
def is_node(obj, node_types):
"""
Validates if an object is one of the provided node types.
:param obj: object to check type for
:param int|tuple|list node_types: node type(s) to check against
:return: True if the object is one of the node types, False otherwise
:rtype: bool
"""
type_classes = []
if isinstance(node_types, (tuple, list)):
for node_type in node_types:
type_class = get_node_class(node_type)
type_classes.append(type_class)
else:
type_class = get_node_class(node_types)
type_classes.append(type_class)
return isinstance(obj, tuple(type_classes))

View file

@ -0,0 +1,722 @@
"""
TODO: probably goes away, or implement the usage of "unshare", or docker formal.
"""
import logging
import socket
import threading
from socket import AF_INET
from socket import AF_INET6
from core import CoreCommandError, utils
from core import constants
from core.nodes.base import CoreNetworkBase
from core.emulator.data import LinkData
from core.emulator.enumerations import LinkTypes
from core.emulator.enumerations import NodeTypes
from core.emulator.enumerations import RegisterTlvs
from core.nodes import ipaddress
from core.nodes.interface import GreTap
from core.nodes.interface import Veth
from core.nodes.network import EbtablesQueue
from core.nodes.network import GreTapBridge
# a global object because all WLANs share the same queue
# cannot have multiple threads invoking the ebtables commnd
ebtables_queue = EbtablesQueue()
ebtables_lock = threading.Lock()
utils.check_executables([
constants.IP_BIN,
constants.EBTABLES_BIN,
constants.TC_BIN
])
def ebtables_commands(call, commands):
with ebtables_lock:
for command in commands:
call(command)
class OvsNet(CoreNetworkBase):
"""
Used to be LxBrNet.
Base class for providing Openvswitch functionality to objects that create bridges.
"""
policy = "DROP"
def __init__(self, session, _id=None, name=None, start=True, policy=None):
"""
Creates an OvsNet instance.
:param core.session.Session session: session this object is a part of
:param _id:
:param name:
:param start:
:param policy:
:return:
"""
CoreNetworkBase.__init__(self, session, _id, name, start)
if policy:
self.policy = policy
else:
self.policy = self.__class__.policy
session_id = self.session.short_session_id()
self.bridge_name = "b.%s.%s" % (str(self.id), session_id)
self.up = False
if start:
self.startup()
ebtables_queue.startupdateloop(self)
def startup(self):
"""
:return:
:raises CoreCommandError: when there is a command exception
"""
utils.check_cmd([constants.OVS_BIN, "add-br", self.bridge_name])
# turn off spanning tree protocol and forwarding delay
# TODO: appears stp and rstp are off by default, make sure this always holds true
# TODO: apears ovs only supports rstp forward delay and again it's off by default
utils.check_cmd([constants.IP_BIN, "link", "set", self.bridge_name, "up"])
# create a new ebtables chain for this bridge
ebtables_commands(utils.check_cmd, [
[constants.EBTABLES_BIN, "-N", self.bridge_name, "-P", self.policy],
[constants.EBTABLES_BIN, "-A", "FORWARD", "--logical-in", self.bridge_name, "-j", self.bridge_name]
])
self.up = True
def shutdown(self):
if not self.up:
logging.info("exiting shutdown, object is not up")
return
ebtables_queue.stopupdateloop(self)
try:
utils.check_cmd([constants.IP_BIN, "link", "set", self.bridge_name, "down"])
utils.check_cmd([constants.OVS_BIN, "del-br", self.bridge_name])
ebtables_commands(utils.check_cmd, [
[constants.EBTABLES_BIN, "-D", "FORWARD", "--logical-in", self.bridge_name, "-j", self.bridge_name],
[constants.EBTABLES_BIN, "-X", self.bridge_name]
])
except CoreCommandError:
logging.exception("error bringing bridge down and removing it")
# removes veth pairs used for bridge-to-bridge connections
for interface in self.netifs():
interface.shutdown()
self._netif.clear()
self._linked.clear()
del self.session
self.up = False
def attach(self, interface):
if self.up:
utils.check_cmd([constants.OVS_BIN, "add-port", self.bridge_name, interface.localname])
utils.check_cmd([constants.IP_BIN, "link", "set", interface.localname, "up"])
CoreNetworkBase.attach(self, interface)
def detach(self, interface):
if self.up:
utils.check_cmd([constants.OVS_BIN, "del-port", self.bridge_name, interface.localname])
CoreNetworkBase.detach(self, interface)
def linked(self, interface_one, interface_two):
# check if the network interfaces are attached to this network
if self._netif[interface_one.netifi] != interface_one:
raise ValueError("inconsistency for interface %s" % interface_one.name)
if self._netif[interface_two.netifi] != interface_two:
raise ValueError("inconsistency for interface %s" % interface_two.name)
try:
linked = self._linked[interface_one][interface_two]
except KeyError:
if self.policy == "ACCEPT":
linked = True
elif self.policy == "DROP":
linked = False
else:
raise ValueError("unknown policy: %s" % self.policy)
self._linked[interface_one][interface_two] = linked
return linked
def unlink(self, interface_one, interface_two):
"""
Unlink two PyCoreNetIfs, resulting in adding or removing ebtables
filtering rules.
"""
with self._linked_lock:
if not self.linked(interface_one, interface_two):
return
self._linked[interface_one][interface_two] = False
ebtables_queue.ebchange(self)
def link(self, interface_one, interface_two):
"""
Link two PyCoreNetIfs together, resulting in adding or removing
ebtables filtering rules.
"""
with self._linked_lock:
if self.linked(interface_one, interface_two):
return
self._linked[interface_one][interface_two] = True
ebtables_queue.ebchange(self)
def linkconfig(self, netif, bw=None, delay=None, loss=None, duplicate=None,
jitter=None, netif2=None, devname=None):
"""
Configure link parameters by applying tc queuing disciplines on the
interface.
"""
if not devname:
devname = netif.localname
tc = [constants.TC_BIN, "qdisc", "replace", "dev", devname]
parent = ["root"]
# attempt to set bandwidth and update as needed if value changed
bandwidth_changed = netif.setparam("bw", bw)
if bandwidth_changed:
# from tc-tbf(8): minimum value for burst is rate / kernel_hz
if bw > 0:
if self.up:
burst = max(2 * netif.mtu, bw / 1000)
limit = 0xffff # max IP payload
tbf = ["tbf", "rate", str(bw), "burst", str(burst), "limit", str(limit)]
logging.info("linkconfig: %s" % [tc + parent + ["handle", "1:"] + tbf])
utils.check_cmd(tc + parent + ["handle", "1:"] + tbf)
netif.setparam("has_tbf", True)
elif netif.getparam("has_tbf") and bw <= 0:
tcd = [] + tc
tcd[2] = "delete"
if self.up:
utils.check_cmd(tcd + parent)
netif.setparam("has_tbf", False)
# removing the parent removes the child
netif.setparam("has_netem", False)
if netif.getparam("has_tbf"):
parent = ["parent", "1:1"]
netem = ["netem"]
delay_changed = netif.setparam("delay", delay)
if loss is not None:
loss = float(loss)
loss_changed = netif.setparam("loss", loss)
if duplicate is not None:
duplicate = float(duplicate)
duplicate_changed = netif.setparam("duplicate", duplicate)
jitter_changed = netif.setparam("jitter", jitter)
# if nothing changed return
if not any([bandwidth_changed, delay_changed, loss_changed, duplicate_changed, jitter_changed]):
return
# jitter and delay use the same delay statement
if delay is not None:
netem += ["delay", "%sus" % delay]
else:
netem += ["delay", "0us"]
if jitter is not None:
netem += ["%sus" % jitter, "25%"]
if loss is not None and loss > 0:
netem += ["loss", "%s%%" % min(loss, 100)]
if duplicate is not None and duplicate > 0:
netem += ["duplicate", "%s%%" % min(duplicate, 100)]
if delay <= 0 and jitter <= 0 and loss <= 0 and duplicate <= 0:
# possibly remove netem if it exists and parent queue wasn"t removed
if not netif.getparam("has_netem"):
return
tc[2] = "delete"
if self.up:
logging.info("linkconfig: %s" % ([tc + parent + ["handle", "10:"]],))
utils.check_cmd(tc + parent + ["handle", "10:"])
netif.setparam("has_netem", False)
elif len(netem) > 1:
if self.up:
logging.info("linkconfig: %s" % ([tc + parent + ["handle", "10:"] + netem],))
utils.check_cmd(tc + parent + ["handle", "10:"] + netem)
netif.setparam("has_netem", True)
def linknet(self, network):
"""
Link this bridge with another by creating a veth pair and installing
each device into each bridge.
"""
session_id = self.session.short_session_id()
try:
_id = "%x" % self.id
except TypeError:
_id = "%s" % self.id
try:
network_id = "%x" % network.id
except TypeError:
network_id = "%s" % network.id
localname = "veth%s.%s.%s" % (_id, network_id, session_id)
if len(localname) >= 16:
raise ValueError("interface local name %s too long" % localname)
name = "veth%s.%s.%s" % (network_id, _id, session_id)
if len(name) >= 16:
raise ValueError("interface name %s too long" % name)
interface = Veth(node=None, name=name, localname=localname, mtu=1500, net=self, start=self.up)
self.attach(interface)
if network.up:
# this is similar to net.attach() but uses netif.name instead
# of localname
utils.check_cmd([constants.OVS_BIN, "add-port", network.bridge_name, interface.name])
utils.check_cmd([constants.IP_BIN, "link", "set", interface.name, "up"])
# TODO: is there a native method for this? see if this causes issues
# i = network.newifindex()
# network._netif[i] = interface
# with network._linked_lock:
# network._linked[interface] = {}
# this method call is equal to the above, with a interface.netifi = call
network.attach(interface)
interface.net = self
interface.othernet = network
return interface
def getlinknetif(self, network):
"""
Return the interface of that links this net with another net
(that were linked using linknet()).
"""
for interface in self.netifs():
if hasattr(interface, "othernet") and interface.othernet == network:
return interface
return None
def addrconfig(self, addresses):
"""
Set addresses on the bridge.
"""
if not self.up:
return
for address in addresses:
utils.check_cmd([constants.IP_BIN, "addr", "add", str(address), "dev", self.bridge_name])
class OvsCtrlNet(OvsNet):
policy = "ACCEPT"
CTRLIF_IDX_BASE = 99 # base control interface index
DEFAULT_PREFIX_LIST = [
"172.16.0.0/24 172.16.1.0/24 172.16.2.0/24 172.16.3.0/24 172.16.4.0/24",
"172.17.0.0/24 172.17.1.0/24 172.17.2.0/24 172.17.3.0/24 172.17.4.0/24",
"172.18.0.0/24 172.18.1.0/24 172.18.2.0/24 172.18.3.0/24 172.18.4.0/24",
"172.19.0.0/24 172.19.1.0/24 172.19.2.0/24 172.19.3.0/24 172.19.4.0/24"
]
def __init__(self, session, _id="ctrlnet", name=None, prefix=None, hostid=None,
start=True, assign_address=True, updown_script=None, serverintf=None):
self.prefix = ipaddress.Ipv4Prefix(prefix)
self.hostid = hostid
self.assign_address = assign_address
self.updown_script = updown_script
self.serverintf = serverintf
OvsNet.__init__(self, session, _id=_id, name=name, start=start)
def startup(self):
if self.detectoldbridge():
return
OvsNet.startup(self)
if self.hostid:
addr = self.prefix.addr(self.hostid)
else:
addr = self.prefix.max_addr()
message = "Added control network bridge: %s %s" % (self.bridge_name, self.prefix)
addresses = ["%s/%s" % (addr, self.prefix.prefixlen)]
if self.assign_address:
self.addrconfig(addresses=addresses)
message += " address %s" % addr
logging.info(message)
if self.updown_script:
logging.info("interface %s updown script %s startup called" % (self.bridge_name, self.updown_script))
utils.check_cmd([self.updown_script, self.bridge_name, "startup"])
if self.serverintf:
utils.check_cmd([constants.OVS_BIN, "add-port", self.bridge_name, self.serverintf])
utils.check_cmd([constants.IP_BIN, "link", "set", self.serverintf, "up"])
def detectoldbridge(self):
"""
Occasionally, control net bridges from previously closed sessions are not cleaned up.
Check if there are old control net bridges and delete them
"""
output = utils.check_cmd([constants.OVS_BIN, "list-br"])
output = output.strip()
if output:
for line in output.split("\n"):
bride_name = line.split(".")
if bride_name[0] == "b" and bride_name[1] == self.id:
logging.error("older session may still be running with conflicting id for bridge: %s", line)
return True
return False
def shutdown(self):
if self.serverintf:
try:
utils.check_cmd([constants.OVS_BIN, "del-port", self.bridge_name, self.serverintf])
except CoreCommandError:
logging.exception("error deleting server interface %s to controlnet bridge %s",
self.serverintf, self.bridge_name)
if self.updown_script:
try:
logging.info("interface %s updown script (%s shutdown) called", self.bridge_name, self.updown_script)
utils.check_cmd([self.updown_script, self.bridge_name, "shutdown"])
except CoreCommandError:
logging.exception("error during updown script shutdown")
OvsNet.shutdown(self)
def all_link_data(self, flags):
"""
Do not include CtrlNet in link messages describing this session.
"""
return []
class OvsPtpNet(OvsNet):
policy = "ACCEPT"
def attach(self, interface):
if len(self._netif) >= 2:
raise ValueError("point-to-point links support at most 2 network interfaces")
OvsNet.attach(self, interface)
def data(self, message_type, lat=None, lon=None, alt=None):
"""
Do not generate a Node Message for point-to-point links. They are
built using a link message instead.
"""
return None
def all_link_data(self, flags):
"""
Build CORE API TLVs for a point-to-point link. One Link message describes this network.
"""
all_links = []
if len(self._netif) != 2:
return all_links
if1, if2 = self._netif.values()
unidirectional = 0
if if1.getparams() != if2.getparams():
unidirectional = 1
interface1_ip4 = None
interface1_ip4_mask = None
interface1_ip6 = None
interface1_ip6_mask = None
for address in if1.addrlist:
ip, _sep, mask = address.partition("/")
mask = int(mask)
if ipaddress.is_ipv4_address(ip):
family = AF_INET
ipl = socket.inet_pton(family, ip)
interface1_ip4 = ipaddress.IpAddress(af=family, address=ipl)
interface1_ip4_mask = mask
else:
family = AF_INET6
ipl = socket.inet_pton(family, ip)
interface1_ip6 = ipaddress.IpAddress(af=family, address=ipl)
interface1_ip6_mask = mask
interface2_ip4 = None
interface2_ip4_mask = None
interface2_ip6 = None
interface2_ip6_mask = None
for address in if2.addrlist:
ip, _sep, mask = address.partition("/")
mask = int(mask)
if ipaddress.is_ipv4_address(ip):
family = AF_INET
ipl = socket.inet_pton(family, ip)
interface2_ip4 = ipaddress.IpAddress(af=family, address=ipl)
interface2_ip4_mask = mask
else:
family = AF_INET6
ipl = socket.inet_pton(family, ip)
interface2_ip6 = ipaddress.IpAddress(af=family, address=ipl)
interface2_ip6_mask = mask
# TODO: not currently used
# loss=netif.getparam("loss")
link_data = LinkData(
message_type=flags,
node1_id=if1.node.id,
node2_id=if2.node.id,
link_type=self.linktype,
unidirectional=unidirectional,
delay=if1.getparam("delay"),
bandwidth=if1.getparam("bw"),
dup=if1.getparam("duplicate"),
jitter=if1.getparam("jitter"),
interface1_id=if1.node.getifindex(if1),
interface1_mac=if1.hwaddr,
interface1_ip4=interface1_ip4,
interface1_ip4_mask=interface1_ip4_mask,
interface1_ip6=interface1_ip6,
interface1_ip6_mask=interface1_ip6_mask,
interface2_id=if2.node.getifindex(if2),
interface2_mac=if2.hwaddr,
interface2_ip4=interface2_ip4,
interface2_ip4_mask=interface2_ip4_mask,
interface2_ip6=interface2_ip6,
interface2_ip6_mask=interface2_ip6_mask,
)
all_links.append(link_data)
# build a 2nd link message for the upstream link parameters
# (swap if1 and if2)
if unidirectional:
link_data = LinkData(
message_type=0,
node1_id=if2.node.id,
node2_id=if1.node.id,
delay=if1.getparam("delay"),
bandwidth=if1.getparam("bw"),
dup=if1.getparam("duplicate"),
jitter=if1.getparam("jitter"),
unidirectional=1,
interface1_id=if2.node.getifindex(if2),
interface2_id=if1.node.getifindex(if1)
)
all_links.append(link_data)
return all_links
class OvsSwitchNode(OvsNet):
apitype = NodeTypes.SWITCH.value
policy = "ACCEPT"
type = "lanswitch"
class OvsHubNode(OvsNet):
apitype = NodeTypes.HUB.value
policy = "ACCEPT"
type = "hub"
def __init__(self, session, _id=None, name=None, start=True):
"""
the Hub node forwards packets to all bridge ports by turning off
the MAC address learning
"""
OvsNet.__init__(self, session, _id, name, start)
if start:
# TODO: verify that the below flow accomplishes what is desired for a "HUB"
# TODO: replace "brctl setageing 0"
utils.check_cmd([constants.OVS_FLOW_BIN, "add-flow", self.bridge_name, "action=flood"])
class OvsWlanNode(OvsNet):
apitype = NodeTypes.WIRELESS_LAN.value
linktype = LinkTypes.WIRELESS.value
policy = "DROP"
type = "wlan"
def __init__(self, session, _id=None, name=None, start=True, policy=None):
OvsNet.__init__(self, session, _id, name, start, policy)
# wireless model such as basic range
self.model = None
# mobility model such as scripted
self.mobility = None
def attach(self, interface):
OvsNet.attach(self, interface)
if self.model:
interface.poshook = self.model.position_callback
if interface.node is None:
return
x, y, z = interface.node.position.get()
# invokes any netif.poshook
interface.setposition(x, y, z)
# self.model.setlinkparams()
def setmodel(self, model, config=None):
"""
Mobility and wireless model.
"""
logging.info("adding model %s", model.name)
if model.type == RegisterTlvs.WIRELESS.value:
self.model = model(session=self.session, _id=self.id, config=config)
if self.model.position_callback:
for interface in self.netifs():
interface.poshook = self.model.position_callback
if interface.node is not None:
x, y, z = interface.node.position.get()
interface.poshook(interface, x, y, z)
self.model.setlinkparams()
elif model.type == RegisterTlvs.MOBILITY.value:
self.mobility = model(session=self.session, _id=self.id, config=config)
def updatemodel(self, config):
if not self.model:
raise ValueError("no model set to update for node(%s)", self.id)
logging.info("node(%s) updating model(%s): %s", self.id, self.model.name, config)
self.model.set_configs(config, node_id=self.id)
if self.model.position_callback:
for netif in self.netifs():
netif.poshook = self.model.position_callback
if netif.node is not None:
x, y, z = netif.node.position.get()
netif.poshook(netif, x, y, z)
self.model.updateconfig()
def all_link_data(self, flags):
all_links = OvsNet.all_link_data(self, flags)
if self.model:
all_links.extend(self.model.all_link_data(flags))
return all_links
class OvsTunnelNode(GreTapBridge):
apitype = NodeTypes.TUNNEL.value
policy = "ACCEPT"
type = "tunnel"
class OvsGreTapBridge(OvsNet):
"""
A network consisting of a bridge with a gretap device for tunneling to
another system.
"""
def __init__(self, session, remoteip=None, _id=None, name=None, policy="ACCEPT",
localip=None, ttl=255, key=None, start=True):
OvsNet.__init__(self, session=session, _id=_id, name=name, policy=policy, start=False)
self.grekey = key
if self.grekey is None:
self.grekey = self.session.id ^ self.id
self.localnum = None
self.remotenum = None
self.remoteip = remoteip
self.localip = localip
self.ttl = ttl
if remoteip is None:
self.gretap = None
else:
self.gretap = GreTap(node=self, session=session, remoteip=remoteip,
localip=localip, ttl=ttl, key=self.grekey)
if start:
self.startup()
def startup(self):
"""
Creates a bridge and adds the gretap device to it.
"""
OvsNet.startup(self)
if self.gretap:
self.attach(self.gretap)
def shutdown(self):
"""
Detach the gretap device and remove the bridge.
"""
if self.gretap:
self.detach(self.gretap)
self.gretap.shutdown()
self.gretap = None
OvsNet.shutdown(self)
def addrconfig(self, addresses):
"""
Set the remote tunnel endpoint. This is a one-time method for
creating the GreTap device, which requires the remoteip at startup.
The 1st address in the provided list is remoteip, 2nd optionally
specifies localip.
"""
if self.gretap:
raise ValueError("gretap already exists for %s" % self.name)
remoteip = addresses[0].split("/")[0]
localip = None
if len(addresses) > 1:
localip = addresses[1].split("/")[0]
self.gretap = GreTap(session=self.session, remoteip=remoteip,
localip=localip, ttl=self.ttl, key=self.grekey)
self.attach(self.gretap)
def setkey(self, key):
"""
Set the GRE key used for the GreTap device. This needs to be set
prior to instantiating the GreTap device (before addrconfig).
"""
self.grekey = key
OVS_NODES = {
NodeTypes.SWITCH: OvsSwitchNode,
NodeTypes.HUB: OvsHubNode,
NodeTypes.WIRELESS_LAN: OvsWlanNode,
NodeTypes.TUNNEL: OvsTunnelNode,
NodeTypes.TAP_BRIDGE: OvsGreTapBridge,
NodeTypes.PEER_TO_PEER: OvsPtpNet,
NodeTypes.CONTROL_NET: OvsCtrlNet
}

View file

@ -0,0 +1,544 @@
"""
PhysicalNode class for including real systems in the emulated network.
"""
import logging
import os
import subprocess
import threading
from core import CoreCommandError, utils
from core import constants
from core.nodes.base import CoreNodeBase
from core.nodes.interface import CoreInterface
from core.emulator.enumerations import NodeTypes
from core.nodes.network import GreTap
from core.nodes.network import CoreNetwork
class PhysicalNode(CoreNodeBase):
def __init__(self, session, _id=None, name=None, nodedir=None, start=True):
CoreNodeBase.__init__(self, session, _id, name, start=start)
self.nodedir = nodedir
self.up = start
self.lock = threading.RLock()
self._mounts = []
if start:
self.startup()
def startup(self):
with self.lock:
self.makenodedir()
def shutdown(self):
if not self.up:
return
with self.lock:
while self._mounts:
_source, target = self._mounts.pop(-1)
self.umount(target)
for netif in self.netifs():
netif.shutdown()
self.rmnodedir()
def termcmdstring(self, sh="/bin/sh"):
"""
Create a terminal command string.
:param str sh: shell to execute command in
:return: str
"""
return sh
def cmd(self, args, wait=True):
"""
Runs shell command on node, with option to not wait for a result.
:param list[str]|str args: command to run
:param bool wait: wait for command to exit, defaults to True
:return: exit status for command
:rtype: int
"""
os.chdir(self.nodedir)
status = utils.cmd(args, wait)
return status
def cmd_output(self, args):
"""
Runs shell command on node and get exit status and output.
:param list[str]|str args: command to run
:return: exit status and combined stdout and stderr
:rtype: tuple[int, str]
"""
os.chdir(self.nodedir)
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
stdout, _ = p.communicate()
status = p.wait()
return status, stdout.strip()
def check_cmd(self, args):
"""
Runs shell command on node.
:param list[str]|str args: command to run
:return: combined stdout and stderr
:rtype: str
:raises CoreCommandError: when a non-zero exit status occurs
"""
status, output = self.cmd_output(args)
if status:
raise CoreCommandError(status, args, output)
return output.strip()
def shcmd(self, cmdstr, sh="/bin/sh"):
return self.cmd([sh, "-c", cmdstr])
def sethwaddr(self, ifindex, addr):
"""
same as SimpleLxcNode.sethwaddr()
"""
self._netif[ifindex].sethwaddr(addr)
ifname = self.ifname(ifindex)
if self.up:
self.check_cmd([constants.IP_BIN, "link", "set", "dev", ifname, "address", str(addr)])
def addaddr(self, ifindex, addr):
"""
same as SimpleLxcNode.addaddr()
"""
if self.up:
self.check_cmd([constants.IP_BIN, "addr", "add", str(addr), "dev", self.ifname(ifindex)])
self._netif[ifindex].addaddr(addr)
def deladdr(self, ifindex, addr):
"""
same as SimpleLxcNode.deladdr()
"""
try:
self._netif[ifindex].deladdr(addr)
except ValueError:
logging.exception("trying to delete unknown address: %s", addr)
if self.up:
self.check_cmd([constants.IP_BIN, "addr", "del", str(addr), "dev", self.ifname(ifindex)])
def adoptnetif(self, netif, ifindex, hwaddr, addrlist):
"""
The broker builds a GreTap tunnel device to this physical node.
When a link message is received linking this node to another part of
the emulation, no new interface is created; instead, adopt the
GreTap netif as the node interface.
"""
netif.name = "gt%d" % ifindex
netif.node = self
self.addnetif(netif, ifindex)
# use a more reasonable name, e.g. "gt0" instead of "gt.56286.150"
if self.up:
self.check_cmd([constants.IP_BIN, "link", "set", "dev", netif.localname, "down"])
self.check_cmd([constants.IP_BIN, "link", "set", netif.localname, "name", netif.name])
netif.localname = netif.name
if hwaddr:
self.sethwaddr(ifindex, hwaddr)
for addr in utils.make_tuple(addrlist):
self.addaddr(ifindex, addr)
if self.up:
self.check_cmd([constants.IP_BIN, "link", "set", "dev", netif.localname, "up"])
def linkconfig(self, netif, bw=None, delay=None, loss=None, duplicate=None, jitter=None, netif2=None):
"""
Apply tc queing disciplines using LxBrNet.linkconfig()
"""
# borrow the tc qdisc commands from LxBrNet.linkconfig()
linux_bridge = CoreNetwork(session=self.session, start=False)
linux_bridge.up = True
linux_bridge.linkconfig(netif, bw=bw, delay=delay, loss=loss, duplicate=duplicate, jitter=jitter, netif2=netif2)
del linux_bridge
def newifindex(self):
with self.lock:
while self.ifindex in self._netif:
self.ifindex += 1
ifindex = self.ifindex
self.ifindex += 1
return ifindex
def newnetif(self, net=None, addrlist=None, hwaddr=None, ifindex=None, ifname=None):
logging.info("creating interface")
if not addrlist:
addrlist = []
if self.up and net is None:
raise NotImplementedError
if ifindex is None:
ifindex = self.newifindex()
if self.up:
# this is reached when this node is linked to a network node
# tunnel to net not built yet, so build it now and adopt it
gt = self.session.broker.addnettunnel(net.id)
if gt is None or len(gt) != 1:
raise ValueError("error building tunnel from adding a new network interface: %s" % gt)
gt = gt[0]
net.detach(gt)
self.adoptnetif(gt, ifindex, hwaddr, addrlist)
return ifindex
# this is reached when configuring services (self.up=False)
if ifname is None:
ifname = "gt%d" % ifindex
netif = GreTap(node=self, name=ifname, session=self.session, start=False)
self.adoptnetif(netif, ifindex, hwaddr, addrlist)
return ifindex
def privatedir(self, path):
if path[0] != "/":
raise ValueError("path not fully qualified: %s" % path)
hostpath = os.path.join(self.nodedir, os.path.normpath(path).strip('/').replace('/', '.'))
os.mkdir(hostpath)
self.mount(hostpath, path)
def mount(self, source, target):
source = os.path.abspath(source)
logging.info("mounting %s at %s", source, target)
os.makedirs(target)
self.check_cmd([constants.MOUNT_BIN, "--bind", source, target])
self._mounts.append((source, target))
def umount(self, target):
logging.info("unmounting '%s'" % target)
try:
self.check_cmd([constants.UMOUNT_BIN, "-l", target])
except CoreCommandError:
logging.exception("unmounting failed for %s", target)
def opennodefile(self, filename, mode="w"):
dirname, basename = os.path.split(filename)
if not basename:
raise ValueError("no basename for filename: " + filename)
if dirname and dirname[0] == "/":
dirname = dirname[1:]
dirname = dirname.replace("/", ".")
dirname = os.path.join(self.nodedir, dirname)
if not os.path.isdir(dirname):
os.makedirs(dirname, mode=0755)
hostfilename = os.path.join(dirname, basename)
return open(hostfilename, mode)
def nodefile(self, filename, contents, mode=0644):
with self.opennodefile(filename, "w") as node_file:
node_file.write(contents)
os.chmod(node_file.name, mode)
logging.info("created nodefile: '%s'; mode: 0%o", node_file.name, mode)
class Rj45Node(CoreNodeBase, CoreInterface):
"""
RJ45Node is a physical interface on the host linked to the emulated
network.
"""
apitype = NodeTypes.RJ45.value
type = "rj45"
def __init__(self, session, _id=None, name=None, mtu=1500, start=True):
"""
Create an RJ45Node instance.
:param core.session.Session session: core session instance
:param int _id: node id
:param str name: node name
:param mtu: rj45 mtu
:param bool start: start flag
:return:
"""
CoreNodeBase.__init__(self, session, _id, name, start=start)
CoreInterface.__init__(self, node=self, name=name, mtu=mtu)
self.up = False
self.lock = threading.RLock()
self.ifindex = None
# the following are PyCoreNetIf attributes
self.transport_type = "raw"
self.localname = name
self.old_up = False
self.old_addrs = []
if start:
self.startup()
def startup(self):
"""
Set the interface in the up state.
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
# interface will also be marked up during net.attach()
self.savestate()
utils.check_cmd([constants.IP_BIN, "link", "set", self.localname, "up"])
self.up = True
def shutdown(self):
"""
Bring the interface down. Remove any addresses and queuing
disciplines.
:return: nothing
"""
if not self.up:
return
try:
utils.check_cmd([constants.IP_BIN, "link", "set", self.localname, "down"])
utils.check_cmd([constants.IP_BIN, "addr", "flush", "dev", self.localname])
utils.check_cmd([constants.TC_BIN, "qdisc", "del", "dev", self.localname, "root"])
except CoreCommandError:
logging.exception("error shutting down")
self.up = False
self.restorestate()
# TODO: issue in that both classes inherited from provide the same method with different signatures
def attachnet(self, net):
"""
Attach a network.
:param core.coreobj.PyCoreNet net: network to attach
:return: nothing
"""
CoreInterface.attachnet(self, net)
# TODO: issue in that both classes inherited from provide the same method with different signatures
def detachnet(self):
"""
Detach a network.
:return: nothing
"""
CoreInterface.detachnet(self)
def newnetif(self, net=None, addrlist=None, hwaddr=None, ifindex=None, ifname=None):
"""
This is called when linking with another node. Since this node
represents an interface, we do not create another object here,
but attach ourselves to the given network.
:param core.coreobj.PyCoreNet net: new network instance
:param list[str] addrlist: address list
:param str hwaddr: hardware address
:param int ifindex: interface index
:param str ifname: interface name
:return: interface index
:rtype: int
:raises ValueError: when an interface has already been created, one max
"""
with self.lock:
if ifindex is None:
ifindex = 0
if self.net is not None:
raise ValueError("RJ45 nodes support at most 1 network interface")
self._netif[ifindex] = self
# PyCoreNetIf.node is self
self.node = self
self.ifindex = ifindex
if net is not None:
self.attachnet(net)
if addrlist:
for addr in utils.make_tuple(addrlist):
self.addaddr(addr)
return ifindex
def delnetif(self, ifindex):
"""
Delete a network interface.
:param int ifindex: interface index to delete
:return: nothing
"""
if ifindex is None:
ifindex = 0
self._netif.pop(ifindex)
if ifindex == self.ifindex:
self.shutdown()
else:
raise ValueError("ifindex %s does not exist" % ifindex)
def netif(self, ifindex, net=None):
"""
This object is considered the network interface, so we only
return self here. This keeps the RJ45Node compatible with
real nodes.
:param int ifindex: interface index to retrieve
:param net: network to retrieve
:return: a network interface
:rtype: core.coreobj.PyCoreNetIf
"""
if net is not None and net == self.net:
return self
if ifindex is None:
ifindex = 0
if ifindex == self.ifindex:
return self
return None
def getifindex(self, netif):
"""
Retrieve network interface index.
:param core.coreobj.PyCoreNetIf netif: network interface to retrieve index for
:return: interface index, None otherwise
:rtype: int
"""
if netif != self:
return None
return self.ifindex
def addaddr(self, addr):
"""
Add address to to network interface.
:param str addr: address to add
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
if self.up:
utils.check_cmd([constants.IP_BIN, "addr", "add", str(addr), "dev", self.name])
CoreInterface.addaddr(self, addr)
def deladdr(self, addr):
"""
Delete address from network interface.
:param str addr: address to delete
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
if self.up:
utils.check_cmd([constants.IP_BIN, "addr", "del", str(addr), "dev", self.name])
CoreInterface.deladdr(self, addr)
def savestate(self):
"""
Save the addresses and other interface state before using the
interface for emulation purposes. TODO: save/restore the PROMISC flag
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
self.old_up = False
self.old_addrs = []
args = [constants.IP_BIN, "addr", "show", "dev", self.localname]
output = utils.check_cmd(args)
for line in output.split("\n"):
items = line.split()
if len(items) < 2:
continue
if items[1] == "%s:" % self.localname:
flags = items[2][1:-1].split(",")
if "UP" in flags:
self.old_up = True
elif items[0] == "inet":
self.old_addrs.append((items[1], items[3]))
elif items[0] == "inet6":
if items[1][:4] == "fe80":
continue
self.old_addrs.append((items[1], None))
def restorestate(self):
"""
Restore the addresses and other interface state after using it.
:return: nothing
:raises CoreCommandError: when there is a command exception
"""
for addr in self.old_addrs:
if addr[1] is None:
utils.check_cmd([constants.IP_BIN, "addr", "add", addr[0], "dev", self.localname])
else:
utils.check_cmd([constants.IP_BIN, "addr", "add", addr[0], "brd", addr[1], "dev", self.localname])
if self.old_up:
utils.check_cmd([constants.IP_BIN, "link", "set", self.localname, "up"])
def setposition(self, x=None, y=None, z=None):
"""
Uses setposition from both parent classes.
:param float x: x position
:param float y: y position
:param float z: z position
:return: True if position changed, False otherwise
:rtype: bool
"""
result = CoreNodeBase.setposition(self, x, y, z)
CoreInterface.setposition(self, x, y, z)
return result
def check_cmd(self, args):
"""
Runs shell command on node.
:param list[str]|str args: command to run
:return: exist status and combined stdout and stderr
:rtype: tuple[int, str]
:raises CoreCommandError: when a non-zero exit status occurs
"""
raise NotImplementedError
def cmd(self, args, wait=True):
"""
Runs shell command on node, with option to not wait for a result.
:param list[str]|str args: command to run
:param bool wait: wait for command to exit, defaults to True
:return: exit status for command
:rtype: int
"""
raise NotImplementedError
def cmd_output(self, args):
"""
Runs shell command on node and get exit status and output.
:param list[str]|str args: command to run
:return: exit status and combined stdout and stderr
:rtype: tuple[int, str]
"""
raise NotImplementedError
def termcmdstring(self, sh):
"""
Create a terminal command string.
:param str sh: shell to execute command in
:return: str
"""
raise NotImplementedError