neutron-fwaas/neutron_fwaas/services/firewall/service_drivers/agents/l2/fwaas_v2.py

493 lines
19 KiB
Python

# Copyright 2017-2018 FUJITSU LIMITED
#
# 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.
from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_log import log as logging
from neutron.agent import securitygroups_rpc
from neutron import manager
from neutron.plugins.ml2.drivers.openvswitch.agent import vlanmanager
from neutron_lib.agent import l2_extension
from neutron_lib import constants as nl_const
from neutron_lib.exceptions import firewall_v2 as f_exc
from neutron_lib import rpc as n_rpc
from neutron_lib.utils import net as nl_net
from neutron_fwaas._i18n import _
from neutron_fwaas.common import fwaas_constants as consts
from neutron_fwaas.services.firewall.service_drivers.agents import\
firewall_agent_api as api
LOG = logging.getLogger(__name__)
FWAAS_L2_DRIVER = 'neutron.agent.l2.firewall_drivers'
SG_OVS_DRIVER = 'openvswitch'
class FWaaSL2PluginApi(api.FWaaSPluginApiMixin):
"""L2 agent side of FWaaS agent-to-plugin RPC API"""
def get_firewall_group_for_port(self, context, port_id):
"""Get firewall group is associated with a port"""
LOG.debug("Get firewall group is associated with port %s", port_id)
cctxt = self.client.prepare()
return cctxt.call(context, 'get_firewall_group_for_port',
port_id=port_id)
def set_firewall_group_status(self, context, fwg_id, status, host):
"""Set the status of a group operation."""
LOG.debug("Fetch firewall group changing status")
cctxt = self.client.prepare()
return cctxt.call(context, 'set_firewall_group_status',
fwg_id=fwg_id, status=status, host=host)
def firewall_group_deleted(self, context, fwg_id, host):
"""Notifies the plugin that a firewall group has been deleted."""
LOG.debug("Notify to the plugin that firewall group has been deleted")
cctxt = self.client.prepare()
return cctxt.call(context, 'firewall_group_deleted',
fwg_id=fwg_id, host=host)
class FWaaSV2AgentExtension(l2_extension.L2AgentExtension):
def initialize(self, connection, driver_type):
"""Perform Agent Extension initialization"""
self.conf = cfg.CONF
self.vlan_manager = vlanmanager.LocalVlanManager()
fw_l2_driver_cls = self._load_l2_driver_class(driver_type)
sg_enabled = securitygroups_rpc.is_firewall_enabled()
sg_firewall_driver = self.conf.SECURITYGROUP.firewall_driver
sg_with_ovs = sg_enabled and (sg_firewall_driver == SG_OVS_DRIVER)
self.driver = manager.NeutronManager.load_class_for_provider(
FWAAS_L2_DRIVER, fw_l2_driver_cls)(self.agent_api, sg_with_ovs)
self.plugin_rpc = FWaaSL2PluginApi(
consts.FIREWALL_PLUGIN, self.conf.host)
self.start_rpc_listeners()
self.fwg_map = PortFirewallGroupMap()
def consume_api(self, agent_api):
self.agent_api = agent_api
def start_rpc_listeners(self):
self.conn = n_rpc.Connection()
endpoints = [self]
self.conn.create_consumer(consts.FW_AGENT, endpoints, fanout=False)
return self.conn.consume_in_threads()
def _load_l2_driver_class(self, driver_type):
driver = self.conf.fwaas.firewall_l2_driver or 'noop'
if driver == api.FW_L2_NOOP_DRIVER:
return driver
if driver != driver_type:
raise Exception(
_("Firewall l2 driver: %s is not compatible"), driver_type)
return driver
def _is_port_layer2(self, port):
"""This function checks if a port belongs to a L2 case.
Currently both DHCP and router ports are eliminated.
"""
return port and port.get('device_owner', '').startswith(
nl_const.DEVICE_OWNER_COMPUTE_PREFIX)
def _get_firewall_group_ports(self, fwg, host, to_delete=False):
port_list = []
port_ids = fwg['del-port-ids'] if to_delete else fwg['add-port-ids']
LOG.debug("_get_fwg fwg=%(fwg)s ports=%(port)s to_delete=%(delete)s",
{'fwg': fwg, 'port': port_ids, 'delete': to_delete})
for fw_port in port_ids:
port_detail = fwg['port_details'].get(fw_port)
if (self._is_port_layer2(port_detail) and
port_detail.get('host') == host):
port_list.append(port_detail)
return port_list
@staticmethod
def _has_ports(fwg, event):
"""Verifying fwg has ports or not
This function verify applying firewall group on ports
:param fwg: a fwg object
:param event: create/update firewall group or
create/update/delete port
:return: True if applying firewall group is fine. Otherwise is False
"""
if event == consts.UPDATE_FWG and 'last-port' in fwg:
return not fwg['last-port']
else:
return bool(fwg['ports'])
@staticmethod
def _has_policy(fwg):
"""Verifying fwg has policy or not"""
return bool(fwg['ingress_firewall_policy_id'] or
fwg['egress_firewall_policy_id'])
def _compute_status(self, fwg, result, event=consts.CREATE_FWG):
"""Compute a status of specified firewall group for update
Validates 'ACTIVE', 'DOWN', 'INACTIVE', 'ERROR' and None as follows:
- "ERROR" : if result is not True
- "ACTIVE" : admin_state_up is True and exists ports
- "INACTIVE" : admin_state_up is True and with no ports
- "DOWN" : admin_state_up is False
- None : In case of 'delete_firewall_group'
"""
if not result:
return nl_const.ERROR
if not fwg['admin_state_up']:
return nl_const.DOWN
if event == consts.DELETE_FWG:
# This firewall_group will be deleted. No need to update status.
return
if (self._has_ports(fwg, event) and self._has_policy(fwg)):
return nl_const.ACTIVE
return nl_const.INACTIVE
def _get_network_id(self, fwg_port):
port_id = fwg_port.get('port_id', fwg_port.get('id'))
port_details = fwg_port.get('port_details')
if port_details:
target = port_details.get(port_id)
if target:
return target.get('network_id')
return
return fwg_port.get('network_id')
def _add_local_vlan_to_ports(self, fwg_ports):
"""Add local VLAN to ports if found
This function tries to add local VLAN related to ports.
"""
ports_with_lvlan = []
for fwg_port in fwg_ports:
try:
network_id = self._get_network_id(fwg_port)
l_vlan = self.vlan_manager.get(network_id).vlan
fwg_port['lvlan'] = int(l_vlan)
except vlanmanager.MappingNotFound:
LOG.warning("No Local VLAN found in network %s", network_id)
# NOTE(yushiro): We ignore this exception because we should send
# all selected ports to driver layer. It depends on driver's
# behavior whether it occurs an error with no local VLAN or not.
ports_with_lvlan.append(fwg_port)
return ports_with_lvlan
def _apply_fwg_rules(self, fwg, ports, event=consts.UPDATE_FWG):
"""This function invokes the driver create/update routine. """
# Set firewall group status; will be overwritten if call to driver
# fails.
if event in [consts.CREATE_FWG, consts.UPDATE_FWG]:
ports_for_driver = self._add_local_vlan_to_ports(ports)
else:
ports_for_driver = ports
# apply firewall group to driver
try:
if event == consts.UPDATE_FWG:
self.driver.update_firewall_group(ports_for_driver, fwg)
elif event == consts.DELETE_FWG:
self.driver.delete_firewall_group(ports_for_driver, fwg)
elif event == consts.CREATE_FWG:
self.driver.create_firewall_group(ports_for_driver, fwg)
except f_exc.FirewallInternalDriverError:
msg = _("FWaaS driver error in %(event)s_firewall_group "
"for firewall group: %(fwg_id)s")
LOG.exception(msg, {'event': event, 'fwg_id': fwg['id']})
return False
return True
def _send_fwg_status(self, context, fwg_id, status, host):
"""Send firewall group's status to plugin.
:returns: True if no exception occurred otherwise False
:rtype: boolean
"""
try:
self.plugin_rpc.set_firewall_group_status(
context, fwg_id, status, host)
LOG.debug("Successfully sent status(%s) for firewall_group(%s)",
status, fwg_id)
except Exception:
msg = _("Failed to send status for firewall_group(%s)")
LOG.exception(msg, fwg_id)
def _create_firewall_group(self, context, fwg, host,
event=consts.CREATE_FWG):
"""Handles RPC from plugin to create a firewall group. """
add_ports = self._get_firewall_group_ports(fwg, host)
if not add_ports:
status = nl_const.INACTIVE
else:
ret = self._apply_fwg_rules(fwg, add_ports, event)
# cleanup port_map
for port in add_ports:
self.fwg_map.remove_port(port)
status = self._compute_status(fwg, ret, event)
for port in add_ports:
self.fwg_map.set_port_fwg(port, fwg)
# Update status of firewall group which is associated with ports
# after updating.
self._send_fwg_status(context, fwg['id'], status, host)
def _delete_firewall_group(self, context, fwg, host,
event=consts.DELETE_FWG):
"""Handles RPC from plugin to delete a firewall group. """
del_ports = self._get_firewall_group_ports(fwg, host, to_delete=True)
if not del_ports:
return
# cleanup all flows of del_ports
ret = self._apply_fwg_rules(fwg, del_ports, event=consts.DELETE_FWG)
del_port_ids = []
for port in del_ports:
del_port_ids.append(port['id'])
self.fwg_map.remove_port(port)
if event == consts.DELETE_FWG:
self.fwg_map.remove_fwg(fwg)
self.plugin_rpc.firewall_group_deleted(
context, fwg['id'], host=self.conf.host)
else:
status = self._compute_status(fwg, ret, event)
self._send_fwg_status(context, fwg['id'], status, self.conf.host)
@lockutils.synchronized('fwg')
def create_firewall_group(self, context, firewall_group, host):
"""Handles create firewall group event"""
# TODO(chandanc): Fix agent RPC endpoint to remove host arg
host = cfg.CONF.host
with self.driver.defer_apply():
try:
self._create_firewall_group(context, firewall_group, host)
except Exception as exc:
LOG.exception(
"Exception caught in create_firewall_group %s", exc)
self._send_fwg_status(context, firewall_group['id'],
status=nl_const.ERROR, host=host)
@lockutils.synchronized('fwg')
def delete_firewall_group(self, context, firewall_group, host):
"""Handles delete firewall group event"""
# TODO(chandanc): Fix agent RPC endpoint to remove host arg
host = cfg.CONF.host
with self.driver.defer_apply():
try:
self._delete_firewall_group(context, firewall_group, host)
except Exception as exc:
LOG.exception(
"Exception caught in delete_firewall_group %s", exc)
self._send_fwg_status(context, firewall_group['id'],
status=nl_const.ERROR, host=host)
@lockutils.synchronized('fwg')
def update_firewall_group(self, context, firewall_group, host):
"""Handles update firewall group event"""
# TODO(chandanc): Fix agent RPC endpoint to remove host arg
host = cfg.CONF.host
with self.driver.defer_apply():
try:
self._delete_firewall_group(
context, firewall_group, host, event=consts.UPDATE_FWG)
self._create_firewall_group(
context, firewall_group, host, event=consts.UPDATE_FWG)
except Exception as exc:
LOG.exception(
"Exception caught in update_firewall_group %s", exc)
self._send_fwg_status(context, firewall_group['id'],
status=nl_const.ERROR, host=host)
@lockutils.synchronized('fwg-port')
def handle_port(self, context, port):
"""Handle port update event"""
# Check if port is trusted and called at once.
if nl_net.is_port_trusted(port) and not self.fwg_map.get_port(port):
self._add_rule_for_trusted_port(port)
self.fwg_map.set_port(port)
return
if not self._is_port_layer2(port):
return
# check if port is already assigned to a fwg
if self.fwg_map.get_port_fwg(port):
return
fwg = self.plugin_rpc.get_firewall_group_for_port(
context, port.get('port_id'))
if not fwg:
LOG.info("Firewall group applied to port %s is "
"not available on server.", port['port_id'])
return
ret = self._apply_fwg_rules(fwg, [port])
status = self._compute_status(fwg, ret, event=consts.HANDLE_PORT)
self.fwg_map.set_port_fwg(port, fwg)
self._send_fwg_status(
context, fwg_id=fwg['id'], status=status, host=self.conf.host)
def _add_rule_for_trusted_port(self, port):
self._add_local_vlan_to_ports([port])
self.driver.process_trusted_ports([port])
def _delete_rule_for_trusted_port(self, port):
self.driver.remove_trusted_ports([port['port_id']])
def delete_port(self, context, port):
"""This is being called when a port is deleted by the agent. """
# delete_port should be handled only unbound timing for a port.
# If 'vif_port' is included in the port dict, this is called after
# deleted the port and should be ignored.
if 'vif_port' in port:
return
port = self.fwg_map.get_port(port)
if port and nl_net.is_port_trusted(port):
self._delete_rule_for_trusted_port(port)
self.fwg_map.remove_port(port)
return
if not self._is_port_layer2(port):
return
fwg = self.fwg_map.get_port_fwg(port)
if not fwg:
LOG.info("Firewall group associated to port %(port_id)s is "
"not available on server.", {'port_id': port['port_id']})
return
ret = self._apply_fwg_rules(fwg, [port], event=consts.DELETE_FWG)
port_id = self.fwg_map.port_id(port)
if port_id in fwg['ports']:
fwg['ports'].remove(port_id)
# update the fwg dict to known_fwgs
self.fwg_map.set_fwg(fwg)
self.fwg_map.remove_port(port)
status = self._compute_status(fwg, ret, event=consts.DELETE_PORT)
self._send_fwg_status(context, fwg['id'], status, self.conf.host)
class PortFirewallGroupMap(object):
"""Store relations between Port and Firewall Group and trusted port
This map is used in deleting firewall_group because the firewall_group has
been deleted at that time. Therefore, it is impossible to refer 'ports'.
This map enables to refer 'ports' for specified firewall_group.
Furthermore, it is necessary to check 'device_owner' for trusted port, this
Map also stores trusted port data.
"""
def __init__(self):
self.known_fwgs = {}
self.port_fwg = {}
self.port_detail = {}
# TODO(yushiro): If agent is restarted, this map doesn't have any
# information. Need to consider map initialization in __init__()
def port_id(self, port):
return (port if isinstance(port, str)
else port.get('port_id', port.get('id')))
def get_fwg(self, fwg_id):
return self.known_fwgs.get(fwg_id)
def set_fwg(self, fwg):
self.known_fwgs[fwg['id']] = fwg
def get_port(self, port):
return self.port_detail.get(self.port_id(port))
def get_port_fwg(self, port):
fwg_id = self.port_fwg.get(self.port_id(port))
if fwg_id:
return self.get_fwg(fwg_id)
def set_port(self, port):
"""Add a new port into port_detail"""
port_id = self.port_id(port)
self.port_detail[port_id] = port
def set_port_fwg(self, port, fwg):
"""Add a new port into fwg['ports']"""
port_id = self.port_id(port)
# Update fwg['ports'] data
fwg['ports'] = list(set(fwg['ports'] + [port_id]))
# Update fwg_id -> firewall_group data
self.known_fwgs[fwg['id']] = fwg
# Update port_id -> port data
self.port_detail[port_id] = port
# Update port_id -> firewall_group_id relation
self.port_fwg[port_id] = fwg['id']
def remove_port(self, port):
"""Remove port from fwg['ports'] and port_fwg dictionary
When removing 'port' from several cases, the port should be removed
from this map.
"""
port_id = self.port_id(port)
# Check if 'port_id' has registered in port_fwg dictionary.
# Update firewall_group
if port_id in self.port_fwg:
fwg_id = self.port_fwg.get(port_id)
if not fwg_id:
# This case is trusted port. Try to delete port_detail dict
try:
del self.port_detail[port_id]
except KeyError:
pass
return
new_fwg = self.known_fwgs[fwg_id]
new_fwg['ports'] = [p for p in new_fwg['ports'] if p != port_id]
self.known_fwgs[fwg_id] = new_fwg
del self.port_fwg[port_id]
del self.port_detail[port_id]
def remove_fwg(self, fwg):
"""Remove firewall_group from known_fwgs dictionary
When removing firewall_group, it should be removed from this map
"""
if fwg['id'] in self.known_fwgs:
del self.known_fwgs[fwg['id']]