blob: 7b172b004d651fed2ec79cd31db34744510bee19 [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
Eduardo Olivares088707b2020-12-01 21:13:45 +010060 def configure_vlan(self, addresses, port, vlan_tag, subport_ips):
61 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)
69 self.set_link(device=subport_device, state='up')
70 for subport_ip in subport_ips:
71 self.add_address(address=subport_ip, device=subport_device)
72 return subport_device
73
Federico Ressic2ed23d2018-10-25 09:31:47 +020074 def configure_vlan_subport(self, port, subport, vlan_tag, subnets):
75 addresses = self.list_addresses()
76 try:
77 subport_device = get_port_device_name(addresses=addresses,
78 port=subport)
79 except ValueError:
80 pass
81 else:
82 LOG.debug('Interface %r already configured.', subport_device)
83 return subport_device
84
85 subport_ips = [
86 "{!s}/{!s}".format(ip, prefix_len)
87 for ip, prefix_len in _get_ip_address_prefix_len_pairs(
88 port=subport, subnets=subnets)]
89 if not subport_ips:
90 raise ValueError(
91 "Unable to get IP address and subnet prefix lengths for "
92 "subport")
93
Eduardo Olivares088707b2020-12-01 21:13:45 +010094 return self.configure_vlan(addresses, port, vlan_tag, subport_ips)
Federico Ressic2ed23d2018-10-25 09:31:47 +020095
Eduardo Olivares088707b2020-12-01 21:13:45 +010096 def configure_vlan_transparent(self, port, vlan_tag, ip_addresses):
97 addresses = self.list_addresses()
98 try:
99 subport_device = get_vlan_device_name(addresses, ip_addresses)
100 except ValueError:
101 pass
102 else:
103 LOG.debug('Interface %r already configured.', subport_device)
104 return subport_device
105
106 return self.configure_vlan(addresses, port, vlan_tag, ip_addresses)
Federico Ressic2ed23d2018-10-25 09:31:47 +0200107
Slawek Kaplonski8033af72020-05-05 12:01:37 +0200108 def list_namespaces(self):
109 namespaces_output = self.execute("netns")
110 ns_list = []
111 for ns_line in namespaces_output.split("\n"):
112 ns_list.append(ns_line.split(" ", 1)[0])
113 return ns_list
114
Federico Ressic2ed23d2018-10-25 09:31:47 +0200115 def list_addresses(self, device=None, ip_addresses=None, port=None,
116 subnets=None):
117 command = ['list']
118 if device:
119 command += ['dev', device]
120 output = self.execute('address', *command)
121 addresses = list(parse_addresses(output))
122
123 return list_ip_addresses(addresses=addresses,
124 ip_addresses=ip_addresses, port=port,
125 subnets=subnets)
126
127 def add_link(self, name, link_type, link=None, segmentation_id=None):
128 command = ['add']
129 if link:
130 command += ['link', link]
131 command += ['name', name, 'type', link_type]
132 if id:
133 command += ['id', segmentation_id]
134 return self.execute('link', *command)
135
136 def set_link(self, device, state=None):
137 command = ['set', 'dev', device]
138 if state:
139 command.append(state)
140 return self.execute('link', *command)
141
142 def add_address(self, address, device):
143 # ip addr add 192.168.1.1/24 dev em1
144 return self.execute('address', 'add', address, 'dev', device)
145
Eduardo Olivares088707b2020-12-01 21:13:45 +0100146 def delete_address(self, address, device):
147 # ip addr del 192.168.1.1/24 dev em1
148 return self.execute('address', 'del', address, 'dev', device)
149
150 def add_route(self, address, device, gateway=None):
151 if gateway:
152 # ip route add 192.168.1.0/24 via 192.168.22.1 dev em1
153 return self.execute(
154 'route', 'add', address, 'via', gateway, 'dev', device)
155 else:
156 # ip route add 192.168.1.0/24 dev em1
157 return self.execute('route', 'add', address, 'dev', device)
158
159 def delete_route(self, address, device):
160 # ip route del 192.168.1.0/24 dev em1
161 return self.execute('route', 'del', address, 'dev', device)
162
Federico Ressic2ed23d2018-10-25 09:31:47 +0200163 def list_routes(self, *args):
164 output = self.execute('route', 'show', *args)
165 return list(parse_routes(output))
166
Slawek Kaplonskia1e88c42020-03-03 03:00:48 +0100167 def get_nic_name_by_mac(self, mac_address):
168 nics = self.execute("-o", "link")
169 for nic_line in nics.split("\n"):
170 if mac_address in nic_line:
171 return nic_line.split(":")[1].strip()
172
Federico Ressic2ed23d2018-10-25 09:31:47 +0200173
174def parse_addresses(command_output):
175 address = device = None
176 addresses = []
177 for i, line in enumerate(command_output.split('\n')):
178 try:
179 line_number = i + 1
180 fields = line.strip().split()
181 if not fields:
182 continue
183 indent = line.index(fields[0] + ' ')
184 if indent == 0:
185 # example of line
186 # 2: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 # noqa
187 address = None
188 name = fields[1]
189 if name.endswith(':'):
190 name = name[:-1]
191 if '@' in name:
192 name, parent = name.split('@', 1)
193 else:
194 parent = None
195
196 if len(fields) > 2:
197 # flags example: <LOOPBACK,UP,LOWER_UP>
198 flags = fields[2]
199 if flags.startswith('<'):
200 flags = flags[1:]
201 if flags.startswith('>'):
202 flags = flags[:-1]
203 flags = flags.split(',')
204
205 device = Device(name=name, parent=parent, flags=flags,
206 properties=dict(parse_properties(fields[3:])))
207 LOG.debug("Device parsed: %r", device)
208
209 elif indent == 4:
210 address = Address.create(
211 family=fields[0], address=fields[1], device=device,
212 properties=dict(parse_properties(fields[2:])))
213 addresses.append(address)
214 LOG.debug("Address parsed: %r", address)
215
216 elif indent == 7:
217 address.properties.update(parse_properties(fields))
218 LOG.debug("Address properties parsed: %r", address.properties)
219
220 else:
221 assert False, "Invalid line indentation: {!r}".format(indent)
222
223 except Exception:
224 with excutils.save_and_reraise_exception():
225 LOG.exception("Error parsing ip command output at line %d:\n"
226 "%r\n",
227 line_number, line)
228 raise
229
230 return addresses
231
232
233def parse_properties(fields):
234 for i, field in enumerate(fields):
235 if i % 2 == 0:
236 key = field
237 else:
238 yield key, field
239
240
241class HasProperties(object):
242
243 def __getattr__(self, name):
244 try:
245 return self.properties[name]
246 except KeyError:
247 pass
248 # This should raise AttributeError
249 return getattr(super(HasProperties, self), name)
250
251
252class Address(HasProperties,
253 collections.namedtuple('Address',
254 ['family', 'address', 'device',
255 'properties'])):
256
257 _subclasses = {}
258
259 @classmethod
260 def create(cls, family, address, device, properties):
261 cls = cls._subclasses.get(family, cls)
262 return cls(family=family, address=address, device=device,
263 properties=properties)
264
265 @classmethod
266 def register_subclass(cls, family, subclass=None):
267 if not issubclass(subclass, cls):
268 msg = "{!r} is not sub-class of {!r}".format(cls, Address)
269 raise TypeError(msg)
270 cls._subclasses[family] = subclass
271
272
273class Device(HasProperties,
274 collections.namedtuple('Device',
275 ['name', 'parent', 'flags',
276 'properties'])):
277 pass
278
279
280def register_address_subclass(families):
281
282 def decorator(subclass):
283 for family in families:
284 Address.register_subclass(family=family, subclass=subclass)
285 return subclass
286
287 return decorator
288
289
290@register_address_subclass(['inet', 'inet6'])
291class InetAddress(Address):
292
293 @property
294 def ip(self):
295 return self.network.ip
296
297 @property
298 def network(self):
299 return netaddr.IPNetwork(self.address)
300
301
302def parse_routes(command_output):
303 for line in command_output.split('\n'):
304 fields = line.strip().split()
305 if fields:
306 dest = fields[0]
307 properties = dict(parse_properties(fields[1:]))
308 if dest == 'default':
309 dest = constants.IPv4_ANY
310 via = properties.get('via')
311 if via:
312 dest = constants.IP_ANY[netaddr.IPAddress(via).version]
313 yield Route(dest=dest, properties=properties)
314
315
316def list_ip_addresses(addresses, ip_addresses=None, port=None,
317 subnets=None):
318 if port:
319 # filter addresses by port IP addresses
320 ip_addresses = set(ip_addresses) if ip_addresses else set()
321 ip_addresses.update(list_port_ip_addresses(port=port,
322 subnets=subnets))
323 if ip_addresses:
324 addresses = [a for a in addresses if (hasattr(a, 'ip') and
325 str(a.ip) in ip_addresses)]
326 return addresses
327
328
329def list_port_ip_addresses(port, subnets=None):
330 fixed_ips = port['fixed_ips']
331 if subnets:
332 subnets = {subnet['id']: subnet for subnet in subnets}
333 fixed_ips = [fixed_ip
334 for fixed_ip in fixed_ips
335 if fixed_ip['subnet_id'] in subnets]
336 return [ip['ip_address'] for ip in port['fixed_ips']]
337
338
339def get_port_device_name(addresses, port):
340 for address in list_ip_addresses(addresses=addresses, port=port):
341 return address.device.name
342
Bernard Cafarellic3bec862020-09-10 13:59:49 +0200343 msg = "Port {0!r} fixed IPs not found on server.".format(port['id'])
Federico Ressic2ed23d2018-10-25 09:31:47 +0200344 raise ValueError(msg)
345
346
Eduardo Olivares088707b2020-12-01 21:13:45 +0100347def get_vlan_device_name(addresses, ip_addresses):
348 for address in list_ip_addresses(addresses=addresses,
349 ip_addresses=ip_addresses):
350 return address.device.name
351
352 msg = "Fixed IPs {0!r} not found on server.".format(' '.join(ip_addresses))
353 raise ValueError(msg)
354
355
Federico Ressic2ed23d2018-10-25 09:31:47 +0200356def _get_ip_address_prefix_len_pairs(port, subnets):
357 subnets = {subnet['id']: subnet for subnet in subnets}
358 for fixed_ip in port['fixed_ips']:
359 subnet = subnets.get(fixed_ip['subnet_id'])
360 if subnet:
361 yield (fixed_ip['ip_address'],
362 netaddr.IPNetwork(subnet['cidr']).prefixlen)
363
364
Slawek Kaplonski8033af72020-05-05 12:01:37 +0200365def arp_table(namespace=None):
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000366 # 192.168.0.16 0x1 0x2 dc:a6:32:06:56:51 * enp0s31f6
367 regex_str = (r"([^ ]+)\s+(0x\d+)\s+(0x\d+)\s+(\w{2}\:\w{2}\:\w{2}\:\w{2}\:"
368 r"\w{2}\:\w{2})\s+([\w+\*]+)\s+([\-\w]+)")
369 regex = re.compile(regex_str)
370 arp_table = []
Slawek Kaplonski8033af72020-05-05 12:01:37 +0200371 cmd = ""
372 if namespace:
373 cmd = "sudo ip netns exec %s " % namespace
374 cmd += "cat /proc/net/arp"
375 arp_entries = shell.execute(cmd).stdout.split("\n")
376 for line in arp_entries:
377 m = regex.match(line)
378 if m:
379 arp_table.append(ARPregister(
380 ip_address=m.group(1), hw_type=m.group(2),
381 flags=m.group(3), mac_address=m.group(4),
382 mask=m.group(5), device=m.group(6)))
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000383 return arp_table
384
385
Federico Ressic2ed23d2018-10-25 09:31:47 +0200386class Route(HasProperties,
387 collections.namedtuple('Route',
388 ['dest', 'properties'])):
389
390 @property
391 def dest_ip(self):
392 return netaddr.IPNetwork(self.dest)
393
394 @property
395 def via_ip(self):
396 return netaddr.IPAddress(self.via)
397
398 @property
399 def src_ip(self):
400 return netaddr.IPAddress(self.src)
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000401
402 def __str__(self):
403 properties_str = ' '.join('%s %s' % (k, v)
404 for k, v in self.properties.items())
405 return '%(dest)s %(properties)s' % {'dest': self.dest,
406 'properties': properties_str}
407
408
409class ARPregister(collections.namedtuple(
410 'ARPregister',
411 ['ip_address', 'hw_type', 'flags', 'mac_address', 'mask', 'device'])):
412
413 def __str__(self):
414 return '%s %s %s %s %s %s' % (self.ip_address, self.hw_type,
415 self.flags, self.mac_address, self.mask,
416 self.device)
ccamposr3e1921b2020-01-29 11:10:05 +0100417
418
419def find_valid_cidr(valid_cidr='10.0.0.0/8', used_cidr=None):
420 total_ips = netaddr.IPSet(netaddr.IPNetwork(valid_cidr))
421 if used_cidr:
422 used_network = netaddr.IPNetwork(used_cidr)
423 netmask = used_network.netmask.netmask_bits()
424 valid_ips = total_ips.difference(netaddr.IPSet(used_network))
425 else:
426 valid_ips = total_ips
427 netmask = 24
428
429 for ip in valid_ips:
430 valid_network = netaddr.IPNetwork('%s/%s' % (ip, netmask))
431 if valid_network in valid_ips:
432 return valid_network.cidr
433
434 exception_str = 'No valid CIDR found in %s' % valid_cidr
435 if used_cidr:
436 exception_str += ', used CIDR %s' % used_cidr
437 raise Exception(exception_str)
Rodolfo Alonso Hernandez0adf8a22020-06-11 11:28:25 +0000438
439
440def wait_for_interface_status(client, server_id, port_id, status,
441 ssh_client=None, mac_address=None):
442 """Waits for an interface to reach a given status and checks VM NIC
443
444 This method enhances the tempest one. Apart from checking the interface
445 status returned by Nova, this methods access the VM to check if the NIC
446 interface is already detected by the kernel.
447 """
448 body = waiters.wait_for_interface_status(client, server_id, port_id,
449 status)
450
451 if ssh_client and mac_address:
452 ip_command = IPCommand(ssh_client)
453 common_utils.wait_until_true(
454 lambda: ip_command.get_nic_name_by_mac(mac_address),
455 timeout=10,
456 exception=RuntimeError('Interface with MAC %s not present in the '
457 'VM' % mac_address))
458
459 return body