summaryrefslogtreecommitdiffstats
path: root/build/interface.py
blob: 709fd67794084313e7e01ffa29d21b1afb1e88b8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# Copyright 2012 OpenStack Foundation
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import abc
import eventlet
import netaddr
from oslo_config import cfg
from oslo_log import log as logging
import six

from neutron._i18n import _, _LE, _LI, _LW
from neutron.agent.common import ovs_lib
from neutron.agent.linux import ip_lib
from neutron.agent.linux import utils
from neutron.common import constants as n_const
from neutron.common import exceptions
from neutron.common import ipv6_utils


LOG = logging.getLogger(__name__)

OPTS = [
    cfg.StrOpt('ovs_integration_bridge',
               default='br-int',
               help=_('Name of Open vSwitch bridge to use')),
    cfg.BoolOpt('ovs_use_veth',
                default=False,
                help=_('Uses veth for an OVS interface or not. '
                       'Support kernels with limited namespace support '
                       '(e.g. RHEL 6.5) so long as ovs_use_veth is set to '
                       'True.')),
    cfg.IntOpt('network_device_mtu',
               deprecated_for_removal=True,
               help=_('MTU setting for device. This option will be removed in '
                      'Newton. Please use the system-wide segment_mtu setting '
                      'which the agents will take into account when wiring '
                      'VIFs.')),
]


