493 lines
19 KiB
Python
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']]
|