File: pd.py

package info (click to toggle)
neutron 2:13.0.2-15
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 30,764 kB
  • sloc: python: 188,554; sh: 1,060; makefile: 246
file content (420 lines) | stat: -rw-r--r-- 16,738 bytes parent folder | download | duplicates (2)
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
# Copyright 2015 Cisco Systems
# 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 functools
import signal

import eventlet
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib import constants as n_const
from neutron_lib.utils import runtime
from oslo_log import log as logging
from oslo_utils import netutils
import six
from stevedore import driver

from neutron.common import constants as l3_constants
from neutron.common import utils

LOG = logging.getLogger(__name__)


class PrefixDelegation(object):
    def __init__(self, context, pmon, intf_driver, notifier, pd_update_cb,
                 agent_conf):
        self.context = context
        self.pmon = pmon
        self.intf_driver = intf_driver
        self.notifier = notifier
        self.routers = {}
        self.pd_update_cb = pd_update_cb
        self.agent_conf = agent_conf
        self.pd_dhcp_driver = driver.DriverManager(
            namespace='neutron.agent.linux.pd_drivers',
            name=agent_conf.prefix_delegation_driver,
        ).driver
        registry.subscribe(add_router,
                           resources.ROUTER,
                           events.BEFORE_CREATE)
        registry.subscribe(update_router,
                           resources.ROUTER,
                           events.AFTER_UPDATE)
        registry.subscribe(remove_router,
                           resources.ROUTER,
                           events.AFTER_DELETE)
        self._get_sync_data()

    def _is_pd_master_router(self, router):
        return router['master']

    @runtime.synchronized("l3-agent-pd")
    def enable_subnet(self, router_id, subnet_id, prefix, ri_ifname, mac):
        router = self.routers.get(router_id)
        if router is None:
            return

        pd_info = router['subnets'].get(subnet_id)
        if not pd_info:
            pd_info = PDInfo(ri_ifname=ri_ifname, mac=mac)
            router['subnets'][subnet_id] = pd_info

        pd_info.bind_lla = self._get_lla(mac)
        if pd_info.sync:
            pd_info.mac = mac
            pd_info.old_prefix = prefix
        elif self._is_pd_master_router(router):
            self._add_lla(router, pd_info.get_bind_lla_with_mask())

    def _delete_pd(self, router, pd_info):
        if not self._is_pd_master_router(router):
            return
        self._delete_lla(router, pd_info.get_bind_lla_with_mask())
        if pd_info.client_started:
            pd_info.driver.disable(self.pmon, router['ns_name'])

    @runtime.synchronized("l3-agent-pd")
    def disable_subnet(self, router_id, subnet_id):
        prefix_update = {}
        router = self.routers.get(router_id)
        if not router:
            return
        pd_info = router['subnets'].get(subnet_id)
        if not pd_info:
            return
        self._delete_pd(router, pd_info)
        if self._is_pd_master_router(router):
            prefix_update[subnet_id] = n_const.PROVISIONAL_IPV6_PD_PREFIX
            LOG.debug("Update server with prefixes: %s", prefix_update)
            self.notifier(self.context, prefix_update)
        del router['subnets'][subnet_id]

    @runtime.synchronized("l3-agent-pd")
    def update_subnet(self, router_id, subnet_id, prefix):
        router = self.routers.get(router_id)
        if router is not None:
            pd_info = router['subnets'].get(subnet_id)
            if pd_info and pd_info.old_prefix != prefix:
                old_prefix = pd_info.old_prefix
                pd_info.old_prefix = prefix
                pd_info.prefix = prefix
                return old_prefix

    @runtime.synchronized("l3-agent-pd")
    def add_gw_interface(self, router_id, gw_ifname):
        router = self.routers.get(router_id)
        if not router:
            return
        router['gw_interface'] = gw_ifname
        if not self._is_pd_master_router(router):
            return
        prefix_update = {}
        for pd_info in six.itervalues(router['subnets']):
            # gateway is added after internal router ports.
            # If a PD is being synced, and if the prefix is available,
            # send update if prefix out of sync; If not available,
            # start the PD client
            bind_lla_with_mask = pd_info.get_bind_lla_with_mask()
            if pd_info.sync:
                pd_info.sync = False
                if pd_info.client_started:
                    if pd_info.prefix != pd_info.old_prefix:
                        prefix_update['subnet_id'] = pd_info.prefix
                else:
                    self._delete_lla(router, bind_lla_with_mask)
                    self._add_lla(router, bind_lla_with_mask)
            else:
                self._add_lla(router, bind_lla_with_mask)
        if prefix_update:
            LOG.debug("Update server with prefixes: %s", prefix_update)
            self.notifier(self.context, prefix_update)

    def delete_router_pd(self, router):
        if not self._is_pd_master_router(router):
            return
        prefix_update = {}
        for subnet_id, pd_info in router['subnets'].items():
            self._delete_lla(router, pd_info.get_bind_lla_with_mask())
            if pd_info.client_started:
                pd_info.driver.disable(self.pmon, router['ns_name'])
                pd_info.prefix = None
                pd_info.client_started = False
                prefix = n_const.PROVISIONAL_IPV6_PD_PREFIX
                prefix_update[subnet_id] = prefix
        if prefix_update:
            LOG.debug("Update server with prefixes: %s", prefix_update)
            self.notifier(self.context, prefix_update)

    @runtime.synchronized("l3-agent-pd")
    def remove_gw_interface(self, router_id):
        router = self.routers.get(router_id)
        if router is not None:
            router['gw_interface'] = None
            self.delete_router_pd(router)

    @runtime.synchronized("l3-agent-pd")
    def get_preserve_ips(self, router_id):
        preserve_ips = []
        router = self.routers.get(router_id)
        if router is not None:
            for pd_info in six.itervalues(router['subnets']):
                preserve_ips.append(pd_info.get_bind_lla_with_mask())
        return preserve_ips

    @runtime.synchronized("l3-agent-pd")
    def sync_router(self, router_id):
        router = self.routers.get(router_id)
        if router is not None and router['gw_interface'] is None:
            self.delete_router_pd(router)

    @runtime.synchronized("l3-agent-pd")
    def remove_stale_ri_ifname(self, router_id, stale_ifname):
        router = self.routers.get(router_id)
        if router is not None:
            subnet_to_delete = None
            for subnet_id, pd_info in six.iteritems(router['subnets']):
                if pd_info.ri_ifname == stale_ifname:
                    self._delete_pd(router, pd_info)
                    subnet_to_delete = subnet_id
                    break
            if subnet_to_delete:
                del router['subnets'][subnet_to_delete]

    @staticmethod
    def _get_lla(mac):
        lla = netutils.get_ipv6_addr_by_EUI64(n_const.IPv6_LLA_PREFIX,
                                              mac)
        return lla

    def _get_llas(self, gw_ifname, ns_name):
        try:
            return self.intf_driver.get_ipv6_llas(gw_ifname, ns_name)
        except RuntimeError:
            # The error message was printed as part of the driver call
            # This could happen if the gw_ifname was removed
            # simply return and exit the thread
            return

    def _add_lla(self, router, lla_with_mask):
        if router['gw_interface']:
            self.intf_driver.add_ipv6_addr(router['gw_interface'],
                                           lla_with_mask,
                                           router['ns_name'],
                                           'link')
            # There is a delay before the LLA becomes active.
            # This is because the kernel runs DAD to make sure LLA uniqueness
            # Spawn a thread to wait for the interface to be ready
            self._spawn_lla_thread(router['gw_interface'],
                                   router['ns_name'],
                                   lla_with_mask)

    def _spawn_lla_thread(self, gw_ifname, ns_name, lla_with_mask):
        eventlet.spawn_n(self._ensure_lla_task,
                         gw_ifname,
                         ns_name,
                         lla_with_mask)

    def _delete_lla(self, router, lla_with_mask):
        if lla_with_mask and router['gw_interface']:
            try:
                self.intf_driver.delete_ipv6_addr(router['gw_interface'],
                                                  lla_with_mask,
                                                  router['ns_name'])
            except RuntimeError:
                # Ignore error if the lla doesn't exist
                pass

    def _ensure_lla_task(self, gw_ifname, ns_name, lla_with_mask):
        # It would be insane for taking so long unless DAD test failed
        # In that case, the subnet would never be assigned a prefix.
        utils.wait_until_true(functools.partial(self._lla_available,
                                                gw_ifname,
                                                ns_name,
                                                lla_with_mask),
                              timeout=l3_constants.LLA_TASK_TIMEOUT,
                              sleep=2)

    def _lla_available(self, gw_ifname, ns_name, lla_with_mask):
        llas = self._get_llas(gw_ifname, ns_name)
        if self._is_lla_active(lla_with_mask, llas):
            LOG.debug("LLA %s is active now", lla_with_mask)
            self.pd_update_cb()
            return True

    @staticmethod
    def _is_lla_active(lla_with_mask, llas):
        for lla in llas:
            if lla_with_mask == lla['cidr']:
                return not lla['tentative']
        return False

    @runtime.synchronized("l3-agent-pd")
    def process_ha_state(self, router_id, master):
        router = self.routers.get(router_id)
        if router is None or router['master'] == master:
            return

        router['master'] = master
        if master:
            for pd_info in six.itervalues(router['subnets']):
                bind_lla_with_mask = pd_info.get_bind_lla_with_mask()
                self._add_lla(router, bind_lla_with_mask)
        else:
            for pd_info in six.itervalues(router['subnets']):
                self._delete_lla(router, pd_info.get_bind_lla_with_mask())
                if pd_info.client_started:
                    pd_info.driver.disable(self.pmon,
                                           router['ns_name'],
                                           switch_over=True)
                    pd_info.client_started = False

    @runtime.synchronized("l3-agent-pd")
    def process_prefix_update(self):
        LOG.debug("Processing IPv6 PD Prefix Update")

        prefix_update = {}
        for router_id, router in self.routers.items():
            if not (self._is_pd_master_router(router) and
                    router['gw_interface']):
                continue

            llas = None
            for subnet_id, pd_info in router['subnets'].items():
                if pd_info.client_started:
                    prefix = pd_info.driver.get_prefix()
                    if prefix != pd_info.prefix:
                        pd_info.prefix = prefix
                        prefix_update[subnet_id] = prefix
                else:
                    if not llas:
                        llas = self._get_llas(router['gw_interface'],
                                              router['ns_name'])

                    if self._is_lla_active(pd_info.get_bind_lla_with_mask(),
                                           llas):
                        if not pd_info.driver:
                            pd_info.driver = self.pd_dhcp_driver(
                                router_id, subnet_id, pd_info.ri_ifname)
                        prefix = None
                        if (pd_info.prefix !=
                                n_const.PROVISIONAL_IPV6_PD_PREFIX):
                            prefix = pd_info.prefix

                        pd_info.driver.enable(self.pmon, router['ns_name'],
                                              router['gw_interface'],
                                              pd_info.bind_lla,
                                              prefix)
                        pd_info.client_started = True

        if prefix_update:
            LOG.debug("Update server with prefixes: %s", prefix_update)
            self.notifier(self.context, prefix_update)

    def after_start(self):
        LOG.debug('SIGUSR1 signal handler set')
        signal.signal(signal.SIGUSR1, self._handle_sigusr1)

    def _handle_sigusr1(self, signum, frame):
        """Update PD on receiving SIGUSR1.

        The external DHCPv6 client uses SIGUSR1 to notify agent
        of prefix changes.
        """
        self.pd_update_cb()

    def _get_sync_data(self):
        sync_data = self.pd_dhcp_driver.get_sync_data()
        for pd_info in sync_data:
            router_id = pd_info.router_id
            if not self.routers.get(router_id):
                self.routers[router_id] = {'master': True,
                                           'gw_interface': None,
                                           'ns_name': None,
                                           'subnets': {}}
            new_pd_info = PDInfo(pd_info=pd_info)
            subnets = self.routers[router_id]['subnets']
            subnets[pd_info.subnet_id] = new_pd_info


