1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
|
# 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
# FIXME(lucasagomes): If you don't import the agent module the tests in
# this file will fail, it was working before because the agent module was
# being imported at tests/agent.py
from ironic_python_agent.cmd import agent # noqa
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.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.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)s',
{'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
|