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