Files
neutron-fwaas/neutron_fwaas/services/firewall/fwaas_plugin_v2.py
Édouard Thuleau 2a7994851c Move port validation support into the driver
Each firewall driver have specific checks to do on port validation (like
checks if the VIF port type corresponds to a type supported by the driver
(aka the SDN controller)). This patch adds two methods to the driver
interface to validate if the VM or the router port is supported (just
have to return a boolean).

Change-Id: I8fdf0956ac5428558aae413e610d13c4a4a56273
Closes-Bug: #1803723
2018-11-29 16:39:36 +01:00

430 lines
18 KiB
Python

# 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.
from neutron.db import servicetype_db as st_db
from neutron import service
from neutron.services import provider_configuration as provider_conf
from neutron.services import service_base
from neutron_lib.api.definitions import firewall_v2
from neutron_lib.api.definitions import portbindings as pb_def
from neutron_lib.api import validators
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib import constants as nl_constants
from neutron_lib.exceptions import firewall_v2 as f_exc
from neutron_lib.plugins import constants as plugin_const
from neutron_lib.plugins import directory
from oslo_log import helpers as log_helpers
from oslo_log import log as logging
from neutron_fwaas.common import exceptions
from neutron_fwaas.common import fwaas_constants
from neutron_fwaas.extensions.firewall_v2 import Firewallv2PluginBase
from neutron_fwaas.services.firewall.service_drivers import driver_api
from neutron_fwaas.services.logapi.agents.drivers.iptables \
import driver as logging_driver
LOG = logging.getLogger(__name__)
@registry.has_registry_receivers
class FirewallPluginV2(Firewallv2PluginBase):
"""Firewall v2 Neutron service plugin class"""
supported_extension_aliases = [firewall_v2.ALIAS]
path_prefix = firewall_v2.API_PREFIX
def __init__(self):
super(FirewallPluginV2, self).__init__()
"""Do the initialization for the firewall service plugin here."""
# Initialize the Firewall v2 service plugin
service_type_manager = st_db.ServiceTypeManager.get_instance()
service_type_manager.add_provider_configuration(
fwaas_constants.FIREWALL_V2,
provider_conf.ProviderConfiguration('neutron_fwaas'))
# Load the default driver
drivers, default_provider = service_base.load_drivers(
fwaas_constants.FIREWALL_V2, self)
LOG.info("Firewall v2 Service Plugin using Service Driver: %s",
default_provider)
if len(drivers) > 1:
LOG.warning("Multiple drivers configured for Firewall v2, "
"although running multiple drivers in parallel is "
"not yet supported")
self.driver_name = default_provider
self.driver = drivers[default_provider]
# start rpc listener if driver required
if isinstance(self.driver, driver_api.FirewallDriverRPCMixin):
rpc_worker = service.RpcWorker([self], worker_process_count=0)
self.add_worker(rpc_worker)
log_plugin = directory.get_plugin(plugin_const.LOG_API)
logging_driver.register()
# If log_plugin was loaded before firewall plugin
if log_plugin:
# Register logging driver with LoggingServiceDriverManager again
log_plugin.driver_manager.register_driver(logging_driver.DRIVER)
def start_rpc_listeners(self):
return self.driver.start_rpc_listener()
@property
def _core_plugin(self):
return directory.get_plugin()
def _ensure_update_firewall_group(self, context, fwg_id):
"""Checks if the firewall group can be updated
Raises FirewallGroupInPendingState if the firewall group is in pending
state.
:param context: neutron context
:param fwg_id: firewall group ID to check
:return: Firewall group dict
"""
fwg = self.get_firewall_group(context, fwg_id)
if fwg['status'] in [nl_constants.PENDING_CREATE,
nl_constants.PENDING_UPDATE,
nl_constants.PENDING_DELETE]:
raise f_exc.FirewallGroupInPendingState(
firewall_id=fwg_id, pending_state=fwg['status'])
return fwg
def _ensure_update_firewall_policy(self, context, fwp_id):
"""Checks if the firewall policy can be updated
Fetch firewall group associated to the policy and checks if they can be
updated.
:param context: neutron context
:param fwp_id: firewall policy ID to check
"""
fwp = self.get_firewall_policy(context, fwp_id)
ing_fwg_ids, eg_fwg_ids = self._get_fwgs_with_policy(context, fwp)
for fwg_id in list(set(ing_fwg_ids + eg_fwg_ids)):
self._ensure_update_firewall_group(context, fwg_id)
def _ensure_update_firewall_rule(self, context, fwr_id):
"""Checks if the firewall rule can be updated
Fetch firewall policy associated to the rule and checks if they can be
updated.
:param context: neutron context
:param fwr_id: firewall policy ID to check
"""
fwr = self.get_firewall_rule(context, fwr_id)
fwp_ids = self._get_policies_with_rule(context, fwr)
for fwp_id in fwp_ids:
self._ensure_update_firewall_policy(context, fwp_id)
def _validate_firewall_policies_for_firewall_group(self, context, fwg):
"""Validate firewall group and policy owner
Check if the firewall policy is not shared, it have the same project
owner than the friewall group.
:param context: neutron context
:param fwg: firewall group to validate
"""
for policy_type in ['ingress_firewall_policy_id',
'egress_firewall_policy_id']:
if fwg.get(policy_type):
fwp = self.get_firewall_policy(context, fwg[policy_type])
if fwg['tenant_id'] != fwp['tenant_id'] and not fwp['shared']:
raise f_exc.FirewallPolicyConflict(
firewall_policy_id=fwg[policy_type])
def _validate_ports_for_firewall_group(self, context, tenant_id,
fwg_ports):
"""Validate firewall group associated ports
Check if the firewall group associated ports have the same project
owner and is router interface type or a compute layer 2 and supported
by the firewall driver
:param context: neutron context
:param tenant_id: firewall group project ID
:param fwg_ports: firewall group associated ports
"""
# TODO(sridar): elevated context and do we want to use public ?
for port_id in fwg_ports:
port = self._core_plugin.get_port(context, port_id)
if port['tenant_id'] != tenant_id:
raise f_exc.FirewallGroupPortInvalidProject(
port_id=port_id, project_id=port['tenant_id'])
device_owner = port.get('device_owner', '')
if device_owner in nl_constants.ROUTER_INTERFACE_OWNERS:
if not self.driver.is_supported_l3_port(port):
raise exceptions.FirewallGroupPortNotSupported(
driver_name=self.driver_name, port_id=port_id)
elif device_owner.startswith(
nl_constants.DEVICE_OWNER_COMPUTE_PREFIX):
if not self._is_supported_l2_port(context, port_id):
raise exceptions.FirewallGroupPortNotSupported(
driver_name=self.driver_name, port_id=port_id)
else:
raise f_exc.FirewallGroupPortInvalid(port_id=port_id)
def _is_supported_l2_port(self, context, port_id):
"""Whether this l2 port is supported"""
# Re-fetch to get up-to-date data from db
port = self._core_plugin.get_port(context, id=port_id)
# Skip port binding is unbound or failed
if port[pb_def.VIF_TYPE] in [pb_def.VIF_TYPE_UNBOUND,
pb_def.VIF_TYPE_BINDING_FAILED]:
return False
return self.driver.is_supported_l2_port(port)
def _validate_if_firewall_group_on_ports(self, context, firewall_group,
id=None):
"""Validate if ports are not associated with any firewall_group.
If any of the ports in the list is already associated with
a firewall group, raise an exception else just return.
:param context: neutron context
:param fwg: firewall group to validate
"""
if 'ports' not in firewall_group or not firewall_group['ports']:
return
filters = {
'tenant_id': [firewall_group['tenant_id']],
'ports': firewall_group['ports'],
}
ports_in_use = set()
for fwg in self.get_firewall_groups(context, filters=filters):
if id is not None and fwg['id'] == id:
continue
ports_in_use |= set(fwg.get('ports', [])) & \
set(firewall_group['ports'])
if ports_in_use:
raise f_exc.FirewallGroupPortInUse(port_ids=list(ports_in_use))
def _get_fwgs_with_policy(self, context, firewall_policy):
"""List firewall group IDs which use a firewall policy
List all firewall group IDs which have the given firewall policy as
ingress or egress.
:param context: neutron context
:param firewall_policy: firewall policy to filter
"""
filters = {
'tenant_id': [firewall_policy['tenant_id']],
'ingress_firewall_policy_id': [firewall_policy['id']],
}
ingress_fwp_ids = [fwg['id']
for fwg in self.get_firewall_groups(
context, filters=filters)]
filters = {
'tenant_id': [firewall_policy['tenant_id']],
'egress_firewall_policy_id': [firewall_policy['id']],
}
egress_fwp_ids = [fwg['id']
for fwg in self.get_firewall_groups(
context, filters=filters)]
return ingress_fwp_ids, egress_fwp_ids
def _get_policies_with_rule(self, context, firewall_rule):
filters = {
'tenant_id': [firewall_rule['tenant_id']],
'firewall_rules': [firewall_rule['id']],
}
return [fwp['id'] for fwp in self.get_firewall_policies(
context, filters=filters)]
def _validate_insert_remove_rule_request(self, rule_info):
"""Validate rule_info dict
Check that all mandatory fields are present, otherwise raise
proper exception.
"""
if not rule_info or 'firewall_rule_id' not in rule_info:
raise f_exc.FirewallRuleInfoMissing()
# Validator doesn't return anything if the check passes
if validators.validate_uuid(rule_info['firewall_rule_id']):
raise f_exc.FirewallRuleNotFound(
firewall_rule_id=rule_info['firewall_rule_id'])
@registry.receives(resources.PORT, [events.AFTER_UPDATE])
def handle_update_port(self, resource, event, trigger, **kwargs):
updated_port = kwargs['port']
if not updated_port['device_owner'].startswith(
nl_constants.DEVICE_OWNER_COMPUTE_PREFIX):
return
if (kwargs.get('original_port')[pb_def.VIF_TYPE] !=
pb_def.VIF_TYPE_UNBOUND):
# Checking newly vm port binding allows us to avoid call to DB
# when a port update_event like restart, setting name, etc...
# Moreover, that will help us in case of tenant admin wants to
# only attach security group to vm port.
return
context = kwargs['context']
port_id = updated_port['id']
# Check port is supported by firewall driver
if not self._is_supported_l2_port(context, port_id):
return
project_id = updated_port['project_id']
fwgs = self.get_firewall_groups(
context,
filters={
'tenant_id': [project_id],
'name': [fwaas_constants.DEFAULT_FWG],
},
fields=['id', 'ports'],
)
if len(fwgs) != 1:
# Cannot found default Firewall Group, abandon
LOG.warning("Cannot found default firewall group of project %s",
project_id)
return
default_fwg = fwgs[0]
# Add default firewall group to the port
port_ids = default_fwg.get('ports', []) + [port_id]
try:
self.update_firewall_group(context, default_fwg['id'],
{'firewall_group': {'ports': port_ids}})
except f_exc.FirewallGroupPortInUse:
LOG.warning("Port %s has been already associated with default "
"firewall group %s and skip association", port_id,
default_fwg['id'])
# Firewall Group
@log_helpers.log_method_call
def create_firewall_group(self, context, firewall_group):
firewall_group = firewall_group['firewall_group']
ports = firewall_group.get('ports', [])
self._validate_firewall_policies_for_firewall_group(context,
firewall_group)
# Validate ports owner type and project
self._validate_ports_for_firewall_group(context,
firewall_group['tenant_id'],
ports)
self._validate_if_firewall_group_on_ports(context, firewall_group)
return self.driver.create_firewall_group(context, firewall_group)
@log_helpers.log_method_call
def delete_firewall_group(self, context, id):
# if no such group exists -> don't raise an exception according to
# 80fe2ba1, return None
try:
fwg = self.get_firewall_group(context, id)
except f_exc.FirewallGroupNotFound:
return
if fwg['status'] == nl_constants.ACTIVE:
raise f_exc.FirewallGroupInUse(firewall_id=id)
self.driver.delete_firewall_group(context, id)
@log_helpers.log_method_call
def get_firewall_group(self, context, id, fields=None):
return self.driver.get_firewall_group(context, id, fields=fields)
@log_helpers.log_method_call
def get_firewall_groups(self, context, filters=None, fields=None):
return self.driver.get_firewall_groups(context, filters, fields)
@log_helpers.log_method_call
def update_firewall_group(self, context, id, firewall_group):
firewall_group = firewall_group['firewall_group']
ports = firewall_group.get('ports', [])
old_firewall_group = self._ensure_update_firewall_group(context, id)
firewall_group['tenant_id'] = old_firewall_group['tenant_id']
self._validate_firewall_policies_for_firewall_group(context,
firewall_group)
# Validate ports owner type and project
self._validate_ports_for_firewall_group(context,
firewall_group['tenant_id'],
ports)
self._validate_if_firewall_group_on_ports(context, firewall_group,
id=id)
return self.driver.update_firewall_group(context, id, firewall_group)
# Firewall Policy
@log_helpers.log_method_call
def create_firewall_policy(self, context, firewall_policy):
firewall_policy = firewall_policy['firewall_policy']
return self.driver.create_firewall_policy(context, firewall_policy)
@log_helpers.log_method_call
def delete_firewall_policy(self, context, id):
self.driver.delete_firewall_policy(context, id)
@log_helpers.log_method_call
def get_firewall_policy(self, context, id, fields=None):
return self.driver.get_firewall_policy(context, id, fields)
@log_helpers.log_method_call
def get_firewall_policies(self, context, filters=None, fields=None):
return self.driver.get_firewall_policies(context, filters, fields)
@log_helpers.log_method_call
def update_firewall_policy(self, context, id, firewall_policy):
firewall_policy = firewall_policy['firewall_policy']
self._ensure_update_firewall_policy(context, id)
return self.driver.update_firewall_policy(context, id, firewall_policy)
# Firewall Rule
@log_helpers.log_method_call
def create_firewall_rule(self, context, firewall_rule):
firewall_rule = firewall_rule['firewall_rule']
return self.driver.create_firewall_rule(context, firewall_rule)
@log_helpers.log_method_call
def delete_firewall_rule(self, context, id):
self.driver.delete_firewall_rule(context, id)
@log_helpers.log_method_call
def get_firewall_rule(self, context, id, fields=None):
return self.driver.get_firewall_rule(context, id, fields)
@log_helpers.log_method_call
def get_firewall_rules(self, context, filters=None, fields=None):
return self.driver.get_firewall_rules(context, filters, fields)
@log_helpers.log_method_call
def update_firewall_rule(self, context, id, firewall_rule):
firewall_rule = firewall_rule['firewall_rule']
self._ensure_update_firewall_rule(context, id)
return self.driver.update_firewall_rule(context, id, firewall_rule)
@log_helpers.log_method_call
def insert_rule(self, context, policy_id, rule_info):
self._ensure_update_firewall_policy(context, policy_id)
self._validate_insert_remove_rule_request(rule_info)
return self.driver.insert_rule(context, policy_id, rule_info)
@log_helpers.log_method_call
def remove_rule(self, context, policy_id, rule_info):
self._ensure_update_firewall_policy(context, policy_id)
self._validate_insert_remove_rule_request(rule_info)
return self.driver.remove_rule(context, policy_id, rule_info)