@six.add_metaclass(abc.ABCMeta)
class LinuxInterfaceDriver(object):

    # from linux IF_NAMESIZE
    DEV_NAME_LEN = 14
    DEV_NAME_PREFIX = n_const.TAP_DEVICE_PREFIX

    def __init__(self, conf):
        self.conf = conf
        if self.conf.network_device_mtu:
            self._validate_network_device_mtu()

    def _validate_network_device_mtu(self):
        if (ipv6_utils.is_enabled() and
            self.conf.network_device_mtu < n_const.IPV6_MIN_MTU):
            LOG.error(_LE("IPv6 protocol requires a minimum MTU of "
                          "%(min_mtu)s, while the configured value is "
                          "%(current_mtu)s"), {'min_mtu': n_const.IPV6_MIN_MTU,
                          'current_mtu': self.conf.network_device_mtu})
            raise SystemExit(1)

    @property
    def use_gateway_ips(self):
        """Whether to use gateway IPs instead of unique IP allocations.

        In each place where the DHCP agent runs, and for each subnet for
        which DHCP is handling out IP addresses, the DHCP port needs -
        at the Linux level - to have an IP address within that subnet.
        Generally this needs to be a unique Neutron-allocated IP
        address, because the subnet's underlying L2 domain is bridged
        across multiple compute hosts and network nodes, and for HA
        there may be multiple DHCP agents running on that same bridged
        L2 domain.

        However, if the DHCP ports - on multiple compute/network nodes
        but for the same network - are _not_ bridged to each other,
        they do not need each to have a unique IP address.  Instead
        they can all share the same address from the relevant subnet.
        This works, without creating any ambiguity, because those
        ports are not all present on the same L2 domain, and because
        no data within the network is ever sent to that address.
        (DHCP requests are broadcast, and it is the network's job to
        ensure that such a broadcast will reach at least one of the
        available DHCP servers.  DHCP responses will be sent _from_
        the DHCP port address.)

        Specifically, for networking backends where it makes sense,
        the DHCP agent allows all DHCP ports to use the subnet's
        gateway IP address, and thereby to completely avoid any unique
        IP address allocation.  This behaviour is selected by running
        the DHCP agent with a configured interface driver whose
        'use_gateway_ips' property is True.

        When an operator deploys Neutron with an interface driver that
        makes use_gateway_ips True, they should also ensure that a
        gateway IP address is defined for each DHCP-enabled subnet,
        and that the gateway IP address doesn't change during the
        subnet's lifetime.
        """
        return False

    def init_l3(self, device_name, ip_cidrs, namespace=None,
                preserve_ips=None, clean_connections=False):
        """Set the L3 settings for the interface using data from the port.

        ip_cidrs: list of 'X.X.X.X/YY' strings
        preserve_ips: list of ip cidrs that should not be removed from device
        clean_connections: Boolean to indicate if we should cleanup connections
          associated to removed ips
        """
        preserve_ips = preserve_ips or []
        device = ip_lib.IPDevice(device_name, namespace=namespace)

        # The LLA generated by the operating system is not known to
        # Neutron, so it would be deleted if we added it to the 'previous'
        # list here
        default_ipv6_lla = ip_lib.get_ipv6_lladdr(device.link.address)
        previous = {addr['cidr'] for addr in device.addr.list(
            filters=['permanent'])} - {default_ipv6_lla}

        # add new addresses
        for ip_cidr in ip_cidrs:

            net = netaddr.IPNetwork(ip_cidr)
            # Convert to compact IPv6 address because the return values of
            # "ip addr list" are compact.
            if net.version == 6:
                ip_cidr = str(net)
            if ip_cidr in previous:
                previous.remove(ip_cidr)
                continue

            device.addr.add(ip_cidr)

        # clean up any old addresses
        for ip_cidr in previous:
            if ip_cidr not in preserve_ips:
                if clean_connections:
                    device.delete_addr_and_conntrack_state(ip_cidr)
                else:
                    device.addr.delete(ip_cidr)

    def init_router_port(self,
                         device_name,
                         ip_cidrs,
                         namespace,
                         preserve_ips=None,
                         extra_subnets=None,
                         clean_connections=False):
        """Set the L3 settings for a router interface using data from the port.

        ip_cidrs: list of 'X.X.X.X/YY' strings
        preserve_ips: list of ip cidrs that should not be removed from device
        clean_connections: Boolean to indicate if we should cleanup connections
          associated to removed ips
        extra_subnets: An iterable of cidrs to add as routes without address
        """
        LOG.debug("init_router_port: device_name(%s), namespace(%s)",
                  device_name, namespace)
        self.init_l3(device_name=device_name,
                     ip_cidrs=ip_cidrs,
                     namespace=namespace,
                     preserve_ips=preserve_ips or [],
                     clean_connections=clean_connections)

        device = ip_lib.IPDevice(device_name, namespace=namespace)

        # Manage on-link routes (routes without an associated address)
        new_onlink_cidrs = set(s['cidr'] for s in extra_subnets or [])

        v4_onlink = device.route.list_onlink_routes(n_const.IP_VERSION_4)
        v6_onlink = device.route.list_onlink_routes(n_const.IP_VERSION_6)
        existing_onlink_cidrs = set(r['cidr'] for r in v4_onlink + v6_onlink)

        for route in new_onlink_cidrs - existing_onlink_cidrs:
            LOG.debug("adding onlink route(%s)", route)
            device.route.add_onlink_route(route)
        for route in (existing_onlink_cidrs - new_onlink_cidrs -
                      set(preserve_ips or [])):
            LOG.debug("deleting onlink route(%s)", route)
            device.route.delete_onlink_route(route)

    def add_ipv6_addr(self, device_name, v6addr, namespace, scope='global'):
        device = ip_lib.IPDevice(device_name,
                                 namespace=namespace)
        net = netaddr.IPNetwork(v6addr)
        device.addr.add(str(net), scope)

    def delete_ipv6_addr(self, device_name, v6addr, namespace):
        device = ip_lib.IPDevice(device_name,
                                 namespace=namespace)
        device.delete_addr_and_conntrack_state(v6addr)

    def delete_ipv6_addr_with_prefix(self, device_name, prefix, namespace):
        """Delete the first listed IPv6 address that falls within a given
        prefix.
        """
        device = ip_lib.IPDevice(device_name, namespace=namespace)
        net = netaddr.IPNetwork(prefix)
        for address in device.addr.list(scope='global', filters=['permanent']):
            ip_address = netaddr.IPNetwork(address['cidr'])
            if ip_address in net:
                device.delete_addr_and_conntrack_state(address['cidr'])
                break

    def get_ipv6_llas(self, device_name, namespace):
        device = ip_lib.IPDevice(device_name,
                                 namespace=namespace)

        return device.addr.list(scope='link', ip_version=6)

    def check_bridge_exists(self, bridge):
        if not ip_lib.device_exists(bridge):
            raise exceptions.BridgeDoesNotExist(bridge=bridge)

    def get_device_name(self, port):
        return (self.DEV_NAME_PREFIX + port.id)[:self.DEV_NAME_LEN]

    @staticmethod
    def configure_ipv6_ra(namespace, dev_name):
        """Configure acceptance of IPv6 route advertisements on an intf."""
        # Learn the default router's IP address via RAs
        ip_lib.IPWrapper(namespace=namespace).netns.execute(
            ['sysctl', '-w', 'net.ipv6.conf.%s.accept_ra=2' % dev_name])

    @abc.abstractmethod
    def plug_new(self, network_id, port_id, device_name, mac_address,
                 bridge=None, namespace=None, prefix=None, mtu=None):
        """Plug in the interface only for new devices that don't exist yet."""

    def plug(self, network_id, port_id, device_name, mac_address,
             bridge=None, namespace=None, prefix=None, mtu=None):
        if not ip_lib.device_exists(device_name,
                                    namespace=namespace):
            try:
                self.plug_new(network_id, port_id, device_name, mac_address,
                              bridge, namespace, prefix, mtu)
            except TypeError:
                self.plug_new(network_id, port_id, device_name, mac_address,
                              bridge, namespace, prefix)
        else:
            LOG.info(_LI("Device %s already exists"), device_name)

    @abc.abstractmethod
    def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
        """Unplug the interface."""

    @property
    def bridged(self):
        """Whether the DHCP port is bridged to the VM TAP interfaces.

        When the DHCP port is bridged to the TAP interfaces for the
        VMs for which it is providing DHCP service - as is the case
        for most Neutron network implementations - the DHCP server
        only needs to listen on the DHCP port, and will still receive
        DHCP requests from all the relevant VMs.

        If the DHCP port is not bridged to the relevant VM TAP
        interfaces, the DHCP server needs to listen explicitly on
        those TAP interfaces, and to treat those as aliases of the
        DHCP port where the IP subnet is defined.
        """
        return True


