#    (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
#    (c) Copyright 2015 Cisco Systems Inc.
#    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 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_query
from neutron_lib.db import utils as db_utils
from neutron_lib.exceptions import l3 as l3_exception
from neutron_lib.exceptions import vpn as vpn_exception
from neutron_lib.plugins import constants as p_constants
from neutron_lib.plugins import directory
from neutron_lib.plugins import utils
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import uuidutils
import sqlalchemy as sa
from sqlalchemy.orm import exc


from neutron_vpnaas.db.vpn import vpn_models
from neutron_vpnaas.db.vpn import vpn_validator
from neutron_vpnaas.extensions import vpn_endpoint_groups
from neutron_vpnaas.extensions import vpnaas
from neutron_vpnaas.services.vpn.common import constants as v_constants

LOG = logging.getLogger(__name__)


class VPNPluginDb(vpnaas.VPNPluginBase,
                  vpn_endpoint_groups.VPNEndpointGroupsPluginBase):
    """VPN plugin database class using SQLAlchemy models."""

    def _get_validator(self):
        """Obtain validator to use for attribute validation.

        Subclasses may override this with a different validator, as needed.
        Note: some UTs will directly create a VPNPluginDb object and then
        call its methods, instead of creating a VPNDriverPlugin, which
        will have a service driver associated that will provide a
        validator object. As a result, we use the reference validator here.
        """
        return vpn_validator.VpnReferenceValidator()

    def update_status(self, context, model, v_id, status):
        with db_api.CONTEXT_WRITER.using(context):
            v_db = self._get_resource(context, model, v_id)
            v_db.update({'status': status})

    def _get_resource(self, context, model, v_id):
        try:
            r = model_query.get_by_id(context, model, v_id)
        except exc.NoResultFound:
            with excutils.save_and_reraise_exception(reraise=False) as ctx:
                if issubclass(model, vpn_models.IPsecSiteConnection):
                    raise vpn_exception.IPsecSiteConnectionNotFound(
                        ipsec_site_conn_id=v_id
                    )
                if issubclass(model, vpn_models.IKEPolicy):
                    raise vpn_exception.IKEPolicyNotFound(ikepolicy_id=v_id)
                if issubclass(model, vpn_models.IPsecPolicy):
                    raise vpn_exception.IPsecPolicyNotFound(
                        ipsecpolicy_id=v_id)
                if issubclass(model, vpn_models.VPNService):
                    raise vpn_exception.VPNServiceNotFound(vpnservice_id=v_id)
                if issubclass(model, vpn_models.VPNEndpointGroup):
                    raise vpn_exception.VPNEndpointGroupNotFound(
                        endpoint_group_id=v_id)
                ctx.reraise = True
        return r

    def assert_update_allowed(self, obj):
        status = getattr(obj, 'status', None)
        _id = getattr(obj, 'id', None)
        if utils.in_pending_status(status):
            raise vpn_exception.VPNStateInvalidToUpdate(id=_id, state=status)

    def _make_ipsec_site_connection_dict(self, ipsec_site_conn, fields=None):

        res = {'id': ipsec_site_conn['id'],
               'tenant_id': ipsec_site_conn['tenant_id'],
               'name': ipsec_site_conn['name'],
               'description': ipsec_site_conn['description'],
               'peer_address': ipsec_site_conn['peer_address'],
               'peer_id': ipsec_site_conn['peer_id'],
               'local_id': ipsec_site_conn['local_id'],
               'route_mode': ipsec_site_conn['route_mode'],
               'mtu': ipsec_site_conn['mtu'],
               'auth_mode': ipsec_site_conn['auth_mode'],
               'psk': ipsec_site_conn['psk'],
               'initiator': ipsec_site_conn['initiator'],
               'dpd': {
                   'action': ipsec_site_conn['dpd_action'],
                   'interval': ipsec_site_conn['dpd_interval'],
                   'timeout': ipsec_site_conn['dpd_timeout']
               },
               'admin_state_up': ipsec_site_conn['admin_state_up'],
               'status': ipsec_site_conn['status'],
               'vpnservice_id': ipsec_site_conn['vpnservice_id'],
               'ikepolicy_id': ipsec_site_conn['ikepolicy_id'],
               'ipsecpolicy_id': ipsec_site_conn['ipsecpolicy_id'],
               'peer_cidrs': [pcidr['cidr']
                              for pcidr in ipsec_site_conn['peer_cidrs']],
               'local_ep_group_id': ipsec_site_conn['local_ep_group_id'],
               'peer_ep_group_id': ipsec_site_conn['peer_ep_group_id'],
               }

        return db_utils.resource_fields(res, fields)

    def get_endpoint_info(self, context, ipsec_sitecon):
        """Obtain all endpoint info, and store in connection for validation."""
        ipsec_sitecon['local_epg_subnets'] = self.get_endpoint_group(
            context, ipsec_sitecon['local_ep_group_id'])
        ipsec_sitecon['peer_epg_cidrs'] = self.get_endpoint_group(
            context, ipsec_sitecon['peer_ep_group_id'])

    def validate_connection_info(self, context, validator, ipsec_sitecon,
                                 vpnservice):
        """Collect info and validate connection.

        If endpoint groups used (default), collect the group info and
        do not specify the IP version (as it will come from endpoints).
        Otherwise, get the IP version from the (legacy) subnet for
        validation purposes.

        NOTE: Once the deprecated subnet is removed, the caller can just
        call get_endpoint_info() and validate_ipsec_site_connection().
        """
        if ipsec_sitecon['local_ep_group_id']:
            self.get_endpoint_info(context, ipsec_sitecon)
            ip_version = None
        else:
            ip_version = vpnservice.subnet.ip_version
        validator.validate_ipsec_site_connection(context, ipsec_sitecon,
                                                 ip_version, vpnservice)

    def create_ipsec_site_connection(self, context, ipsec_site_connection):
        ipsec_sitecon = ipsec_site_connection['ipsec_site_connection']
        validator = self._get_validator()
        validator.assign_sensible_ipsec_sitecon_defaults(ipsec_sitecon)
        with db_api.CONTEXT_WRITER.using(context):
            #Check permissions
            vpnservice_id = ipsec_sitecon['vpnservice_id']
            self._get_resource(context, vpn_models.VPNService, vpnservice_id)
            self._get_resource(context, vpn_models.IKEPolicy,
                               ipsec_sitecon['ikepolicy_id'])
            self._get_resource(context, vpn_models.IPsecPolicy,
                               ipsec_sitecon['ipsecpolicy_id'])
            vpnservice = self._get_vpnservice(context, vpnservice_id)
            validator.validate_ipsec_conn_optional_args(ipsec_sitecon,
                                                        vpnservice.subnet)
            self.validate_connection_info(context, validator, ipsec_sitecon,
                                          vpnservice)
            validator.resolve_peer_address(ipsec_sitecon, vpnservice.router)

            ipsec_site_conn_db = vpn_models.IPsecSiteConnection(
                id=uuidutils.generate_uuid(),
                tenant_id=ipsec_sitecon['tenant_id'],
                name=ipsec_sitecon['name'],
                description=ipsec_sitecon['description'],
                peer_address=ipsec_sitecon['peer_address'],
                peer_id=ipsec_sitecon['peer_id'],
                local_id=ipsec_sitecon['local_id'],
                route_mode='static',
                mtu=ipsec_sitecon['mtu'],
                auth_mode='psk',
                psk=ipsec_sitecon['psk'],
                initiator=ipsec_sitecon['initiator'],
                dpd_action=ipsec_sitecon['dpd_action'],
                dpd_interval=ipsec_sitecon['dpd_interval'],
                dpd_timeout=ipsec_sitecon['dpd_timeout'],
                admin_state_up=ipsec_sitecon['admin_state_up'],
                status=lib_constants.PENDING_CREATE,
                vpnservice_id=vpnservice_id,
                ikepolicy_id=ipsec_sitecon['ikepolicy_id'],
                ipsecpolicy_id=ipsec_sitecon['ipsecpolicy_id'],
                local_ep_group_id=ipsec_sitecon['local_ep_group_id'],
                peer_ep_group_id=ipsec_sitecon['peer_ep_group_id']
            )
            context.session.add(ipsec_site_conn_db)
            for cidr in ipsec_sitecon['peer_cidrs']:
                peer_cidr_db = vpn_models.IPsecPeerCidr(
                    cidr=cidr,
                    ipsec_site_connection_id=ipsec_site_conn_db.id
                )
                context.session.add(peer_cidr_db)
        return self._make_ipsec_site_connection_dict(ipsec_site_conn_db)

    def update_ipsec_site_connection(
            self, context,
            ipsec_site_conn_id, ipsec_site_connection):
        ipsec_sitecon = ipsec_site_connection['ipsec_site_connection']
        changed_peer_cidrs = False
        validator = self._get_validator()
        with db_api.CONTEXT_WRITER.using(context):
            ipsec_site_conn_db = self._get_resource(
                context, vpn_models.IPsecSiteConnection, ipsec_site_conn_id)
            vpnservice_id = ipsec_site_conn_db['vpnservice_id']
            vpnservice = self._get_vpnservice(context, vpnservice_id)

            validator.assign_sensible_ipsec_sitecon_defaults(
                ipsec_sitecon, ipsec_site_conn_db)
            validator.validate_ipsec_conn_optional_args(ipsec_sitecon,
                                                        vpnservice.subnet)
            self.validate_connection_info(context, validator, ipsec_sitecon,
                                          vpnservice)
            if 'peer_address' in ipsec_sitecon:
                validator.resolve_peer_address(ipsec_sitecon,
                                               vpnservice.router)
            self.assert_update_allowed(ipsec_site_conn_db)

            if "peer_cidrs" in ipsec_sitecon:
                changed_peer_cidrs = True
                old_peer_cidr_list = ipsec_site_conn_db['peer_cidrs']
                old_peer_cidr_dict = {
                    peer_cidr['cidr']: peer_cidr
                    for peer_cidr in old_peer_cidr_list}
                new_peer_cidr_set = set(ipsec_sitecon["peer_cidrs"])
                old_peer_cidr_set = set(old_peer_cidr_dict)

                new_peer_cidrs = list(new_peer_cidr_set)
                for peer_cidr in old_peer_cidr_set - new_peer_cidr_set:
                    context.session.delete(old_peer_cidr_dict[peer_cidr])
                for peer_cidr in new_peer_cidr_set - old_peer_cidr_set:
                    pcidr = vpn_models.IPsecPeerCidr(
                        cidr=peer_cidr,
                        ipsec_site_connection_id=ipsec_site_conn_id)
                    context.session.add(pcidr)
            # Note: Unconditionally remove peer_cidrs, as they will be set to
            # previous, if unchanged (to be able to validate above).
            del ipsec_sitecon["peer_cidrs"]
            if ipsec_sitecon:
                ipsec_site_conn_db.update(ipsec_sitecon)
            result = self._make_ipsec_site_connection_dict(ipsec_site_conn_db)
        if changed_peer_cidrs:
            result['peer_cidrs'] = new_peer_cidrs
        return result

    def delete_ipsec_site_connection(self, context, ipsec_site_conn_id):
        with db_api.CONTEXT_WRITER.using(context):
            ipsec_site_conn_db = self._get_resource(
                context, vpn_models.IPsecSiteConnection, ipsec_site_conn_id)
            context.session.delete(ipsec_site_conn_db)

    def _get_ipsec_site_connection(
            self, context, ipsec_site_conn_id):
        return self._get_resource(
            context, vpn_models.IPsecSiteConnection, ipsec_site_conn_id)

    @db_api.CONTEXT_READER
    def get_ipsec_site_connection(self, context,
                                  ipsec_site_conn_id, fields=None):
        ipsec_site_conn_db = self._get_ipsec_site_connection(
            context, ipsec_site_conn_id)
        return self._make_ipsec_site_connection_dict(
            ipsec_site_conn_db, fields)

    @db_api.CONTEXT_READER
    def get_ipsec_site_connections(self, context, filters=None, fields=None):
        return model_query.get_collection(
            context, vpn_models.IPsecSiteConnection,
            self._make_ipsec_site_connection_dict,
            filters=filters, fields=fields)

    def update_ipsec_site_conn_status(self, context, conn_id, new_status):
        with db_api.CONTEXT_WRITER.using(context):
            self._update_connection_status(context, conn_id, new_status, True)

    def _update_connection_status(self, context, conn_id, new_status,
                                  updated_pending):
        """Update the connection status, if changed.

        If the connection is not in a pending state, unconditionally update
        the status. Likewise, if in a pending state, and have an indication
        that the status has changed, then update the database.
        """
        try:
            conn_db = self._get_ipsec_site_connection(context, conn_id)
        except vpn_exception.IPsecSiteConnectionNotFound:
            return
        if not utils.in_pending_status(conn_db.status) or updated_pending:
            conn_db.status = new_status

    def _make_ikepolicy_dict(self, ikepolicy, fields=None):
        res = {'id': ikepolicy['id'],
               'tenant_id': ikepolicy['tenant_id'],
               'name': ikepolicy['name'],
               'description': ikepolicy['description'],
               'auth_algorithm': ikepolicy['auth_algorithm'],
               'encryption_algorithm': ikepolicy['encryption_algorithm'],
               'phase1_negotiation_mode': ikepolicy['phase1_negotiation_mode'],
               'lifetime': {
                   'units': ikepolicy['lifetime_units'],
                   'value': ikepolicy['lifetime_value'],
               },
               'ike_version': ikepolicy['ike_version'],
               'pfs': ikepolicy['pfs']
               }

        return db_utils.resource_fields(res, fields)

    def create_ikepolicy(self, context, ikepolicy):
        ike = ikepolicy['ikepolicy']
        validator = self._get_validator()
        lifetime_info = ike['lifetime']
        lifetime_units = lifetime_info.get('units', 'seconds')
        lifetime_value = lifetime_info.get('value', 3600)

        with db_api.CONTEXT_WRITER.using(context):
            validator.validate_ike_policy(context, ike)
            ike_db = vpn_models.IKEPolicy(
                id=uuidutils.generate_uuid(),
                tenant_id=ike['tenant_id'],
                name=ike['name'],
                description=ike['description'],
                auth_algorithm=ike['auth_algorithm'],
                encryption_algorithm=ike['encryption_algorithm'],
                phase1_negotiation_mode=ike['phase1_negotiation_mode'],
                lifetime_units=lifetime_units,
                lifetime_value=lifetime_value,
                ike_version=ike['ike_version'],
                pfs=ike['pfs']
            )

            context.session.add(ike_db)
        return self._make_ikepolicy_dict(ike_db)

    def update_ikepolicy(self, context, ikepolicy_id, ikepolicy):
        ike = ikepolicy['ikepolicy']
        validator = self._get_validator()
        with db_api.CONTEXT_WRITER.using(context):
            validator.validate_ike_policy(context, ike)
            if context.session.query(vpn_models.IPsecSiteConnection).filter_by(
                    ikepolicy_id=ikepolicy_id).first():
                raise vpn_exception.IKEPolicyInUse(ikepolicy_id=ikepolicy_id)
            ike_db = self._get_resource(
                context, vpn_models.IKEPolicy, ikepolicy_id)
            if ike:
                lifetime_info = ike.get('lifetime')
                if lifetime_info:
                    if lifetime_info.get('units'):
                        ike['lifetime_units'] = lifetime_info['units']
                    if lifetime_info.get('value'):
                        ike['lifetime_value'] = lifetime_info['value']
                ike_db.update(ike)
        return self._make_ikepolicy_dict(ike_db)

    def delete_ikepolicy(self, context, ikepolicy_id):
        with db_api.CONTEXT_WRITER.using(context):
            if context.session.query(vpn_models.IPsecSiteConnection).filter_by(
                    ikepolicy_id=ikepolicy_id).first():
                raise vpn_exception.IKEPolicyInUse(ikepolicy_id=ikepolicy_id)
            ike_db = self._get_resource(
                context, vpn_models.IKEPolicy, ikepolicy_id)
            context.session.delete(ike_db)

    @db_api.CONTEXT_READER
    def get_ikepolicy(self, context, ikepolicy_id, fields=None):
        ike_db = self._get_resource(
            context, vpn_models.IKEPolicy, ikepolicy_id)
        return self._make_ikepolicy_dict(ike_db, fields)

    @db_api.CONTEXT_READER
    def get_ikepolicies(self, context, filters=None, fields=None):
        return model_query.get_collection(context, vpn_models.IKEPolicy,
                                          self._make_ikepolicy_dict,
                                          filters=filters, fields=fields)

    def _make_ipsecpolicy_dict(self, ipsecpolicy, fields=None):

        res = {'id': ipsecpolicy['id'],
               'tenant_id': ipsecpolicy['tenant_id'],
               'name': ipsecpolicy['name'],
               'description': ipsecpolicy['description'],
               'transform_protocol': ipsecpolicy['transform_protocol'],
               'auth_algorithm': ipsecpolicy['auth_algorithm'],
               'encryption_algorithm': ipsecpolicy['encryption_algorithm'],
               'encapsulation_mode': ipsecpolicy['encapsulation_mode'],
               'lifetime': {
                   'units': ipsecpolicy['lifetime_units'],
                   'value': ipsecpolicy['lifetime_value'],
               },
               'pfs': ipsecpolicy['pfs']
               }

        return db_utils.resource_fields(res, fields)

    def create_ipsecpolicy(self, context, ipsecpolicy):
        ipsecp = ipsecpolicy['ipsecpolicy']
        validator = self._get_validator()
        lifetime_info = ipsecp['lifetime']
        lifetime_units = lifetime_info.get('units', 'seconds')
        lifetime_value = lifetime_info.get('value', 3600)

        with db_api.CONTEXT_WRITER.using(context):
            validator.validate_ipsec_policy(context, ipsecp)
            ipsecp_db = vpn_models.IPsecPolicy(
                id=uuidutils.generate_uuid(),
                tenant_id=ipsecp['tenant_id'],
                name=ipsecp['name'],
                description=ipsecp['description'],
                transform_protocol=ipsecp['transform_protocol'],
                auth_algorithm=ipsecp['auth_algorithm'],
                encryption_algorithm=ipsecp['encryption_algorithm'],
                encapsulation_mode=ipsecp['encapsulation_mode'],
                lifetime_units=lifetime_units,
                lifetime_value=lifetime_value,
                pfs=ipsecp['pfs'])
            context.session.add(ipsecp_db)
        return self._make_ipsecpolicy_dict(ipsecp_db)

    def update_ipsecpolicy(self, context, ipsecpolicy_id, ipsecpolicy):
        ipsecp = ipsecpolicy['ipsecpolicy']
        validator = self._get_validator()
        with db_api.CONTEXT_WRITER.using(context):
            validator.validate_ipsec_policy(context, ipsecp)
            if context.session.query(vpn_models.IPsecSiteConnection).filter_by(
                    ipsecpolicy_id=ipsecpolicy_id).first():
                raise vpn_exception.IPsecPolicyInUse(
                    ipsecpolicy_id=ipsecpolicy_id)
            ipsecp_db = self._get_resource(
                context, vpn_models.IPsecPolicy, ipsecpolicy_id)
            if ipsecp:
                lifetime_info = ipsecp.get('lifetime')
                if lifetime_info:
                    if lifetime_info.get('units'):
                        ipsecp['lifetime_units'] = lifetime_info['units']
                    if lifetime_info.get('value'):
                        ipsecp['lifetime_value'] = lifetime_info['value']
                ipsecp_db.update(ipsecp)
        return self._make_ipsecpolicy_dict(ipsecp_db)

    def delete_ipsecpolicy(self, context, ipsecpolicy_id):
        with db_api.CONTEXT_WRITER.using(context):
            if context.session.query(vpn_models.IPsecSiteConnection).filter_by(
                    ipsecpolicy_id=ipsecpolicy_id).first():
                raise vpn_exception.IPsecPolicyInUse(
                    ipsecpolicy_id=ipsecpolicy_id)
            ipsec_db = self._get_resource(
                context, vpn_models.IPsecPolicy, ipsecpolicy_id)
            context.session.delete(ipsec_db)

    @db_api.CONTEXT_READER
    def get_ipsecpolicy(self, context, ipsecpolicy_id, fields=None):
        ipsec_db = self._get_resource(
            context, vpn_models.IPsecPolicy, ipsecpolicy_id)
        return self._make_ipsecpolicy_dict(ipsec_db, fields)

    @db_api.CONTEXT_READER
    def get_ipsecpolicies(self, context, filters=None, fields=None):
        return model_query.get_collection(context, vpn_models.IPsecPolicy,
                                          self._make_ipsecpolicy_dict,
                                          filters=filters, fields=fields)

    def _make_vpnservice_dict(self, vpnservice, fields=None):
        res = {'id': vpnservice['id'],
               'name': vpnservice['name'],
               'description': vpnservice['description'],
               'tenant_id': vpnservice['tenant_id'],
               'subnet_id': vpnservice['subnet_id'],
               'router_id': vpnservice['router_id'],
               'flavor_id': vpnservice['flavor_id'],
               'admin_state_up': vpnservice['admin_state_up'],
               'external_v4_ip': vpnservice['external_v4_ip'],
               'external_v6_ip': vpnservice['external_v6_ip'],
               'status': vpnservice['status']}
        return db_utils.resource_fields(res, fields)

    def create_vpnservice(self, context, vpnservice):
        vpns = vpnservice['vpnservice']
        flavor_id = vpns.get('flavor_id', None)
        validator = self._get_validator()
        with db_api.CONTEXT_WRITER.using(context):
            validator.validate_vpnservice(context, vpns)
            vpnservice_db = vpn_models.VPNService(
                id=uuidutils.generate_uuid(),
                tenant_id=vpns['tenant_id'],
                name=vpns['name'],
                description=vpns['description'],
                subnet_id=vpns['subnet_id'],
                router_id=vpns['router_id'],
                flavor_id=flavor_id,
                admin_state_up=vpns['admin_state_up'],
                status=lib_constants.PENDING_CREATE)
            context.session.add(vpnservice_db)
        return self._make_vpnservice_dict(vpnservice_db)

    def set_external_tunnel_ips(self, context, vpnservice_id, v4_ip=None,
                                v6_ip=None):
        """Update the external tunnel IP(s) for service."""
        vpns = {'external_v4_ip': v4_ip, 'external_v6_ip': v6_ip}
        with db_api.CONTEXT_WRITER.using(context):
            vpns_db = self._get_resource(context, vpn_models.VPNService,
                                         vpnservice_id)
            vpns_db.update(vpns)
        return self._make_vpnservice_dict(vpns_db)

    def set_vpnservice_status(self, context, vpnservice_id, status,
                              updated_pending_status=False):
        vpns = {'status': status}
        with db_api.CONTEXT_WRITER.using(context):
            vpns_db = self._get_resource(context, vpn_models.VPNService,
                                         vpnservice_id)
            if (utils.in_pending_status(vpns_db.status) and
                    not updated_pending_status):
                raise vpnaas.VPNStateInvalidToUpdate(
                    id=vpnservice_id, state=vpns_db.status)
            vpns_db.update(vpns)
        return self._make_vpnservice_dict(vpns_db)

    def update_vpnservice(self, context, vpnservice_id, vpnservice):
        vpns = vpnservice['vpnservice']
        with db_api.CONTEXT_WRITER.using(context):
            vpns_db = self._get_resource(context, vpn_models.VPNService,
                                         vpnservice_id)
            self.assert_update_allowed(vpns_db)
            if vpns:
                vpns_db.update(vpns)
        return self._make_vpnservice_dict(vpns_db)

    def delete_vpnservice(self, context, vpnservice_id):
        with db_api.CONTEXT_WRITER.using(context):
            if context.session.query(vpn_models.IPsecSiteConnection).filter_by(
                vpnservice_id=vpnservice_id
            ).first():
                raise vpn_exception.VPNServiceInUse(
                    vpnservice_id=vpnservice_id)
            vpns_db = self._get_resource(context, vpn_models.VPNService,
                                         vpnservice_id)
            context.session.delete(vpns_db)

    @db_api.CONTEXT_READER
    def _get_vpnservice(self, context, vpnservice_id):
        return self._get_resource(context, vpn_models.VPNService,
                                  vpnservice_id)

    @db_api.CONTEXT_READER
    def get_vpnservice(self, context, vpnservice_id, fields=None):
        vpns_db = self._get_resource(context, vpn_models.VPNService,
                                     vpnservice_id)
        return self._make_vpnservice_dict(vpns_db, fields)

    @db_api.CONTEXT_READER
    def get_vpnservices(self, context, filters=None, fields=None):
        return model_query.get_collection(context, vpn_models.VPNService,
                                          self._make_vpnservice_dict,
                                          filters=filters, fields=fields)

    def check_router_in_use(self, context, router_id):
        vpnservices = self.get_vpnservices(
            context, filters={'router_id': [router_id]})
        if vpnservices:
            plural = "s" if len(vpnservices) > 1 else ""
            services = ",".join([v['id'] for v in vpnservices])
            raise l3_exception.RouterInUse(
                router_id=router_id,
                reason="is currently used by VPN service%(plural)s "
                       "(%(services)s)" % {'plural': plural,
                                           'services': services})

    def check_subnet_in_use(self, context, subnet_id, router_id):
        with db_api.CONTEXT_READER.using(context):
            vpnservices = context.session.query(
                vpn_models.VPNService).filter_by(
                    subnet_id=subnet_id, router_id=router_id).first()
            if vpnservices:
                raise vpn_exception.SubnetInUseByVPNService(
                    subnet_id=subnet_id,
                    vpnservice_id=vpnservices['id'])

            query = context.session.query(vpn_models.IPsecSiteConnection)
            query = query.join(
                vpn_models.VPNEndpointGroup,
                vpn_models.VPNEndpointGroup.id ==
                vpn_models.IPsecSiteConnection.local_ep_group_id).filter(
                vpn_models.VPNEndpointGroup.endpoint_type ==
                v_constants.SUBNET_ENDPOINT)
            query = query.join(
                vpn_models.VPNEndpoint,
                vpn_models.VPNEndpoint.endpoint_group_id ==
                vpn_models.IPsecSiteConnection.local_ep_group_id).filter(
                vpn_models.VPNEndpoint.endpoint == subnet_id)
            query = query.join(
                vpn_models.VPNService,
                vpn_models.VPNService.id ==
                vpn_models.IPsecSiteConnection.vpnservice_id).filter(
                vpn_models.VPNService.router_id == router_id)
            connection = query.first()
            if connection:
                raise vpn_exception.SubnetInUseByIPsecSiteConnection(
                    subnet_id=subnet_id,
                    ipsec_site_connection_id=connection['id'])

    def check_subnet_in_use_by_endpoint_group(self, context, subnet_id):
        with db_api.CONTEXT_READER.using(context):
            query = context.session.query(vpn_models.VPNEndpointGroup)
            query = query.filter(vpn_models.VPNEndpointGroup.endpoint_type ==
                                 v_constants.SUBNET_ENDPOINT)
            query = query.join(
                vpn_models.VPNEndpoint,
                sa.and_(vpn_models.VPNEndpoint.endpoint_group_id ==
                        vpn_models.VPNEndpointGroup.id,
                        vpn_models.VPNEndpoint.endpoint == subnet_id))
            group = query.first()
            if group:
                raise vpn_exception.SubnetInUseByEndpointGroup(
                    subnet_id=subnet_id, group_id=group['id'])

    def _make_endpoint_group_dict(self, endpoint_group, fields=None):
        res = {'id': endpoint_group['id'],
               'tenant_id': endpoint_group['tenant_id'],
               'name': endpoint_group['name'],
               'description': endpoint_group['description'],
               'type': endpoint_group['endpoint_type'],
               'endpoints': [ep['endpoint']
                             for ep in endpoint_group['endpoints']]}
        return db_utils.resource_fields(res, fields)

    def create_endpoint_group(self, context, endpoint_group):
        group = endpoint_group['endpoint_group']
        validator = self._get_validator()
        with db_api.CONTEXT_WRITER.using(context):
            validator.validate_endpoint_group(context, group)
            endpoint_group_db = vpn_models.VPNEndpointGroup(
                id=uuidutils.generate_uuid(),
                tenant_id=group['tenant_id'],
                name=group['name'],
                description=group['description'],
                endpoint_type=group['type'])
            context.session.add(endpoint_group_db)
            for endpoint in group['endpoints']:
                endpoint_db = vpn_models.VPNEndpoint(
                    endpoint=endpoint,
                    endpoint_group_id=endpoint_group_db.id
                )
                context.session.add(endpoint_db)
        return self._make_endpoint_group_dict(endpoint_group_db)

    def update_endpoint_group(self, context, endpoint_group_id,
                              endpoint_group):
        group_changes = endpoint_group['endpoint_group']
        # Note: Endpoints cannot be changed, so will not do validation
        with db_api.CONTEXT_WRITER.using(context):
            endpoint_group_db = self._get_resource(context,
                                                   vpn_models.VPNEndpointGroup,
                                                   endpoint_group_id)
            endpoint_group_db.update(group_changes)
        return self._make_endpoint_group_dict(endpoint_group_db)

    def delete_endpoint_group(self, context, endpoint_group_id):
        with db_api.CONTEXT_WRITER.using(context):
            self.check_endpoint_group_not_in_use(context, endpoint_group_id)
            endpoint_group_db = self._get_resource(
                context, vpn_models.VPNEndpointGroup, endpoint_group_id)
            context.session.delete(endpoint_group_db)

    @db_api.CONTEXT_READER
    def get_endpoint_group(self, context, endpoint_group_id, fields=None):
        endpoint_group_db = self._get_resource(
            context, vpn_models.VPNEndpointGroup, endpoint_group_id)
        return self._make_endpoint_group_dict(endpoint_group_db, fields)

    @db_api.CONTEXT_READER
    def get_endpoint_groups(self, context, filters=None, fields=None):
        return model_query.get_collection(context, vpn_models.VPNEndpointGroup,
                                          self._make_endpoint_group_dict,
                                          filters=filters, fields=fields)

    def check_endpoint_group_not_in_use(self, context, group_id):
        query = context.session.query(vpn_models.IPsecSiteConnection)
        query = query.filter(
            sa.or_(
                vpn_models.IPsecSiteConnection.local_ep_group_id == group_id,
                vpn_models.IPsecSiteConnection.peer_ep_group_id == group_id)
        )
        if query.first():
            raise vpn_exception.EndpointGroupInUse(group_id=group_id)

    def get_vpnservice_router_id(self, context, vpnservice_id):
        with db_api.CONTEXT_READER.using(context):
            vpnservice = self._get_vpnservice(context, vpnservice_id)
            return vpnservice['router_id']

    @db_api.CONTEXT_READER
    def get_peer_cidrs_for_router(self, context, router_id):
        filters = {'router_id': [router_id]}
        vpnservices = model_query.get_collection_query(
            context, vpn_models.VPNService, filters=filters).all()
        cidrs = []
        for vpnservice in vpnservices:
            for ipsec_site_connection in vpnservice.ipsec_site_connections:
                if ipsec_site_connection.peer_cidrs:
                    for peer_cidr in ipsec_site_connection.peer_cidrs:
                        cidrs.append(peer_cidr.cidr)
                if ipsec_site_connection.peer_ep_group is not None:
                    for ep in ipsec_site_connection.peer_ep_group.endpoints:
                        cidrs.append(ep.endpoint)
        return cidrs


