From 85a341d645b57b7cd88a26ed2ea0a314704240ea Mon Sep 17 00:00:00 2001 From: Jordan Augé Date: Fri, 24 Feb 2017 14:58:01 +0100 Subject: Initial commit: vICN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I7ce66c4e84a6a1921c63442f858b49e083adc7a7 Signed-off-by: Jordan Augé --- vicn/resource/linux/net_device.py | 519 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 519 insertions(+) create mode 100644 vicn/resource/linux/net_device.py (limited to 'vicn/resource/linux/net_device.py') diff --git a/vicn/resource/linux/net_device.py b/vicn/resource/linux/net_device.py new file mode 100644 index 00000000..f0a08991 --- /dev/null +++ b/vicn/resource/linux/net_device.py @@ -0,0 +1,519 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import re +import math +import random +import string + +from netmodel.model.type import Integer, String, Bool +from vicn.core.address_mgr import AddressManager +from vicn.core.attribute import Attribute +from vicn.core.exception import ResourceNotFound +from vicn.core.resource import BaseResource +from vicn.core.task import BashTask, task, EmptyTask +from vicn.resource.application import Application +from vicn.resource.interface import Interface + +# parse_ip_addr inspired from: +# From: https://github.com/ohmu/poni/blob/master/poni/cloud_libvirt.py + +LXD_FIX = lambda cmd: 'sleep 1 && {}'.format(cmd) + +MAX_DEVICE_NAME_SIZE = 15 + +CMD_FLUSH_IP = 'ip addr flush dev {device_name}' + +CMD_INTERFACE_LIST = 'ip link show | grep -A 1 @{}' +RX_INTERFACE_LIST = '.*?(?P[^ ]*)@{}:' +CMD_INTERFACE_GET = 'ip link show | grep -A 1 {}@{}' +RX_INTERFACE_GET = '.*?(?P{})@{}:' + +log = logging.getLogger(__name__) + +CMD_GET = LXD_FIX('ip link show {netdevice.device_name}') +CMD_CREATE = 'ip link add name {netdevice.device_name} ' \ + 'type {netdevice.netdevice_type}' +CMD_CREATE_PARENT = 'ip link add name {netdevice.device_name} ' \ + 'link {netdevice.parent.device_name} ' \ + 'type {netdevice.netdevice_type}' +CMD_DELETE = 'ip link delete {netdevice.device_name}' +CMD_SET_MAC_ADDRESS = 'ip link set dev {netdevice.device_name} ' \ + 'address {netdevice.mac_address}' +CMD_GET_IP_ADDRESS = 'ip addr show {netdevice.device_name}' +CMD_SET_IP_ADDRESS = 'ip addr add dev {netdevice.device_name} ' \ + '{netdevice.ip_address} brd + || true' +CMD_SET_PROMISC = 'ip link set dev {netdevice.device_name} promisc {on_off}' +CMD_SET_UP = 'ip link set {netdevice.device_name} {up_down}' +CMD_SET_CAPACITY='\n'.join([ + 'tc qdisc del dev {netdevice.device_name} root || true', + 'tc qdisc add dev {netdevice.device_name} root handle 1: tbf rate ' + '{netdevice.capacity}Mbit burst {burst}kb latency 70ms' + 'tc qdisc add dev {netdevice.device_name} parent 1:1 codel', +]) +CMD_GET_PCI_ADDRESS='ethtool -i {netdevice.device_name} | ' \ + "sed -n '/bus-info/{{s/.*: [^:]*:\(.*\)/\\1/p}}'" +CMD_GET_OFFLOAD='ethtool -k {netdevice.device_name} | ' \ + 'grep rx-checksumming | cut -d " " -f 2' +CMD_SET_OFFLOAD='ethtool -K {netdevice.device_name} rx on tx on' +CMD_UNSET_OFFLOAD='ethtool -K {netdevice.device_name} rx off tx off' + +CMD_UNSET_RP_FILTER = ''' +sysctl -w net.ipv4.conf.all.rp_filter=0 +sysctl -w net.ipv4.conf.{netdevice.device_name}.rp_filter=0 +''' +CMD_SET_RP_FILTER = 'sysctl -w ' \ + 'net.ipv4.conf.{netdevice.device_name}.rp_filter=1' +CMD_GET_RP_FILTER = ''' +sysctl net.ipv4.conf.all.rp_filter +sysctl net.ipv4.conf.{netdevice.device_name}.rp_filter +''' + +#------------------------------------------------------------------------------- + +# FIXME GPL code + +# Copyright 2015 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +"""Utility to parse 'ip link [show]'. + +Example dictionary returned by parse_ip_link(): + +{u'eth0': {u'flags': set([u'BROADCAST', u'LOWER_UP', u'MULTICAST', u'UP']), + u'index': 2, + u'mac': u'80:fa:5c:0d:43:5e', + u'name': u'eth0', + u'settings': {u'group': u'default', + u'mode': u'DEFAULT', + u'mtu': u'1500', + u'qdisc': u'pfifo_fast', + u'qlen': u'1000', + u'state': u'UP'}}, + u'lo': {u'flags': set([u'LOOPBACK', u'LOWER_UP', u'UP']), + u'index': 1, + u'name': u'lo', + u'settings': {u'group': u'default', + u'mode': u'DEFAULT', + u'mtu': u'65536', + u'qdisc': u'noqueue', + u'state': u'UNKNOWN'}}} + +The dictionary above is generated given the following input: + + 1: lo: mtu 65536 qdisc noqueue state UNKNOWN \ +mode DEFAULT group default + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + 2: eth0: mtu 1500 qdisc pfifo_fast \ +state UP mode DEFAULT group default qlen 1000 + link/ether 80:fa:5c:0d:43:5e brd ff:ff:ff:ff:ff:ff +""" + +def _get_settings_dict(settings_line): + """ + Given a string of the format: + "[[ ] ][...]" + Returns a dictionary mapping each key to its corresponding value. + :param settings_line: unicode + :return: dict + """ + settings = settings_line.strip().split() + num_tokens = len(settings) + assert num_tokens % 2 == 0 + return { + settings[2 * i]: settings[2 * i + 1] for i in range(num_tokens // 2) + } + + +def _parse_interface_definition(line): + """Given a string of the format: + : : + Returns a dictionary containing the component parts. + :param line: unicode + :return: dict + :raises: ValueError if a malformed interface definition line is presented + """ + interface = {} + + # This line is in the format: + # : : + [index, name, properties] = map( + lambda s: s.strip(), line.split(':')) + + interface['index'] = int(index) + if '@' in name: + name, parent = name.split('@') + interface['name'] = name + interface['parent'] = parent + else: + interface['name'] = name + interface['parent'] = None + + # Now parse the part from above. + # This will be in the form " key1 value1 key2 value2 ..." + matches = re.match(r"^<(.*)>(.*)", properties) + if matches: + flags = matches.group(1) + if len(flags) > 0: + flags = flags.split(',') + else: + flags = [] + interface['flags'] = set(flags) + interface['settings'] = _get_settings_dict(matches.group(2)) + else: + raise ValueError("Malformed 'ip link' line (%s)" % line) + return interface + + +def _add_additional_interface_properties(interface, line): + """ + Given the specified interface and a specified follow-on line containing + more interface settings, adds any additional settings to the interface + dictionary. (currently, the only relevant setting is the interface MAC.) + :param interface: dict + :param line: unicode + """ + settings = _get_settings_dict(line) + # We only care about the MAC address for Ethernet interfaces. + mac = settings.get('link/ether') + if mac is not None: + interface['mac'] = mac + + +def parse_ip_link(output): + """ + Given the full output from 'ip link [show]', parses it and returns a + dictionary mapping each interface name to its settings. + :param output: string or unicode + :return: dict + """ + interfaces = {} + interface = None + for line in output.splitlines(): + if re.match(r'^[0-9]', line): + interface = _parse_interface_definition(line) + if interface is not None: + interfaces[interface['name']] = interface + else: + if interface is not None: + _add_additional_interface_properties(interface, line) + return interfaces + +#------------------------------------------------------------------------------ + +_IP_ADDR_SPLIT_RE = re.compile("^[0-9]+: ", flags=re.MULTILINE) + +def parse_ip_addr(data): + """ + Parse addresses from 'ip addr' output + """ + + for iface in _IP_ADDR_SPLIT_RE.split(data.strip()): + if not iface: + continue + lines = [l.strip() for l in iface.splitlines()] + # XXX @ in name not supported + name = lines.pop(0).partition(":")[0] + info = { + "ip-addresses": [], + "hardware-address": None, + } + if '@' in name: + name, parent = name.split('@') + info['name'] = name + info['parent'] = parent + else: + info['name'] = name + info['parent'] = None + + for line in lines: + words = line.split() + if words[0].startswith("link/") and len(words) >= 2: + info["hardware-address"] = words[1] + elif words[0] in ("inet", "inet6"): + addrtype = "ipv6" if words[0] == "inet6" else "ipv4" + addr, _, prefix = words[1].partition("/") + if prefix == '': + prefix = 128 if addrtype == "ipv6" else 32 + info["ip-addresses"].append({"ip-address-type": addrtype, + "ip-address": addr, "prefix": int(prefix)}) + yield info + +#------------------------------------------------------------------------------ + +class BaseNetDevice(Interface, Application): + __type__ = BaseResource + + # XXX note: ethtool only required if we need to get the pci address + __package_names__ = ['ethtool'] + + device_name = Attribute(String, description = 'Name of the NetDevice', + default = lambda x : x._default_device_name(), + max_size = MAX_DEVICE_NAME_SIZE) + capacity = Attribute(Integer, + description = 'Capacity for interface shaping (Mb/s)') + mac_address = Attribute(String, description = 'Mac address of the device') + ip_address = Attribute(String, description = 'IP address of the device') + pci_address = Attribute(String, + description = 'PCI bus address of the device', + ro = True) + promiscuous = Attribute(Bool, description = 'Promiscuous', default = False) + up = Attribute(Bool, description = 'Promiscuous', default = True) + netdevice_type = Attribute(String, description = 'Type of the netdevice', + ro = True) + prefix = Attribute(String, default = 'dev') + rp_filter = Attribute(Bool, description = 'Reverse-path filtering enabled', + default = True) + + #-------------------------------------------------------------------------- + # Constructor and Accessors + #-------------------------------------------------------------------------- + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Dummy member to store the other side of a VethPair + # We use it to disable offloading on interfaces connected to VPP + self.remote = None + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __get__(self): + def check(rv): + if not bool(rv): + raise ResourceNotFound + return BashTask(self.node, CMD_GET, {'netdevice' : self}, output=True, + parse=check) + + __create__ = None + + def __delete__(self): + return BashTask(self.node, CMD_DELETE, {'netdevice': self}) + + #-------------------------------------------------------------------------- + # Attributes + #-------------------------------------------------------------------------- + + def _get_mac_address(self): + # Merge into parse_ip_link + def parse(rv): + assert rv is not None + + nds = parse_ip_link(rv.stdout) + # This will raise an exception is the interface does not exist + nd = nds[self.device_name] + attrs = { 'mac_address': nd['mac'], } + return attrs + + return BashTask(self.node, CMD_GET, {'netdevice' : self}, output=True, + parse=parse) + + def _set_mac_address(self): + return BashTask(self.node, CMD_SET_MAC_ADDRESS, {'netdevice': self}) + + def _get_ip_address(self): + """ + NOTE: Incidently, this will also give the MAC address, as well as other + attributes... + """ + def parse(rv): + attrs = dict() + + assert rv is not None + + nds = list(parse_ip_addr(rv.stdout)) + + assert nds + assert len(nds) <= 1 + + nd = nds[0] + + assert nd['name'] == self.device_name + + attrs['mac_address'] = nd['hardware-address'] + + # We assume a single IPv4 address for now... + ips = [ip for ip in nd['ip-addresses'] + if ip['ip-address-type'] == 'ipv4'] + if len(ips) >= 1: + if len(ips) > 1: + log.warning('Keeping only first of many IP addresses...') + ip = ips[0] + attrs['ip_address'] = ip['ip-address'] + else: + attrs['ip_address'] = None + return attrs + + return BashTask(self.node, CMD_GET_IP_ADDRESS, + {'netdevice': self}, parse=parse) + + def _set_ip_address(self): + if self.ip_address is None: + # Unset IP + return BashTask(self.node, CMD_FLUSH_IP, + {'device_name': self.device_name}) + return BashTask(self.node, CMD_SET_IP_ADDRESS, + {'netdevice': self}) + + @task + def _get_promiscuous(self): + return {'promiscuous': False} + + def _set_promiscuous(self): + on_off = 'on' if self.promiscuous else 'off' + return BashTask(self.node, CMD_SET_PROMISC, + {'netdevice': self, 'on_off' : on_off}) + + @task + def _get_up(self): + return {'up': False} + + def _set_up(self): + up_down = 'up' if self.up else 'down' + return BashTask(self.node, CMD_SET_UP, + {'netdevice': self, 'up_down': up_down}) + + @task + def _get_capacity(self): + return {'capacity': None} + + def _set_capacity(self): + if self.capacity is None: + log.warning('set_capacity(None) not implemented') + return EmptyTask() + + # http://unix.stackexchange.com/questions/100785/bucket-size-in-tbf + MBPS = 1000000 + KBPS = 1024 + BYTES = 8 + HZ = 250 + + # Round to power of two... see manpage + burst = math.ceil((((self.capacity * MBPS) / HZ) / BYTES) / KBPS) + burst = 1 << (burst - 1).bit_length() + + return BashTask(self.node, CMD_SET_CAPACITY, + {'netdevice': self, 'burst': burst}) + + def _get_rp_filter(self): + def parse(rv): + lines = rv.stdout.splitlines() + return (int(lines[0][-1]) + int(lines[1][-1]) > 0) + return BashTask(self.node, CMD_GET_RP_FILTER, {'netdevice' :self}, + parse = parse) + + def _set_rp_filter(self): + cmd = CMD_SET_RP_FILTER if self.rp_filter else CMD_UNSET_RP_FILTER + return BashTask(self.node, cmd, {'netdevice' : self}) + + #-------------------------------------------------------------------------- + # Internal methods + #-------------------------------------------------------------------------- + + def _remote_node_name(self): + remote_interface = self._remote_interface() + if remote_interface: + return remote_interface.node.name + else: + rnd = ''.join(random.choice(string.ascii_uppercase + string.digits) + for _ in range(3)) + return 'unk{}'.format(rnd) + + def _remote_interface(self): + if not self.channel: + return None + interfaces = self.channel.interfaces + for interface in interfaces: + if interface == self: + continue + return interface + + def _default_device_name(self): + remote_node_name = self._remote_node_name() + if remote_node_name: + return remote_node_name + else: + return AddressManager().get('device_name', self, + prefix = self.prefix, scope = self.prefix) + +#------------------------------------------------------------------------------ + +class NonTapBaseNetDevice(BaseNetDevice): + # Tap devices for instance don't have offload + offload = Attribute(Bool, description = 'Offload', default=True) + + #-------------------------------------------------------------------------- + # Attributes + #-------------------------------------------------------------------------- + + def _get_offload(self): + return BashTask(self.node, CMD_GET_OFFLOAD, {'netdevice': self}, + parse = lambda rv : rv.stdout.strip() == 'on') + + def _set_offload(self): + cmd = None + if self.offload: + cmd = CMD_SET_OFFLOAD + else: + cmd = CMD_UNSET_OFFLOAD + return BashTask(self.node, cmd, {'netdevice' : self}) + +#------------------------------------------------------------------------------ + +class NetDevice(NonTapBaseNetDevice): + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __create__(self): + return BashTask(self.node, CMD_CREATE, {'netdevice': self}) + +#------------------------------------------------------------------------------ + +class SlaveBaseNetDevice(BaseNetDevice): + parent = Attribute(NetDevice, description = 'Parent NetDevice') + + host = Attribute(NetDevice, description = 'Host interface', + default = lambda x : x._default_host()) + + def _default_host(self): + if self.node.__class__.__name__ == 'LxcContainer': + host = self.node.node + else: + host = self.node + max_len = MAX_DEVICE_NAME_SIZE - len(self.node.name) - 1 + device_name = self.device_name[:max_len] + + return NetDevice(node = host, + device_name = '{}-{}'.format(self.node.name, device_name), + managed = False) + +#------------------------------------------------------------------------------ + +class SlaveNetDevice(SlaveBaseNetDevice): + + #-------------------------------------------------------------------------- + # Resource lifecycle + #-------------------------------------------------------------------------- + + def __create__(self): + return BashTask(self.node, CMD_CREATE_PARENT, {'netdevice': self}) -- cgit 1.2.3-korg