This patch adds support to retrieve IPv6 address. A new field ``ipv6_address`` is added to NetworkInterface and store the assigned IPv6 address (if any). Co-Authored-By: Kaifeng Wang <kaifeng.w@gmail.com> Change-Id: Ia527a5aa48e3daf66d2be190e43935b38b3bd6f9 Closes-Bug: #1744064 Story: #1744064 Task: #11604
		
			
				
	
	
		
			254 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			254 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# Copyright 2014 Rackspace, Inc.
 | 
						|
#
 | 
						|
# 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 ctypes
 | 
						|
import fcntl
 | 
						|
import select
 | 
						|
import socket
 | 
						|
import struct
 | 
						|
import sys
 | 
						|
 | 
						|
import netifaces
 | 
						|
from oslo_config import cfg
 | 
						|
from oslo_log import log as logging
 | 
						|
from oslo_utils import netutils
 | 
						|
 | 
						|
LOG = logging.getLogger(__name__)
 | 
						|
CONF = cfg.CONF
 | 
						|
 | 
						|
LLDP_ETHERTYPE = 0x88cc
 | 
						|
IFF_PROMISC = 0x100
 | 
						|
SIOCGIFFLAGS = 0x8913
 | 
						|
SIOCSIFFLAGS = 0x8914
 | 
						|
INFINIBAND_ADDR_LEN = 59
 | 
						|
 | 
						|
 | 
						|
class ifreq(ctypes.Structure):
 | 
						|
    """Class for setting flags on a socket."""
 | 
						|
    _fields_ = [("ifr_ifrn", ctypes.c_char * 16),
 | 
						|
                ("ifr_flags", ctypes.c_short)]
 | 
						|
 | 
						|
 | 
						|
class RawPromiscuousSockets(object):
 | 
						|
    def __init__(self, interface_names, protocol):
 | 
						|
        """Initialize context manager.
 | 
						|
 | 
						|
        :param interface_names: a list of interface names to bind to
 | 
						|
        :param protocol: the protocol to listen for
 | 
						|
        :returns: A list of tuple of (interface_name, bound_socket), or [] if
 | 
						|
                  there is an exception binding or putting the sockets in
 | 
						|
                  promiscuous mode
 | 
						|
        """
 | 
						|
        if not interface_names:
 | 
						|
            raise ValueError('interface_names must be a non-empty list of '
 | 
						|
                             'network interface names to bind to.')
 | 
						|
        self.protocol = protocol
 | 
						|
        # A 3-tuple of (interface_name, socket, ifreq object)
 | 
						|
        self.interfaces = [(name, self._get_socket(), ifreq())
 | 
						|
                           for name in interface_names]
 | 
						|
 | 
						|
    def __enter__(self):
 | 
						|
        for interface_name, sock, ifr in self.interfaces:
 | 
						|
            LOG.info('Interface %s entering promiscuous mode to capture ',
 | 
						|
                     interface_name)
 | 
						|
            try:
 | 
						|
                ifr.ifr_ifrn = interface_name.encode()
 | 
						|
                # Get current flags
 | 
						|
                fcntl.ioctl(sock.fileno(), SIOCGIFFLAGS, ifr)  # G for Get
 | 
						|
                # bitwise or the flags with promiscuous mode, set the new flags
 | 
						|
                ifr.ifr_flags |= IFF_PROMISC
 | 
						|
                fcntl.ioctl(sock.fileno(), SIOCSIFFLAGS, ifr)  # S for Set
 | 
						|
                # Bind the socket so it can be used
 | 
						|
                LOG.debug('Binding interface %(interface)s for protocol '
 | 
						|
                          '%(proto)s', {'interface': interface_name,
 | 
						|
                                        'proto': self.protocol})
 | 
						|
                sock.bind((interface_name, self.protocol))
 | 
						|
            except Exception:
 | 
						|
                LOG.error('Failed to open all RawPromiscuousSockets, '
 | 
						|
                          'attempting to close any opened sockets.')
 | 
						|
                self.__exit__(*sys.exc_info())
 | 
						|
                raise
 | 
						|
 | 
						|
        # No need to return each interfaces ifreq.
 | 
						|
        return [(sock[0], sock[1]) for sock in self.interfaces]
 | 
						|
 | 
						|
    def __exit__(self, exception_type, exception_val, trace):
 | 
						|
        for name, sock, ifr in self.interfaces:
 | 
						|
            # bitwise or with the opposite of promiscuous mode to remove
 | 
						|
            ifr.ifr_flags &= ~IFF_PROMISC
 | 
						|
            try:
 | 
						|
                fcntl.ioctl(sock.fileno(), SIOCSIFFLAGS, ifr)
 | 
						|
                sock.close()
 | 
						|
            except Exception:
 | 
						|
                LOG.exception('Failed to close raw socket for interface %s',
 | 
						|
                              name)
 | 
						|
 | 
						|
    def _get_socket(self):
 | 
						|
        return socket.socket(socket.AF_PACKET, socket.SOCK_RAW, self.protocol)
 | 
						|
 | 
						|
 | 
						|
