326 lines
13 KiB
Python
326 lines
13 KiB
Python
# 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 keystoneclient import exceptions as k_exceptions
|
|
from keystoneclient.v2_0 import client as keyclient
|
|
|
|
from gbpservice.common import utils
|
|
from gbpservice.contrib.nfp.config_orchestrator.common import topics
|
|
from gbpservice.nfp.core import log as nfp_logging
|
|
import netaddr
|
|
|
|
from neutron.db import l3_db
|
|
from neutron.db.l3_db import DEVICE_OWNER_ROUTER_INTF
|
|
from neutron.db.l3_db import EXTERNAL_GW_INFO
|
|
from neutron.db.models.l3 import RouterPort
|
|
from neutron.db import models_v2
|
|
from neutron.extensions import l3
|
|
from neutron.plugins.common import constants as n_const
|
|
from neutron_lib import constants as nlib_const
|
|
from neutron_lib import exceptions as n_exc
|
|
|
|
import neutron_fwaas.extensions
|
|
from neutron_fwaas.services.firewall import fwaas_plugin as ref_fw_plugin
|
|
from oslo_config import cfg
|
|
from oslo_utils import excutils
|
|
from oslo_utils import uuidutils
|
|
from sqlalchemy import orm
|
|
|
|
from neutron_fwaas.db.firewall import firewall_db as n_firewall
|
|
|
|
LOG = nfp_logging.getLogger(__name__)
|
|
|
|
|
|
class NFPFirewallPlugin(ref_fw_plugin.FirewallPlugin):
|
|
def __init__(self):
|
|
# Monkey patch L3 agent topic
|
|
# L3 agent was where reference firewall agent runs
|
|
# patch that topic to the NFP firewall agent's topic name
|
|
ref_fw_plugin.f_const.L3_AGENT = topics.FW_NFP_CONFIGAGENT_TOPIC
|
|
#n_topics.L3_AGENT = topics.FW_NFP_CONFIGAGENT_TOPIC
|
|
|
|
# Ensure neutron fwaas extensions are loaded
|
|
ext_path = neutron_fwaas.extensions.__path__[0]
|
|
if ext_path not in cfg.CONF.api_extensions_path.split(':'):
|
|
cfg.CONF.set_override(
|
|
'api_extensions_path',
|
|
cfg.CONF.api_extensions_path + ':' + ext_path)
|
|
|
|
super(NFPFirewallPlugin, self).__init__()
|
|
|
|
# Modifying following plugin function, to relax same router validation
|
|
def _get_routers_for_create_firewall(self, tenant_id, context, firewall):
|
|
|
|
# pop router_id as this goes in the router association db
|
|
# and not firewall db
|
|
router_ids = firewall['firewall'].pop('router_ids', None)
|
|
if router_ids == nlib_const.ATTR_NOT_SPECIFIED:
|
|
return tenant_id
|
|
|
|
def set_routers_for_firewall(self, context, fw):
|
|
"""Sets the routers associated with the fw."""
|
|
pass
|
|
|
|
def get_firewall_routers(self, context, fwid):
|
|
"""Gets all routers associated with a firewall."""
|
|
fw_rtrs = ['1234567890']
|
|
return fw_rtrs
|
|
|
|
def validate_firewall_routers_not_in_use(
|
|
self, context, router_ids, fwid=None):
|
|
"""Validate if router-ids not associated with any firewall.
|
|
|
|
If any of the router-ids in the list is already associated with
|
|
a firewall, raise an exception else just return.
|
|
"""
|
|
pass
|
|
|
|
def update_firewall_routers(self, context, fw):
|
|
"""Update the firewall with new routers.
|
|
|
|
This involves removing existing router associations and replacing
|
|
it with the new router associations provided in the update method.
|
|
"""
|
|
return fw
|
|
|
|
|
|
# Monkey patching the create_firewall db method
|
|
def create_firewall(self, context, firewall, status=None):
|
|
fw = firewall['firewall']
|
|
tenant_id = fw['tenant_id']
|
|
# distributed routers may required a more complex state machine;
|
|
# the introduction of a new 'CREATED' state allows this, whilst
|
|
# keeping a backward compatible behavior of the logical resource.
|
|
if not status:
|
|
status = n_const.PENDING_CREATE
|
|
with context.session.begin(subtransactions=True):
|
|
self._validate_fw_parameters(context, fw, tenant_id)
|
|
firewall_db = n_firewall.Firewall(
|
|
id=uuidutils.generate_uuid(),
|
|
tenant_id=tenant_id,
|
|
name=fw['name'],
|
|
description=fw['description'],
|
|
firewall_policy_id=fw['firewall_policy_id'],
|
|
admin_state_up=fw['admin_state_up'],
|
|
status=status)
|
|
context.session.add(firewall_db)
|
|
return self._make_firewall_dict(firewall_db)
|
|
|
|
n_firewall.Firewall_db_mixin.create_firewall = create_firewall
|
|
|
|
|
|
# Monkey patching l3_db's _get_router_for_floatingip method to associate
|
|
# floatingip if corresponding routes is present.
|
|
def _is_net_reachable_from_net(self, context, tenant_id, from_net_id,
|
|
to_net_id):
|
|
"""Check whether a network is reachable.
|
|
|
|
Follow the paths of networks connected by devices, to determine
|
|
whether a network is reachable from another.
|
|
@param context: neutron api request context
|
|
@param tenant_id: the owning tenant
|
|
@param from_net_id: the source network for the search
|
|
@param to_net_id: the destination network for the search
|
|
@return: True or False whether a path exists
|
|
"""
|
|
original_context = context
|
|
context = elevate_context(context)
|
|
tenant_id = context.tenant_id
|
|
|
|
def nexthop_nets_query(nets, visited):
|
|
"""query networks connected to devices on nets but not visited."""
|
|
Port = models_v2.Port
|
|
devices_on_nets = context.session.query(Port.device_id).filter(
|
|
Port.tenant_id == tenant_id,
|
|
Port.device_owner.notin_([nlib_const.DEVICE_OWNER_DHCP]),
|
|
Port.network_id.in_(nets)).subquery()
|
|
return context.session.query(Port.network_id).filter(
|
|
Port.tenant_id == tenant_id,
|
|
Port.network_id.notin_(visited),
|
|
Port.device_id.in_(devices_on_nets))
|
|
visited = set([])
|
|
nets = set([from_net_id])
|
|
while nets:
|
|
if to_net_id in nets:
|
|
context = original_context
|
|
return True
|
|
visited |= nets
|
|
nets = set((tup[0] for tup in nexthop_nets_query(nets, visited)))
|
|
context = original_context
|
|
return False
|
|
|
|
|
|
def _find_net_for_nexthop(self, context, tenant_id, router_id, nexthop):
|
|
"""Find the network to which the nexthop belongs.
|
|
|
|
Iterate over the router interfaces to find the network of nexthop.
|
|
@param context: neutron api request context
|
|
@param tenant_id: the owning tenant
|
|
@param router_id: a router id
|
|
@param nexthop: an IP address
|
|
@return: the network id of the nexthop or None if not found
|
|
"""
|
|
interfaces = context.session.query(models_v2.Port).filter_by(
|
|
tenant_id=tenant_id,
|
|
device_id=router_id,
|
|
device_owner=DEVICE_OWNER_ROUTER_INTF)
|
|
for interface in interfaces:
|
|
cidrs = [self._core_plugin._get_subnet(context,
|
|
ip['subnet_id'])['cidr']
|
|
for ip in interface['fixed_ips']]
|
|
if netaddr.all_matching_cidrs(nexthop, cidrs):
|
|
return interface['network_id']
|
|
|
|
|
|
def _find_routers_via_routes_for_floatingip(self, context, internal_port,
|
|
internal_subnet_id,
|
|
external_network_id):
|
|
"""Find routers with route to the internal IP address.
|
|
|
|
Iterate over the routers that belong to the same tenant as
|
|
'internal_port'. For each router check that the router is connected
|
|
to the external network and whether there is a route to the internal
|
|
IP address. Consider only routers for which there is a path from the
|
|
nexthop of the route to the internal port.
|
|
|
|
Sort the list of routers to have the router with the most specific
|
|
route first (largest CIDR prefix mask length).
|
|
|
|
@param context: neutron api request context
|
|
@param internal_port: the port dict for the association
|
|
@param internal_subnet_id: the subnet for the association
|
|
@param external_network_id: the network of the floatingip
|
|
@return: a sorted list of matching routers
|
|
"""
|
|
original_context = context
|
|
context = elevate_context(context)
|
|
internal_ip_address = [
|
|
ip['ip_address'] for ip in internal_port['fixed_ips']
|
|
if ip['subnet_id'] == internal_subnet_id
|
|
][0]
|
|
|
|
# find the tenant routers
|
|
tenant_id = internal_port['tenant_id']
|
|
routers = self.get_routers(context, filters={'tenant_id': [tenant_id]})
|
|
|
|
prefix_routers = []
|
|
for router in routers:
|
|
# verify that the router is on "external_network"
|
|
gw_info = router.get(EXTERNAL_GW_INFO)
|
|
if not gw_info or gw_info['network_id'] != external_network_id:
|
|
continue
|
|
# find a matching route
|
|
if 'routes' not in router:
|
|
continue
|
|
cidr_nexthops = {}
|
|
for route in router['routes']:
|
|
cidr = netaddr.IPNetwork(route['destination'])
|
|
if cidr not in cidr_nexthops:
|
|
cidr_nexthops[cidr] = []
|
|
cidr_nexthops[cidr].append(route['nexthop'])
|
|
smallest_cidr = netaddr.smallest_matching_cidr(
|
|
internal_ip_address,
|
|
cidr_nexthops.keys())
|
|
if not smallest_cidr:
|
|
continue
|
|
# validate that there exists a path to "internal_port"
|
|
for nexthop in cidr_nexthops[smallest_cidr]:
|
|
net_id = self._find_net_for_nexthop(context, context.tenant_id,
|
|
router['id'], nexthop)
|
|
if net_id and self._is_net_reachable_from_net(
|
|
context,
|
|
context.tenant_id,
|
|
net_id,
|
|
internal_port['network_id']):
|
|
prefix_routers.append(
|
|
(smallest_cidr.prefixlen, router['id']))
|
|
break
|
|
context = original_context
|
|
return [p_r[1] for p_r in sorted(prefix_routers, reverse=True)]
|
|
|
|
|
|
def elevate_context(context):
|
|
context = context.elevated()
|
|
context.tenant_id = _resource_owner_tenant_id()
|
|
return context
|
|
|
|
|
|
def _resource_owner_tenant_id():
|
|
user, pwd, tenant, auth_url = utils.get_keystone_creds()
|
|
keystoneclient = keyclient.Client(username=user, password=pwd,
|
|
auth_url=auth_url)
|
|
try:
|
|
tenant = keystoneclient.tenants.find(name=tenant)
|
|
return tenant.id
|
|
except k_exceptions.NotFound:
|
|
with excutils.save_and_reraise_exception(reraise=True):
|
|
LOG.error('No tenant with name %s exists.', tenant)
|
|
except k_exceptions.NoUniqueMatch:
|
|
with excutils.save_and_reraise_exception(reraise=True):
|
|
LOG.error('Multiple tenants matches found for %s', tenant)
|
|
|
|
|
|
def _get_router_for_floatingip(self, context, internal_port,
|
|
internal_subnet_id,
|
|
external_network_id):
|
|
subnet = self._core_plugin.get_subnet(context, internal_subnet_id)
|
|
if not subnet['gateway_ip']:
|
|
msg = (_('Cannot add floating IP to port on subnet %s '
|
|
'which has no gateway_ip') % internal_subnet_id)
|
|
raise n_exc.BadRequest(resource='floatingip', msg=msg)
|
|
|
|
# Find routers(with router_id and interface address) that
|
|
# connect given internal subnet and the external network.
|
|
# Among them, if the router's interface address matches
|
|
# with subnet's gateway-ip, return that router.
|
|
# Otherwise return the first router.
|
|
gw_port = orm.aliased(models_v2.Port, name="gw_port")
|
|
routerport_qry = context.session.query(
|
|
RouterPort.router_id, models_v2.IPAllocation.ip_address).join(
|
|
models_v2.Port, models_v2.IPAllocation).filter(
|
|
models_v2.Port.network_id == internal_port['network_id'],
|
|
RouterPort.port_type.in_(nlib_const.ROUTER_INTERFACE_OWNERS),
|
|
models_v2.IPAllocation.subnet_id == internal_subnet_id
|
|
).join(gw_port, gw_port.device_id == RouterPort.router_id).filter(
|
|
gw_port.network_id == external_network_id).distinct()
|
|
|
|
first_router_id = None
|
|
for router_id, interface_ip in routerport_qry:
|
|
if interface_ip == subnet['gateway_ip']:
|
|
return router_id
|
|
if not first_router_id:
|
|
first_router_id = router_id
|
|
if first_router_id:
|
|
return first_router_id
|
|
|
|
router_ids = self._find_routers_via_routes_for_floatingip(
|
|
context,
|
|
internal_port,
|
|
internal_subnet_id,
|
|
external_network_id)
|
|
if router_ids:
|
|
return router_ids[0]
|
|
|
|
raise l3.ExternalGatewayForFloatingIPNotFound(
|
|
subnet_id=internal_subnet_id,
|
|
external_network_id=external_network_id,
|
|
port_id=internal_port['id'])
|
|
|
|
|
|
l3_db.L3_NAT_dbonly_mixin._get_router_for_floatingip = (
|
|
_get_router_for_floatingip)
|
|
l3_db.L3_NAT_dbonly_mixin._find_routers_via_routes_for_floatingip = (
|
|
_find_routers_via_routes_for_floatingip)
|
|
l3_db.L3_NAT_dbonly_mixin._find_net_for_nexthop = _find_net_for_nexthop
|
|
l3_db.L3_NAT_dbonly_mixin._is_net_reachable_from_net = (
|
|
_is_net_reachable_from_net)
|