@runtime.synchronized("l3-agent-pd")
def remove_router(resource, event, l3_agent, **kwargs):
    router_id = kwargs['router'].router_id
    router = l3_agent.pd.routers.get(router_id)
    l3_agent.pd.delete_router_pd(router)
    del l3_agent.pd.routers[router_id]['subnets']
    del l3_agent.pd.routers[router_id]


def get_router_entry(ns_name, master):
    return {'master': master,
            'gw_interface': None,
            'ns_name': ns_name,
            'subnets': {}}


@runtime.synchronized("l3-agent-pd")
def add_router(resource, event, l3_agent, **kwargs):
    added_router = kwargs['router']
    router = l3_agent.pd.routers.get(added_router.router_id)
    gw_ns_name = added_router.get_gw_ns_name()
    master = added_router.is_router_master()
    if not router:
        l3_agent.pd.routers[added_router.router_id] = (
            get_router_entry(gw_ns_name, master))
    else:
        # This will happen during l3 agent restart
        router['ns_name'] = gw_ns_name
        router['master'] = master


@runtime.synchronized("l3-agent-pd")
def update_router(resource, event, l3_agent, **kwargs):
    updated_router = kwargs['router']
    router = l3_agent.pd.routers.get(updated_router.router_id)
    if not router:
        LOG.exception("Router to be updated is not in internal routers "
                      "list: %s", updated_router.router_id)
    else:
        router['ns_name'] = updated_router.get_gw_ns_name()


class PDInfo(object):
    """A class to simplify storing and passing of information relevant to
    Prefix Delegation operations for a given subnet.
    """
    def __init__(self, pd_info=None, ri_ifname=None, mac=None):
        if pd_info is None:
            self.prefix = n_const.PROVISIONAL_IPV6_PD_PREFIX
            self.old_prefix = n_const.PROVISIONAL_IPV6_PD_PREFIX
            self.ri_ifname = ri_ifname
            self.mac = mac
            self.bind_lla = None
            self.sync = False
            self.driver = None
            self.client_started = False
        else:
            self.prefix = pd_info.prefix
            self.old_prefix = None
            self.ri_ifname = pd_info.ri_ifname
            self.mac = None
            self.bind_lla = None
            self.sync = True
            self.driver = pd_info.driver
            self.client_started = pd_info.client_started

    def get_bind_lla_with_mask(self):
        bind_lla_with_mask = '%s/64' % self.bind_lla
        return bind_lla_with_mask