def get_lldp_info(interface_names):
 | 
						|
    """Get LLDP info from the switch(es) the agent is connected to.
 | 
						|
 | 
						|
    Listens on either a single or all interfaces for LLDP packets, then
 | 
						|
    parses them. If no LLDP packets are received before lldp_timeout,
 | 
						|
    returns a dictionary in the form {'interface': [],...}.
 | 
						|
 | 
						|
    :param interface_names: The interface to listen for packets on. If
 | 
						|
                           None, will listen on each interface.
 | 
						|
    :return: A dictionary in the form
 | 
						|
             {'interface': [(lldp_type, lldp_data)],...}
 | 
						|
    """
 | 
						|
    with RawPromiscuousSockets(interface_names, LLDP_ETHERTYPE) as interfaces:
 | 
						|
        try:
 | 
						|
            return _get_lldp_info(interfaces)
 | 
						|
        except Exception as e:
 | 
						|
            LOG.exception('Error while getting LLDP info: %s', str(e))
 | 
						|
            raise
 | 
						|
 | 
						|
 | 
						|
def _parse_tlv(buff):
 | 
						|
    """Iterate over a buffer and generate structured TLV data.
 | 
						|
 | 
						|
    :param buff: An ethernet packet with the header trimmed off (first
 | 
						|
                 14 bytes)
 | 
						|
    """
 | 
						|
    lldp_info = []
 | 
						|
    while len(buff) >= 2:
 | 
						|
        # TLV structure: type (7 bits), length (9 bits), val (0-511 bytes)
 | 
						|
        tlvhdr = struct.unpack('!H', buff[:2])[0]
 | 
						|
        tlvtype = (tlvhdr & 0xfe00) >> 9
 | 
						|
        tlvlen = (tlvhdr & 0x01ff)
 | 
						|
        tlvdata = buff[2:tlvlen + 2]
 | 
						|
        buff = buff[tlvlen + 2:]
 | 
						|
        lldp_info.append((tlvtype, tlvdata))
 | 
						|
 | 
						|
    if buff:
 | 
						|
        LOG.warning("Trailing byte received in an LLDP package: %r", buff)
 | 
						|
 | 
						|
    return lldp_info
 | 
						|
 | 
						|
 | 
						|
def _receive_lldp_packets(sock):
 | 
						|
    """Receive LLDP packets and process them.
 | 
						|
 | 
						|
    :param sock: A bound socket
 | 
						|
    :return: A list of tuples in the form (lldp_type, lldp_data)
 | 
						|
    """
 | 
						|
    pkt = sock.recv(1600)
 | 
						|
    # Filter invalid packets
 | 
						|
    if not pkt or len(pkt) < 14:
 | 
						|
        return []
 | 
						|
    # Skip header (dst MAC, src MAC, ethertype)
 | 
						|
    pkt = pkt[14:]
 | 
						|
    return _parse_tlv(pkt)
 | 
						|
 | 
						|
 | 
						|
