File: generic_switch_sg.py

package info (click to toggle)
networking-generic-switch 8.0.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,164 kB
  • sloc: python: 9,272; sh: 373; makefile: 23
file content (303 lines) | stat: -rw-r--r-- 13,086 bytes parent folder | download
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
#
#    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 collections

from neutron.objects import ports as ports_obj
from neutron.objects import securitygroup as sg_obj
from neutron_lib.api.definitions import portbindings
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib.services import base as service_base
from oslo_config import cfg
from oslo_log import log as logging

from networking_generic_switch import devices
from networking_generic_switch.devices import utils as device_utils
from networking_generic_switch import exceptions as exc
from networking_generic_switch import utils as ngs_utils


LOG = logging.getLogger(__name__)
CONF = cfg.CONF


class GenericSwitchSecurityGroupHandler(service_base.ServicePluginBase):
    """Security Group Handler for generic networking hardware.

    Registers for the notification of security group updates.
    Once a notification is recieved, it takes appropriate actions by updating
    hardware appropriately.
    """

    def __init__(self):
        super(GenericSwitchSecurityGroupHandler, self).__init__()
        self.switches = {}
        self.subscribe()
        # filter the list of switches to only those that haven't explicitly
        # disabled port security
        for switch_info, switch in devices.get_devices().items():
            if switch.ngs_config.get('ngs_security_groups_enabled', True):
                self.switches[switch_info] = switch

        LOG.info('Devices %s have been loaded', self.switches.keys())
        if not self.switches:
            LOG.error('No devices have been loaded')

        # TODO(stevebaker) A periodic worker can be implemented to ensure
        # switch state is in sync with security group state. It would be
        # created here with:
        # self.add_worker(GenericSwitchSecurityGroupSyncWorker())

    def get_plugin_description(self):
        return "Generic switch baremetal security group service plugin"

    @classmethod
    def get_plugin_type(cls):
        return "generic_switch_security_group"

    def create_security_group(self, resource, event, trigger, payload):
        sg_id = payload.resource_id
        sg = sg_obj.SecurityGroup.get_object(payload.context, id=sg_id)
        for switch_name, switch in self.switches.items():
            try:
                switch.add_security_group(sg)
            except Exception as e:
                LOG.error("Failed to create security group %(sg_id)s "
                          "on device: %(switch)s, reason: %(exc)s",
                          {'sg_id': sg_id, 'switch': switch_name, 'exc': e})
                raise
            else:
                LOG.info('Security group %(sg_id)s has been added on device '
                         '%(device)s',
                         {'sg_id': sg_id, 'device': switch_name})

    def update_security_group_rules(self, resource, event, trigger, payload):
        sgr = payload.latest_state
        sg_id = sgr['security_group_id']
        sg = sg_obj.SecurityGroup.get_object(payload.context, id=sg_id)
        for switch_name, switch in self.switches.items():
            try:
                switch.update_security_group(sg)
            except Exception as e:
                LOG.error("Failed to add rule to security group %(sg_id)s "
                          "on device: %(switch)s, reason: %(exc)s",
                          {'sg_id': sg_id, 'switch': switch_name, 'exc': e})
                raise
            else:
                LOG.info('Rule has been added to security group %(sg_id)s '
                         'on device %(device)s',
                         {'sg_id': sg_id, 'device': switch_name})

    def delete_security_group(self, resource, event, trigger, payload):
        sg_id = payload.resource_id
        for switch_name, switch in self.switches.items():
            try:
                switch.del_security_group(sg_id)
            except Exception as e:
                LOG.error("Failed to delete security group %(sg_id)s "
                          "on device: %(switch)s, reason: %(exc)s",
                          {'sg_id': sg_id, 'switch': switch_name, 'exc': e})
                raise
            else:
                LOG.info('Security group %(sg_id)s has been deleted from '
                         '%(device)s',
                         {'sg_id': sg_id, 'device': switch_name})

    @staticmethod
    def _valid_baremetal_port(port):
        """Check if port is a baremetal port with exactly one security group"""
        if not ngs_utils.is_port_bound(port):
            return False
        if len(port.get('security_groups', [])) > 1:
            LOG.warning('SG provisioning failed for %(port)s. Only one '
                        'SG may be applied per port.',
                        {'port': port['id']})
            raise exc.GenericSwitchNotSupported(
                message='Only one security group can be bound to a port')
        return True

    def _get_switch_and_port_id(self, port):
        """Get the port id from the binding profile.

        The port id is stored in the binding profile as
        'local_link_information' and is used to bind the security group to the
        port on the switch.

        :param port: The port to check
        :returns: The switch object, port id, switch info, and switch id
        """
        binding_profile = port['binding:profile']
        local_link_information = binding_profile.get('local_link_information')
        if not local_link_information:
            return None, None, None, None
        for link in local_link_information:
            switch_info = link.get('switch_info')
            switch_id = link.get('switch_id')
            switch = device_utils.get_switch_device(
                self.switches, switch_info=switch_info,
                ngs_mac_address=switch_id)
            if not switch:
                continue
            port_id = link.get('port_id')
            LOG.info('Found port %(port_id)s on switch %(switch)s',
                     {'port_id': port_id, 'switch': switch_info})
            return switch, port_id, switch_info, switch_id
        return None, None, None, None

    def _all_security_group_ports(self, context, switch_info, switch_id):
        """Find all security groups and ports related to this switch

        :param context: Database context
        :param switch_info: Local link information switch_info
        :param switch_id: Local link information switch_id
        :returns: A dict with key security group ID and value set of
                  port names bound to that group
        """
        # iterate all baremetal ports and collect port names and security
        # group IDs related to this switch
        sg_ports = collections.defaultdict(set)
        ports = ports_obj.Port.get_ports_by_vnic_type_and_host(
            context, portbindings.VNIC_BAREMETAL)
        for port in ports:
            if not port.bindings:
                continue
            if not port.security_group_ids:
                continue

            port_ids = set()
            for binding in port.bindings:
                local_link_information = binding.profile.get(
                    'local_link_information', [])
                for link in local_link_information:
                    if not link.get('port_id'):
                        continue
                    if switch_info and link.get('switch_info') == switch_info:
                        port_ids.add(link.get('port_id'))
                    if switch_id and link.get('switch_id') == switch_id:
                        port_ids.add(link.get('port_id'))

            for sg_id in port.security_group_ids:
                sg_ports[sg_id].update(port_ids)

        return dict(sg_ports)

    def update_port_security_group(self, resource, event, trigger, payload):
        port = payload.latest_state
        prev_port = payload.states[0]
        if not self._valid_baremetal_port(port):
            return
        switch, port_id, switch_info, switch_id = \
            self._get_switch_and_port_id(port)
        if not switch or not port_id:
            return

        sgs = port.get('security_groups', [])
        if (ngs_utils.is_port_bound(port)
                and not ngs_utils.is_port_bound(prev_port)):
            # The port is being bound to a switch interface,
            # treat any security groups as being bound to the interface too
            prev_sgs = []
        else:
            prev_sgs = prev_port.get('security_groups', [])

        if set(sgs) == set(prev_sgs):
            # security groups have not changed, no bind changes required
            return

        sg_ports_all = self._all_security_group_ports(
            payload.context, switch_info, switch_id)

        # _valid_baremetal_port currently enforces there will be zero or
        # one security groups but we implement support for more than one
        # here regardless.
        for sg_id in prev_sgs:
            if sg_id not in sgs:
                # Security group missing from current state, removed in this
                # port update
                sg_ports = sg_ports_all.get(sg_id, [])
                switch.unbind_security_group(sg_id, port_id, sg_ports)
                LOG.info('Security group %(sg_id)s has been removed from '
                         'port %(port_id)s. All bound ports: %(sg_ports)s',
                         {'sg_id': sg_id, 'port_id': port_id,
                          'sg_ports': ', '.join(sg_ports)})
        for sg_id in sgs:
            if sg_id not in prev_sgs:
                # Security group added in this port update
                sg = sg_obj.SecurityGroup.get_object(payload.context, id=sg_id)
                sg_ports = sg_ports_all.get(sg_id, [])
                switch.bind_security_group(sg, port_id, sg_ports)
                LOG.info('Security group %(sg_id)s has been applied to port '
                         '%(port_id)s. All bound ports: %(sg_ports)s',
                         {'sg_id': sg_id, 'port_id': port_id,
                          'sg_ports': ', '.join(sg_ports)})

    def remove_port_security_group(self, resource, event, trigger, payload):
        port = payload.latest_state
        try:
            if not self._valid_baremetal_port(port):
                return
        except exc.GenericSwitchNotSupported:
            # this is a bound port with (somehow) multiple security groups
            # do best effort to unbind all
            pass

        switch, port_id, switch_info, switch_id = \
            self._get_switch_and_port_id(port)
        if not switch or not port_id:
            return

        sg_ports_all = self._all_security_group_ports(
            payload.context, switch_info, switch_id)

        sgs = port.get('security_groups', [])
        for sg_id in sgs:
            sg_ports = sg_ports_all.get(sg_id, [])
            switch.unbind_security_group(sg_id, port_id, sg_ports)
            LOG.info('Security group %(sg_id)s has been removed from '
                     'deleted port %(port_id)s. All bound ports: %(sg_ports)s',
                     {'sg_id': sg_id, 'port_id': port_id,
                      'sg_ports': ', '.join(sg_ports)})

    def subscribe(self):
        # Subscribe to the events related to security groups and rules.

        # Creates a new security group, and the payload may include existing
        # rules.
        registry.subscribe(
            self.create_security_group, resources.SECURITY_GROUP,
            events.AFTER_CREATE)
        # Deletes an existing security group.
        registry.subscribe(
            self.delete_security_group, resources.SECURITY_GROUP,
            events.AFTER_DELETE)

        # Adds one rule to an existing security group.
        registry.subscribe(
            self.update_security_group_rules, resources.SECURITY_GROUP_RULE,
            events.AFTER_CREATE)
        # Deletes one rule from an existing security group
        registry.subscribe(
            self.update_security_group_rules, resources.SECURITY_GROUP_RULE,
            events.AFTER_DELETE)

        # Apply/remove SG rules on AFTER_UPDATE.
        # Binds or unbinds a security group to a port.
        registry.subscribe(
            self.update_port_security_group, resources.PORT,
            events.AFTER_UPDATE)
        # Unbinds a security group from a port as part of port delete.
        registry.subscribe(
            self.remove_port_security_group, resources.PORT,
            events.AFTER_DELETE)