From: Ondrej Lichtner <olichtne(a)redhat.com>
This package contains the implementation of Device
configuration/manipulation classes that are currently supported by LNST.
This implementation has been moved into a separate package that will be
distributed together with the Controller package. These will then be
sent to the Slave during recipe execution so the slave can use them when
required.
The reasoning behind this is that it is often easier to update or make
changes on the Controller than on all the Slaves. Making the Slave
simpler and independent of the implementation of device configuration
means we don't need to update all our slaves as often and also that
changes to the Device configuration implementation don't require any
changes in Slave or Controller code.
Signed-off-by: Ondrej Lichtner <olichtne(a)redhat.com>
---
lnst/Devices/BondDevice.py | 48 ++++++
lnst/Devices/BridgeDevice.py | 49 ++++++
lnst/Devices/Device.py | 368 ++++++++++++++++++++++++++++++++++++++++
lnst/Devices/MacvlanDevice.py | 41 +++++
lnst/Devices/OvsBridgeDevice.py | 116 +++++++++++++
lnst/Devices/RemoteDevice.py | 90 ++++++++++
lnst/Devices/SoftDevice.py | 51 ++++++
lnst/Devices/TeamDevice.py | 64 +++++++
lnst/Devices/VethDevice.py | 59 +++++++
lnst/Devices/VethPair.py | 24 +++
lnst/Devices/VirtualDevice.py | 99 +++++++++++
lnst/Devices/VlanDevice.py | 38 +++++
lnst/Devices/VtiDevice.py | 71 ++++++++
lnst/Devices/VxlanDevice.py | 66 +++++++
lnst/Devices/__init__.py | 45 +++++
15 files changed, 1229 insertions(+)
create mode 100644 lnst/Devices/BondDevice.py
create mode 100644 lnst/Devices/BridgeDevice.py
create mode 100644 lnst/Devices/Device.py
create mode 100644 lnst/Devices/MacvlanDevice.py
create mode 100644 lnst/Devices/OvsBridgeDevice.py
create mode 100644 lnst/Devices/RemoteDevice.py
create mode 100644 lnst/Devices/SoftDevice.py
create mode 100644 lnst/Devices/TeamDevice.py
create mode 100644 lnst/Devices/VethDevice.py
create mode 100644 lnst/Devices/VethPair.py
create mode 100644 lnst/Devices/VirtualDevice.py
create mode 100644 lnst/Devices/VlanDevice.py
create mode 100644 lnst/Devices/VtiDevice.py
create mode 100644 lnst/Devices/VxlanDevice.py
create mode 100644 lnst/Devices/__init__.py
diff --git a/lnst/Devices/BondDevice.py b/lnst/Devices/BondDevice.py
new file mode 100644
index 0000000..9f17863
--- /dev/null
+++ b/lnst/Devices/BondDevice.py
@@ -0,0 +1,48 @@
+"""
+Defines the BondDevice class.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+import logging
+from lnst.Common.ExecCmd import exec_cmd
+from lnst.Devices.SoftDevice import SoftDevice
+
+class BondDevice(SoftDevice):
+ _name_template = "t_bond"
+
+ _modulename = "bonding"
+ _moduleparams = "max_bonds=0"
+
+ def create(self):
+ exec_cmd("ip link add %s type bond" % self.name)
+
+ def slave_add(self, dev):
+ dev.master_set(self)
+
+ def slave_del(self, dev):
+ dev.master_set(None)
+
+ def _get_bond_dir(self):
+ return "/sys/class/net/%s/bonding" % self.name
+
+ def set_option(self, option, value):
+ if option == "primary":
+ '''
+ "primary" option is not direct value but it's
+ a Device reference
+ '''
+ value = value.name
+ exec_cmd('echo "%s" > %s/%s' % (value,
+ self._get_bond_dir(),
+ option))
+
+ def set_options(self, options):
+ for option, value in options:
+ self.set_option(option, value)
diff --git a/lnst/Devices/BridgeDevice.py b/lnst/Devices/BridgeDevice.py
new file mode 100644
index 0000000..4babe8a
--- /dev/null
+++ b/lnst/Devices/BridgeDevice.py
@@ -0,0 +1,49 @@
+"""
+Defines the BridgeDevice class.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+import logging
+from lnst.Common.ExecCmd import exec_cmd
+from lnst.Devices.SoftDevice import SoftDevice
+
+class BridgeDevice(SoftDevice):
+ _name_template = "t_br"
+
+ @property
+ def slaves(self):
+ ret = []
+
+ for dev in self._if_manager.get_devices():
+ if dev.master is self:
+ ret.append(dev)
+ return ret
+
+ def create(self):
+ exec_cmd("ip link add dev {} type bridge".format(self._name))
+
+ def slave_add(self, dev):
+ dev.master_set(self)
+
+ def slave_del(self, dev):
+ dev.master_set(None)
+
+ def _get_bridge_dir(self):
+ return "/sys/class/net/%s/bridge" % self.name
+
+ def set_option(self, option, value):
+ #TODO redo to work with iproute
+ exec_cmd('echo "%s" > %s/%s' % (value,
+ self._get_bridge_dir(),
+ option))
+
+ def set_options(self, options):
+ for option, value in options:
+ self.set_option(option, value)
diff --git a/lnst/Devices/Device.py b/lnst/Devices/Device.py
new file mode 100644
index 0000000..ec182c7
--- /dev/null
+++ b/lnst/Devices/Device.py
@@ -0,0 +1,368 @@
+"""
+Defines the Device class implementing the common methods for all device types.
+Every other device type needs to inherit from this class.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+import re
+import logging
+from socket import AF_INET, AF_INET6
+from lnst.Common.NetUtils import normalize_hwaddr
+from lnst.Common.ExecCmd import exec_cmd
+from lnst.Common.DeviceError import DeviceError, DeviceDeleted
+from lnst.Common.IpAddress import Ip4Address, Ip6Address, IpAddress
+
+try:
+ from pyroute2.netlink.iproute import RTM_NEWLINK
+ from pyroute2.netlink.iproute import RTM_NEWADDR
+ from pyroute2.netlink.iproute import RTM_DELADDR
+except ImportError:
+ from pyroute2.iproute import RTM_NEWLINK
+ from pyroute2.iproute import RTM_NEWADDR
+ from pyroute2.iproute import RTM_DELADDR
+
+class Device(object):
+ """The base Device class
+
+ Implemented using the pyroute2 package to access different attributes of
+ a kernel netdevice object.
+ Changing attributes of a netdevice is right now implemented by calling
+ shell commands (e.g. from iproute2 package).
+
+ The Controller-Slave communication is implemented in such a way that all
+ public methods defined in this and derived class are directly available
+ as a tester facing API.
+ """
+ def __init__(self, if_manager):
+ self.if_index = None
+ self._nl_msg = None
+ self._devlink = None
+ self._if_manager = if_manager
+ self._enabled = True
+ self._deleted = False
+
+ self._ip_addrs = []
+ # TODO self._netns = None ???
+
+ def create(self):
+ """Creates a new netdevice of the corresponding type
+
+ Method to be implemented by derived classes where applicable.
+ """
+ msg = "Can't create a hardware ethernet device."
+ raise DeviceError(msg)
+
+ def destroy(self):
+ """Destroys the netdevice of the corresponding type
+
+ For the basic eth device it calls the destroy method of it's master
+ device (if there is one). Flushes the configured IP addresses and sets
+ the device 'down'.
+ """
+ if self.master:
+ self.master.destroy()
+ self.ip_flush()
+ self.down()
+ return True
+
+ def enable(self):
+ """Enables the Device object"""
+ self._enabled = True
+
+ def disable(self):
+ """Disables the Device object
+
+ When a Device object is disabled, any calls to it's methods will result
+ in a "no operation", however attribute access will still work.
+
+ The justification for this is to disable the Device used by the
+ Controller-Slave connection to avoid accidental disconnects.
+ """
+ self._enabled = False
+
+ def __getattribute__(self, name):
+ what = object.__getattribute__(self, name)
+
+ if object.__getattribute__(self, "_deleted"):
+ raise DeviceDeleted()
+
+ if not callable(what):
+ return what
+ else:
+ if (object.__getattribute__(self, "_enabled") or
+ name in ["enable", "disable"]):
+ return what
+ else:
+ def noop(*args, **kwargs):
+ pass
+ return noop
+
+ def _set_devlink(self, devlink_port_data):
+ self._devlink = devlink_port_data
+
+ def _init_netlink(self, nl_msg):
+ self.if_index = nl_msg['index']
+
+ self._nl_msg = nl_msg
+
+ def _update_netlink(self, nl_msg):
+ if self.if_index != nl_msg['index']:
+ msg = "if_index of netlink message (%s) doesn't match "\
+ "the device's (%s)." % (nl_msg['index'],
self.if_index)
+ raise DeviceError(msg)
+
+ if nl_msg['header']['type'] == RTM_NEWLINK:
+ if self.if_index != nl_msg['index']:
+ raise DeviceError("RTM_NEWLINK message passed to incorrect "\
+ "Device object.")
+
+ self._nl_msg = nl_msg
+ elif nl_msg['header']['type'] == RTM_NEWADDR:
+ if self.if_index != nl_msg['index']:
+ raise DeviceError("RTM_NEWADDR message passed to incorrect "\
+ "Device object.")
+
+ addr = IpAddress(nl_msg.get_attr('IFA_ADDRESS'))
+ addr.prefixlen = nl_msg["prefixlen"]
+
+ if addr not in self._ip_addrs:
+ self._ip_addrs.append(addr)
+ elif nl_msg['header']['type'] == RTM_DELADDR:
+ if self.if_index != nl_msg['index']:
+ raise DeviceError("RTM_DELADDR message passed to incorrect "\
+ "Device object.")
+
+ addr = IpAddress(nl_msg.get_attr('IFA_ADDRESS'))
+ addr.prefixlen = nl_msg["prefixlen"]
+
+ if addr in self._ip_addrs:
+ self._ip_addrs.remove(addr)
+
+ @property
+ def ifi_type(self):
+ """ifi_type attribute
+
+ Returns the integer type of the device as reported by the kernel.
+ """
+ return self._nl_msg['ifi_type']
+
+ @property
+ def name(self):
+ """name attribute
+
+ Returns string name of the device as reported by the kernel.
+ """
+ return self._nl_msg.get_attr("IFLA_IFNAME")
+
+ @property
+ def hwaddr(self):
+ """hwaddr attribute
+
+ Returns string hardware address of the device as reported by the kernel.
+ """
+ return normalize_hwaddr(self._nl_msg.get_attr("IFLA_ADDRESS"))
+
+ @property
+ def state(self):
+ """state attribute
+
+ Returns string state of the device as reported by the kernel.
+ """
+ #TODO check flags for admin up and lower up!!!
+ #TODO or expand this to check all possibilities?
+ #TODO also, add passive wait until lower up, with timeout
+ return self._nl_msg.get_attr("IFLA_OPERSTATE")
+
+ @property
+ def ips(self):
+ """list of configured ip addresses
+
+ Returns list of BaseIpAddress objects.
+ """
+ return self._ip_addrs
+
+ @property
+ def mtu(self):
+ """mtu attribute
+
+ Returns integer mtu as reported by the kernel.
+ """
+ return self._nl_msg.get_attr("IFLA_MTU")
+
+ @property
+ def master(self):
+ """master device
+
+ Returns Device object of the master device or None when the device has
+ no master.
+ """
+ master_if_index = self._nl_msg.get_attr("IFLA_MASTER")
+ if master_if_index is not None:
+ return self._if_manager.get_device(master_if_index)
+ else:
+ return None
+
+ @property
+ def driver(self):
+ """driver attribute
+
+ Returns string name of the device driver based on an ethtool -i call
+ """
+ if self.ifi_type == 772: #loopback ifi type
+ return 'loopback'
+ out, _ = exec_cmd("ethtool -i %s" % self.name, False, False, False)
+ match = re.search("^driver: (.*)$", out, re.MULTILINE)
+ if match is not None:
+ return match.group(1)
+ else:
+ return None
+
+ @property
+ def link_stats(self):
+ """Link statistics
+
+ Returns dictionary of interface statistics returned by ip -s link show
+ """
+ stats = {"devname": self.name,
+ "hwaddr": self.hwaddr}
+ out, _ = exec_cmd("ip -s link show %s" % self.name)
+ lines = iter(out.split("\n"))
+ for line in lines:
+ if (len(line.split()) == 0):
+ continue
+ if (line.split()[0] == "RX:"):
+ rx_stats = map(int, lines.next().split())
+ stats.update({"rx_bytes" : rx_stats[0],
+ "rx_packets": rx_stats[1],
+ "rx_errors" : rx_stats[2],
+ "rx_dropped": rx_stats[3],
+ "rx_overrun": rx_stats[4],
+ "rx_mcast" : rx_stats[5]})
+ if (line.split()[0] == "TX:"):
+ tx_stats = map(int, lines.next().split())
+ stats.update({"tx_bytes" : tx_stats[0],
+ "tx_packets": tx_stats[1],
+ "tx_errors" : tx_stats[2],
+ "tx_dropped": tx_stats[3],
+ "tx_carrier": tx_stats[4],
+ "tx_collsns": tx_stats[5]})
+ return stats
+
+ def _clear_ips(self):
+ self._ip_addrs = []
+
+ def _clear_tc_qdisc(self):
+ exec_cmd("tc qdisc replace dev %s root pfifo" % self.name)
+ out, _ = exec_cmd("tc filter show dev %s" % self.name)
+ ingress_handles = re.findall("ingress (\\d+):", out)
+ for ingress_handle in ingress_handles:
+ exec_cmd("tc qdisc del dev %s handle %s: ingress" %
+ (self.name, ingress_handle))
+ out, _ = exec_cmd("tc qdisc show dev %s" % self.name)
+ ingress_qdiscs = re.findall("qdisc ingress (\\w+):", out)
+ if len(ingress_qdiscs) != 0:
+ exec_cmd("tc qdisc del dev %s ingress" % self.name)
+
+ def _clear_tc_filters(self):
+ out, _ = exec_cmd("tc filter show dev %s" % self.name)
+ egress_prefs = re.findall("pref (\\d+) .* handle", out)
+
+ for egress_pref in egress_prefs:
+ exec_cmd("tc filter del dev %s pref %s" % (self.name,
+ egress_pref))
+
+ def ip_add(self, addr):
+ """add an ip address
+
+ Args:
+ addr -- accepts a BaseIpAddress object
+ """
+ if addr not in self.ips:
+ exec_cmd("ip addr add %s/%d dev %s" % (addr, addr.prefixlen,
+ self.name))
+
+ def ip_del(self, addr):
+ """remove an ip address
+
+ Args:
+ addr -- accepts a BaseIpAddress object
+ """
+ if addr in self.ips:
+ exec_cmd("ip addr del %s/%d dev %s" % (addr, addr.prefixlen,
+ self.name))
+
+ def ip_flush(self):
+ """flush all ip addresses of the device"""
+ for ip in self.ips:
+ self.ip_del(ip)
+
+ def master_set(self, dev):
+ """set dev as the master of this device
+
+ Args:
+ dev -- accepts a Device object of the master object.
+ When None, removes the current master from the Device."""
+ if isinstance(dev, Device):
+ exec_cmd("ip link set %s master %s" % (self.name, dev.name))
+ elif dev is None:
+ exec_cmd("ip link set %s nomaster" % self.name)
+ else:
+ raise DeviceError("Invalid dev argument.")
+
+ def up(self):
+ """set device up"""
+ exec_cmd("ip link set %s up" % self.name)
+
+ def down(self):
+ """set device down"""
+ exec_cmd("ip link set %s down" % self.name)
+
+ def add_route(self, dest):
+ """add specified route for this device
+
+ Args:
+ dest -- string accepted by the "ip route add " command
+ """
+ exec_cmd("ip route add %s dev %s" % (dest, self.name))
+
+ def del_route(self, dest):
+ """remove specified route for this device
+
+ Args:
+ dest -- string accepted by the "ip route del " command
+ """
+ exec_cmd("ip route del %s dev %s" % (dest, self.name))
+
+ def _get_if_data(self):
+ if_data = {"if_index": self.if_index,
+ "hwaddr": self.hwaddr,
+ "name": self.name,
+ "ip_addrs": self.ips,
+ "ifi_type": self.ifi_type,
+ "state": self.state,
+ "master": self.master,
+ "mtu": self.mtu,
+ "driver": self.driver,
+ "devlink": self._devlink}
+ return if_data
+
+ def set_speed(self, speed):
+ """set the device speed
+
+ Also disables automatic speed negotiation
+
+ Args:
+ speed -- string accepted by the 'ethtool -s dev speed ' command
+ """
+ exec_cmd("ethtool -s %s speed %s autoneg off" % (self.name, speed))
+
+ def set_autoneg(self):
+ """enable automatic negotiation of speed for this
device"""
+ exec_cmd("ethtool -s %s autoneg on" % self.name)
diff --git a/lnst/Devices/MacvlanDevice.py b/lnst/Devices/MacvlanDevice.py
new file mode 100644
index 0000000..9d2cb34
--- /dev/null
+++ b/lnst/Devices/MacvlanDevice.py
@@ -0,0 +1,41 @@
+"""
+Defines the MacvlanDevice class
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+import logging
+from lnst.Common.ExecCmd import exec_cmd
+from lnst.Devices.SoftDevice import SoftDevice
+
+class MacvlanDevice(SoftDevice):
+ _name_template = "t_macvlan"
+
+ _modulename = "macvlan"
+
+ def __init__(self, ifmanager, *args, **kwargs):
+ super(MacvlanDevice, self).__init__(ifmanager, args, kwargs)
+
+ self._real_dev = kwargs["realdev"]
+ self._mode = kwargs.get("mode", None)
+ self._hwaddr = kwargs.get("hwaddr", None)
+
+ def create(self):
+ create_cmd = "ip link add link {} {}".format(self._real_dev.name,
+ self.name)
+
+ if self._hwaddr is not None:
+ create_cmd += " address {}".format(self._hwaddr)
+
+ if self._mode is not None:
+ create_cmd += " mode {}".format(self._mode)
+
+ create_cmd += " type macvlan"
+
+ exec_cmd(create_cmd)
diff --git a/lnst/Devices/OvsBridgeDevice.py b/lnst/Devices/OvsBridgeDevice.py
new file mode 100644
index 0000000..a948e69
--- /dev/null
+++ b/lnst/Devices/OvsBridgeDevice.py
@@ -0,0 +1,116 @@
+"""
+Defines the OvsBridgeDevice class.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+import logging
+from lnst.Common.ExecCmd import exec_cmd
+from lnst.Devices.Device import Device, DeviceError
+from lnst.Devices.SoftDevice import SoftDevice
+
+class OvsBridgeDevice(SoftDevice):
+ _name_template = "t_ovsbr"
+
+ _modulename = "openvswitch"
+
+ @classmethod
+ def _type_init(cls):
+ if cls._modulename and not cls._type_initialized:
+ exec_cmd("modprobe %s %s" % (cls._modulename, cls._moduleparams))
+
+ exec_cmd("mkdir -p /var/run/openvswitch/")
+ exec_cmd("ovsdb-server --detach --pidfile "\
+ "--remote=punix:/var/run/openvswitch/db.sock",
+ die_on_err=False)
+ exec_cmd("ovs-vswitchd --detach --pidfile", die_on_err=False)
+
+ cls._type_initialized = True
+
+ def create(self):
+ exec_cmd("ovs-vsctl add-br %s" % self.name)
+
+ def destroy(self):
+ exec_cmd("ovs-vsctl del-br %s" % self.name)
+
+ def port_add(self, dev, **kwargs):
+ options = ""
+ for opt_name, opt_value in kwargs.items():
+ options += " %s=%s" % (opt_name, opt_value)
+
+ exec_cmd("ovs-vsctl add-port %s %s%s" % (self.name, dev.name,
options))
+
+ def port_del(self, dev):
+ if isinstance(dev, Device):
+ exec_cmd("ovs-vsctl del-port %s %s" % (self.name, dev.name))
+ elif isinstance(dev, str):
+ exec_cmd("ovs-vsctl del-port %s %s" % (self.name, dev))
+ else:
+ raise DeviceError("Invalid port_del argument %s" % str(dev))
+
+ def bond_add(self, port_name, devices, **kwargs):
+ dev_names = ""
+ for dev in devices:
+ dev_names += " %s" % dev.name
+
+ options = ""
+ for opt_name, opt_value in kwargs.items():
+ options += " %s=%s" % (opt_name, opt_value)
+
+ exec_cmd("ovs-vsctl add-bond %s %s %s %s" % (self.name, port_name,
+ dev_names, options))
+
+ def bond_del(self, dev):
+ self.port_del(dev)
+
+ def internal_port_add(self, **kwargs):
+ name = self._if_manager.assign_name("int")
+
+ options = ""
+ for opt_name, opt_value in kwargs.items():
+ if opt_name == "name":
+ name = opt_value
+ continue
+
+ options += " %s=%s" % (opt_name, opt_value)
+
+ exec_cmd("ovs-vsctl add-port %s %s -- set Interface %s "\
+ "type=internal %s" % (self.name, name,
+ name, options))
+
+ dev = self._if_manager.get_device_by_name(name)
+ return dev
+
+ def tunnel_add(self, tunnel_type, options):
+ name = self._if_manager.assign_name(tunnel_type)
+
+ options = ""
+ for opt_name, opt_value in options.items():
+ if opt_name == "name":
+ name = opt_value
+ continue
+
+ options += " %s=%s" % (opt_name, opt_value)
+
+ exec_cmd("ovs-vsctl add-port %s %s -- set Interface %s "\
+ "type=%s %s" % (self.name, name, name,
+ tunnel_type, options))
+
+ def tunnel_del(self, name):
+ self.port_del(name)
+
+ def flow_add(self, entry):
+ exec_cmd("ovs-ofctl add-flow %s '%s'" % (self.name, entry))
+
+ def flows_add(self, entries):
+ for entry in entries:
+ self.flow_add(entry)
+
+ def flows_del(self, entry):
+ exec_cmd("ovs-ofctl del-flows %s" % (self.name))
diff --git a/lnst/Devices/RemoteDevice.py b/lnst/Devices/RemoteDevice.py
new file mode 100644
index 0000000..c6d3060
--- /dev/null
+++ b/lnst/Devices/RemoteDevice.py
@@ -0,0 +1,90 @@
+"""
+Defines the RemoteDevice class. This class wraps all other Device classes
+when creating device instances on the Controller.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+from lnst.Common.DeviceRef import DeviceRef
+from lnst.Common.DeviceError import DeviceError, DeviceDeleted
+
+def remotedev_decorator(cls):
+ def func(*args, **kwargs):
+ return RemoteDevice(cls, args, kwargs)
+ return func
+
+class RemoteDevice(object):
+ """Wraps all other Device classes on the Controller
+
+ Ensures that all public methods of Device objects also act as the tester
+ facing API even though the Device objects are instantiated on the Slave,
+ not where the recipe script is actually running.
+ """
+ def __init__(self, dev_cls, args=[], kwargs={}):
+ self.__dev_cls = dev_cls
+ self.__dev_args = args
+ self.__dev_kwargs = kwargs
+
+ self.host = None
+ self.if_index = None
+ self.deleted = False
+
+ @property
+ def _dev_cls(self):
+ return self.__dev_cls
+
+ @property
+ def _dev_args(self):
+ return self.__dev_args
+
+ @property
+ def _dev_kwargs(self):
+ return self.__dev_kwargs
+
+ def _get_dev_cls(self):
+ return self._dev_cls
+
+ def __getattr__(self, name):
+ attr = getattr(self._dev_cls, name)
+
+ if self.deleted:
+ raise DeviceDeleted("This device was deleted on the slave and does not
exist anymore.")
+
+ if callable(attr):
+ def dev_method(*args, **kwargs):
+ return self.host.rpc_call("dev_method", self.if_index,
+ name, args, kwargs)
+ return dev_method
+ else:
+ return self.host.rpc_call("dev_attr", self.if_index, name)
+
+ def __iter__(self):
+ for x in dir(self._dev_cls):
+ if x[0] == '_' or x[0:1] == "__":
+ continue
+ attr = getattr(self._dev_cls, x)
+
+ if not callable(attr):
+ yield (x, getattr(self, x))
+
+ def _match_update_data(self, data):
+ return False
+
+class PairedRemoteDevice(RemoteDevice):
+ """RemoteDevice class for paired Devices (such as
veth)"""
+ def __init__(self, peer, dev_cls, args=[], kwargs={}):
+ super(PairedRemoteDevice, self).__init__(dev_cls, args, kwargs)
+
+ self._peer = peer
+
+ @property
+ def _dev_kwargs(self):
+ ret = super(PairedRemoteDevice, self)._dev_kwargs
+ ret["peer_if_id"] = self._peer.if_index
+ return ret
diff --git a/lnst/Devices/SoftDevice.py b/lnst/Devices/SoftDevice.py
new file mode 100644
index 0000000..3dc3b7f
--- /dev/null
+++ b/lnst/Devices/SoftDevice.py
@@ -0,0 +1,51 @@
+"""
+Defines the SoftDevice class.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+from lnst.Common.ExecCmd import exec_cmd
+from lnst.Common.DeviceError import DeviceError
+from lnst.Devices.Device import Device
+
+class SoftDevice(Device):
+ _name_template = "soft_dev"
+
+ _modulename = ""
+ _moduleparams = ""
+ _type_initialized = False
+
+ def __init__(self, ifmanager, *args, **kwargs):
+ super(SoftDevice, self).__init__(ifmanager)
+
+ self._name = kwargs.get("name", None)
+ if self._name is None:
+ self._name = ifmanager.assign_name(self._name_template)
+
+ self._type_init()
+
+ @classmethod
+ def _type_init(cls):
+ if cls._modulename and not cls._type_initialized:
+ exec_cmd("modprobe %s %s" % (cls._modulename, cls._moduleparams))
+ cls._type_initialized = True
+
+ @property
+ def name(self):
+ try:
+ return super(SoftDevice, self).name
+ except:
+ return self._name
+
+ def create(self):
+ msg = "Classes derived from SoftDevice MUST define a create method."
+ raise DeviceError(msg)
+
+ def destroy(self):
+ exec_cmd("ip link del dev %s" % self.name)
diff --git a/lnst/Devices/TeamDevice.py b/lnst/Devices/TeamDevice.py
new file mode 100644
index 0000000..86a3d6a
--- /dev/null
+++ b/lnst/Devices/TeamDevice.py
@@ -0,0 +1,64 @@
+"""
+Defines the TeamDevice class.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+import re
+import logging
+from lnst.Common.ExecCmd import exec_cmd
+from lnst.Common.Utils import bool_it
+from lnst.Devices.SoftDevice import SoftDevice
+
+def prepare_json_str(json_str):
+ if not json_str:
+ return "{}"
+ json_str = json_str.replace('"', '\\"')
+ json_str = re.sub('\s+', ' ', json_str)
+ return json_str
+
+class TeamDevice(SoftDevice):
+ _name_template = "t_team"
+
+ def __init__(self, ifmanager, *args, **kwargs):
+ super(TeamDevice, self).__init__(ifmanager, args, kwargs)
+
+ self._config = kwargs.get("config", None)
+ self._dbus = not bool_it(kwargs.get("disable_dbus", False))
+
+ @property
+ def config(self):
+ return self._config
+
+ @property
+ def dbus(self):
+ return self._dbus
+
+ def create(self):
+ teamd_config = prepare_json_str(self.config)
+
+ exec_cmd("teamd -r -d -c \"%s\" -t %s %s" %\
+ (teamd_config,
+ self.name,
+ " -D" if self.dbus else ""))
+
+ def destroy(self):
+ exec_cmd("teamd -k -t %s" % self.name)
+
+ def slave_add(self, dev, port_config=None):
+ exec_cmd("teamdctl %s %s port config update %s \"%s\"" %\
+ (" -D" if self.dbus else "",
+ self.name,
+ dev.name,
+ prepare_json_str(port_config)))
+
+ dev.master_set(self)
+
+ def slave_del(self, dev):
+ dev.master_set(None)
diff --git a/lnst/Devices/VethDevice.py b/lnst/Devices/VethDevice.py
new file mode 100644
index 0000000..92aba28
--- /dev/null
+++ b/lnst/Devices/VethDevice.py
@@ -0,0 +1,59 @@
+"""
+Defines the VethDevice and PairedVethDevice classes.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+import logging
+from copy import deepcopy
+from lnst.Common.ExecCmd import exec_cmd
+from lnst.Devices.Device import Device
+from lnst.Devices.SoftDevice import SoftDevice
+
+class VethDevice(SoftDevice):
+ _name_template = "veth"
+
+ def __init__(self, ifmanager, *args, **kwargs):
+ super(VethDevice, self).__init__(ifmanager, args, kwargs)
+
+ self._name = kwargs.get("name", None)
+ self._peer_name = kwargs.get("peer_name", None)
+
+ if self._name is None:
+ self._name = ifmanager.assign_name(self._name_template)
+
+ if self._peer_name is None:
+ self._peer_name =
ifmanager.assign_name("peer_"+self._name_template)
+
+ def create(self):
+ exec_cmd("ip link add {name} type veth peer name {peer}".
+ format(name=self.name,
+ peer=self._peer_name))
+
+ @property
+ def peer(self):
+ if self._nl_msg is None:
+ return None
+
+ peer_if_id = self._nl_msg.get_attr("IFLA_LINK")
+ return self._if_manager.get_device(peer_if_id)
+
+class PairedVethDevice(VethDevice):
+ def __init__(self, ifmanager, *args, **kwargs):
+ Device.__init__(self, ifmanager)
+
+ self._peer_if_id = kwargs["peer_if_id"]
+
+ def create(self):
+ peer = self._if_manager.get_device(self._peer_if_id)
+ me = peer.peer
+ self._init_netlink(me._nl_msg)
+ self._ip_addrs = deepcopy(me._ip_addrs)
+
+ self._if_manager.replace_dev(self.if_index, self)
diff --git a/lnst/Devices/VethPair.py b/lnst/Devices/VethPair.py
new file mode 100644
index 0000000..2cf3ff1
--- /dev/null
+++ b/lnst/Devices/VethPair.py
@@ -0,0 +1,24 @@
+"""
+Defines the VethPair factory method.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+from lnst.Devices.VethDevice import VethDevice, PairedVethDevice
+from lnst.Devices.RemoteDevice import RemoteDevice, PairedRemoteDevice
+
+def VethPair(*args, **kwargs):
+ """Creates a pair of Veth Devices
+
+ Args:
+ args, kwargs passed to the VethDevice constructor on the Slave.
+ """
+ first = RemoteDevice(VethDevice, args, kwargs)
+ second = PairedRemoteDevice(first, PairedVethDevice)
+ return (first, second)
diff --git a/lnst/Devices/VirtualDevice.py b/lnst/Devices/VirtualDevice.py
new file mode 100644
index 0000000..a0be66b
--- /dev/null
+++ b/lnst/Devices/VirtualDevice.py
@@ -0,0 +1,99 @@
+"""
+Defines the VirtualDevice class.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+import logging
+from time import sleep
+from functools import wraps
+from lnst.Common.Utils import check_process_running
+from lnst.Common.NetUtils import normalize_hwaddr
+from lnst.Devices.Device import Device, DeviceError
+from lnst.Devices.RemoteDevice import RemoteDevice
+
+# conditional support for libvirt
+if check_process_running("libvirtd"):
+ from lnst.Controller.VirtUtils import VirtNetCtl
+
+class VirtualDevice(RemoteDevice):
+ """Remote eth device created on the controller through libvirt
+
+ To support creation of new devices on virtual machines, we derive from
+ the RemoteDevice class and override the create method (which would be
+ remotely called on the slave for the Device class type). Instead, the
+ create method is called on the controller where libvirt is running.
+
+ The Tester shouldn't create instances of this class. They're created
+ automatically if matching virtual machines is allowed.
+
+ Theoretically if a match is virtual the tester could also dynamically add
+ devices during test execution, however this is NOT SUPPORTED at the moment.
+ """
+ def __init__(self, network, driver=None, hwaddr=None):
+ super(VirtualDevice, self).__init__(Device, None, None)
+
+ self.virt_driver = driver if driver is not None else "virtio"
+ self.orig_hwaddr = hwaddr
+ self._network = network
+
+ @property
+ def network(self):
+ return self._network
+
+ @network.setter
+ def network(self, network):
+ self._network = network
+
+ def _match_update_data(self, data):
+ if self.orig_hwaddr == data["hwaddr"]:
+ return True
+
+ return super(VirtualDevice, self)._match_update_data(data)
+
+ def create(self):
+ domain_ctl = self.host.get_domain_ctl()
+
+ if self.orig_hwaddr:
+ if self.host.get_dev_by_hwaddr(self.orig_hwaddr):
+ msg = "Device with hwaddr %s already exists" %
self.orig_hwaddr
+ raise DeviceError(msg)
+ else:
+ mac_pool = self.host.get_mac_pool()
+ while True:
+ hwaddr = normalize_hwaddr(mac_pool.get_addr())
+ if not self.host.get_dev_by_hwaddr(hwaddr):
+ self.orig_hwaddr = hwaddr
+ break
+
+ bridges = self.host.get_network_bridges()
+ if self.network in bridges:
+ net_ctl = bridges[self.network]
+ else:
+ bridges[self.network] = net_ctl = VirtNetCtl()
+ net_ctl.init()
+
+ net_name = net_ctl.get_name()
+
+ logging.info("Creating virtual device with hwaddr='%s' on machine
%s",
+ self.orig_hwaddr, self.host.get_id())
+
+ domain_ctl.attach_interface(self.orig_hwaddr,
+ net_name,
+ self.virt_driver)
+ # The sleep here is necessary, because udev sometimes renames the
+ # newly created device
+ sleep(1)
+
+ def destroy(self):
+ logging.info("Destroying virtual device with hwaddr='%s' on machine
%s",
+ self.orig_hwaddr, self.host.get_id())
+
+ domain_ctl = self.host.get_domain_ctl()
+ domain_ctl.detach_interface(self.orig_hwaddr)
diff --git a/lnst/Devices/VlanDevice.py b/lnst/Devices/VlanDevice.py
new file mode 100644
index 0000000..93e71c1
--- /dev/null
+++ b/lnst/Devices/VlanDevice.py
@@ -0,0 +1,38 @@
+"""
+Defines the VlanDevice class
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+import logging
+from lnst.Common.ExecCmd import exec_cmd
+from lnst.Devices.SoftDevice import SoftDevice
+
+class VlanDevice(SoftDevice):
+ _name_template = "t_vlan"
+
+ _module_name = "8021q"
+
+ def __init__(self, ifmanager, *args, **kwargs):
+ super(VlanDevice, self).__init__(ifmanager, args, kwargs)
+
+ self._real_dev = kwargs["real_dev"]
+ self._vlan_id = int(kwargs["vlan_id"])
+
+ @property
+ def real_dev(self):
+ return self._real_dev
+
+ @property
+ def vlan_id(self):
+ return self._vlan_id
+
+ def create(self):
+ exec_cmd("ip link add link %s %s type vlan id %d" %\
+ (self.real_dev.name, self.name, self.vlan_id))
diff --git a/lnst/Devices/VtiDevice.py b/lnst/Devices/VtiDevice.py
new file mode 100644
index 0000000..53f6712
--- /dev/null
+++ b/lnst/Devices/VtiDevice.py
@@ -0,0 +1,71 @@
+"""
+Defines the VtiDevice and Vti6Device classes.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+from lnst.Common.ExecCmd import exec_cmd
+from lnst.Devices.Device import DeviceError
+from lnst.Devices.SoftDevice import SoftDevice
+
+class _BaseVtiDevice(SoftDevice):
+ def __init__(self, ifmanager, *args, **kwargs):
+ super(_BaseVtiDevice, self).__init__(ifmanager, args, kwargs)
+
+ self._key = kwargs["key"]
+ self._local = kwargs.get("local", None)
+ self._remote = kwargs.get("remote", None)
+ self._device = kwargs.get("dev", None)
+
+ if self.local is None and self.remote is None:
+ raise DeviceError("One of local/remote MUST be defined.")
+
+ @property
+ def key(self):
+ return self._key
+
+ @property
+ def local(self):
+ return self._local
+
+ @property
+ def remote(self):
+ return self._remote
+
+ @property
+ def device(self):
+ return self._device
+
+ @property
+ def vti_type(self):
+ raise NotImplementedError
+
+ def create(self):
+ exec_cmd("ip link add {name} type {type}{local}{remote}{key}{device}".
+ format(name=self.name,
+ type=self.vti_type,
+ local=(" local " + str(self.local)
+ if self.local
+ else ""),
+ remote=(" remote " + str(self.remote)
+ if self.remote
+ else ""),
+ key=" key " + self.key,
+ device=(" dev " + self.device.name
+ if self.device
+ else "")))
+
+
+class VtiDevice(_BaseVtiDevice):
+ vti_type = "vti"
+ _name_template = "t_vti"
+
+class Vti6Device(_BaseVtiDevice):
+ vti_type = "vti6"
+ _name_template = "t_ip6vti"
diff --git a/lnst/Devices/VxlanDevice.py b/lnst/Devices/VxlanDevice.py
new file mode 100644
index 0000000..f973e0d
--- /dev/null
+++ b/lnst/Devices/VxlanDevice.py
@@ -0,0 +1,66 @@
+"""
+Defines the VxlanDevice class.
+
+Copyright 2017 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+olichtne(a)redhat.com (Ondrej Lichtner)
+"""
+
+import logging
+from lnst.Common.ExecCmd import exec_cmd
+from lnst.Devices.Device import DeviceError
+from lnst.Devices.SoftDevice import SoftDevice
+
+class VxlanDevice(SoftDevice):
+ _name_template = "t_vxlan"
+
+ def __init__(self, ifmanager, *args, **kwargs):
+ super(VxlanDevice, self).__init__(ifmanager, args, kwargs)
+
+ self._vxlan_id = int(kwargs["vxlan_id"])
+ self._real_dev = kwargs.get("real_dev", None)
+ self._group_ip = kwargs.get("group_ip", None)
+ self._remote_ip = kwargs.get("remote_ip", None)
+ self._dstport = int(kwargs.get("dst_port", 0))
+
+ if self.group_ip is None and self.remote_ip is None:
+ raise DeviceError("group or remote must be specified for vxlan")
+
+ @property
+ def real_dev(self):
+ return self._real_dev
+
+ @property
+ def vxlan_id(self):
+ return self._vxlan_id
+
+ @property
+ def group_ip(self):
+ return self._group_ip
+
+ @property
+ def remote_ip(self):
+ return self._remote_ip
+
+ @property
+ def dst_port(self):
+ return self._dst_port
+
+ def create(self):
+ dev_param = "dev %s" % self.real_dev.name if self.real_dev else
""
+
+ if self.group_ip:
+ group_or_remote = "group %s" % self.group_ip
+ elif self.remote_ip:
+ group_or_remote = "remote %s" % self.remote_ip
+
+ exec_cmd("ip link add %s type vxlan id %d %s %s dstport %d"
+ % (self.name,
+ self.vxlan_id,
+ dev_param,
+ group_or_remote,
+ self.dstport))
diff --git a/lnst/Devices/__init__.py b/lnst/Devices/__init__.py
new file mode 100644
index 0000000..0eb1310
--- /dev/null
+++ b/lnst/Devices/__init__.py
@@ -0,0 +1,45 @@
+from lnst.Devices.Device import Device, DeviceError
+from lnst.Devices.SoftDevice import SoftDevice
+from lnst.Devices.BridgeDevice import BridgeDevice
+from lnst.Devices.OvsBridgeDevice import OvsBridgeDevice
+from lnst.Devices.BondDevice import BondDevice
+from lnst.Devices.TeamDevice import TeamDevice
+from lnst.Devices.MacvlanDevice import MacvlanDevice
+from lnst.Devices.VlanDevice import VlanDevice
+from lnst.Devices.VxlanDevice import VxlanDevice
+from lnst.Devices.VtiDevice import VtiDevice, Vti6Device
+from lnst.Devices.VethDevice import VethDevice, PairedVethDevice
+from lnst.Devices.VethPair import VethPair
+from lnst.Devices.VirtualDevice import VirtualDevice
+from lnst.Devices.RemoteDevice import RemoteDevice, remotedev_decorator
+
+device_classes = [
+ ("Device", Device),
+ ("SoftDevice", SoftDevice),
+ ("BridgeDevice", BridgeDevice),
+ ("OvsBridgeDevice", OvsBridgeDevice),
+ ("MacvlanDevice", MacvlanDevice),
+ ("VlanDevice", VlanDevice),
+ ("VxlanDevice", VxlanDevice),
+ ("VethDevice", VethDevice),
+ ("PairedVethDevice", PairedVethDevice),
+ ("VtiDevice", VtiDevice),
+ ("Vti6Device", Vti6Device),
+ ("BondDevice", BondDevice),
+ ("TeamDevice", TeamDevice)]
+
+class Devices(object):
+ def __init__(self, host):
+ self._host = host
+
+ def __iter__(self):
+ for x in self._host._device_database.values():
+ if isinstance(x, RemoteDevice):
+ yield x
+
+for name, cls in device_classes:
+ globals()[name] = remotedev_decorator(cls)
+
+#Remove the PairedVethDevice from globals... doesn't make sense to use it on
+#it's own, not even for isinstance... VethDevice works fine for that
+del globals()["PairedVethDevice"]
--
2.12.2