class VPNPluginRpcDbMixin:
    def _build_local_subnet_cidr_map(self, context):
        """Build a dict of all local endpoint subnets, with list of CIDRs."""
        query = context.session.query(models_v2.Subnet.id,
                                      models_v2.Subnet.cidr)
        query = query.join(vpn_models.VPNEndpoint,
                           vpn_models.VPNEndpoint.endpoint ==
                           models_v2.Subnet.id)
        query = query.join(vpn_models.VPNEndpointGroup,
                           vpn_models.VPNEndpointGroup.id ==
                           vpn_models.VPNEndpoint.endpoint_group_id)
        query = query.join(vpn_models.IPsecSiteConnection,
                           vpn_models.IPsecSiteConnection.local_ep_group_id ==
                           vpn_models.VPNEndpointGroup.id)
        return {sn.id: sn.cidr for sn in query.all()}

    def update_status_by_agent(self, context, service_status_info_list):
        """Updating vpnservice and vpnconnection status.

        :param context: context variable
        :param service_status_info_list: list of status
        The structure is
        [{id: vpnservice_id,
          status: ACTIVE|DOWN|ERROR,
          updated_pending_status: True|False
          ipsec_site_connections: {
              ipsec_site_connection_id: {
                  status: ACTIVE|DOWN|ERROR,
                  updated_pending_status: True|False
              }
          }]
        The agent will set updated_pending_status as True,
        when agent updates any pending status.
        """
        with db_api.CONTEXT_WRITER.using(context):
            for vpnservice in service_status_info_list:
                try:
                    vpnservice_db = self._get_vpnservice(
                        context, vpnservice['id'])
                except vpn_exception.VPNServiceNotFound:
                    LOG.warning('vpnservice %s in db is already deleted',
                                vpnservice['id'])
                    continue

                if (not utils.in_pending_status(vpnservice_db.status) or
                    vpnservice['updated_pending_status']):
                    vpnservice_db.status = vpnservice['status']
                for conn_id, conn in vpnservice[
                    'ipsec_site_connections'].items():
                    self._update_connection_status(
                        context, conn_id, conn['status'],
                        conn['updated_pending_status'])


