blob: 265adf726fecf5637f867456fae1f8bc867f85da [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
25
26from neutron_tempest_plugin.common import shell
27
28
29LOG = log.getLogger(__name__)
30
31
32class IPCommand(object):
33
34 sudo = 'sudo'
35 ip_path = '/sbin/ip'
36
37 def __init__(self, ssh_client=None, timeout=None):
38 self.ssh_client = ssh_client
39 self.timeout = timeout
40
41 def get_command(self, obj, *command):
42 command_line = '{sudo!s} {ip_path!r} {object!s} {command!s}'.format(
43 sudo=self.sudo, ip_path=self.ip_path, object=obj,
44 command=subprocess.list2cmdline([str(c) for c in command]))
45 return command_line
46
47 def execute(self, obj, *command):
48 command_line = self.get_command(obj, *command)
49 return shell.execute(command_line, ssh_client=self.ssh_client,
50 timeout=self.timeout).stdout
51
52 def configure_vlan_subport(self, port, subport, vlan_tag, subnets):
53 addresses = self.list_addresses()
54 try:
55 subport_device = get_port_device_name(addresses=addresses,
56 port=subport)
57 except ValueError:
58 pass
59 else:
60 LOG.debug('Interface %r already configured.', subport_device)
61 return subport_device
62
63 subport_ips = [
64 "{!s}/{!s}".format(ip, prefix_len)
65 for ip, prefix_len in _get_ip_address_prefix_len_pairs(
66 port=subport, subnets=subnets)]
67 if not subport_ips:
68 raise ValueError(
69 "Unable to get IP address and subnet prefix lengths for "
70 "subport")
71
72 port_device = get_port_device_name(addresses=addresses, port=port)
73 subport_device = '{!s}.{!s}'.format(port_device, vlan_tag)
74 LOG.debug('Configuring VLAN subport interface %r on top of interface '
75 '%r with IPs: %s', subport_device, port_device,
76 ', '.join(subport_ips))
77
78 self.add_link(link=port_device, name=subport_device, link_type='vlan',
79 segmentation_id=vlan_tag)
80 self.set_link(device=subport_device, state='up')
81 for subport_ip in subport_ips:
82 self.add_address(address=subport_ip, device=subport_device)
83 return subport_device
84
85 def list_addresses(self, device=None, ip_addresses=None, port=None,
86 subnets=None):
87 command = ['list']
88 if device:
89 command += ['dev', device]
90 output = self.execute('address', *command)
91 addresses = list(parse_addresses(output))
92
93 return list_ip_addresses(addresses=addresses,
94 ip_addresses=ip_addresses, port=port,
95 subnets=subnets)
96
97 def add_link(self, name, link_type, link=None, segmentation_id=None):
98 command = ['add']
99 if link:
100 command += ['link', link]
101 command += ['name', name, 'type', link_type]
102 if id:
103 command += ['id', segmentation_id]
104 return self.execute('link', *command)
105
106 def set_link(self, device, state=None):
107 command = ['set', 'dev', device]
108 if state:
109 command.append(state)
110 return self.execute('link', *command)
111
112 def add_address(self, address, device):
113 # ip addr add 192.168.1.1/24 dev em1
114 return self.execute('address', 'add', address, 'dev', device)
115
116 def list_routes(self, *args):
117 output = self.execute('route', 'show', *args)
118 return list(parse_routes(output))
119
120
121def parse_addresses(command_output):
122 address = device = None
123 addresses = []
124 for i, line in enumerate(command_output.split('\n')):
125 try:
126 line_number = i + 1
127 fields = line.strip().split()
128 if not fields:
129 continue
130 indent = line.index(fields[0] + ' ')
131 if indent == 0:
132 # example of line
133 # 2: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 # noqa
134 address = None
135 name = fields[1]
136 if name.endswith(':'):
137 name = name[:-1]
138 if '@' in name:
139 name, parent = name.split('@', 1)
140 else:
141 parent = None
142
143 if len(fields) > 2:
144 # flags example: <LOOPBACK,UP,LOWER_UP>
145 flags = fields[2]
146 if flags.startswith('<'):
147 flags = flags[1:]
148 if flags.startswith('>'):
149 flags = flags[:-1]
150 flags = flags.split(',')
151
152 device = Device(name=name, parent=parent, flags=flags,
153 properties=dict(parse_properties(fields[3:])))
154 LOG.debug("Device parsed: %r", device)
155
156 elif indent == 4:
157 address = Address.create(
158 family=fields[0], address=fields[1], device=device,
159 properties=dict(parse_properties(fields[2:])))
160 addresses.append(address)
161 LOG.debug("Address parsed: %r", address)
162
163 elif indent == 7:
164 address.properties.update(parse_properties(fields))
165 LOG.debug("Address properties parsed: %r", address.properties)
166
167 else:
168 assert False, "Invalid line indentation: {!r}".format(indent)
169
170 except Exception:
171 with excutils.save_and_reraise_exception():
172 LOG.exception("Error parsing ip command output at line %d:\n"
173 "%r\n",
174 line_number, line)
175 raise
176
177 return addresses
178
179
180def parse_properties(fields):
181 for i, field in enumerate(fields):
182 if i % 2 == 0:
183 key = field
184 else:
185 yield key, field
186
187
188class HasProperties(object):
189
190 def __getattr__(self, name):
191 try:
192 return self.properties[name]
193 except KeyError:
194 pass
195 # This should raise AttributeError
196 return getattr(super(HasProperties, self), name)
197
198
199class Address(HasProperties,
200 collections.namedtuple('Address',
201 ['family', 'address', 'device',
202 'properties'])):
203
204 _subclasses = {}
205
206 @classmethod
207 def create(cls, family, address, device, properties):
208 cls = cls._subclasses.get(family, cls)
209 return cls(family=family, address=address, device=device,
210 properties=properties)
211
212 @classmethod
213 def register_subclass(cls, family, subclass=None):
214 if not issubclass(subclass, cls):
215 msg = "{!r} is not sub-class of {!r}".format(cls, Address)
216 raise TypeError(msg)
217 cls._subclasses[family] = subclass
218
219
220class Device(HasProperties,
221 collections.namedtuple('Device',
222 ['name', 'parent', 'flags',
223 'properties'])):
224 pass
225
226
227def register_address_subclass(families):
228
229 def decorator(subclass):
230 for family in families:
231 Address.register_subclass(family=family, subclass=subclass)
232 return subclass
233
234 return decorator
235
236
237@register_address_subclass(['inet', 'inet6'])
238class InetAddress(Address):
239
240 @property
241 def ip(self):
242 return self.network.ip
243
244 @property
245 def network(self):
246 return netaddr.IPNetwork(self.address)
247
248
249def parse_routes(command_output):
250 for line in command_output.split('\n'):
251 fields = line.strip().split()
252 if fields:
253 dest = fields[0]
254 properties = dict(parse_properties(fields[1:]))
255 if dest == 'default':
256 dest = constants.IPv4_ANY
257 via = properties.get('via')
258 if via:
259 dest = constants.IP_ANY[netaddr.IPAddress(via).version]
260 yield Route(dest=dest, properties=properties)
261
262
263def list_ip_addresses(addresses, ip_addresses=None, port=None,
264 subnets=None):
265 if port:
266 # filter addresses by port IP addresses
267 ip_addresses = set(ip_addresses) if ip_addresses else set()
268 ip_addresses.update(list_port_ip_addresses(port=port,
269 subnets=subnets))
270 if ip_addresses:
271 addresses = [a for a in addresses if (hasattr(a, 'ip') and
272 str(a.ip) in ip_addresses)]
273 return addresses
274
275
276def list_port_ip_addresses(port, subnets=None):
277 fixed_ips = port['fixed_ips']
278 if subnets:
279 subnets = {subnet['id']: subnet for subnet in subnets}
280 fixed_ips = [fixed_ip
281 for fixed_ip in fixed_ips
282 if fixed_ip['subnet_id'] in subnets]
283 return [ip['ip_address'] for ip in port['fixed_ips']]
284
285
286def get_port_device_name(addresses, port):
287 for address in list_ip_addresses(addresses=addresses, port=port):
288 return address.device.name
289
290 msg = "Port %r fixed IPs not found on server.".format(port['id'])
291 raise ValueError(msg)
292
293
294def _get_ip_address_prefix_len_pairs(port, subnets):
295 subnets = {subnet['id']: subnet for subnet in subnets}
296 for fixed_ip in port['fixed_ips']:
297 subnet = subnets.get(fixed_ip['subnet_id'])
298 if subnet:
299 yield (fixed_ip['ip_address'],
300 netaddr.IPNetwork(subnet['cidr']).prefixlen)
301
302
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000303def arp_table():
304 # 192.168.0.16 0x1 0x2 dc:a6:32:06:56:51 * enp0s31f6
305 regex_str = (r"([^ ]+)\s+(0x\d+)\s+(0x\d+)\s+(\w{2}\:\w{2}\:\w{2}\:\w{2}\:"
306 r"\w{2}\:\w{2})\s+([\w+\*]+)\s+([\-\w]+)")
307 regex = re.compile(regex_str)
308 arp_table = []
309 with open('/proc/net/arp', 'r') as proc_file:
310 for line in proc_file.readlines():
311 m = regex.match(line)
312 if m:
313 arp_table.append(ARPregister(
314 ip_address=m.group(1), hw_type=m.group(2),
315 flags=m.group(3), mac_address=m.group(4),
316 mask=m.group(5), device=m.group(6)))
317 return arp_table
318
319
Federico Ressic2ed23d2018-10-25 09:31:47 +0200320class Route(HasProperties,
321 collections.namedtuple('Route',
322 ['dest', 'properties'])):
323
324 @property
325 def dest_ip(self):
326 return netaddr.IPNetwork(self.dest)
327
328 @property
329 def via_ip(self):
330 return netaddr.IPAddress(self.via)
331
332 @property
333 def src_ip(self):
334 return netaddr.IPAddress(self.src)
Rodolfo Alonso Hernandez4849f002020-01-16 16:01:10 +0000335
336 def __str__(self):
337 properties_str = ' '.join('%s %s' % (k, v)
338 for k, v in self.properties.items())
339 return '%(dest)s %(properties)s' % {'dest': self.dest,
340 'properties': properties_str}
341
342
343class ARPregister(collections.namedtuple(
344 'ARPregister',
345 ['ip_address', 'hw_type', 'flags', 'mac_address', 'mask', 'device'])):
346
347 def __str__(self):
348 return '%s %s %s %s %s %s' % (self.ip_address, self.hw_type,
349 self.flags, self.mac_address, self.mask,
350 self.device)