blob: e87219b3b50dcd30427e7a5bbab5c9501989db51 [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
Rodolfo Alonso Hernandez0adf8a22020-06-11 11:28:25 +000025from tempest.common import waiters
Federico Ressic2ed23d2018-10-25 09:31:47 +020026
27from neutron_tempest_plugin.common import shell
Rodolfo Alonso Hernandez0adf8a22020-06-11 11:28:25 +000028from neutron_tempest_plugin.common import utils as common_utils
Federico Ressic2ed23d2018-10-25 09:31:47 +020029
30
31LOG = log.getLogger(__name__)
32
33
34class IPCommand(object):
35
36 sudo = 'sudo'
37 ip_path = '/sbin/ip'
38
Slawek Kaplonski8033af72020-05-05 12:01:37 +020039 def __init__(self, ssh_client=None, timeout=None, namespace=None):
Federico Ressic2ed23d2018-10-25 09:31:47 +020040 self.ssh_client = ssh_client
41 self.timeout = timeout
Slawek Kaplonski8033af72020-05-05 12:01:37 +020042 self.namespace = namespace
Federico Ressic2ed23d2018-10-25 09:31:47 +020043
44 def get_command(self, obj, *command):
Slawek Kaplonski8033af72020-05-05 12:01:37 +020045 command_line = '{sudo!s} {ip_path!r} '.format(sudo=self.sudo,
46 ip_path=self.ip_path)
47 if self.namespace:
48 command_line += 'netns exec {ns_name!s} {ip_path!r} '.format(
49 ns_name=self.namespace, ip_path=self.ip_path)
50 command_line += '{object!s} {command!s}'.format(
51 object=obj,
Federico Ressic2ed23d2018-10-25 09:31:47 +020052 command=subprocess.list2cmdline([str(c) for c in command]))
53 return command_line
54
55 def execute(self, obj, *command):
56 command_line = self.get_command(obj, *command)
57 return shell.execute(command_line, ssh_client=self.ssh_client,
58 timeout=self.timeout).stdout
59
Miguel Angel Nieto Jimenez823b1a02022-06-03 12:58:55 +000060 def configure_vlan(self, addresses, port, vlan_tag, subport_ips, mac=None):
Eduardo Olivares088707b2020-12-01 21:13:45 +010061 port_device = get_port_device_name(addresses=addresses, port=port)
62 subport_device = '{!s}.{!s}'.format(port_device, vlan_tag)
63 LOG.debug('Configuring VLAN subport interface %r on top of interface '
64 '%r with IPs: %s', subport_device, port_device,
65 ', '.join(subport_ips))
66
67 self.add_link(link=port_device, name=subport_device, link_type='vlan',
68 segmentation_id=vlan_tag)
Miguel Angel Nieto Jimenez823b1a02022-06-03 12:58:55 +000069 if mac:
70 self.set_link_address(address=mac, device=subport_device)
Eduardo Olivares088707b2020-12-01 21:13:45 +010071 self.set_link(device=subport_device, state='up')
72 for subport_ip in subport_ips:
73 self.add_address(address=subport_ip, device=subport_device)
74 return subport_device
75
Federico Ressic2ed23d2018-10-25 09:31:47 +020076 def configure_vlan_subport(self, port, subport, vlan_tag, subnets):
77 addresses = self.list_addresses()
78 try:
79 subport_device = get_port_device_name(addresses=addresses,
80 port=subport)
81 except ValueError:
82 pass
83 else:
84 LOG.debug('Interface %r already configured.', subport_device)
85 return subport_device
86
87 subport_ips = [
88 "{!s}/{!s}".format(ip, prefix_len)
89 for ip, prefix_len in _get_ip_address_prefix_len_pairs(
90 port=subport, subnets=subnets)]
91 if not subport_ips:
92 raise ValueError(
93 "Unable to get IP address and subnet prefix lengths for "
94 "subport")
95
Miguel Angel Nieto Jimenez823b1a02022-06-03 12:58:55 +000096 return self.configure_vlan(addresses, port, vlan_tag, subport_ips,
97 subport['mac_address'])
Federico Ressic2ed23d2018-10-25 09:31:47 +020098
Slawek Kaplonskid4c707e2024-12-16 14:46:16 +010099 def configure_inner_vlan(self, port, vlan_tag, ip_addresses):
Eduardo Olivares088707b2020-12-01 21:13:45 +0100100 addresses = self.list_addresses()
101 try:
102 subport_device = get_vlan_device_name(addresses, ip_addresses)
103 except ValueError:
104 pass
105 else:
106 LOG.debug('Interface %r already configured.', subport_device)
107 return subport_device
108
109 return self.configure_vlan(addresses, port, vlan_tag, ip_addresses)
Federico Ressic2ed23d2018-10-25 09:31:47 +0200110
Rodolfo Alonso Hernandezeb7f7b02025-02-26 10:24:06 +0000111 # NOTE(ralonsoh): some projects, like whitebox-neutron-tempest-plugin, are
112 # using ``configure_vlan_transparent`` method. The concept of "inner VLAN"
113 # does not exist in the VLAN transparency feature.
114 configure_vlan_transparent = configure_inner_vlan
115
Slawek Kaplonski8033af72020-05-05 12:01:37 +0200116 def list_namespaces(self):
117 namespaces_output = self.execute("netns")
118 ns_list = []
119 for ns_line in namespaces_output.split("\n"):
120 ns_list.append(ns_line.split(" ", 1)[0])
121 return ns_list
122
Federico Ressic2ed23d2018-10-25 09:31:47 +0200123 def list_addresses(self, device=None, ip_addresses=None, port=None,
124 subnets=None):
125 command = ['list']
126 if device:
127 command += ['dev', device]
128 output = self.execute('address', *command)
129 addresses = list(parse_addresses(output))
130
131 return list_ip_addresses(addresses=addresses,
132 ip_addresses=ip_addresses, port=port,
133 subnets=subnets)
134
135 def add_link(self, name, link_type, link=None, segmentation_id=None):
136 command = ['add']
137 if link:
138 command += ['link', link]
139 command += ['name', name, 'type', link_type]
140 if id:
141 command += ['id', segmentation_id]
142 return self.execute('link', *command)
143
Miguel Angel Nieto Jimenez823b1a02022-06-03 12:58:55 +0000144 def set_link_address(self, address, device):
145 command = ['set', 'address', address, 'dev', device]
146 return self.execute('link', *command)
147
Federico Ressic2ed23d2018-10-25 09:31:47 +0200148 def set_link(self, device, state=None):
149 command = ['set', 'dev', device]
150 if state:
151 command.append(state)
152 return self.execute('link', *command)
153
154 def add_address(self, address, device):
155 # ip addr add 192.168.1.1/24 dev em1
156 return self.execute('address', 'add', address, 'dev', device)
157
Eduardo Olivares088707b2020-12-01 21:13:45 +0100158 def delete_address(self, address, device):
159 # ip addr del 192.168.1.1/24 dev em1
160 return self.execute('address', 'del', address, 'dev', device)
161
Renjing Xiao81817e42025-07-01 09:55:10 +0100162 def add_route(self, address, device, gateway=None, ip_version=4):
Eduardo Olivares088707b2020-12-01 21:13:45 +0100163 if gateway:
Eduardo Olivares088707b2020-12-01 21:13:45 +0100164 return self.execute(
165 'route', 'add', address, 'via', gateway, 'dev', device)
166 else:
Renjing Xiao81817e42025-07-01 09:55:10 +0100167 return self.execute(
168 f'-{ip_version}', 'route', 'add', address, 'dev', device)
Eduardo Olivares088707b2020-12-01 21:13:45 +0100169
Renjing Xiao81817e42025-07-01 09:55:10 +0100170 def delete_route(self, address, device, ip_version=4):
171 return self.execute(
172 f'-{ip_version}', 'route', 'del', address, 'dev', device)
Eduardo Olivares088707b2020-12-01 21:13:45 +0100173
Renjing Xiao81817e42025-07-01 09:55:10 +0100174 def list_routes(self, *args, device=None, ip_version=4):
175 if not args and device:
176 args = ("dev", device)
177 output = self.execute(f'-{ip_version}', 'route', 'show', *args)
Federico Ressic2ed23d2018-10-25 09:31:47 +0200178 return list(parse_routes(output))
179
Slawek Kaplonskia1e88c42020-03-03 03:00:48 +0100180 def get_nic_name_by_mac(self, mac_address):
181 nics = self.execute("-o", "link")
182 for nic_line in nics.split("\n"):
183 if mac_address in nic_line:
184 return nic_line.split(":")[1].strip()
185
Federico Ressic2ed23d2018-10-25 09:31:47 +0200186
187def parse_addresses(command_output):
188 address = device = None
189 addresses = []
190 for i, line in enumerate(command_output.split('\n')):
191 try:
192 line_number = i + 1
193 fields = line.strip().split()
194 if not fields:
195 continue
196 indent = line.index(fields[0] + ' ')
197 if indent == 0:
198 # example of line
199 # 2: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 # noqa
200 address = None
201 name = fields[1]
202 if name.endswith(':'):
203 name = name[:-1]
204 if '@' in name:
205 name, parent = name.split('@', 1)
206 else:
207 parent = None
208
209 if len(fields) > 2:
210 # flags example: <LOOPBACK,UP,LOWER_UP>
211 flags = fields[2]
212 if flags.startswith('<'):
213 flags = flags[1:]
214 if flags.startswith('>'):
215 flags = flags[:-1]
216 flags = flags.split(',')
217
218 device = Device(name=name, parent=parent, flags=flags,
219 properties=dict(parse_properties(fields[3:])))
220 LOG.debug("Device parsed: %r", device)
221
222 elif indent == 4:
223 address = Address.create(
224 family=fields[0], address=fields[1], device=device,
225 properties=dict(parse_properties(fields[2:])))
226 addresses.append(address)
227 LOG.debug("Address parsed: %r", address)
228
229 elif indent == 7:
230 address.properties.update(parse_properties(fields))
231 LOG.debug("Address properties parsed: %r", address.properties)
232
233 else:
234 assert False, "Invalid line indentation: {!r}".format(indent)
235
236 except Exception:
237 with excutils.save_and_reraise_exception():
238 LOG.exception("Error parsing ip command output at line %d:\n"
239 "%r\n",
240 line_number, line)
241 raise
242
243 return addresses
244
245
246def parse_properties(fields):
247 for i, field in enumerate(fields):
248 if i % 2 == 0:
249 key = field
250 else:
251 yield key, field
252
253
254class HasProperties(object):
255
256 def __getattr__(self, name):
257 try:
258 return self.properties[name]
259 except KeyError:
260 pass
261 # This should raise AttributeError
262 return getattr(super(HasProperties, self), name)
263
264
265class Address(HasProperties,
266 collections.namedtuple('Address',
267 ['family', 'address', 'device',
268 'properties'])):
269
270 _subclasses = {}
271
272 @classmethod
273 def create(cls, family, address, device, properties):
274 cls = cls._subclasses.get(family, cls)
275 return cls(family=family, address=address, device=device,
276 properties=properties)
277
278 @classmethod
279 def register_subclass(cls, family, subclass=None):
280 if not issubclass(subclass, cls):
281 msg = "{!r} is not sub-class of {!r}".format(cls, Address)
282 raise TypeError(msg)
283 cls._subclasses[family] = subclass
284
285
286class Device(HasProperties,
287 collections.namedtuple('Device',
288 ['name', 'parent', 'flags',
289 'properties'])):
290 pass
291
292
293def register_address_subclass(families):
294
295 def decorator(subclass):
296 for family in families:
297 Address.register_subclass(family=family, subclass=subclass)
298 return subclass
299
300 return decorator
301
302
303@register_address_subclass(['inet', 'inet6'])
304class InetAddress(Address):
305
306 @property
307 def ip(self):
308 return self.network.ip
309
310 @property
311 def network(self):
312 return netaddr.IPNetwork(self.address)
313
314
315def parse_routes(command_output):
316 for line in command_output.split('\n'):
317 fields = line.strip().split()
318 if fields:
319 dest = fields[0]
320 properties = dict(parse_properties(fields[1:]))
321 if dest == 'default':
322 dest = constants.IPv4_ANY
323 via = properties.get('via')
324 if via:
325 dest = constants.IP_ANY[netaddr.IPAddress(via).version]
326 yield Route(dest=dest, properties=properties)
327
328
329def list_ip_addresses(addresses, ip_addresses=None, port=None,
330 subnets=None):
331 if port:
332 # filter addresses by port IP addresses
333 ip_addresses = set(ip_addresses) if ip_addresses else set()
334 ip_addresses.update(list_port_ip_addresses(port=port,
335 subnets=subnets))
336 if ip_addresses:
337 addresses = [a for a in addresses if (hasattr(a, 'ip') and
338 str(a.ip) in ip_addresses)]
339 return addresses
340
341
342def list_port_ip_addresses(port, subnets=None):
343 fixed_ips = port['fixed_ips']
344 if subnets:
345 subnets = {subnet['id']: subnet for subnet in subnets}
346 fixed_ips = [fixed_ip
347 for fixed_ip in fixed_ips
348 if fixed_ip['subnet_id'] in subnets]
349 return [ip['ip_address'] for ip in port['fixed_ips']]
350
351
352def get_port_device_name(addresses, port):
353 for address in list_ip_addresses(addresses=addresses, port=port):
354 return address.device.name
355
Bernard Cafarellic3bec862020-09-10 13:59:49 +0200356 msg = "Port {0!r} fixed IPs not found on server.".format(port['id'])
Federico Ressic2ed23d2018-10-25 09:31:47 +0200357 raise ValueError(msg)
358
359
Eduardo Olivares088707b2020-12-01 21:13:45 +0100360def get_vlan_device_name(addresses, ip_addresses):
361 for address in list_ip_addresses(addresses=addresses,
362 ip_addresses=ip_addresses):
363 return address.device.name
364
365 msg = "Fixed IPs {0!r} not found on server.".format(' '.join(ip_addresses))
366 raise ValueError(msg)
367
368
Federico Ressic2ed23d2018-10-25 09:31:47 +0200369def _get_ip_address_prefix_len_pairs(port, subnets):
370 subnets = {subnet['id']: subnet for subnet in subnets}
371 for fixed_ip in port['fixed_ips']:
372 subnet = subnets.get(fixed_ip['subnet_id'])
373 if subnet:
374 yield (fixed_ip['ip_address'],
375 netaddr.IPNetwork(subnet['cidr']).prefixlen)
376
377
Slawek Kaplonski8033af72020-05-05 12:01:37 +0200378def arp_table(namespace=None):
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000379 # 192.168.0.16 0x1 0x2 dc:a6:32:06:56:51 * enp0s31f6
380 regex_str = (r"([^ ]+)\s+(0x\d+)\s+(0x\d+)\s+(\w{2}\:\w{2}\:\w{2}\:\w{2}\:"
381 r"\w{2}\:\w{2})\s+([\w+\*]+)\s+([\-\w]+)")
382 regex = re.compile(regex_str)
383 arp_table = []
Slawek Kaplonski8033af72020-05-05 12:01:37 +0200384 cmd = ""
385 if namespace:
386 cmd = "sudo ip netns exec %s " % namespace
387 cmd += "cat /proc/net/arp"
388 arp_entries = shell.execute(cmd).stdout.split("\n")
389 for line in arp_entries:
390 m = regex.match(line)
391 if m:
392 arp_table.append(ARPregister(
393 ip_address=m.group(1), hw_type=m.group(2),
394 flags=m.group(3), mac_address=m.group(4),
395 mask=m.group(5), device=m.group(6)))
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000396 return arp_table
397
398
Rodolfo Alonso Hernandezc134ea92021-04-14 15:15:01 +0000399def list_iptables(version=constants.IP_VERSION_4, namespace=None):
Brian Haley52582a02023-05-11 11:45:13 -0400400 cmd = 'sudo '
Rodolfo Alonso Hernandezc134ea92021-04-14 15:15:01 +0000401 if namespace:
Brian Haley52582a02023-05-11 11:45:13 -0400402 cmd += 'ip netns exec %s ' % namespace
Rodolfo Alonso Hernandezc134ea92021-04-14 15:15:01 +0000403 cmd += ('iptables-save' if version == constants.IP_VERSION_4 else
404 'ip6tables-save')
405 return shell.execute(cmd).stdout
406
407
408def list_listening_sockets(namespace=None):
409 cmd = ''
410 if namespace:
411 cmd = 'sudo ip netns exec %s ' % namespace
412 cmd += 'netstat -nlp'
413 return shell.execute(cmd).stdout
414
415
Federico Ressic2ed23d2018-10-25 09:31:47 +0200416class Route(HasProperties,
417 collections.namedtuple('Route',
418 ['dest', 'properties'])):
419
420 @property
421 def dest_ip(self):
422 return netaddr.IPNetwork(self.dest)
423
424 @property
425 def via_ip(self):
426 return netaddr.IPAddress(self.via)
427
428 @property
429 def src_ip(self):
430 return netaddr.IPAddress(self.src)
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000431
432 def __str__(self):
433 properties_str = ' '.join('%s %s' % (k, v)
434 for k, v in self.properties.items())
435 return '%(dest)s %(properties)s' % {'dest': self.dest,
436 'properties': properties_str}
437
438
439class ARPregister(collections.namedtuple(
440 'ARPregister',
441 ['ip_address', 'hw_type', 'flags', 'mac_address', 'mask', 'device'])):
442
443 def __str__(self):
444 return '%s %s %s %s %s %s' % (self.ip_address, self.hw_type,
445 self.flags, self.mac_address, self.mask,
446 self.device)
ccamposr3e1921b2020-01-29 11:10:05 +0100447
448
449def find_valid_cidr(valid_cidr='10.0.0.0/8', used_cidr=None):
450 total_ips = netaddr.IPSet(netaddr.IPNetwork(valid_cidr))
451 if used_cidr:
452 used_network = netaddr.IPNetwork(used_cidr)
453 netmask = used_network.netmask.netmask_bits()
454 valid_ips = total_ips.difference(netaddr.IPSet(used_network))
455 else:
456 valid_ips = total_ips
457 netmask = 24
458
459 for ip in valid_ips:
460 valid_network = netaddr.IPNetwork('%s/%s' % (ip, netmask))
461 if valid_network in valid_ips:
462 return valid_network.cidr
463
464 exception_str = 'No valid CIDR found in %s' % valid_cidr
465 if used_cidr:
466 exception_str += ', used CIDR %s' % used_cidr
467 raise Exception(exception_str)
Rodolfo Alonso Hernandez0adf8a22020-06-11 11:28:25 +0000468
469
470def wait_for_interface_status(client, server_id, port_id, status,
471 ssh_client=None, mac_address=None):
472 """Waits for an interface to reach a given status and checks VM NIC
473
474 This method enhances the tempest one. Apart from checking the interface
475 status returned by Nova, this methods access the VM to check if the NIC
476 interface is already detected by the kernel.
477 """
478 body = waiters.wait_for_interface_status(client, server_id, port_id,
479 status)
480
481 if ssh_client and mac_address:
482 ip_command = IPCommand(ssh_client)
483 common_utils.wait_until_true(
484 lambda: ip_command.get_nic_name_by_mac(mac_address),
485 timeout=10,
486 exception=RuntimeError('Interface with MAC %s not present in the '
487 'VM' % mac_address))
488
489 return body