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