def vpn_router_gateway_callback(resource, event, trigger, payload=None):
    # the event payload objects
    vpn_plugin = directory.get_plugin(p_constants.VPN)
    if vpn_plugin:
        context = payload.context
        router_id = payload.resource_id
        if resource == resources.ROUTER_GATEWAY:
            vpn_plugin.check_router_in_use(context, router_id)
        elif resource == resources.ROUTER_INTERFACE:
            subnet_id = payload.metadata.get('subnet_id')
            vpn_plugin.check_subnet_in_use(context, subnet_id, router_id)


def migration_callback(resource, event, trigger, payload):
    context = payload.context
    router = payload.latest_state
    vpn_plugin = directory.get_plugin(p_constants.VPN)
    if vpn_plugin:
        vpn_plugin.check_router_in_use(context, router['id'])
    return True


def subnet_callback(resource, event, trigger, payload=None):
    """Respond to subnet based notifications - see if subnet in use."""
    context = payload.context
    subnet_id = payload.resource_id
    vpn_plugin = directory.get_plugin(p_constants.VPN)
    if vpn_plugin:
        vpn_plugin.check_subnet_in_use_by_endpoint_group(context, subnet_id)


def subscribe():
    registry.subscribe(
        vpn_router_gateway_callback, resources.ROUTER_GATEWAY,
        events.BEFORE_DELETE)
    registry.subscribe(
        vpn_router_gateway_callback, resources.ROUTER_INTERFACE,
        events.BEFORE_DELETE)
    registry.subscribe(
        migration_callback, resources.ROUTER, events.BEFORE_UPDATE)
    registry.subscribe(
        subnet_callback, resources.SUBNET, events.BEFORE_DELETE)


# NOTE(armax): multiple VPN service plugins (potentially out of tree) may
# inherit from vpn_db and may need the callbacks to be processed. Having an
# implicit subscription (through the module import) preserves the existing
# behavior, and at the same time it avoids fixing it manually in each and
# every vpn plugin outta there. That said, The subscription is also made
# explicitly in the reference vpn plugin. The subscription operation is
# idempotent so there is no harm in registering the same callback multiple
# times.
subscribe()
