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:
parent
9517740704
commit
66e603906a
100 changed files with 10283 additions and 3489 deletions
0
daemon/core/nodes/__init__.py
Normal file
0
daemon/core/nodes/__init__.py
Normal file
1158
daemon/core/nodes/base.py
Normal 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
293
daemon/core/nodes/client.py
Normal 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
|
482
daemon/core/nodes/interface.py
Normal file
482
daemon/core/nodes/interface.py
Normal 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 []
|
446
daemon/core/nodes/ipaddress.py
Normal file
446
daemon/core/nodes/ipaddress.py
Normal 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
1069
daemon/core/nodes/network.py
Normal file
File diff suppressed because it is too large
Load diff
29
daemon/core/nodes/nodemaps.py
Normal file
29
daemon/core/nodes/nodemaps.py
Normal 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
|
||||
}
|
97
daemon/core/nodes/nodeutils.py
Normal file
97
daemon/core/nodes/nodeutils.py
Normal 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))
|
722
daemon/core/nodes/openvswitch.py
Normal file
722
daemon/core/nodes/openvswitch.py
Normal 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
|
||||
}
|
544
daemon/core/nodes/physical.py
Normal file
544
daemon/core/nodes/physical.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue