# (c) Copyright 2016 IBM Corporation, All Rights Reserved. # (c) Copyright 2023 SysEleven GmbH # 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.models import l3 as l3_models from neutron.db import models_v2 from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources from neutron_lib import constants as lib_constants from neutron_lib.db import api as db_api from neutron_lib.db import model_base from neutron_lib.db import model_query from neutron_lib import exceptions as n_exc from neutron_lib.plugins import constants as plugin_const from neutron_lib.plugins import directory from oslo_log import log as logging from oslo_utils import uuidutils import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.orm import exc from neutron_vpnaas._i18n import _ from neutron_vpnaas.services.vpn.common import constants as v_constants LOG = logging.getLogger(__name__) class RouterIsNotVPNExternal(n_exc.BadRequest): message = _("Router %(router_id)s has no VPN external network gateway set") class RouterHasVPNExternal(n_exc.BadRequest): message = _( "Router %(router_id)s already has VPN external network gateway") class VPNNetworkInUse(n_exc.NetworkInUse): message = _("Network %(network_id)s is used by VPN service") class VPNExtGW(model_base.BASEV2, model_base.HasId, model_base.HasProject): __tablename__ = 'vpn_ext_gws' router_id = sa.Column(sa.String(36), sa.ForeignKey('routers.id'), nullable=False, unique=True) status = sa.Column(sa.String(16), nullable=False) gw_port_id = sa.Column( sa.String(36), sa.ForeignKey('ports.id', ondelete='SET NULL')) transit_port_id = sa.Column( sa.String(36), sa.ForeignKey('ports.id', ondelete='SET NULL')) transit_network_id = sa.Column( sa.String(36), sa.ForeignKey('networks.id', ondelete='SET NULL')) transit_subnet_id = sa.Column( sa.String(36), sa.ForeignKey('subnets.id', ondelete='SET NULL')) gw_port = orm.relationship(models_v2.Port, lazy='joined', foreign_keys=[gw_port_id]) transit_port = orm.relationship(models_v2.Port, lazy='joined', foreign_keys=[transit_port_id]) transit_network = orm.relationship(models_v2.Network) transit_subnet = orm.relationship(models_v2.Subnet) router = orm.relationship(l3_models.Router) @registry.has_registry_receivers class VPNExtGWPlugin_db(object): """DB class to support vpn external ports configuration.""" @property def _core_plugin(self): return directory.get_plugin() @property def _vpn_plugin(self): return directory.get_plugin(plugin_const.VPN) @staticmethod @registry.receives(resources.PORT, [events.BEFORE_DELETE]) def _prevent_vpn_port_delete_callback(resource, event, trigger, payload=None): vpn_plugin = directory.get_plugin(plugin_const.VPN) if vpn_plugin: vpn_plugin.prevent_vpn_port_deletion(payload.context, payload.resource_id) @db_api.CONTEXT_READER def _id_used(self, context, id_column, resource_id): return context.session.query(VPNExtGW).filter( sa.and_( id_column == resource_id, VPNExtGW.status != lib_constants.PENDING_DELETE ) ).count() > 0 def prevent_vpn_port_deletion(self, context, port_id): """Checks to make sure a port is allowed to be deleted. Raises an exception if this is not the case. This should be called by any plugin when the API requests the deletion of a port, since some ports for L3 are not intended to be deleted directly via a DELETE to /ports, but rather via other API calls that perform the proper deletion checks. """ try: port = self._core_plugin.get_port(context, port_id) except n_exc.PortNotFound: # non-existent ports don't need to be protected from deletion return port_id_column = { v_constants.DEVICE_OWNER_VPN_ROUTER_GW: VPNExtGW.gw_port_id, v_constants.DEVICE_OWNER_TRANSIT_NETWORK: VPNExtGW.transit_port_id, }.get(port['device_owner']) if not port_id_column: # This is not a VPN port return if self._id_used(context, port_id_column, port_id): reason = _('has device owner %s') % port['device_owner'] raise n_exc.ServicePortInUse(port_id=port['id'], reason=reason) @staticmethod @registry.receives(resources.SUBNET, [events.BEFORE_DELETE]) def _prevent_vpn_subnet_delete_callback(resource, event, trigger, payload=None): vpn_plugin = directory.get_plugin(plugin_const.VPN) if vpn_plugin: vpn_plugin.prevent_vpn_subnet_deletion(payload.context, payload.resource_id) def prevent_vpn_subnet_deletion(self, context, subnet_id): if self._id_used(context, VPNExtGW.transit_subnet_id, subnet_id): reason = _('Subnet is used by VPN service') raise n_exc.SubnetInUse(subnet_id=subnet_id, reason=reason) @staticmethod @registry.receives(resources.NETWORK, [events.BEFORE_DELETE]) def _prevent_vpn_network_delete_callback(resource, event, trigger, payload=None): vpn_plugin = directory.get_plugin(plugin_const.VPN) if vpn_plugin: vpn_plugin.prevent_vpn_network_deletion(payload.context, payload.resource_id) def prevent_vpn_network_deletion(self, context, network_id): if self._id_used(context, VPNExtGW.transit_network_id, network_id): raise VPNNetworkInUse(network_id=network_id) def _make_vpn_ext_gw_dict(self, gateway_db): if not gateway_db: return None gateway = { 'id': gateway_db['id'], 'tenant_id': gateway_db['tenant_id'], 'router_id': gateway_db['router_id'], 'status': gateway_db['status'], } if gateway_db.gw_port: gateway['network_id'] = gateway_db.gw_port['network_id'] gateway['external_fixed_ips'] = [ {'subnet_id': ip["subnet_id"], 'ip_address': ip["ip_address"]} for ip in gateway_db.gw_port['fixed_ips'] ] for key in ('gw_port_id', 'transit_port_id', 'transit_network_id', 'transit_subnet_id'): value = gateway_db.get(key) if value: gateway[key] = value return gateway def _get_vpn_gw_by_router_id(self, context, router_id): try: gateway_db = context.session.query(VPNExtGW).filter( VPNExtGW.router_id == router_id).one() except exc.NoResultFound: return None return gateway_db @db_api.CONTEXT_READER def get_vpn_gw_by_router_id(self, context, router_id): return self._get_vpn_gw_by_router_id(context, router_id) @db_api.CONTEXT_READER def get_vpn_gw_dict_by_router_id(self, context, router_id, refresh=False): gateway_db = self._get_vpn_gw_by_router_id(context, router_id) if gateway_db and refresh: context.session.refresh(gateway_db) return self._make_vpn_ext_gw_dict(gateway_db) def create_gateway(self, context, gateway): info = gateway['gateway'] with db_api.CONTEXT_WRITER.using(context): gateway_db = VPNExtGW( id=uuidutils.generate_uuid(), tenant_id=info['tenant_id'], router_id=info['router_id'], status=lib_constants.PENDING_CREATE, gw_port_id=info.get('gw_port_id'), transit_port_id=info.get('transit_port_id'), transit_network_id=info.get('transit_network_id'), transit_subnet_id=info.get('transit_subnet_id')) context.session.add(gateway_db) return self._make_vpn_ext_gw_dict(gateway_db) def update_gateway(self, context, gateway_id, gateway): info = gateway['gateway'] with db_api.CONTEXT_WRITER.using(context): gateway_db = model_query.get_by_id(context, VPNExtGW, gateway_id) gateway_db.update(info) return self._make_vpn_ext_gw_dict(gateway_db) def delete_gateway(self, context, gateway_id): with db_api.CONTEXT_WRITER.using(context): query = context.session.query(VPNExtGW) return query.filter(VPNExtGW.id == gateway_id).delete()