From c2ed23d106d61d1fd7a0aa9766c5ae9537fecd7d Mon Sep 17 00:00:00 2001 From: Federico Ressi Date: Thu, 25 Oct 2018 09:31:47 +0200 Subject: [PATCH] Add ip command tool for manually manage devices on VM. Introduce 'ip' command tool wrapper to help performing below operations: - inspect actual guest instance IP address - inspect network port device name - setup trunk sub-port Change-Id: I2afe6c82b2a58be349dda873b56aea02b9d72fd1 --- neutron_tempest_plugin/common/ip.py | 316 ++++++++++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 neutron_tempest_plugin/common/ip.py diff --git a/neutron_tempest_plugin/common/ip.py b/neutron_tempest_plugin/common/ip.py new file mode 100644 index 00000000..1702bd31 --- /dev/null +++ b/neutron_tempest_plugin/common/ip.py @@ -0,0 +1,316 @@ +# Copyright (c) 2018 Red Hat, Inc. +# +# All Rights Reserved. +# +# 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 collections +import subprocess + +import netaddr +from neutron_lib import constants +from oslo_log import log +from oslo_utils import excutils + +from neutron_tempest_plugin.common import shell + + +LOG = log.getLogger(__name__) + + +class IPCommand(object): + + sudo = 'sudo' + ip_path = '/sbin/ip' + + def __init__(self, ssh_client=None, timeout=None): + self.ssh_client = ssh_client + self.timeout = timeout + + def get_command(self, obj, *command): + command_line = '{sudo!s} {ip_path!r} {object!s} {command!s}'.format( + sudo=self.sudo, ip_path=self.ip_path, object=obj, + command=subprocess.list2cmdline([str(c) for c in command])) + return command_line + + def execute(self, obj, *command): + command_line = self.get_command(obj, *command) + return shell.execute(command_line, ssh_client=self.ssh_client, + timeout=self.timeout).stdout + + def configure_vlan_subport(self, port, subport, vlan_tag, subnets): + addresses = self.list_addresses() + try: + subport_device = get_port_device_name(addresses=addresses, + port=subport) + except ValueError: + pass + else: + LOG.debug('Interface %r already configured.', subport_device) + return subport_device + + subport_ips = [ + "{!s}/{!s}".format(ip, prefix_len) + for ip, prefix_len in _get_ip_address_prefix_len_pairs( + port=subport, subnets=subnets)] + if not subport_ips: + raise ValueError( + "Unable to get IP address and subnet prefix lengths for " + "subport") + + port_device = get_port_device_name(addresses=addresses, port=port) + subport_device = '{!s}.{!s}'.format(port_device, vlan_tag) + LOG.debug('Configuring VLAN subport interface %r on top of interface ' + '%r with IPs: %s', subport_device, port_device, + ', '.join(subport_ips)) + + self.add_link(link=port_device, name=subport_device, link_type='vlan', + segmentation_id=vlan_tag) + self.set_link(device=subport_device, state='up') + for subport_ip in subport_ips: + self.add_address(address=subport_ip, device=subport_device) + return subport_device + + def list_addresses(self, device=None, ip_addresses=None, port=None, + subnets=None): + command = ['list'] + if device: + command += ['dev', device] + output = self.execute('address', *command) + addresses = list(parse_addresses(output)) + + return list_ip_addresses(addresses=addresses, + ip_addresses=ip_addresses, port=port, + subnets=subnets) + + def add_link(self, name, link_type, link=None, segmentation_id=None): + command = ['add'] + if link: + command += ['link', link] + command += ['name', name, 'type', link_type] + if id: + command += ['id', segmentation_id] + return self.execute('link', *command) + + def set_link(self, device, state=None): + command = ['set', 'dev', device] + if state: + command.append(state) + return self.execute('link', *command) + + def add_address(self, address, device): + # ip addr add 192.168.1.1/24 dev em1 + return self.execute('address', 'add', address, 'dev', device) + + def list_routes(self, *args): + output = self.execute('route', 'show', *args) + return list(parse_routes(output)) + + +def parse_addresses(command_output): + address = device = None + addresses = [] + for i, line in enumerate(command_output.split('\n')): + try: + line_number = i + 1 + fields = line.strip().split() + if not fields: + continue + indent = line.index(fields[0] + ' ') + if indent == 0: + # example of line + # 2: enp0s25: mtu 1500 qdisc pfifo_fast state UP qlen 1000 # noqa + address = None + name = fields[1] + if name.endswith(':'): + name = name[:-1] + if '@' in name: + name, parent = name.split('@', 1) + else: + parent = None + + if len(fields) > 2: + # flags example: + flags = fields[2] + if flags.startswith('<'): + flags = flags[1:] + if flags.startswith('>'): + flags = flags[:-1] + flags = flags.split(',') + + device = Device(name=name, parent=parent, flags=flags, + properties=dict(parse_properties(fields[3:]))) + LOG.debug("Device parsed: %r", device) + + elif indent == 4: + address = Address.create( + family=fields[0], address=fields[1], device=device, + properties=dict(parse_properties(fields[2:]))) + addresses.append(address) + LOG.debug("Address parsed: %r", address) + + elif indent == 7: + address.properties.update(parse_properties(fields)) + LOG.debug("Address properties parsed: %r", address.properties) + + else: + assert False, "Invalid line indentation: {!r}".format(indent) + + except Exception: + with excutils.save_and_reraise_exception(): + LOG.exception("Error parsing ip command output at line %d:\n" + "%r\n", + line_number, line) + raise + + return addresses + + +def parse_properties(fields): + for i, field in enumerate(fields): + if i % 2 == 0: + key = field + else: + yield key, field + + +class HasProperties(object): + + def __getattr__(self, name): + try: + return self.properties[name] + except KeyError: + pass + # This should raise AttributeError + return getattr(super(HasProperties, self), name) + + +class Address(HasProperties, + collections.namedtuple('Address', + ['family', 'address', 'device', + 'properties'])): + + _subclasses = {} + + @classmethod + def create(cls, family, address, device, properties): + cls = cls._subclasses.get(family, cls) + return cls(family=family, address=address, device=device, + properties=properties) + + @classmethod + def register_subclass(cls, family, subclass=None): + if not issubclass(subclass, cls): + msg = "{!r} is not sub-class of {!r}".format(cls, Address) + raise TypeError(msg) + cls._subclasses[family] = subclass + + +class Device(HasProperties, + collections.namedtuple('Device', + ['name', 'parent', 'flags', + 'properties'])): + pass + + +def register_address_subclass(families): + + def decorator(subclass): + for family in families: + Address.register_subclass(family=family, subclass=subclass) + return subclass + + return decorator + + +@register_address_subclass(['inet', 'inet6']) +class InetAddress(Address): + + @property + def ip(self): + return self.network.ip + + @property + def network(self): + return netaddr.IPNetwork(self.address) + + +def parse_routes(command_output): + for line in command_output.split('\n'): + fields = line.strip().split() + if fields: + dest = fields[0] + properties = dict(parse_properties(fields[1:])) + if dest == 'default': + dest = constants.IPv4_ANY + via = properties.get('via') + if via: + dest = constants.IP_ANY[netaddr.IPAddress(via).version] + yield Route(dest=dest, properties=properties) + + +def list_ip_addresses(addresses, ip_addresses=None, port=None, + subnets=None): + if port: + # filter addresses by port IP addresses + ip_addresses = set(ip_addresses) if ip_addresses else set() + ip_addresses.update(list_port_ip_addresses(port=port, + subnets=subnets)) + if ip_addresses: + addresses = [a for a in addresses if (hasattr(a, 'ip') and + str(a.ip) in ip_addresses)] + return addresses + + +def list_port_ip_addresses(port, subnets=None): + fixed_ips = port['fixed_ips'] + if subnets: + subnets = {subnet['id']: subnet for subnet in subnets} + fixed_ips = [fixed_ip + for fixed_ip in fixed_ips + if fixed_ip['subnet_id'] in subnets] + return [ip['ip_address'] for ip in port['fixed_ips']] + + +def get_port_device_name(addresses, port): + for address in list_ip_addresses(addresses=addresses, port=port): + return address.device.name + + msg = "Port %r fixed IPs not found on server.".format(port['id']) + raise ValueError(msg) + + +def _get_ip_address_prefix_len_pairs(port, subnets): + subnets = {subnet['id']: subnet for subnet in subnets} + for fixed_ip in port['fixed_ips']: + subnet = subnets.get(fixed_ip['subnet_id']) + if subnet: + yield (fixed_ip['ip_address'], + netaddr.IPNetwork(subnet['cidr']).prefixlen) + + +class Route(HasProperties, + collections.namedtuple('Route', + ['dest', 'properties'])): + + @property + def dest_ip(self): + return netaddr.IPNetwork(self.dest) + + @property + def via_ip(self): + return netaddr.IPAddress(self.via) + + @property + def src_ip(self): + return netaddr.IPAddress(self.src)