blob: 592f040d647c5e5b560f64ba24c44732bcaa615d [file] [log] [blame]
Federico Ressic2ed23d2018-10-25 09:31:47 +02001# Copyright (c) 2018 Red Hat, Inc.
2#
3# All Rights Reserved.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
16
17import collections
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +000018import re
Federico Ressic2ed23d2018-10-25 09:31:47 +020019import subprocess
20
21import netaddr
22from neutron_lib import constants
23from oslo_log import log
24from oslo_utils import excutils
25
26from neutron_tempest_plugin.common import shell
27
28
29LOG = log.getLogger(__name__)
30
31
32class IPCommand(object):
33
34 sudo = 'sudo'
35 ip_path = '/sbin/ip'
36
37 def __init__(self, ssh_client=None, timeout=None):
38 self.ssh_client = ssh_client
39 self.timeout = timeout
40
41 def get_command(self, obj, *command):
42 command_line = '{sudo!s} {ip_path!r} {object!s} {command!s}'.format(
43 sudo=self.sudo, ip_path=self.ip_path, object=obj,
44 command=subprocess.list2cmdline([str(c) for c in command]))
45 return command_line
46
47 def execute(self, obj, *command):
48 command_line = self.get_command(obj, *command)
49 return shell.execute(command_line, ssh_client=self.ssh_client,
50 timeout=self.timeout).stdout
51
52 def configure_vlan_subport(self, port, subport, vlan_tag, subnets):
53 addresses = self.list_addresses()
54 try:
55 subport_device = get_port_device_name(addresses=addresses,
56 port=subport)
57 except ValueError:
58 pass
59 else:
60 LOG.debug('Interface %r already configured.', subport_device)
61 return subport_device
62
63 subport_ips = [
64 "{!s}/{!s}".format(ip, prefix_len)
65 for ip, prefix_len in _get_ip_address_prefix_len_pairs(
66 port=subport, subnets=subnets)]
67 if not subport_ips:
68 raise ValueError(
69 "Unable to get IP address and subnet prefix lengths for "
70 "subport")
71
72 port_device = get_port_device_name(addresses=addresses, port=port)
73 subport_device = '{!s}.{!s}'.format(port_device, vlan_tag)
74 LOG.debug('Configuring VLAN subport interface %r on top of interface '
75 '%r with IPs: %s', subport_device, port_device,
76 ', '.join(subport_ips))
77
78 self.add_link(link=port_device, name=subport_device, link_type='vlan',
79 segmentation_id=vlan_tag)
80 self.set_link(device=subport_device, state='up')
81 for subport_ip in subport_ips:
82 self.add_address(address=subport_ip, device=subport_device)
83 return subport_device
84
85 def list_addresses(self, device=None, ip_addresses=None, port=None,
86 subnets=None):
87 command = ['list']
88 if device:
89 command += ['dev', device]
90 output = self.execute('address', *command)
91 addresses = list(parse_addresses(output))
92
93 return list_ip_addresses(addresses=addresses,
94 ip_addresses=ip_addresses, port=port,
95 subnets=subnets)
96
97 def add_link(self, name, link_type, link=None, segmentation_id=None):
98 command = ['add']
99 if link:
100 command += ['link', link]
101 command += ['name', name, 'type', link_type]
102 if id:
103 command += ['id', segmentation_id]
104 return self.execute('link', *command)
105
106 def set_link(self, device, state=None):
107 command = ['set', 'dev', device]
108 if state:
109 command.append(state)
110 return self.execute('link', *command)
111
112 def add_address(self, address, device):
113 # ip addr add 192.168.1.1/24 dev em1
114 return self.execute('address', 'add', address, 'dev', device)
115
116 def list_routes(self, *args):
117 output = self.execute('route', 'show', *args)
118 return list(parse_routes(output))
119
Slawek Kaplonskia1e88c42020-03-03 03:00:48 +0100120 def get_nic_name_by_mac(self, mac_address):
121 nics = self.execute("-o", "link")
122 for nic_line in nics.split("\n"):
123 if mac_address in nic_line:
124 return nic_line.split(":")[1].strip()
125
Federico Ressic2ed23d2018-10-25 09:31:47 +0200126
127def parse_addresses(command_output):
128 address = device = None
129 addresses = []
130 for i, line in enumerate(command_output.split('\n')):
131 try:
132 line_number = i + 1
133 fields = line.strip().split()
134 if not fields:
135 continue
136 indent = line.index(fields[0] + ' ')
137 if indent == 0:
138 # example of line
139 # 2: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 # noqa
140 address = None
141 name = fields[1]
142 if name.endswith(':'):
143 name = name[:-1]
144 if '@' in name:
145 name, parent = name.split('@', 1)
146 else:
147 parent = None
148
149 if len(fields) > 2:
150 # flags example: <LOOPBACK,UP,LOWER_UP>
151 flags = fields[2]
152 if flags.startswith('<'):
153 flags = flags[1:]
154 if flags.startswith('>'):
155 flags = flags[:-1]
156 flags = flags.split(',')
157
158 device = Device(name=name, parent=parent, flags=flags,
159 properties=dict(parse_properties(fields[3:])))
160 LOG.debug("Device parsed: %r", device)
161
162 elif indent == 4:
163 address = Address.create(
164 family=fields[0], address=fields[1], device=device,
165 properties=dict(parse_properties(fields[2:])))
166 addresses.append(address)
167 LOG.debug("Address parsed: %r", address)
168
169 elif indent == 7:
170 address.properties.update(parse_properties(fields))
171 LOG.debug("Address properties parsed: %r", address.properties)
172
173 else:
174 assert False, "Invalid line indentation: {!r}".format(indent)
175
176 except Exception:
177 with excutils.save_and_reraise_exception():
178 LOG.exception("Error parsing ip command output at line %d:\n"
179 "%r\n",
180 line_number, line)
181 raise
182
183 return addresses
184
185
186def parse_properties(fields):
187 for i, field in enumerate(fields):
188 if i % 2 == 0:
189 key = field
190 else:
191 yield key, field
192
193
194class HasProperties(object):
195
196 def __getattr__(self, name):
197 try:
198 return self.properties[name]
199 except KeyError:
200 pass
201 # This should raise AttributeError
202 return getattr(super(HasProperties, self), name)
203
204
205class Address(HasProperties,
206 collections.namedtuple('Address',
207 ['family', 'address', 'device',
208 'properties'])):
209
210 _subclasses = {}
211
212 @classmethod
213 def create(cls, family, address, device, properties):
214 cls = cls._subclasses.get(family, cls)
215 return cls(family=family, address=address, device=device,
216 properties=properties)
217
218 @classmethod
219 def register_subclass(cls, family, subclass=None):
220 if not issubclass(subclass, cls):
221 msg = "{!r} is not sub-class of {!r}".format(cls, Address)
222 raise TypeError(msg)
223 cls._subclasses[family] = subclass
224
225
226class Device(HasProperties,
227 collections.namedtuple('Device',
228 ['name', 'parent', 'flags',
229 'properties'])):
230 pass
231
232
233def register_address_subclass(families):
234
235 def decorator(subclass):
236 for family in families:
237 Address.register_subclass(family=family, subclass=subclass)
238 return subclass
239
240 return decorator
241
242
243@register_address_subclass(['inet', 'inet6'])
244class InetAddress(Address):
245
246 @property
247 def ip(self):
248 return self.network.ip
249
250 @property
251 def network(self):
252 return netaddr.IPNetwork(self.address)
253
254
255def parse_routes(command_output):
256 for line in command_output.split('\n'):
257 fields = line.strip().split()
258 if fields:
259 dest = fields[0]
260 properties = dict(parse_properties(fields[1:]))
261 if dest == 'default':
262 dest = constants.IPv4_ANY
263 via = properties.get('via')
264 if via:
265 dest = constants.IP_ANY[netaddr.IPAddress(via).version]
266 yield Route(dest=dest, properties=properties)
267
268
269def list_ip_addresses(addresses, ip_addresses=None, port=None,
270 subnets=None):
271 if port:
272 # filter addresses by port IP addresses
273 ip_addresses = set(ip_addresses) if ip_addresses else set()
274 ip_addresses.update(list_port_ip_addresses(port=port,
275 subnets=subnets))
276 if ip_addresses:
277 addresses = [a for a in addresses if (hasattr(a, 'ip') and
278 str(a.ip) in ip_addresses)]
279 return addresses
280
281
282def list_port_ip_addresses(port, subnets=None):
283 fixed_ips = port['fixed_ips']
284 if subnets:
285 subnets = {subnet['id']: subnet for subnet in subnets}
286 fixed_ips = [fixed_ip
287 for fixed_ip in fixed_ips
288 if fixed_ip['subnet_id'] in subnets]
289 return [ip['ip_address'] for ip in port['fixed_ips']]
290
291
292def get_port_device_name(addresses, port):
293 for address in list_ip_addresses(addresses=addresses, port=port):
294 return address.device.name
295
296 msg = "Port %r fixed IPs not found on server.".format(port['id'])
297 raise ValueError(msg)
298
299
300def _get_ip_address_prefix_len_pairs(port, subnets):
301 subnets = {subnet['id']: subnet for subnet in subnets}
302 for fixed_ip in port['fixed_ips']:
303 subnet = subnets.get(fixed_ip['subnet_id'])
304 if subnet:
305 yield (fixed_ip['ip_address'],
306 netaddr.IPNetwork(subnet['cidr']).prefixlen)
307
308
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000309def arp_table():
310 # 192.168.0.16 0x1 0x2 dc:a6:32:06:56:51 * enp0s31f6
311 regex_str = (r"([^ ]+)\s+(0x\d+)\s+(0x\d+)\s+(\w{2}\:\w{2}\:\w{2}\:\w{2}\:"
312 r"\w{2}\:\w{2})\s+([\w+\*]+)\s+([\-\w]+)")
313 regex = re.compile(regex_str)
314 arp_table = []
315 with open('/proc/net/arp', 'r') as proc_file:
316 for line in proc_file.readlines():
317 m = regex.match(line)
318 if m:
319 arp_table.append(ARPregister(
320 ip_address=m.group(1), hw_type=m.group(2),
321 flags=m.group(3), mac_address=m.group(4),
322 mask=m.group(5), device=m.group(6)))
323 return arp_table
324
325
Federico Ressic2ed23d2018-10-25 09:31:47 +0200326class Route(HasProperties,
327 collections.namedtuple('Route',
328 ['dest', 'properties'])):
329
330 @property
331 def dest_ip(self):
332 return netaddr.IPNetwork(self.dest)
333
334 @property
335 def via_ip(self):
336 return netaddr.IPAddress(self.via)
337
338 @property
339 def src_ip(self):
340 return netaddr.IPAddress(self.src)
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000341
342 def __str__(self):
343 properties_str = ' '.join('%s %s' % (k, v)
344 for k, v in self.properties.items())
345 return '%(dest)s %(properties)s' % {'dest': self.dest,
346 'properties': properties_str}
347
348
349class ARPregister(collections.namedtuple(
350 'ARPregister',
351 ['ip_address', 'hw_type', 'flags', 'mac_address', 'mask', 'device'])):
352
353 def __str__(self):
354 return '%s %s %s %s %s %s' % (self.ip_address, self.hw_type,
355 self.flags, self.mac_address, self.mask,
356 self.device)
ccamposr3e1921b2020-01-29 11:10:05 +0100357
358
359def find_valid_cidr(valid_cidr='10.0.0.0/8', used_cidr=None):
360 total_ips = netaddr.IPSet(netaddr.IPNetwork(valid_cidr))
361 if used_cidr:
362 used_network = netaddr.IPNetwork(used_cidr)
363 netmask = used_network.netmask.netmask_bits()
364 valid_ips = total_ips.difference(netaddr.IPSet(used_network))
365 else:
366 valid_ips = total_ips
367 netmask = 24
368
369 for ip in valid_ips:
370 valid_network = netaddr.IPNetwork('%s/%s' % (ip, netmask))
371 if valid_network in valid_ips:
372 return valid_network.cidr
373
374 exception_str = 'No valid CIDR found in %s' % valid_cidr
375 if used_cidr:
376 exception_str += ', used CIDR %s' % used_cidr
377 raise Exception(exception_str)