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