Use LLDP to get switch port mapping
Provides a function to listen for LLDP packets on the network. Listens on one or all of the network interfaces, and then parses the found packets. Change-Id: I1545a41f46cd0916aab9c43ce036865454fa66e0 Co-Authored-By: Josh Gachnang <josh@pcsforeducation.com>
This commit is contained in:

committed by
Josh Gachnang

parent
cfdedd30d3
commit
15aaa03833
ironic_python_agent
197
ironic_python_agent/netutils.py
Normal file
197
ironic_python_agent/netutils.py
Normal file
@ -0,0 +1,197 @@
|
||||
# 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 logging
|
||||
import select
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
LLDP_ETHERTYPE = 0x88cc
|
||||
IFF_PROMISC = 0x100
|
||||
SIOCGIFFLAGS = 0x8913
|
||||
SIOCSIFFLAGS = 0x8914
|
||||
|
||||
|
||||
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
|
||||
# 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) for protocol '
|
||||
'%(proto)s: %s', {'interface': interface_name,
|
||||
'proto': self.protocol})
|
||||
sock.bind((interface_name, self.protocol))
|
||||
except Exception:
|
||||
LOG.warning('Failed to open all RawPromiscuousSockets, '
|
||||
'attempting to close any opened sockets.')
|
||||
if self.__exit__(*sys.exc_info()):
|
||||
return []
|
||||
else:
|
||||
LOG.exception('Could not successfully close all opened '
|
||||
'RawPromiscuousSockets.')
|
||||
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):
|
||||
if exception_type:
|
||||
LOG.exception('Error while using raw socket: %(type)s: %(val)',
|
||||
{'type': exception_type, 'val': exception_val})
|
||||
|
||||
for _name, sock, ifr in self.interfaces:
|
||||
# bitwise or with the opposite of promiscuous mode to remove
|
||||
ifr.ifr_flags &= ~IFF_PROMISC
|
||||
# If these raise, they shouldn't be caught
|
||||
fcntl.ioctl(sock.fileno(), SIOCSIFFLAGS, ifr)
|
||||
sock.close()
|
||||
# Return True to signify exit correctly, only used internally
|
||||
return True
|
||||
|
||||
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 buff:
|
||||
# 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))
|
||||
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 {}
|
||||
|
||||
socks = [interface[1] for interface in interfaces]
|
||||
|
||||
while interfaces:
|
||||
LOG.info('Waiting on LLDP info for interfaces: %(interfaces)s, '
|
||||
'timeout: %(timeout)s', {'interfaces': interfaces,
|
||||
'timeout': CONF.lldp_timeout})
|
||||
|
||||
# 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]:
|
||||
LOG.info('Found LLDP info for interface: %s',
|
||||
interface[0])
|
||||
lldp_info[interface[0]] = (
|
||||
_receive_lldp_packets(s))
|
||||
# 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
|
Reference in New Issue
Block a user