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