class NullDriver(LinuxInterfaceDriver):
    def plug_new(self, network_id, port_id, device_name, mac_address,
                 bridge=None, namespace=None, prefix=None, mtu=None):
        pass

    def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
        pass

class NSDriver(LinuxInterfaceDriver):
    """Device independent driver enabling creation of a non device specific
    interface in network spaces.  Attachment to the device is not performed.
    """
    MAX_TIME_FOR_DEVICE_EXISTENCE = 30

    @classmethod
    def _device_is_created_in_time(cls, device_name):
        """See if device is created, within time limit."""
        attempt = 0
        while attempt < NSDriver.MAX_TIME_FOR_DEVICE_EXISTENCE:
            if ip_lib.device_exists(device_name):
                return True
            attempt += 1
            eventlet.sleep(1)
        LOG.error(_LE("Device %(dev)s was not created in %(time)d seconds"),
                  {'dev': device_name,
                   'time': NSDriver.MAX_TIME_FOR_DEVICE_EXISTENCE})
        return False

    def _configure_mtu(self, ns_dev, mtu=None):
        # Need to set MTU, after added to namespace. See review
        # https://review.openstack.org/327651
        try:
            # Note: network_device_mtu will be deprecated in future
            mtu_override = self.conf.network_device_mtu
        except cfg.NoSuchOptError:
            LOG.warning(_LW("Config setting for MTU deprecated - any "
                            "override will be ignored."))
            mtu_override = None
        if mtu_override:
            mtu = mtu_override
            LOG.debug("Overriding MTU to %d", mtu)
        if mtu:
            ns_dev.link.set_mtu(mtu)
        else:
            LOG.debug("No MTU provided - skipping setting value")

    def plug(self, network_id, port_id, device_name, mac_address,
             bridge=None, namespace=None, prefix=None, mtu=None):

        # Overriding this, we still want to add an existing device into the
        # namespace.
        self.plug_new(network_id, port_id, device_name, mac_address,
                      bridge, namespace, prefix, mtu)

    def plug_new(self, network_id, port_id, device_name, mac_address,
                 bridge=None, namespace=None, prefix=None, mtu=None):

        ip = ip_lib.IPWrapper()
        ns_dev = ip.device(device_name)

        LOG.debug("Plugging dev: '%s' into namespace: '%s' ",
                  device_name, namespace)

        # Wait for device creation
        if not self._device_is_created_in_time(device_name):
            return

        ns_dev.link.set_address(mac_address)

        if namespace:
            namespace_obj = ip.ensure_namespace(namespace)
            namespace_obj.add_device_to_namespace(ns_dev)

        self._configure_mtu(ns_dev, mtu)

        ns_dev.link.set_up()

    def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
        # Device removal is done externally. Just remove the namespace
        LOG.debug("Removing namespace: '%s'", namespace)
        ip_lib.IPWrapper(namespace).garbage_collect_namespace()


