# Copyright 2016 VMware, 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_lib.db import model_base
from oslo_utils import uuidutils
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.orm import exc

from neutron.api.v2 import attributes
from neutron.common import utils as n_utils
from neutron.db import api as db_api
from neutron.db import db_base_plugin_v2
from neutron.db.models import securitygroup as securitygroups_db  # noqa
from neutron.extensions import securitygroup as ext_sg
from neutron_lib.api import validators
from neutron_lib import constants as n_constants

from vmware_nsx._i18n import _
from vmware_nsx.extensions import providersecuritygroup as provider_sg
from vmware_nsx.extensions import securitygrouplogging as sg_logging


class NsxExtendedSecurityGroupProperties(model_base.BASEV2):
    __tablename__ = 'nsx_extended_security_group_properties'

    security_group_id = sa.Column(sa.String(36),
                                  sa.ForeignKey('securitygroups.id',
                                                ondelete="CASCADE"),
                                  primary_key=True)
    logging = sa.Column(sa.Boolean, default=False, nullable=False)
    provider = sa.Column(sa.Boolean, default=False, nullable=False)
    security_group = orm.relationship(
        securitygroups_db.SecurityGroup,
        backref=orm.backref('ext_properties', lazy='joined',
                            uselist=False, cascade='delete'))


