blob: d5966d24f85a5f8a059179cf4dfe7d21d029d138 [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
Rodolfo Alonso Hernandez80df3662025-08-28 09:04:14 +000022from neutron_lib._i18n import _
Federico Ressic2ed23d2018-10-25 09:31:47 +020023from neutron_lib import constants
24from oslo_log import log
25from oslo_utils import excutils
Rodolfo Alonso Hernandez0adf8a22020-06-11 11:28:25 +000026from tempest.common import waiters
Federico Ressic2ed23d2018-10-25 09:31:47 +020027
28from neutron_tempest_plugin.common import shell
Rodolfo Alonso Hernandez0adf8a22020-06-11 11:28:25 +000029from neutron_tempest_plugin.common import utils as common_utils
Federico Ressic2ed23d2018-10-25 09:31:47 +020030
31
32LOG = log.getLogger(__name__)
33
34
35class IPCommand(object):
36
37 sudo = 'sudo'
38 ip_path = '/sbin/ip'
39
Slawek Kaplonski8033af72020-05-05 12:01:37 +020040 def __init__(self, ssh_client=None, timeout=None, namespace=None):
Federico Ressic2ed23d2018-10-25 09:31:47 +020041 self.ssh_client = ssh_client
42 self.timeout = timeout
Slawek Kaplonski8033af72020-05-05 12:01:37 +020043 self.namespace = namespace
Federico Ressic2ed23d2018-10-25 09:31:47 +020044
45 def get_command(self, obj, *command):
Slawek Kaplonski8033af72020-05-05 12:01:37 +020046 command_line = '{sudo!s} {ip_path!r} '.format(sudo=self.sudo,
47 ip_path=self.ip_path)
48 if self.namespace:
49 command_line += 'netns exec {ns_name!s} {ip_path!r} '.format(
50 ns_name=self.namespace, ip_path=self.ip_path)
51 command_line += '{object!s} {command!s}'.format(
52 object=obj,
Federico Ressic2ed23d2018-10-25 09:31:47 +020053 command=subprocess.list2cmdline([str(c) for c in command]))
54 return command_line
55
56 def execute(self, obj, *command):
57 command_line = self.get_command(obj, *command)
58 return shell.execute(command_line, ssh_client=self.ssh_client,
59 timeout=self.timeout).stdout
60
Miguel Angel Nieto Jimenez823b1a02022-06-03 12:58:55 +000061 def configure_vlan(self, addresses, port, vlan_tag, subport_ips, mac=None):
Eduardo Olivares088707b2020-12-01 21:13:45 +010062 port_device = get_port_device_name(addresses=addresses, port=port)
63 subport_device = '{!s}.{!s}'.format(port_device, vlan_tag)
64 LOG.debug('Configuring VLAN subport interface %r on top of interface '
65 '%r with IPs: %s', subport_device, port_device,
66 ', '.join(subport_ips))
67
68 self.add_link(link=port_device, name=subport_device, link_type='vlan',
69 segmentation_id=vlan_tag)
Miguel Angel Nieto Jimenez823b1a02022-06-03 12:58:55 +000070 if mac:
71 self.set_link_address(address=mac, device=subport_device)
Eduardo Olivares088707b2020-12-01 21:13:45 +010072 self.set_link(device=subport_device, state='up')
73 for subport_ip in subport_ips:
74 self.add_address(address=subport_ip, device=subport_device)
75 return subport_device
76
Federico Ressic2ed23d2018-10-25 09:31:47 +020077 def configure_vlan_subport(self, port, subport, vlan_tag, subnets):
78 addresses = self.list_addresses()
79 try:
80 subport_device = get_port_device_name(addresses=addresses,
81 port=subport)
82 except ValueError:
83 pass
84 else:
85 LOG.debug('Interface %r already configured.', subport_device)
86 return subport_device
87
88 subport_ips = [
89 "{!s}/{!s}".format(ip, prefix_len)
90 for ip, prefix_len in _get_ip_address_prefix_len_pairs(
91 port=subport, subnets=subnets)]
92 if not subport_ips:
Rodolfo Alonso Hernandez80df3662025-08-28 09:04:14 +000093 raise ValueError(_(
Federico Ressic2ed23d2018-10-25 09:31:47 +020094 "Unable to get IP address and subnet prefix lengths for "
Rodolfo Alonso Hernandez80df3662025-08-28 09:04:14 +000095 "subport"))
Federico Ressic2ed23d2018-10-25 09:31:47 +020096
Miguel Angel Nieto Jimenez823b1a02022-06-03 12:58:55 +000097 return self.configure_vlan(addresses, port, vlan_tag, subport_ips,
98 subport['mac_address'])
Federico Ressic2ed23d2018-10-25 09:31:47 +020099
Slawek Kaplonskid4c707e2024-12-16 14:46:16 +0100100 def configure_inner_vlan(self, port, vlan_tag, ip_addresses):
Eduardo Olivares088707b2020-12-01 21:13:45 +0100101 addresses = self.list_addresses()
102 try:
103 subport_device = get_vlan_device_name(addresses, ip_addresses)
104 except ValueError:
105 pass
106 else:
107 LOG.debug('Interface %r already configured.', subport_device)
108 return subport_device
109
110 return self.configure_vlan(addresses, port, vlan_tag, ip_addresses)
Federico Ressic2ed23d2018-10-25 09:31:47 +0200111
Rodolfo Alonso Hernandezeb7f7b02025-02-26 10:24:06 +0000112 # NOTE(ralonsoh): some projects, like whitebox-neutron-tempest-plugin, are
113 # using ``configure_vlan_transparent`` method. The concept of "inner VLAN"
114 # does not exist in the VLAN transparency feature.
115 configure_vlan_transparent = configure_inner_vlan
116
Slawek Kaplonski8033af72020-05-05 12:01:37 +0200117 def list_namespaces(self):
118 namespaces_output = self.execute("netns")
119 ns_list = []
120 for ns_line in namespaces_output.split("\n"):
121 ns_list.append(ns_line.split(" ", 1)[0])
122 return ns_list
123
Federico Ressic2ed23d2018-10-25 09:31:47 +0200124 def list_addresses(self, device=None, ip_addresses=None, port=None,
125 subnets=None):
126 command = ['list']
127 if device:
128 command += ['dev', device]
129 output = self.execute('address', *command)
130 addresses = list(parse_addresses(output))
131
132 return list_ip_addresses(addresses=addresses,
133 ip_addresses=ip_addresses, port=port,
134 subnets=subnets)
135
136 def add_link(self, name, link_type, link=None, segmentation_id=None):
137 command = ['add']
138 if link:
139 command += ['link', link]
140 command += ['name', name, 'type', link_type]
141 if id:
142 command += ['id', segmentation_id]
143 return self.execute('link', *command)
144
Miguel Angel Nieto Jimenez823b1a02022-06-03 12:58:55 +0000145 def set_link_address(self, address, device):
146 command = ['set', 'address', address, 'dev', device]
147 return self.execute('link', *command)
148
Federico Ressic2ed23d2018-10-25 09:31:47 +0200149 def set_link(self, device, state=None):
150 command = ['set', 'dev', device]
151 if state:
152 command.append(state)
153 return self.execute('link', *command)
154
155 def add_address(self, address, device):
156 # ip addr add 192.168.1.1/24 dev em1
157 return self.execute('address', 'add', address, 'dev', device)
158
Eduardo Olivares088707b2020-12-01 21:13:45 +0100159 def delete_address(self, address, device):
160 # ip addr del 192.168.1.1/24 dev em1
161 return self.execute('address', 'del', address, 'dev', device)
162
Renjing Xiao81817e42025-07-01 09:55:10 +0100163 def add_route(self, address, device, gateway=None, ip_version=4):
Eduardo Olivares088707b2020-12-01 21:13:45 +0100164 if gateway:
Eduardo Olivares088707b2020-12-01 21:13:45 +0100165 return self.execute(
166 'route', 'add', address, 'via', gateway, 'dev', device)
167 else:
Renjing Xiao81817e42025-07-01 09:55:10 +0100168 return self.execute(
169 f'-{ip_version}', 'route', 'add', address, 'dev', device)
Eduardo Olivares088707b2020-12-01 21:13:45 +0100170
Renjing Xiao81817e42025-07-01 09:55:10 +0100171 def delete_route(self, address, device, ip_version=4):
172 return self.execute(
173 f'-{ip_version}', 'route', 'del', address, 'dev', device)
Eduardo Olivares088707b2020-12-01 21:13:45 +0100174
Renjing Xiao81817e42025-07-01 09:55:10 +0100175 def list_routes(self, *args, device=None, ip_version=4):
176 if not args and device:
177 args = ("dev", device)
178 output = self.execute(f'-{ip_version}', 'route', 'show', *args)
Federico Ressic2ed23d2018-10-25 09:31:47 +0200179 return list(parse_routes(output))
180
Slawek Kaplonskia1e88c42020-03-03 03:00:48 +0100181 def get_nic_name_by_mac(self, mac_address):
182 nics = self.execute("-o", "link")
183 for nic_line in nics.split("\n"):
184 if mac_address in nic_line:
185 return nic_line.split(":")[1].strip()
186
Yatin Karele5aa9ea2026-03-02 12:58:29 +0530187 def has_dadfailed(self, device):
188 """Check if device has any IPv6 addresses in dadfailed state"""
189 output = self.execute('address', 'show', 'dev', device)
190 return 'dadfailed' in output
191
Federico Ressic2ed23d2018-10-25 09:31:47 +0200192
193def parse_addresses(command_output):
194 address = device = None
195 addresses = []
196 for i, line in enumerate(command_output.split('\n')):
197 try:
198 line_number = i + 1
199 fields = line.strip().split()
200 if not fields:
201 continue
202 indent = line.index(fields[0] + ' ')
203 if indent == 0:
204 # example of line
205 # 2: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 # noqa
206 address = None
207 name = fields[1]
208 if name.endswith(':'):
209 name = name[:-1]
210 if '@' in name:
211 name, parent = name.split('@', 1)
212 else:
213 parent = None
214
215 if len(fields) > 2:
216 # flags example: <LOOPBACK,UP,LOWER_UP>
217 flags = fields[2]
218 if flags.startswith('<'):
219 flags = flags[1:]
220 if flags.startswith('>'):
221 flags = flags[:-1]
222 flags = flags.split(',')
223
224 device = Device(name=name, parent=parent, flags=flags,
225 properties=dict(parse_properties(fields[3:])))
226 LOG.debug("Device parsed: %r", device)
227
228 elif indent == 4:
229 address = Address.create(
230 family=fields[0], address=fields[1], device=device,
231 properties=dict(parse_properties(fields[2:])))
232 addresses.append(address)
233 LOG.debug("Address parsed: %r", address)
234
235 elif indent == 7:
236 address.properties.update(parse_properties(fields))
237 LOG.debug("Address properties parsed: %r", address.properties)
238
239 else:
240 assert False, "Invalid line indentation: {!r}".format(indent)
241
242 except Exception:
243 with excutils.save_and_reraise_exception():
244 LOG.exception("Error parsing ip command output at line %d:\n"
245 "%r\n",
246 line_number, line)
247 raise
248
249 return addresses
250
251
252def parse_properties(fields):
253 for i, field in enumerate(fields):
254 if i % 2 == 0:
255 key = field
256 else:
257 yield key, field
258
259
260class HasProperties(object):
261
262 def __getattr__(self, name):
263 try:
264 return self.properties[name]
265 except KeyError:
266 pass
267 # This should raise AttributeError
268 return getattr(super(HasProperties, self), name)
269
270
271class Address(HasProperties,
272 collections.namedtuple('Address',
273 ['family', 'address', 'device',
274 'properties'])):
275
276 _subclasses = {}
277
278 @classmethod
279 def create(cls, family, address, device, properties):
280 cls = cls._subclasses.get(family, cls)
281 return cls(family=family, address=address, device=device,
282 properties=properties)
283
284 @classmethod
285 def register_subclass(cls, family, subclass=None):
286 if not issubclass(subclass, cls):
287 msg = "{!r} is not sub-class of {!r}".format(cls, Address)
288 raise TypeError(msg)
289 cls._subclasses[family] = subclass
290
291
292class Device(HasProperties,
293 collections.namedtuple('Device',
294 ['name', 'parent', 'flags',
295 'properties'])):
296 pass
297
298
299def register_address_subclass(families):
300
301 def decorator(subclass):
302 for family in families:
303 Address.register_subclass(family=family, subclass=subclass)
304 return subclass
305
306 return decorator
307
308
309@register_address_subclass(['inet', 'inet6'])
310class InetAddress(Address):
311
312 @property
313 def ip(self):
314 return self.network.ip
315
316 @property
317 def network(self):
318 return netaddr.IPNetwork(self.address)
319
320
321def parse_routes(command_output):
322 for line in command_output.split('\n'):
323 fields = line.strip().split()
324 if fields:
325 dest = fields[0]
326 properties = dict(parse_properties(fields[1:]))
327 if dest == 'default':
328 dest = constants.IPv4_ANY
329 via = properties.get('via')
330 if via:
331 dest = constants.IP_ANY[netaddr.IPAddress(via).version]
332 yield Route(dest=dest, properties=properties)
333
334
335def list_ip_addresses(addresses, ip_addresses=None, port=None,
336 subnets=None):
337 if port:
338 # filter addresses by port IP addresses
339 ip_addresses = set(ip_addresses) if ip_addresses else set()
340 ip_addresses.update(list_port_ip_addresses(port=port,
341 subnets=subnets))
342 if ip_addresses:
343 addresses = [a for a in addresses if (hasattr(a, 'ip') and
344 str(a.ip) in ip_addresses)]
345 return addresses
346
347
348def list_port_ip_addresses(port, subnets=None):
349 fixed_ips = port['fixed_ips']
350 if subnets:
351 subnets = {subnet['id']: subnet for subnet in subnets}
352 fixed_ips = [fixed_ip
353 for fixed_ip in fixed_ips
354 if fixed_ip['subnet_id'] in subnets]
355 return [ip['ip_address'] for ip in port['fixed_ips']]
356
357
358def get_port_device_name(addresses, port):
359 for address in list_ip_addresses(addresses=addresses, port=port):
360 return address.device.name
361
Rodolfo Alonso Hernandez80df3662025-08-28 09:04:14 +0000362 msg = _("Port {0!r} fixed IPs not found on server.".format(port['id']))
Federico Ressic2ed23d2018-10-25 09:31:47 +0200363 raise ValueError(msg)
364
365
Eduardo Olivares088707b2020-12-01 21:13:45 +0100366def get_vlan_device_name(addresses, ip_addresses):
367 for address in list_ip_addresses(addresses=addresses,
368 ip_addresses=ip_addresses):
369 return address.device.name
370
Rodolfo Alonso Hernandez80df3662025-08-28 09:04:14 +0000371 msg = _(
372 "Fixed IPs {0!r} not found on server.".format(' '.join(ip_addresses)))
Eduardo Olivares088707b2020-12-01 21:13:45 +0100373 raise ValueError(msg)
374
375
Federico Ressic2ed23d2018-10-25 09:31:47 +0200376def _get_ip_address_prefix_len_pairs(port, subnets):
377 subnets = {subnet['id']: subnet for subnet in subnets}
378 for fixed_ip in port['fixed_ips']:
379 subnet = subnets.get(fixed_ip['subnet_id'])
380 if subnet:
381 yield (fixed_ip['ip_address'],
382 netaddr.IPNetwork(subnet['cidr']).prefixlen)
383
384
Slawek Kaplonski8033af72020-05-05 12:01:37 +0200385def arp_table(namespace=None):
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000386 # 192.168.0.16 0x1 0x2 dc:a6:32:06:56:51 * enp0s31f6
387 regex_str = (r"([^ ]+)\s+(0x\d+)\s+(0x\d+)\s+(\w{2}\:\w{2}\:\w{2}\:\w{2}\:"
388 r"\w{2}\:\w{2})\s+([\w+\*]+)\s+([\-\w]+)")
389 regex = re.compile(regex_str)
390 arp_table = []
Slawek Kaplonski8033af72020-05-05 12:01:37 +0200391 cmd = ""
392 if namespace:
393 cmd = "sudo ip netns exec %s " % namespace
394 cmd += "cat /proc/net/arp"
395 arp_entries = shell.execute(cmd).stdout.split("\n")
396 for line in arp_entries:
397 m = regex.match(line)
398 if m:
399 arp_table.append(ARPregister(
400 ip_address=m.group(1), hw_type=m.group(2),
401 flags=m.group(3), mac_address=m.group(4),
402 mask=m.group(5), device=m.group(6)))
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000403 return arp_table
404
405
Rodolfo Alonso Hernandezc134ea92021-04-14 15:15:01 +0000406def list_iptables(version=constants.IP_VERSION_4, namespace=None):
Brian Haley52582a02023-05-11 11:45:13 -0400407 cmd = 'sudo '
Rodolfo Alonso Hernandezc134ea92021-04-14 15:15:01 +0000408 if namespace:
Brian Haley52582a02023-05-11 11:45:13 -0400409 cmd += 'ip netns exec %s ' % namespace
Rodolfo Alonso Hernandezc134ea92021-04-14 15:15:01 +0000410 cmd += ('iptables-save' if version == constants.IP_VERSION_4 else
411 'ip6tables-save')
412 return shell.execute(cmd).stdout
413
414
415def list_listening_sockets(namespace=None):
416 cmd = ''
417 if namespace:
418 cmd = 'sudo ip netns exec %s ' % namespace
419 cmd += 'netstat -nlp'
420 return shell.execute(cmd).stdout
421
422
Federico Ressic2ed23d2018-10-25 09:31:47 +0200423class Route(HasProperties,
424 collections.namedtuple('Route',
425 ['dest', 'properties'])):
426
427 @property
428 def dest_ip(self):
429 return netaddr.IPNetwork(self.dest)
430
431 @property
432 def via_ip(self):
433 return netaddr.IPAddress(self.via)
434
435 @property
436 def src_ip(self):
437 return netaddr.IPAddress(self.src)
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000438
439 def __str__(self):
440 properties_str = ' '.join('%s %s' % (k, v)
441 for k, v in self.properties.items())
442 return '%(dest)s %(properties)s' % {'dest': self.dest,
443 'properties': properties_str}
444
445
446class ARPregister(collections.namedtuple(
447 'ARPregister',
448 ['ip_address', 'hw_type', 'flags', 'mac_address', 'mask', 'device'])):
449
450 def __str__(self):
451 return '%s %s %s %s %s %s' % (self.ip_address, self.hw_type,
452 self.flags, self.mac_address, self.mask,
453 self.device)
ccamposr3e1921b2020-01-29 11:10:05 +0100454
455
456def find_valid_cidr(valid_cidr='10.0.0.0/8', used_cidr=None):
457 total_ips = netaddr.IPSet(netaddr.IPNetwork(valid_cidr))
458 if used_cidr:
459 used_network = netaddr.IPNetwork(used_cidr)
460 netmask = used_network.netmask.netmask_bits()
461 valid_ips = total_ips.difference(netaddr.IPSet(used_network))
462 else:
463 valid_ips = total_ips
464 netmask = 24
465
466 for ip in valid_ips:
467 valid_network = netaddr.IPNetwork('%s/%s' % (ip, netmask))
468 if valid_network in valid_ips:
469 return valid_network.cidr
470
471 exception_str = 'No valid CIDR found in %s' % valid_cidr
472 if used_cidr:
473 exception_str += ', used CIDR %s' % used_cidr
474 raise Exception(exception_str)
Rodolfo Alonso Hernandez0adf8a22020-06-11 11:28:25 +0000475
476
477def wait_for_interface_status(client, server_id, port_id, status,
478 ssh_client=None, mac_address=None):
479 """Waits for an interface to reach a given status and checks VM NIC
480
481 This method enhances the tempest one. Apart from checking the interface
482 status returned by Nova, this methods access the VM to check if the NIC
483 interface is already detected by the kernel.
484 """
485 body = waiters.wait_for_interface_status(client, server_id, port_id,
486 status)
487
488 if ssh_client and mac_address:
489 ip_command = IPCommand(ssh_client)
490 common_utils.wait_until_true(
491 lambda: ip_command.get_nic_name_by_mac(mac_address),
492 timeout=10,
493 exception=RuntimeError('Interface with MAC %s not present in the '
494 'VM' % mac_address))
495
496 return body