def _get_lldp_info(interfaces):
 | 
						|
    """Wait for packets on each socket, parse the received LLDP packets."""
 | 
						|
    LOG.debug('Getting LLDP info for interfaces %s', interfaces)
 | 
						|
 | 
						|
    lldp_info = {}
 | 
						|
    if not interfaces:
 | 
						|
        return {}
 | 
						|
 | 
						|
    while interfaces:
 | 
						|
        LOG.info('Waiting on LLDP info for interfaces: %(interfaces)s, '
 | 
						|
                 'timeout: %(timeout)s', {'interfaces': interfaces,
 | 
						|
                                          'timeout': CONF.lldp_timeout})
 | 
						|
 | 
						|
        socks = [interface[1] for interface in interfaces]
 | 
						|
        # rlist is a list of sockets ready for reading
 | 
						|
        rlist, _, _ = select.select(socks, [], [], CONF.lldp_timeout)
 | 
						|
        if not rlist:
 | 
						|
            # Empty read list means timeout on all interfaces
 | 
						|
            LOG.warning('LLDP timed out, remaining interfaces: %s',
 | 
						|
                        interfaces)
 | 
						|
            break
 | 
						|
 | 
						|
        for s in rlist:
 | 
						|
            # Find interface name matching socket ready for read
 | 
						|
            # Create a copy of interfaces to avoid deleting while iterating.
 | 
						|
            for index, interface in enumerate(list(interfaces)):
 | 
						|
                if s == interface[1]:
 | 
						|
                    try:
 | 
						|
                        lldp_info[interface[0]] = _receive_lldp_packets(s)
 | 
						|
                    except socket.error:
 | 
						|
                        LOG.exception('Socket for network interface %s said '
 | 
						|
                                      'that it was ready to read we were '
 | 
						|
                                      'unable to read from the socket while '
 | 
						|
                                      'trying to get LLDP packet. Skipping '
 | 
						|
                                      'this network interface.', interface[0])
 | 
						|
                    else:
 | 
						|
                        LOG.info('Found LLDP info for interface: %s',
 | 
						|
                                 interface[0])
 | 
						|
                    # Remove interface from the list, only need one packet
 | 
						|
                    del interfaces[index]
 | 
						|
 | 
						|
    # Add any interfaces that didn't get a packet as empty lists
 | 
						|
    for name, _sock in interfaces:
 | 
						|
        lldp_info[name] = []
 | 
						|
 | 
						|
    return lldp_info
 | 
						|
 | 
						|
 | 
						|
def get_default_ip_addr(type, interface_id):
 | 
						|
    """Retrieve default IPv4 or IPv6 address."""
 | 
						|
    try:
 | 
						|
        addrs = netifaces.ifaddresses(interface_id)
 | 
						|
        return addrs[type][0]['addr']
 | 
						|
    except (ValueError, IndexError, KeyError):
 | 
						|
        # No default IP address found
 | 
						|
        return None
 | 
						|
 | 
						|
 | 
						|
def get_ipv4_addr(interface_id):
 | 
						|
    return get_default_ip_addr(netifaces.AF_INET, interface_id)
 | 
						|
 | 
						|
 | 
						|
def get_ipv6_addr(interface_id):
 | 
						|
    return get_default_ip_addr(netifaces.AF_INET6, interface_id)
 | 
						|
 | 
						|
 | 
						|
def get_mac_addr(interface_id):
 | 
						|
    try:
 | 
						|
        addrs = netifaces.ifaddresses(interface_id)
 | 
						|
        return addrs[netifaces.AF_LINK][0]['addr']
 | 
						|
    except (ValueError, IndexError, KeyError):
 | 
						|
        # No mac address found
 | 
						|
        return None
 | 
						|
 | 
						|
 | 
						|
def interface_has_carrier(interface_name):
 | 
						|
    path = '/sys/class/net/{}/carrier'.format(interface_name)
 | 
						|
    try:
 | 
						|
        with open(path, 'rt') as fp:
 | 
						|
            return fp.read().strip() == '1'
 | 
						|
    except EnvironmentError:
 | 
						|
        LOG.debug('No carrier information for interface %s',
 | 
						|
                  interface_name)
 | 
						|
        return False
 | 
						|
 | 
						|
 | 
						|
def wrap_ipv6(ip):
 | 
						|
    if netutils.is_valid_ipv6(ip):
 | 
						|
        return "[%s]" % ip
 | 
						|
    return ip
 | 
						|
 | 
						|
 | 
						|
def get_wildcard_address():
 | 
						|
    if netutils.is_ipv6_enabled():
 | 
						|
        return "::"
 | 
						|
    return "0.0.0.0"
 |