class ExtendedSecurityGroupPropertiesMixin(object):

    # NOTE(arosen): here we add a relationship so that from the ports model
    # it provides us access to SecurityGroupPortBinding and
    # NsxExtendedSecurityGroupProperties
    securitygroups_db.SecurityGroupPortBinding.extended_grp = orm.relationship(
        'NsxExtendedSecurityGroupProperties',
        foreign_keys="SecurityGroupPortBinding.security_group_id",
        primaryjoin=("NsxExtendedSecurityGroupProperties.security_group_id"
                     "==SecurityGroupPortBinding.security_group_id"))

    def create_provider_security_group(self, context, security_group):
        """Create a provider security group.

        This method creates a security group that does not by default
        enable egress traffic which normal neutron security groups do.
        """
        s = security_group['security_group']
        tenant_id = s['tenant_id']

        with db_api.autonested_transaction(context.session):
            security_group_db = securitygroups_db.SecurityGroup(
                id=s.get('id') or (uuidutils.generate_uuid()),
                description=s.get('description', ''),
                tenant_id=tenant_id,
                name=s.get('name', ''))
            context.session.add(security_group_db)
        secgroup_dict = self._make_security_group_dict(security_group_db)
        secgroup_dict[provider_sg.PROVIDER] = True
        return secgroup_dict

    def _process_security_group_properties_create(self, context,
                                                  sg_res, sg_req,
                                                  default_sg=False):
        self._validate_security_group_properties_create(
            context, sg_req, default_sg)
        with context.session.begin(subtransactions=True):
            properties = NsxExtendedSecurityGroupProperties(
                security_group_id=sg_res['id'],
                logging=sg_req.get(sg_logging.LOGGING, False),
                provider=sg_req.get(provider_sg.PROVIDER, False))
            context.session.add(properties)
        sg_res[sg_logging.LOGGING] = sg_req.get(sg_logging.LOGGING, False)
        sg_res[provider_sg.PROVIDER] = sg_req.get(provider_sg.PROVIDER, False)

    def _get_security_group_properties(self, context, security_group_id):
        with context.session.begin(subtransactions=True):
            try:
                prop = context.session.query(
                    NsxExtendedSecurityGroupProperties).filter_by(
                        security_group_id=security_group_id).one()
            except exc.NoResultFound:
                raise ext_sg.SecurityGroupNotFound(id=security_group_id)
        return prop

    def _process_security_group_properties_update(self, context,
                                                  sg_res, sg_req):
        if (sg_logging.LOGGING in sg_req
                and (sg_req[sg_logging.LOGGING] !=
                     sg_res.get(sg_logging.LOGGING, False))):
            prop = self._get_security_group_properties(context, sg_res['id'])
            with context.session.begin(subtransactions=True):
                prop.update({sg_logging.LOGGING: sg_req[sg_logging.LOGGING]})
            sg_res[sg_logging.LOGGING] = sg_req[sg_logging.LOGGING]

    def _is_security_group_logged(self, context, security_group_id):
        prop = self._get_security_group_properties(context, security_group_id)
        return prop.logging

    def _is_provider_security_group(self, context, security_group_id):
        sg_prop = self._get_security_group_properties(context,
                                                      security_group_id)
        return sg_prop.provider

    def _check_provider_security_group_exists(self, context,
                                              security_group_id):
        # NOTE(roeyc): We want to retrieve the security-group info by calling
        # get_security_group, this will also validate that the provider
        # security-group belongs to the same tenant this request is made for.
        sg = self.get_security_group(context, security_group_id)
        if not sg[provider_sg.PROVIDER]:
            raise provider_sg.SecurityGroupNotProvider(id=sg)

    def _check_invalid_security_groups_specified(self, context, port):
        if validators.is_attr_set(port.get(ext_sg.SECURITYGROUPS)):
            for sg in port.get(ext_sg.SECURITYGROUPS, []):
                # makes sure user doesn't add non-provider secgrp as secgrp
                if self._is_provider_security_group(context, sg):
                    raise provider_sg.SecurityGroupIsProvider(id=sg)

        if validators.is_attr_set(
                port.get(provider_sg.PROVIDER_SECURITYGROUPS)):

            # also check all provider groups are provider.
            for sg in port.get(provider_sg.PROVIDER_SECURITYGROUPS, []):
                self._check_provider_security_group_exists(context, sg)

    def _get_tenant_provider_security_groups(self, context, tenant_id):
        res = context.session.query(
            NsxExtendedSecurityGroupProperties.security_group_id
        ).join(securitygroups_db.SecurityGroup).filter(
            securitygroups_db.SecurityGroup.tenant_id == tenant_id,
            NsxExtendedSecurityGroupProperties.provider == sa.true()).scalar()
        return [res] if res else []

    def _validate_security_group_properties_create(self, context,
                                                   security_group, default_sg):
        self._validate_provider_security_group_create(context, security_group,
                                                      default_sg)

    def _validate_provider_security_group_create(self, context, security_group,
                                                 default_sg):
        if not security_group.get(provider_sg.PROVIDER, False):
            return

        if default_sg:
            raise provider_sg.DefaultSecurityGroupIsNotProvider()

        tenant_id = security_group['tenant_id']
        ssg = self._get_tenant_provider_security_groups(context, tenant_id)
        if ssg:
            # REVISIT(roeyc): At the moment we only allow on provider
            # security-group per tenant, this might change in the future.
            raise Exception(_("Provider Security-group already exists"
                              "(%(pvdsg)s) for tenant %(tenant_id)s.")
                            % {'pvdsg': ssg, 'tenant_id': tenant_id})

    def _get_provider_security_groups_on_port(self, context, port):
        p = port['port']
        tenant_id = p['tenant_id']
        provider_sgs = p.get(provider_sg.PROVIDER_SECURITYGROUPS,
                             n_constants.ATTR_NOT_SPECIFIED)

        if p.get('device_owner') and n_utils.is_port_trusted(p):
            return

        self._check_invalid_security_groups_specified(context, p)

        if not validators.is_attr_set(provider_sgs):
            if provider_sgs is n_constants.ATTR_NOT_SPECIFIED:
                provider_sgs = self._get_tenant_provider_security_groups(
                    context, tenant_id)
            else:
                # Accept None as indication that this port should not be
                # associated with any provider security-group.
                provider_sgs = []
        return provider_sgs

    def _process_port_create_provider_security_group(self, context, p,
                                                     security_group_ids):
        if validators.is_attr_set(security_group_ids):
            for security_group_id in security_group_ids:
                self._create_port_security_group_binding(context, p['id'],
                                                         security_group_id)
        p[provider_sg.PROVIDER_SECURITYGROUPS] = security_group_ids or []

    def _process_port_update_provider_security_group(self, context, port,
                                                     original_port,
                                                     updated_port):
        p = port['port']
        provider_sg_specified = (provider_sg.PROVIDER_SECURITYGROUPS in p and
                                 p[provider_sg.PROVIDER_SECURITYGROUPS] !=
                                 n_constants.ATTR_NOT_SPECIFIED)
        provider_sg_changed = (
            provider_sg_specified and not n_utils.compare_elements(
                original_port[provider_sg.PROVIDER_SECURITYGROUPS],
                p[provider_sg.PROVIDER_SECURITYGROUPS]))
        sg_changed = (
            set(original_port[ext_sg.SECURITYGROUPS]) !=
            set(updated_port[ext_sg.SECURITYGROUPS]))

        if provider_sg_changed:
            port['port']['tenant_id'] = original_port['id']
            port['port']['id'] = original_port['id']
            updated_port[provider_sg.PROVIDER_SECURITYGROUPS] = (
                self._get_provider_security_groups_on_port(context, port))
        else:
            if sg_changed:
                self._check_invalid_security_groups_specified(context, p)
            updated_port[provider_sg.PROVIDER_SECURITYGROUPS] = (
                original_port[provider_sg.PROVIDER_SECURITYGROUPS])

        if provider_sg_changed or sg_changed:
            if not sg_changed:
                query = context.session.query(
                    securitygroups_db.SecurityGroupPortBinding)
                for sg in original_port[provider_sg.PROVIDER_SECURITYGROUPS]:
                    binding = query.filter_by(
                        port_id=p['id'], security_group_id=sg).one()
                    context.session.delete(binding)
            self._process_port_create_provider_security_group(
                context, updated_port,
                updated_port[provider_sg.PROVIDER_SECURITYGROUPS])

    def _prevent_non_admin_delete_provider_sg(self, context, sg_id):
        # Only someone who is an admin is allowed to delete this.
        if not context.is_admin and self._is_provider_security_group(context,
                                                                     sg_id):
            raise provider_sg.ProviderSecurityGroupDeleteNotAdmin(id=sg_id)

    def _extend_security_group_with_properties(self, sg_res, sg_db):
        if sg_db.ext_properties:
            sg_res[sg_logging.LOGGING] = sg_db.ext_properties.logging
            sg_res[provider_sg.PROVIDER] = sg_db.ext_properties.provider

    def _extend_port_dict_provider_security_group(self, port_res, port_db):
        # NOTE(arosen): this method overrides the one in the base
        # security group db class. The reason this is needed is because
        # we are storing provider security groups in the same security
        # groups db model. We need to do this here to remove the provider
        # security groups and put those on the port resource as their
        # own attribute.

        # Security group bindings will be retrieved from the SQLAlchemy
        # model. As they're loaded eagerly with ports because of the
        # joined load they will not cause an extra query.

        provider_groups = []
        not_provider_groups = []
        for sec_group_mapping in port_db.security_groups:
            if sec_group_mapping.extended_grp.provider is True:
                provider_groups.append(sec_group_mapping['security_group_id'])
            else:
                not_provider_groups.append(
                    sec_group_mapping['security_group_id'])

        port_res[ext_sg.SECURITYGROUPS] = not_provider_groups
        port_res[provider_sg.PROVIDER_SECURITYGROUPS] = provider_groups
        return port_res

    db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
        attributes.PORTS, ['_extend_port_dict_provider_security_group'])

    db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
        ext_sg.SECURITYGROUPS, ['_extend_security_group_with_properties'])
