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