class OVSInterfaceDriver(LinuxInterfaceDriver):
    """Driver for creating an internal interface on an OVS bridge."""

    DEV_NAME_PREFIX = n_const.TAP_DEVICE_PREFIX

    def __init__(self, conf):
        super(OVSInterfaceDriver, self).__init__(conf)
        if self.conf.ovs_use_veth:
            self.DEV_NAME_PREFIX = 'ns-'

    def _get_tap_name(self, dev_name, prefix=None):
        if self.conf.ovs_use_veth:
            dev_name = dev_name.replace(prefix or self.DEV_NAME_PREFIX,
                                        n_const.TAP_DEVICE_PREFIX)
        return dev_name

    def _ovs_add_port(self, bridge, device_name, port_id, mac_address,
                      internal=True):
        attrs = [('external_ids', {'iface-id': port_id,
                                   'iface-status': 'active',
                                   'attached-mac': mac_address})]
        if internal:
            attrs.insert(0, ('type', 'internal'))

        ovs = ovs_lib.OVSBridge(bridge)
        ovs.replace_port(device_name, *attrs)

    def plug_new(self, network_id, port_id, device_name, mac_address,
                 bridge=None, namespace=None, prefix=None, mtu=None):
        """Plug in the interface."""
        if not bridge:
            bridge = self.conf.ovs_integration_bridge

        self.check_bridge_exists(bridge)

        ip = ip_lib.IPWrapper()
        tap_name = self._get_tap_name(device_name, prefix)

        if self.conf.ovs_use_veth:
            # Create ns_dev in a namespace if one is configured.
            root_dev, ns_dev = ip.add_veth(tap_name,
                                           device_name,
                                           namespace2=namespace)
            root_dev.disable_ipv6()
        else:
            ns_dev = ip.device(device_name)

        internal = not self.conf.ovs_use_veth
        self._ovs_add_port(bridge, tap_name, port_id, mac_address,
                           internal=internal)

        ns_dev.link.set_address(mac_address)

        # Add an interface created by ovs to the namespace.
        if not self.conf.ovs_use_veth and namespace:
            namespace_obj = ip.ensure_namespace(namespace)
            namespace_obj.add_device_to_namespace(ns_dev)

        # NOTE(ihrachys): the order here is significant: we must set MTU after
        # the device is moved into a namespace, otherwise OVS bridge does not
        # allow to set MTU that is higher than the least of all device MTUs on
        # the bridge
        mtu = self.conf.network_device_mtu or mtu
        if mtu:
            ns_dev.link.set_mtu(mtu)
            if self.conf.ovs_use_veth:
                root_dev.link.set_mtu(mtu)
        else:
            LOG.warning(_LW("No MTU configured for port %s"), port_id)

        ns_dev.link.set_up()
        if self.conf.ovs_use_veth:
            root_dev.link.set_up()

    def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
        """Unplug the interface."""
        if not bridge:
            bridge = self.conf.ovs_integration_bridge

        tap_name = self._get_tap_name(device_name, prefix)
        self.check_bridge_exists(bridge)
        ovs = ovs_lib.OVSBridge(bridge)

        try:
            ovs.delete_port(tap_name)
            if self.conf.ovs_use_veth:
                device = ip_lib.IPDevice(device_name, namespace=namespace)
                device.link.delete()
                LOG.debug("Unplugged interface '%s'", device_name)
        except RuntimeError:
            LOG.error(_LE("Failed unplugging interface '%s'"),
                      device_name)


