blob: bab9064027a16a82933dd3c5229a70e64c6250a7 [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
Federico Ressic2ed23d2018-10-25 09:31:47 +0200187
188def parse_addresses(command_output):
189 address = device = None
190 addresses = []
191 for i, line in enumerate(command_output.split('\n')):
192 try:
193 line_number = i + 1
194 fields = line.strip().split()
195 if not fields:
196 continue
197 indent = line.index(fields[0] + ' ')
198 if indent == 0:
199 # example of line
200 # 2: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 # noqa
201 address = None
202 name = fields[1]
203 if name.endswith(':'):
204 name = name[:-1]
205 if '@' in name:
206 name, parent = name.split('@', 1)
207 else:
208 parent = None
209
210 if len(fields) > 2:
211 # flags example: <LOOPBACK,UP,LOWER_UP>
212 flags = fields[2]
213 if flags.startswith('<'):
214 flags = flags[1:]
215 if flags.startswith('>'):
216 flags = flags[:-1]
217 flags = flags.split(',')
218
219 device = Device(name=name, parent=parent, flags=flags,
220 properties=dict(parse_properties(fields[3:])))
221 LOG.debug("Device parsed: %r", device)
222
223 elif indent == 4:
224 address = Address.create(
225 family=fields[0], address=fields[1], device=device,
226 properties=dict(parse_properties(fields[2:])))
227 addresses.append(address)
228 LOG.debug("Address parsed: %r", address)
229
230 elif indent == 7:
231 address.properties.update(parse_properties(fields))
232 LOG.debug("Address properties parsed: %r", address.properties)
233
234 else:
235 assert False, "Invalid line indentation: {!r}".format(indent)
236
237 except Exception:
238 with excutils.save_and_reraise_exception():
239 LOG.exception("Error parsing ip command output at line %d:\n"
240 "%r\n",
241 line_number, line)
242 raise
243
244 return addresses
245
246
247def parse_properties(fields):
248 for i, field in enumerate(fields):
249 if i % 2 == 0:
250 key = field
251 else:
252 yield key, field
253
254
255class HasProperties(object):
256
257 def __getattr__(self, name):
258 try:
259 return self.properties[name]
260 except KeyError:
261 pass
262 # This should raise AttributeError
263 return getattr(super(HasProperties, self), name)
264
265
266class Address(HasProperties,
267 collections.namedtuple('Address',
268 ['family', 'address', 'device',
269 'properties'])):
270
271 _subclasses = {}
272
273 @classmethod
274 def create(cls, family, address, device, properties):
275 cls = cls._subclasses.get(family, cls)
276 return cls(family=family, address=address, device=device,
277 properties=properties)
278
279 @classmethod
280 def register_subclass(cls, family, subclass=None):
281 if not issubclass(subclass, cls):
282 msg = "{!r} is not sub-class of {!r}".format(cls, Address)
283 raise TypeError(msg)
284 cls._subclasses[family] = subclass
285
286
287class Device(HasProperties,
288 collections.namedtuple('Device',
289 ['name', 'parent', 'flags',
290 'properties'])):
291 pass
292
293
294def register_address_subclass(families):
295
296 def decorator(subclass):
297 for family in families:
298 Address.register_subclass(family=family, subclass=subclass)
299 return subclass
300
301 return decorator
302
303
304@register_address_subclass(['inet', 'inet6'])
305class InetAddress(Address):
306
307 @property
308 def ip(self):
309 return self.network.ip
310
311 @property
312 def network(self):
313 return netaddr.IPNetwork(self.address)
314
315
316def parse_routes(command_output):
317 for line in command_output.split('\n'):
318 fields = line.strip().split()
319 if fields:
320 dest = fields[0]
321 properties = dict(parse_properties(fields[1:]))
322 if dest == 'default':
323 dest = constants.IPv4_ANY
324 via = properties.get('via')
325 if via:
326 dest = constants.IP_ANY[netaddr.IPAddress(via).version]
327 yield Route(dest=dest, properties=properties)
328
329
330def list_ip_addresses(addresses, ip_addresses=None, port=None,
331 subnets=None):
332 if port:
333 # filter addresses by port IP addresses
334 ip_addresses = set(ip_addresses) if ip_addresses else set()
335 ip_addresses.update(list_port_ip_addresses(port=port,
336 subnets=subnets))
337 if ip_addresses:
338 addresses = [a for a in addresses if (hasattr(a, 'ip') and
339 str(a.ip) in ip_addresses)]
340 return addresses
341
342
343def list_port_ip_addresses(port, subnets=None):
344 fixed_ips = port['fixed_ips']
345 if subnets:
346 subnets = {subnet['id']: subnet for subnet in subnets}
347 fixed_ips = [fixed_ip
348 for fixed_ip in fixed_ips
349 if fixed_ip['subnet_id'] in subnets]
350 return [ip['ip_address'] for ip in port['fixed_ips']]
351
352
353def get_port_device_name(addresses, port):
354 for address in list_ip_addresses(addresses=addresses, port=port):
355 return address.device.name
356
Rodolfo Alonso Hernandez80df3662025-08-28 09:04:14 +0000357 msg = _("Port {0!r} fixed IPs not found on server.".format(port['id']))
Federico Ressic2ed23d2018-10-25 09:31:47 +0200358 raise ValueError(msg)
359
360
Eduardo Olivares088707b2020-12-01 21:13:45 +0100361def get_vlan_device_name(addresses, ip_addresses):
362 for address in list_ip_addresses(addresses=addresses,
363 ip_addresses=ip_addresses):
364 return address.device.name
365
Rodolfo Alonso Hernandez80df3662025-08-28 09:04:14 +0000366 msg = _(
367 "Fixed IPs {0!r} not found on server.".format(' '.join(ip_addresses)))
Eduardo Olivares088707b2020-12-01 21:13:45 +0100368 raise ValueError(msg)
369
370
Federico Ressic2ed23d2018-10-25 09:31:47 +0200371def _get_ip_address_prefix_len_pairs(port, subnets):
372 subnets = {subnet['id']: subnet for subnet in subnets}
373 for fixed_ip in port['fixed_ips']:
374 subnet = subnets.get(fixed_ip['subnet_id'])
375 if subnet:
376 yield (fixed_ip['ip_address'],
377 netaddr.IPNetwork(subnet['cidr']).prefixlen)
378
379
Slawek Kaplonski8033af72020-05-05 12:01:37 +0200380def arp_table(namespace=None):
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000381 # 192.168.0.16 0x1 0x2 dc:a6:32:06:56:51 * enp0s31f6
382 regex_str = (r"([^ ]+)\s+(0x\d+)\s+(0x\d+)\s+(\w{2}\:\w{2}\:\w{2}\:\w{2}\:"
383 r"\w{2}\:\w{2})\s+([\w+\*]+)\s+([\-\w]+)")
384 regex = re.compile(regex_str)
385 arp_table = []
Slawek Kaplonski8033af72020-05-05 12:01:37 +0200386 cmd = ""
387 if namespace:
388 cmd = "sudo ip netns exec %s " % namespace
389 cmd += "cat /proc/net/arp"
390 arp_entries = shell.execute(cmd).stdout.split("\n")
391 for line in arp_entries:
392 m = regex.match(line)
393 if m:
394 arp_table.append(ARPregister(
395 ip_address=m.group(1), hw_type=m.group(2),
396 flags=m.group(3), mac_address=m.group(4),
397 mask=m.group(5), device=m.group(6)))
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000398 return arp_table
399
400
Rodolfo Alonso Hernandezc134ea92021-04-14 15:15:01 +0000401def list_iptables(version=constants.IP_VERSION_4, namespace=None):
Brian Haley52582a02023-05-11 11:45:13 -0400402 cmd = 'sudo '
Rodolfo Alonso Hernandezc134ea92021-04-14 15:15:01 +0000403 if namespace:
Brian Haley52582a02023-05-11 11:45:13 -0400404 cmd += 'ip netns exec %s ' % namespace
Rodolfo Alonso Hernandezc134ea92021-04-14 15:15:01 +0000405 cmd += ('iptables-save' if version == constants.IP_VERSION_4 else
406 'ip6tables-save')
407 return shell.execute(cmd).stdout
408
409
410def list_listening_sockets(namespace=None):
411 cmd = ''
412 if namespace:
413 cmd = 'sudo ip netns exec %s ' % namespace
414 cmd += 'netstat -nlp'
415 return shell.execute(cmd).stdout
416
417
Federico Ressic2ed23d2018-10-25 09:31:47 +0200418class Route(HasProperties,
419 collections.namedtuple('Route',
420 ['dest', 'properties'])):
421
422 @property
423 def dest_ip(self):
424 return netaddr.IPNetwork(self.dest)
425
426 @property
427 def via_ip(self):
428 return netaddr.IPAddress(self.via)
429
430 @property
431 def src_ip(self):
432 return netaddr.IPAddress(self.src)
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000433
434 def __str__(self):
435 properties_str = ' '.join('%s %s' % (k, v)
436 for k, v in self.properties.items())
437 return '%(dest)s %(properties)s' % {'dest': self.dest,
438 'properties': properties_str}
439
440
441class ARPregister(collections.namedtuple(
442 'ARPregister',
443 ['ip_address', 'hw_type', 'flags', 'mac_address', 'mask', 'device'])):
444
445 def __str__(self):
446 return '%s %s %s %s %s %s' % (self.ip_address, self.hw_type,
447 self.flags, self.mac_address, self.mask,
448 self.device)
ccamposr3e1921b2020-01-29 11:10:05 +0100449
450
451def find_valid_cidr(valid_cidr='10.0.0.0/8', used_cidr=None):
452 total_ips = netaddr.IPSet(netaddr.IPNetwork(valid_cidr))
453 if used_cidr:
454 used_network = netaddr.IPNetwork(used_cidr)
455 netmask = used_network.netmask.netmask_bits()
456 valid_ips = total_ips.difference(netaddr.IPSet(used_network))
457 else:
458 valid_ips = total_ips
459 netmask = 24
460
461 for ip in valid_ips:
462 valid_network = netaddr.IPNetwork('%s/%s' % (ip, netmask))
463 if valid_network in valid_ips:
464 return valid_network.cidr
465
466 exception_str = 'No valid CIDR found in %s' % valid_cidr
467 if used_cidr:
468 exception_str += ', used CIDR %s' % used_cidr
469 raise Exception(exception_str)
Rodolfo Alonso Hernandez0adf8a22020-06-11 11:28:25 +0000470
471
472def wait_for_interface_status(client, server_id, port_id, status,
473 ssh_client=None, mac_address=None):
474 """Waits for an interface to reach a given status and checks VM NIC
475
476 This method enhances the tempest one. Apart from checking the interface
477 status returned by Nova, this methods access the VM to check if the NIC
478 interface is already detected by the kernel.
479 """
480 body = waiters.wait_for_interface_status(client, server_id, port_id,
481 status)
482
483 if ssh_client and mac_address:
484 ip_command = IPCommand(ssh_client)
485 common_utils.wait_until_true(
486 lambda: ip_command.get_nic_name_by_mac(mac_address),
487 timeout=10,
488 exception=RuntimeError('Interface with MAC %s not present in the '
489 'VM' % mac_address))
490
491 return body