class IVSInterfaceDriver(LinuxInterfaceDriver):
    """Driver for creating an internal interface on an IVS bridge."""

    DEV_NAME_PREFIX = n_const.TAP_DEVICE_PREFIX

    def __init__(self, conf):
        super(IVSInterfaceDriver, self).__init__(conf)
        self.DEV_NAME_PREFIX = 'ns-'

    def _get_tap_name(self, dev_name, prefix=None):
        dev_name = dev_name.replace(prefix or self.DEV_NAME_PREFIX,
                                    n_const.TAP_DEVICE_PREFIX)
        return dev_name

    def _ivs_add_port(self, device_name, port_id, mac_address):
        cmd = ['ivs-ctl', 'add-port', device_name]
        utils.execute(cmd, run_as_root=True)

    def plug_new(self, network_id, port_id, device_name, mac_address,
                 bridge=None, namespace=None, prefix=None, mtu=None):
        """Plug in the interface."""
        ip = ip_lib.IPWrapper()
        tap_name = self._get_tap_name(device_name, prefix)

        root_dev, ns_dev = ip.add_veth(tap_name, device_name)
        root_dev.disable_ipv6()

        self._ivs_add_port(tap_name, port_id, mac_address)

        ns_dev = ip.device(device_name)
        ns_dev.link.set_address(mac_address)

        mtu = self.conf.network_device_mtu or mtu
        if mtu:
            ns_dev.link.set_mtu(mtu)
            root_dev.link.set_mtu(mtu)
        else:
            LOG.warning(_LW("No MTU configured for port %s"), port_id)

        if namespace:
            namespace_obj = ip.ensure_namespace(namespace)
            namespace_obj.add_device_to_namespace(ns_dev)

        ns_dev.link.set_up()
        root_dev.link.set_up()

    def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
        """Unplug the interface."""
        tap_name = self._get_tap_name(device_name, prefix)
        try:
            cmd = ['ivs-ctl', 'del-port', tap_name]
            utils.execute(cmd, run_as_root=True)
            device = ip_lib.IPDevice(device_name, namespace=namespace)
            device.link.delete()
            LOG.debug("Unplugged interface '%s'", device_name)
        except RuntimeError:
            LOG.error(_LE("Failed unplugging interface '%s'"),
                      device_name)


class BridgeInterfaceDriver(LinuxInterfaceDriver):
    """Driver for creating bridge interfaces."""

    DEV_NAME_PREFIX = 'ns-'

    def plug_new(self, network_id, port_id, device_name, mac_address,
                 bridge=None, namespace=None, prefix=None, mtu=None):
        """Plugin the interface."""
        ip = ip_lib.IPWrapper()

        # Enable agent to define the prefix
        tap_name = device_name.replace(prefix or self.DEV_NAME_PREFIX,
                                       n_const.TAP_DEVICE_PREFIX)
        # Create ns_veth in a namespace if one is configured.
        root_veth, ns_veth = ip.add_veth(tap_name, device_name,
                                         namespace2=namespace)
        root_veth.disable_ipv6()
        ns_veth.link.set_address(mac_address)

        mtu = self.conf.network_device_mtu or mtu
        if mtu:
            root_veth.link.set_mtu(mtu)
            ns_veth.link.set_mtu(mtu)
        else:
            LOG.warning(_LW("No MTU configured for port %s"), port_id)

        root_veth.link.set_up()
        ns_veth.link.set_up()

    def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
        """Unplug the interface."""
        device = ip_lib.IPDevice(device_name, namespace=namespace)
        try:
            device.link.delete()
            LOG.debug("Unplugged interface '%s'", device_name)
        except RuntimeError:
            LOG.error(_LE("Failed unplugging interface '%s'"),
                      device_name)