# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright (C) 2012 Midokura Japan K.K.
# Copyright (C) 2013 Midokura PTE LTD
# 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.
#
# @author: Takaaki Suzuki, Midokura Japan KK
# @author: Tomoe Sugihara, Midokura Japan KK
# @author: Ryu Ishimoto, Midokura Japan KK
# @author: Rossella Sblendido, Midokura Japan KK
# @author: Duarte Nunes, Midokura Japan KK

from midonetclient import api
from oslo.config import cfg
from sqlalchemy.orm import exc as sa_exc

from neutron.api.v2 import attributes
from neutron.common import constants
from neutron.common import exceptions as n_exc
from neutron.common import rpc as n_rpc
from neutron.common import topics
from neutron.db import agents_db
from neutron.db import agentschedulers_db
from neutron.db import db_base_plugin_v2
from neutron.db import dhcp_rpc_base
from neutron.db import external_net_db
from neutron.db import l3_db
from neutron.db import models_v2
from neutron.db import portbindings_db
from neutron.db import securitygroups_db
from neutron.extensions import external_net as ext_net
from neutron.extensions import l3
from neutron.extensions import portbindings
from neutron.extensions import securitygroup as ext_sg
from neutron.openstack.common import excutils
from neutron.openstack.common import log as logging
from neutron.openstack.common import rpc
from neutron.plugins.midonet.common import config  # noqa
from neutron.plugins.midonet.common import net_util
from neutron.plugins.midonet import midonet_lib

LOG = logging.getLogger(__name__)

EXTERNAL_GW_INFO = l3.EXTERNAL_GW_INFO

METADATA_DEFAULT_IP = "169.254.169.254/32"
OS_FLOATING_IP_RULE_KEY = 'OS_FLOATING_IP'
OS_SG_RULE_KEY = 'OS_SG_RULE_ID'
OS_TENANT_ROUTER_RULE_KEY = 'OS_TENANT_ROUTER_RULE'
PRE_ROUTING_CHAIN_NAME = "OS_PRE_ROUTING_%s"
PORT_INBOUND_CHAIN_NAME = "OS_PORT_%s_INBOUND"
PORT_OUTBOUND_CHAIN_NAME = "OS_PORT_%s_OUTBOUND"
POST_ROUTING_CHAIN_NAME = "OS_POST_ROUTING_%s"
SG_INGRESS_CHAIN_NAME = "OS_SG_%s_INGRESS"
SG_EGRESS_CHAIN_NAME = "OS_SG_%s_EGRESS"
SG_PORT_GROUP_NAME = "OS_PG_%s"
SNAT_RULE = 'SNAT'


def _get_nat_ips(type, fip):
    """Get NAT IP address information.

    From the route type given, determine the source and target IP addresses
    from the provided floating IP DB object.
    """
    if type == 'pre-routing':
        return fip["floating_ip_address"], fip["fixed_ip_address"]
    elif type == 'post-routing':
        return fip["fixed_ip_address"], fip["floating_ip_address"]
    else:
        raise ValueError(_("Invalid nat_type %s") % type)


def _nat_chain_names(router_id):
    """Get the chain names for NAT.

    These names are used to associate MidoNet chains to the NAT rules
    applied to the router.  For each of these, there are two NAT types,
    'dnat' and 'snat' that are returned as keys, and the corresponding
    chain names as their values.
    """
    pre_routing_name = PRE_ROUTING_CHAIN_NAME % router_id
    post_routing_name = POST_ROUTING_CHAIN_NAME % router_id
    return {'pre-routing': pre_routing_name, 'post-routing': post_routing_name}


def _sg_chain_names(sg_id):
    """Get the chain names for security group.

    These names are used to associate a security group to MidoNet chains.
    There are two names for ingress and egress security group directions.
    """
    ingress = SG_INGRESS_CHAIN_NAME % sg_id
    egress = SG_EGRESS_CHAIN_NAME % sg_id
    return {'ingress': ingress, 'egress': egress}


def _port_chain_names(port_id):
    """Get the chain names for a port.

    These are chains to hold security group chains.
    """
    inbound = PORT_INBOUND_CHAIN_NAME % port_id
    outbound = PORT_OUTBOUND_CHAIN_NAME % port_id
    return {'inbound': inbound, 'outbound': outbound}


def _sg_port_group_name(sg_id):
    """Get the port group name for security group..

    This name is used to associate a security group to MidoNet  port groups.
    """
    return SG_PORT_GROUP_NAME % sg_id


def _rule_direction(sg_direction):
    """Convert the SG direction to MidoNet direction

    MidoNet terms them 'inbound' and 'outbound' instead of 'ingress' and
    'egress'.  Also, the direction is reversed since MidoNet sees it
    from the network port's point of view, not the VM's.
    """
    if sg_direction == 'ingress':
        return 'outbound'
    elif sg_direction == 'egress':
        return 'inbound'
    else:
        raise ValueError(_("Unrecognized direction %s") % sg_direction)


def _is_router_interface_port(port):
    """Check whether the given port is a router interface port."""
    device_owner = port['device_owner']
    return (device_owner in l3_db.DEVICE_OWNER_ROUTER_INTF)


def _is_router_gw_port(port):
    """Check whether the given port is a router gateway port."""
    device_owner = port['device_owner']
    return (device_owner in l3_db.DEVICE_OWNER_ROUTER_GW)


def _is_vif_port(port):
    """Check whether the given port is a standard VIF port."""
    device_owner = port['device_owner']
    return (not _is_dhcp_port(port) and
            device_owner not in (l3_db.DEVICE_OWNER_ROUTER_GW,
                                 l3_db.DEVICE_OWNER_ROUTER_INTF))


def _is_dhcp_port(port):
    """Check whether the given port is a DHCP port."""
    device_owner = port['device_owner']
    return device_owner.startswith(constants.DEVICE_OWNER_DHCP)


def _check_resource_exists(func, id, name, raise_exc=False):
    """Check whether the given resource exists in MidoNet data store."""
    try:
        func(id)
    except midonet_lib.MidonetResourceNotFound as exc:
        LOG.error(_("There is no %(name)s with ID %(id)s in MidoNet."),
                  {"name": name, "id": id})
        if raise_exc:
            raise MidonetPluginException(msg=exc)


class MidoRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin):
    RPC_API_VERSION = '1.1'

    def create_rpc_dispatcher(self):
        """Get the rpc dispatcher for this manager.

        This a basic implementation that will call the plugin like get_ports
        and handle basic events
        If a manager would like to set an rpc API version, or support more than
        one class as the target of rpc messages, override this method.
        """
        return n_rpc.PluginRpcDispatcher([self,
                                          agents_db.AgentExtRpcCallback()])


class MidonetPluginException(n_exc.NeutronException):
    message = _("%(msg)s")


class MidonetPluginV2(db_base_plugin_v2.NeutronDbPluginV2,
                      portbindings_db.PortBindingMixin,
                      external_net_db.External_net_db_mixin,
                      l3_db.L3_NAT_db_mixin,
                      agentschedulers_db.DhcpAgentSchedulerDbMixin,
                      securitygroups_db.SecurityGroupDbMixin):

    supported_extension_aliases = ['external-net', 'router', 'security-group',
                                   'agent', 'dhcp_agent_scheduler', 'binding']
    __native_bulk_support = False

    def __init__(self):
        super(MidonetPluginV2, self).__init__()
        # Read config values
        midonet_conf = cfg.CONF.MIDONET
        midonet_uri = midonet_conf.midonet_uri
        admin_user = midonet_conf.username
        admin_pass = midonet_conf.password
        admin_project_id = midonet_conf.project_id
        self.provider_router_id = midonet_conf.provider_router_id
        self.provider_router = None

        self.mido_api = api.MidonetApi(midonet_uri, admin_user,
                                       admin_pass,
                                       project_id=admin_project_id)
        self.client = midonet_lib.MidoClient(self.mido_api)

        # self.provider_router_id should have been set.
        if self.provider_router_id is None:
            msg = _('provider_router_id should be configured in the plugin '
                    'config file')
            LOG.exception(msg)
            raise MidonetPluginException(msg=msg)

        self.setup_rpc()

        self.base_binding_dict = {
            portbindings.VIF_TYPE: portbindings.VIF_TYPE_MIDONET,
            portbindings.VIF_DETAILS: {
                # TODO(rkukura): Replace with new VIF security details
                portbindings.CAP_PORT_FILTER:
                'security-group' in self.supported_extension_aliases}}

    def _get_provider_router(self):
        if self.provider_router is None:
            self.provider_router = self.client.get_router(
                self.provider_router_id)
        return self.provider_router

    def _dhcp_mappings(self, context, fixed_ips, mac):
        for fixed_ip in fixed_ips:
            subnet = self._get_subnet(context, fixed_ip["subnet_id"])
            if subnet["ip_version"] == 6:
                # TODO(ryu) handle IPv6
                continue
            if not subnet["enable_dhcp"]:
                # Skip if DHCP is disabled
                continue
            yield subnet['cidr'], fixed_ip["ip_address"], mac

    def _metadata_subnets(self, context, fixed_ips):
        for fixed_ip in fixed_ips:
            subnet = self._get_subnet(context, fixed_ip["subnet_id"])
            if subnet["ip_version"] == 6:
                continue
            yield subnet['cidr'], fixed_ip["ip_address"]

    def _initialize_port_chains(self, port, in_chain, out_chain, sg_ids):

        tenant_id = port["tenant_id"]

        position = 1
        # mac spoofing protection
        self._add_chain_rule(in_chain, action='drop',
                             dl_src=port["mac_address"], inv_dl_src=True,
                             position=position)

        # ip spoofing protection
        for fixed_ip in port["fixed_ips"]:
            position += 1
            self._add_chain_rule(in_chain, action="drop",
                                 src_addr=fixed_ip["ip_address"] + "/32",
                                 inv_nw_src=True, dl_type=0x0800,  # IPv4
                                 position=position)

        # conntrack
        position += 1
        self._add_chain_rule(in_chain, action='accept',
                             match_forward_flow=True,
                             position=position)

        # Reset the position to process egress
        position = 1

        # Add rule for SGs
        if sg_ids:
            for sg_id in sg_ids:
                chain_name = _sg_chain_names(sg_id)["ingress"]
                chain = self.client.get_chain_by_name(tenant_id, chain_name)
                self._add_chain_rule(out_chain, action='jump',
                                     jump_chain_id=chain.get_id(),
                                     jump_chain_name=chain_name,
                                     position=position)
                position += 1

        # add reverse flow matching at the end
        self._add_chain_rule(out_chain, action='accept',
                             match_return_flow=True,
                             position=position)
        position += 1

        # fall back DROP rule at the end except for ARP
        self._add_chain_rule(out_chain, action='drop',
                             dl_type=0x0806,  # ARP
                             inv_dl_type=True, position=position)

    def _bind_port_to_sgs(self, context, port, sg_ids):
        self._process_port_create_security_group(context, port, sg_ids)
        if sg_ids is not None:
            for sg_id in sg_ids:
                pg_name = _sg_port_group_name(sg_id)
                self.client.add_port_to_port_group_by_name(
                    port["tenant_id"], pg_name, port["id"])

    def _unbind_port_from_sgs(self, context, port_id):
        self._delete_port_security_group_bindings(context, port_id)
        self.client.remove_port_from_port_groups(port_id)

    def _create_accept_chain_rule(self, context, sg_rule, chain=None):
        direction = sg_rule["direction"]
        tenant_id = sg_rule["tenant_id"]
        sg_id = sg_rule["security_group_id"]
        chain_name = _sg_chain_names(sg_id)[direction]

        if chain is None:
            chain = self.client.get_chain_by_name(tenant_id, chain_name)

        pg_id = None
        if sg_rule["remote_group_id"] is not None:
            pg_name = _sg_port_group_name(sg_id)
            pg = self.client.get_port_group_by_name(tenant_id, pg_name)
            pg_id = pg.get_id()

        props = {OS_SG_RULE_KEY: str(sg_rule["id"])}

        # Determine source or destination address by looking at direction
        src_pg_id = dst_pg_id = None
        src_addr = dst_addr = None
        src_port_to = dst_port_to = None
        src_port_from = dst_port_from = None
        if direction == "egress":
            dst_pg_id = pg_id
            dst_addr = sg_rule["remote_ip_prefix"]
            dst_port_from = sg_rule["port_range_min"]
            dst_port_to = sg_rule["port_range_max"]
        else:
            src_pg_id = pg_id
            src_addr = sg_rule["remote_ip_prefix"]
            src_port_from = sg_rule["port_range_min"]
            src_port_to = sg_rule["port_range_max"]

        return self._add_chain_rule(
            chain, action='accept', port_group_src=src_pg_id,
            port_group_dst=dst_pg_id,
            src_addr=src_addr, src_port_from=src_port_from,
            src_port_to=src_port_to,
            dst_addr=dst_addr, dst_port_from=dst_port_from,
            dst_port_to=dst_port_to,
            nw_proto=net_util.get_protocol_value(sg_rule["protocol"]),
            dl_type=net_util.get_ethertype_value(sg_rule["ethertype"]),
            properties=props)

    def _remove_nat_rules(self, context, fip):
        router = self.client.get_router(fip["router_id"])
        self.client.remove_static_route(self._get_provider_router(),
                                        fip["floating_ip_address"])

        chain_names = _nat_chain_names(router.get_id())
        for _type, name in chain_names.iteritems():
            self.client.remove_rules_by_property(
                router.get_tenant_id(), name,
                OS_FLOATING_IP_RULE_KEY, fip["id"])

    def setup_rpc(self):
        # RPC support
        self.topic = topics.PLUGIN
        self.conn = rpc.create_connection(new=True)
        self.callbacks = MidoRpcCallbacks()
        self.dispatcher = self.callbacks.create_rpc_dispatcher()
        self.conn.create_consumer(self.topic, self.dispatcher,
                                  fanout=False)
        # Consume from all consumers in a thread
        self.conn.consume_in_thread()

    def create_subnet(self, context, subnet):
        """Create Neutron subnet.

        Creates a Neutron subnet and a DHCP entry in MidoNet bridge.
        """
        LOG.debug(_("MidonetPluginV2.create_subnet called: subnet=%r"), subnet)

        s = subnet["subnet"]
        net = super(MidonetPluginV2, self).get_network(
            context, subnet['subnet']['network_id'], fields=None)

        session = context.session
        with session.begin(subtransactions=True):
            sn_entry = super(MidonetPluginV2, self).create_subnet(context,
                                                                  subnet)
            bridge = self.client.get_bridge(sn_entry['network_id'])

            gateway_ip = s['gateway_ip']
            cidr = s['cidr']
            if s['enable_dhcp']:
                dns_nameservers = None
                host_routes = None
                if s['dns_nameservers'] is not attributes.ATTR_NOT_SPECIFIED:
                    dns_nameservers = s['dns_nameservers']

                if s['host_routes'] is not attributes.ATTR_NOT_SPECIFIED:
                    host_routes = s['host_routes']

                self.client.create_dhcp(bridge, gateway_ip, cidr,
                                        host_rts=host_routes,
                                        dns_servers=dns_nameservers)

            # For external network, link the bridge to the provider router.
            if net['router:external']:
                self._link_bridge_to_gw_router(
                    bridge, self._get_provider_router(), gateway_ip, cidr)

        LOG.debug(_("MidonetPluginV2.create_subnet exiting: sn_entry=%r"),
                  sn_entry)
        return sn_entry

    def delete_subnet(self, context, id):
        """Delete Neutron subnet.

        Delete neutron network and its corresponding MidoNet bridge.
        """
        LOG.debug(_("MidonetPluginV2.delete_subnet called: id=%s"), id)
        subnet = super(MidonetPluginV2, self).get_subnet(context, id,
                                                         fields=None)
        net = super(MidonetPluginV2, self).get_network(context,
                                                       subnet['network_id'],
                                                       fields=None)
        session = context.session
        with session.begin(subtransactions=True):

            super(MidonetPluginV2, self).delete_subnet(context, id)
            bridge = self.client.get_bridge(subnet['network_id'])
            if subnet['enable_dhcp']:
                self.client.delete_dhcp(bridge, subnet['cidr'])

            # If the network is external, clean up routes, links, ports
            if net[ext_net.EXTERNAL]:
                self._unlink_bridge_from_gw_router(
                    bridge, self._get_provider_router())

            LOG.debug(_("MidonetPluginV2.delete_subnet exiting"))

    def create_network(self, context, network):
        """Create Neutron network.

        Create a new Neutron network and its corresponding MidoNet bridge.
        """
        LOG.debug(_('MidonetPluginV2.create_network called: network=%r'),
                  network)
        net_data = network['network']
        tenant_id = self._get_tenant_id_for_create(context, net_data)
        net_data['tenant_id'] = tenant_id
        self._ensure_default_security_group(context, tenant_id)

        bridge = self.client.create_bridge(**net_data)
        net_data['id'] = bridge.get_id()

        session = context.session
        with session.begin(subtransactions=True):
            net = super(MidonetPluginV2, self).create_network(context, network)
            self._process_l3_create(context, net, net_data)

        LOG.debug(_("MidonetPluginV2.create_network exiting: net=%r"), net)
        return net

    def update_network(self, context, id, network):
        """Update Neutron network.

        Update an existing Neutron network and its corresponding MidoNet
        bridge.
        """
        LOG.debug(_("MidonetPluginV2.update_network called: id=%(id)r, "
                    "network=%(network)r"), {'id': id, 'network': network})
        session = context.session
        with session.begin(subtransactions=True):
            net = super(MidonetPluginV2, self).update_network(
                context, id, network)
            self._process_l3_update(context, net, network['network'])
            self.client.update_bridge(id, **network['network'])

        LOG.debug(_("MidonetPluginV2.update_network exiting: net=%r"), net)
        return net

    def get_network(self, context, id, fields=None):
        """Get Neutron network.

        Retrieves a Neutron network and its corresponding MidoNet bridge.
        """
        LOG.debug(_("MidonetPluginV2.get_network called: id=%(id)r, "
                    "fields=%(fields)r"), {'id': id, 'fields': fields})
        qnet = super(MidonetPluginV2, self).get_network(context, id, fields)
        self.client.get_bridge(id)

        LOG.debug(_("MidonetPluginV2.get_network exiting: qnet=%r"), qnet)
        return qnet

    def delete_network(self, context, id):
        """Delete a network and its corresponding MidoNet bridge."""
        LOG.debug(_("MidonetPluginV2.delete_network called: id=%r"), id)
        self.client.delete_bridge(id)
        try:
            with context.session.begin(subtransactions=True):
                self._process_l3_delete(context, id)
                super(MidonetPluginV2, self).delete_network(context, id)
        except Exception:
            with excutils.save_and_reraise_exception():
                LOG.error(_('Failed to delete neutron db, while Midonet '
                            'bridge=%r had been deleted'), id)

    def create_port(self, context, port):
        """Create a L2 port in Neutron/MidoNet."""
        LOG.debug(_("MidonetPluginV2.create_port called: port=%r"), port)
        port_data = port['port']

        # Create a bridge port in MidoNet and set the bridge port ID as the
        # port ID in Neutron.
        bridge = self.client.get_bridge(port_data["network_id"])
        tenant_id = bridge.get_tenant_id()
        asu = port_data.get("admin_state_up", True)
        bridge_port = self.client.add_bridge_port(bridge,
                                                  admin_state_up=asu)
        port_data["id"] = bridge_port.get_id()

        try:
            session = context.session
            with session.begin(subtransactions=True):
                # Create a Neutron port
                new_port = super(MidonetPluginV2, self).create_port(context,
                                                                    port)
                port_data.update(new_port)
                self._ensure_default_security_group_on_port(context,
                                                            port)
                if _is_vif_port(port_data):
                    # Bind security groups to the port
                    sg_ids = self._get_security_groups_on_port(context, port)
                    self._bind_port_to_sgs(context, new_port, sg_ids)

                    # Create port chains
                    port_chains = {}
                    for d, name in _port_chain_names(
                            new_port["id"]).iteritems():
                        port_chains[d] = self.client.create_chain(tenant_id,
                                                                  name)

                    self._initialize_port_chains(port_data,
                                                 port_chains['inbound'],
                                                 port_chains['outbound'],
                                                 sg_ids)

                    # Update the port with the chain
                    self.client.update_port_chains(
                        bridge_port, port_chains["inbound"].get_id(),
                        port_chains["outbound"].get_id())

                    # DHCP mapping is only for VIF ports
                    for cidr, ip, mac in self._dhcp_mappings(
                            context, port_data["fixed_ips"],
                            port_data["mac_address"]):
                        self.client.add_dhcp_host(bridge, cidr, ip, mac)

                elif _is_dhcp_port(port_data):
                    # For DHCP port, add a metadata route
                    for cidr, ip in self._metadata_subnets(
                            context, port_data["fixed_ips"]):
                        self.client.add_dhcp_route_option(bridge, cidr, ip,
                                                          METADATA_DEFAULT_IP)

            self._process_portbindings_create_and_update(context,
                                                         port_data, new_port)
        except Exception as ex:
            # Try removing the MidoNet port before raising an exception.
            with excutils.save_and_reraise_exception():
                LOG.error(_("Failed to create a port on network %(net_id)s: "
                            "%(err)s"),
                          {"net_id": port_data["network_id"], "err": ex})
                self.client.delete_port(bridge_port.get_id())

        LOG.debug(_("MidonetPluginV2.create_port exiting: port=%r"), new_port)
        return new_port

    def get_port(self, context, id, fields=None):
        """Retrieve port."""
        LOG.debug(_("MidonetPluginV2.get_port called: id=%(id)s "
                    "fields=%(fields)r"), {'id': id, 'fields': fields})
        port = super(MidonetPluginV2, self).get_port(context, id, fields)
        "Check if the port exists in MidoNet DB"""
        try:
            self.client.get_port(id)
        except midonet_lib.MidonetResourceNotFound as exc:
            LOG.error(_("There is no port with ID %(id)s in MidoNet."),
                      {"id": id})
            port['status'] = constants.PORT_STATUS_ERROR
            raise exc
        LOG.debug(_("MidonetPluginV2.get_port exiting: port=%r"), port)
        return port

    def get_ports(self, context, filters=None, fields=None):
        """List neutron ports and verify that they exist in MidoNet."""
        LOG.debug(_("MidonetPluginV2.get_ports called: filters=%(filters)s "
                    "fields=%(fields)r"),
                  {'filters': filters, 'fields': fields})
        ports = super(MidonetPluginV2, self).get_ports(context, filters,
                                                       fields)
        return ports

    def delete_port(self, context, id, l3_port_check=True):
        """Delete a neutron port and corresponding MidoNet bridge port."""
        LOG.debug(_("MidonetPluginV2.delete_port called: id=%(id)s "
                    "l3_port_check=%(l3_port_check)r"),
                  {'id': id, 'l3_port_check': l3_port_check})
        # if needed, check to see if this is a port owned by
        # and l3-router.  If so, we should prevent deletion.
        if l3_port_check:
            self.prevent_l3_port_deletion(context, id)

        self.disassociate_floatingips(context, id)
        port = self.get_port(context, id)
        device_id = port['device_id']
        # If this port is for router interface/gw, unlink and delete.
        if _is_router_interface_port(port):
            self._unlink_bridge_from_router(device_id, id)
        elif _is_router_gw_port(port):
            # Gateway removed
            # Remove all the SNAT rules that are tagged.
            router = self._get_router(context, device_id)
            tenant_id = router["tenant_id"]
            chain_names = _nat_chain_names(device_id)
            for _type, name in chain_names.iteritems():
                self.client.remove_rules_by_property(
                    tenant_id, name, OS_TENANT_ROUTER_RULE_KEY,
                    SNAT_RULE)
            # Remove the default routes and unlink
            self._remove_router_gateway(port['device_id'])

        self.client.delete_port(id, delete_chains=True)
        try:
            for cidr, ip, mac in self._dhcp_mappings(
                    context, port["fixed_ips"], port["mac_address"]):
                self.client.delete_dhcp_host(port["network_id"], cidr, ip,
                                             mac)
        except Exception:
            LOG.error(_("Failed to delete DHCP mapping for port %(id)s"),
                      {"id": id})

        super(MidonetPluginV2, self).delete_port(context, id)

    def update_port(self, context, id, port):
        """Handle port update, including security groups and fixed IPs."""
        with context.session.begin(subtransactions=True):

            # Get the port and save the fixed IPs
            old_port = self._get_port(context, id)
            net_id = old_port["network_id"]
            mac = old_port["mac_address"]
            old_ips = old_port["fixed_ips"]
            # update the port DB
            p = super(MidonetPluginV2, self).update_port(context, id, port)

            if "admin_state_up" in port["port"]:
                asu = port["port"]["admin_state_up"]
                mido_port = self.client.update_port(id, admin_state_up=asu)

                # If we're changing the admin_state_up flag and the port is
                # associated with a router, then we also need to update the
                # peer port.
                if _is_router_interface_port(p):
                    self.client.update_port(mido_port.get_peer_id(),
                                            admin_state_up=asu)

            new_ips = p["fixed_ips"]
            if new_ips:
                bridge = self.client.get_bridge(net_id)
                # If it's a DHCP port, add a route to reach the MD server
                if _is_dhcp_port(p):
                    for cidr, ip in self._metadata_subnets(
                        context, new_ips):
                        self.client.add_dhcp_route_option(
                            bridge, cidr, ip, METADATA_DEFAULT_IP)
                else:
                # IPs have changed.  Re-map the DHCP entries
                    for cidr, ip, mac in self._dhcp_mappings(
                            context, old_ips, mac):
                        self.client.remove_dhcp_host(
                            bridge, cidr, ip, mac)

                    for cidr, ip, mac in self._dhcp_mappings(
                        context, new_ips, mac):
                        self.client.add_dhcp_host(
                            bridge, cidr, ip, mac)

            if (self._check_update_deletes_security_groups(port) or
                    self._check_update_has_security_groups(port)):
                self._unbind_port_from_sgs(context, p["id"])
                sg_ids = self._get_security_groups_on_port(context, port)
                self._bind_port_to_sgs(context, p, sg_ids)

            self._process_portbindings_create_and_update(context,
                                                         port['port'],
                                                         p)
        return p

    def create_router(self, context, router):
        """Handle router creation.

        When a new Neutron router is created, its corresponding MidoNet router
        is also created.  In MidoNet, this router is initialized with chains
        for inbound and outbound traffic, which will be used to hold other
        chains that include various rules, such as NAT.

        :param router: Router information provided to create a new router.
        """

        # NOTE(dcahill): Similar to the NSX plugin, we completely override
        # this method in order to be able to use the MidoNet ID as Neutron ID
        # TODO(dcahill): Propose upstream patch for allowing
        # 3rd parties to specify IDs as we do with l2 plugin
        LOG.debug(_("MidonetPluginV2.create_router called: router=%(router)s"),
                  {"router": router})
        r = router['router']
        tenant_id = self._get_tenant_id_for_create(context, r)
        r['tenant_id'] = tenant_id
        mido_router = self.client.create_router(**r)
        mido_router_id = mido_router.get_id()

        try:
            has_gw_info = False
            if EXTERNAL_GW_INFO in r:
                has_gw_info = True
                gw_info = r.pop(EXTERNAL_GW_INFO)
            with context.session.begin(subtransactions=True):
                # pre-generate id so it will be available when
                # configuring external gw port
                router_db = l3_db.Router(id=mido_router_id,
                                         tenant_id=tenant_id,
                                         name=r['name'],
                                         admin_state_up=r['admin_state_up'],
                                         status="ACTIVE")
                context.session.add(router_db)
                if has_gw_info:
                    self._update_router_gw_info(context, router_db['id'],
                                                gw_info)

            router_data = self._make_router_dict(router_db,
                                                 process_extensions=False)

        except Exception:
            # Try removing the midonet router
            with excutils.save_and_reraise_exception():
                self.client.delete_router(mido_router_id)

        # Create router chains
        chain_names = _nat_chain_names(mido_router_id)
        try:
            self.client.add_router_chains(mido_router,
                                          chain_names["pre-routing"],
                                          chain_names["post-routing"])
        except Exception:
            # Set the router status to Error
            with context.session.begin(subtransactions=True):
                r = self._get_router(context, router_data["id"])
                router_data['status'] = constants.NET_STATUS_ERROR
                r['status'] = router_data['status']
                context.session.add(r)

        LOG.debug(_("MidonetPluginV2.create_router exiting: "
                    "router_data=%(router_data)s."),
                  {"router_data": router_data})
        return router_data

    def _set_router_gateway(self, id, gw_router, gw_ip):
        """Set router uplink gateway

        :param ID: ID of the router
        :param gw_router: gateway router to link to
        :param gw_ip: gateway IP address
        """
        LOG.debug(_("MidonetPluginV2.set_router_gateway called: id=%(id)s, "
                    "gw_router=%(gw_router)s, gw_ip=%(gw_ip)s"),
                  {'id': id, 'gw_router': gw_router, 'gw_ip': gw_ip}),

        router = self.client.get_router(id)

        # Create a port in the gw router
        gw_port = self.client.add_router_port(gw_router,
                                              port_address='169.254.255.1',
                                              network_address='169.254.255.0',
                                              network_length=30)

        # Create a port in the router
        port = self.client.add_router_port(router,
                                           port_address='169.254.255.2',
                                           network_address='169.254.255.0',
                                           network_length=30)

        # Link them
        self.client.link(gw_port, port.get_id())

        # Add a route for gw_ip to bring it down to the router
        self.client.add_router_route(gw_router, type='Normal',
                                     src_network_addr='0.0.0.0',
                                     src_network_length=0,
                                     dst_network_addr=gw_ip,
                                     dst_network_length=32,
                                     next_hop_port=gw_port.get_id(),
                                     weight=100)

        # Add default route to uplink in the router
        self.client.add_router_route(router, type='Normal',
                                     src_network_addr='0.0.0.0',
                                     src_network_length=0,
                                     dst_network_addr='0.0.0.0',
                                     dst_network_length=0,
                                     next_hop_port=port.get_id(),
                                     weight=100)

    def _remove_router_gateway(self, id):
        """Clear router gateway

        :param ID: ID of the router
        """
        LOG.debug(_("MidonetPluginV2.remove_router_gateway called: "
                    "id=%(id)s"), {'id': id})
        router = self.client.get_router(id)

        # delete the port that is connected to the gateway router
        for p in router.get_ports():
            if p.get_port_address() == '169.254.255.2':
                peer_port_id = p.get_peer_id()
                if peer_port_id is not None:
                    self.client.unlink(p)
                    self.client.delete_port(peer_port_id)

        # delete default route
        for r in router.get_routes():
            if (r.get_dst_network_addr() == '0.0.0.0' and
                    r.get_dst_network_length() == 0):
                self.client.delete_route(r.get_id())

    def update_router(self, context, id, router):
        """Handle router updates."""
        LOG.debug(_("MidonetPluginV2.update_router called: id=%(id)s "
                    "router=%(router)r"), {"id": id, "router": router})

        router_data = router["router"]

        # Check if the update included changes to the gateway.
        gw_updated = l3_db.EXTERNAL_GW_INFO in router_data
        with context.session.begin(subtransactions=True):

            # Update the Neutron DB
            r = super(MidonetPluginV2, self).update_router(context, id,
                                                           router)
            tenant_id = r["tenant_id"]
            if gw_updated:
                if (l3_db.EXTERNAL_GW_INFO in r and
                        r[l3_db.EXTERNAL_GW_INFO] is not None):
                    # Gateway created
                    gw_port_neutron = self._get_port(
                        context.elevated(), r["gw_port_id"])
                    gw_ip = gw_port_neutron['fixed_ips'][0]['ip_address']

                    # First link routers and set up the routes
                    self._set_router_gateway(r["id"],
                                             self._get_provider_router(),
                                             gw_ip)
                    gw_port_midonet = self.client.get_link_port(
                        self._get_provider_router(), r["id"])

                    # Get the NAT chains and add dynamic SNAT rules.
                    chain_names = _nat_chain_names(r["id"])
                    props = {OS_TENANT_ROUTER_RULE_KEY: SNAT_RULE}
                    self.client.add_dynamic_snat(tenant_id,
                                                 chain_names['pre-routing'],
                                                 chain_names['post-routing'],
                                                 gw_ip,
                                                 gw_port_midonet.get_id(),
                                                 **props)

            self.client.update_router(id, **router_data)

        LOG.debug(_("MidonetPluginV2.update_router exiting: router=%r"), r)
        return r

    def delete_router(self, context, id):
        """Handler for router deletion.

        Deleting a router on Neutron simply means deleting its corresponding
        router in MidoNet.

        :param id: router ID to remove
        """
        LOG.debug(_("MidonetPluginV2.delete_router called: id=%s"), id)

        self.client.delete_router_chains(id)
        self.client.delete_router(id)

        super(MidonetPluginV2, self).delete_router(context, id)

    def _link_bridge_to_gw_router(self, bridge, gw_router, gw_ip, cidr):
        """Link a bridge to the gateway router

        :param bridge:  bridge
        :param gw_router: gateway router to link to
        :param gw_ip: IP address of gateway
        :param cidr: network CIDR
        """
        net_addr, net_len = net_util.net_addr(cidr)

        # create a port on the gateway router
        gw_port = self.client.add_router_port(gw_router, port_address=gw_ip,
                                              network_address=net_addr,
                                              network_length=net_len)

        # create a bridge port, then link it to the router.
        port = self.client.add_bridge_port(bridge)
        self.client.link(gw_port, port.get_id())

        # add a route for the subnet in the gateway router
        self.client.add_router_route(gw_router, type='Normal',
                                     src_network_addr='0.0.0.0',
                                     src_network_length=0,
                                     dst_network_addr=net_addr,
                                     dst_network_length=net_len,
                                     next_hop_port=gw_port.get_id(),
                                     weight=100)

    def _unlink_bridge_from_gw_router(self, bridge, gw_router):
        """Unlink a bridge from the gateway router

        :param bridge: bridge to unlink
        :param gw_router: gateway router to unlink from
        """
        # Delete routes and unlink the router and the bridge.
        routes = self.client.get_router_routes(gw_router.get_id())

        bridge_ports_to_delete = [
            p for p in gw_router.get_peer_ports()
            if p.get_device_id() == bridge.get_id()]

        for p in bridge.get_peer_ports():
            if p.get_device_id() == gw_router.get_id():
                # delete the routes going to the bridge
                for r in routes:
                    if r.get_next_hop_port() == p.get_id():
                        self.client.delete_route(r.get_id())
                self.client.unlink(p)
                self.client.delete_port(p.get_id())

        # delete bridge port
        for port in bridge_ports_to_delete:
            self.client.delete_port(port.get_id())

    def _link_bridge_to_router(self, router, bridge_port, net_addr, net_len,
                               gw_ip, metadata_gw_ip):
        router_port = self.client.add_router_port(
            router, network_length=net_len, network_address=net_addr,
            port_address=gw_ip, admin_state_up=bridge_port['admin_state_up'])
        self.client.link(router_port, bridge_port['id'])
        self.client.add_router_route(router, type='Normal',
                                     src_network_addr='0.0.0.0',
                                     src_network_length=0,
                                     dst_network_addr=net_addr,
                                     dst_network_length=net_len,
                                     next_hop_port=router_port.get_id(),
                                     weight=100)

        if metadata_gw_ip:
            # Add a route for the metadata server.
            # Not all VM images supports DHCP option 121.  Add a route for the
            # Metadata server in the router to forward the packet to the bridge
            # that will send them to the Metadata Proxy.
            md_net_addr, md_net_len = net_util.net_addr(METADATA_DEFAULT_IP)
            self.client.add_router_route(
                router, type='Normal', src_network_addr=net_addr,
                src_network_length=net_len,
                dst_network_addr=md_net_addr,
                dst_network_length=md_net_len,
                next_hop_port=router_port.get_id(),
                next_hop_gateway=metadata_gw_ip)

    def _unlink_bridge_from_router(self, router_id, bridge_port_id):
        """Unlink a bridge from a router."""

        # Remove the routes to the port and unlink the port
        bridge_port = self.client.get_port(bridge_port_id)
        routes = self.client.get_router_routes(router_id)
        self.client.delete_port_routes(routes, bridge_port.get_peer_id())
        self.client.unlink(bridge_port)

    def add_router_interface(self, context, router_id, interface_info):
        """Handle router linking with network."""
        LOG.debug(_("MidonetPluginV2.add_router_interface called: "
                    "router_id=%(router_id)s "
                    "interface_info=%(interface_info)r"),
                  {'router_id': router_id, 'interface_info': interface_info})

        with context.session.begin(subtransactions=True):
            info = super(MidonetPluginV2, self).add_router_interface(
                context, router_id, interface_info)

        try:
            subnet = self._get_subnet(context, info["subnet_id"])
            cidr = subnet["cidr"]
            net_addr, net_len = net_util.net_addr(cidr)
            router = self.client.get_router(router_id)

            # Get the metadata GW IP
            metadata_gw_ip = None
            rport_qry = context.session.query(models_v2.Port)
            dhcp_ports = rport_qry.filter_by(
                network_id=subnet["network_id"],
                device_owner=constants.DEVICE_OWNER_DHCP).all()
            if dhcp_ports and dhcp_ports[0].fixed_ips:
                metadata_gw_ip = dhcp_ports[0].fixed_ips[0].ip_address
            else:
                LOG.warn(_("DHCP agent is not working correctly. No port "
                           "to reach the Metadata server on this network"))
            # Link the router and the bridge
            port = super(MidonetPluginV2, self).get_port(context,
                                                         info["port_id"])
            self._link_bridge_to_router(router, port, net_addr,
                                        net_len, subnet["gateway_ip"],
                                        metadata_gw_ip)
        except Exception:
            LOG.error(_("Failed to create MidoNet resources to add router "
                        "interface. info=%(info)s, router_id=%(router_id)s"),
                      {"info": info, "router_id": router_id})
            with excutils.save_and_reraise_exception():
                with context.session.begin(subtransactions=True):
                    self.remove_router_interface(context, router_id, info)

        LOG.debug(_("MidonetPluginV2.add_router_interface exiting: "
                    "info=%r"), info)
        return info

    def _assoc_fip(self, fip):
        router = self.client.get_router(fip["router_id"])
        link_port = self.client.get_link_port(
            self._get_provider_router(), router.get_id())
        self.client.add_router_route(
            self._get_provider_router(),
            src_network_addr='0.0.0.0',
            src_network_length=0,
            dst_network_addr=fip["floating_ip_address"],
            dst_network_length=32,
            next_hop_port=link_port.get_peer_id())
        props = {OS_FLOATING_IP_RULE_KEY: fip['id']}
        tenant_id = router.get_tenant_id()
        chain_names = _nat_chain_names(router.get_id())
        for chain_type, name in chain_names.items():
            src_ip, target_ip = _get_nat_ips(chain_type, fip)
            if chain_type == 'pre-routing':
                nat_type = 'dnat'
            else:
                nat_type = 'snat'
            self.client.add_static_nat(tenant_id, name, src_ip,
                                       target_ip,
                                       link_port.get_id(),
                                       nat_type, **props)

    def create_floatingip(self, context, floatingip):
        session = context.session
        with session.begin(subtransactions=True):
            fip = super(MidonetPluginV2, self).create_floatingip(
                context, floatingip)
            if fip['port_id']:
                self._assoc_fip(fip)
        return fip

    def update_floatingip(self, context, id, floatingip):
        """Handle floating IP association and disassociation."""
        LOG.debug(_("MidonetPluginV2.update_floatingip called: id=%(id)s "
                    "floatingip=%(floatingip)s "),
                  {'id': id, 'floatingip': floatingip})

        session = context.session
        with session.begin(subtransactions=True):
            if floatingip['floatingip']['port_id']:
                fip = super(MidonetPluginV2, self).update_floatingip(
                    context, id, floatingip)

                self._assoc_fip(fip)

            # disassociate floating IP
            elif floatingip['floatingip']['port_id'] is None:
                fip = super(MidonetPluginV2, self).get_floatingip(context, id)
                self._remove_nat_rules(context, fip)
                super(MidonetPluginV2, self).update_floatingip(context, id,
                                                               floatingip)

        LOG.debug(_("MidonetPluginV2.update_floating_ip exiting: fip=%s"), fip)
        return fip

    def disassociate_floatingips(self, context, port_id):
        """Disassociate floating IPs (if any) from this port."""
        try:
            fip_qry = context.session.query(l3_db.FloatingIP)
            fip_dbs = fip_qry.filter_by(fixed_port_id=port_id)
            for fip_db in fip_dbs:
                self._remove_nat_rules(context, fip_db)
        except sa_exc.NoResultFound:
            pass

        super(MidonetPluginV2, self).disassociate_floatingips(context, port_id)

    def create_security_group(self, context, security_group, default_sg=False):
        """Create security group.

        Create a new security group, including the default security group.
        In MidoNet, this means creating a pair of chains, inbound and outbound,
        as well as a new port group.
        """
        LOG.debug(_("MidonetPluginV2.create_security_group called: "
                    "security_group=%(security_group)s "
                    "default_sg=%(default_sg)s "),
                  {'security_group': security_group, 'default_sg': default_sg})

        sg = security_group.get('security_group')
        tenant_id = self._get_tenant_id_for_create(context, sg)
        if not default_sg:
            self._ensure_default_security_group(context, tenant_id)

        # Create the Neutron sg first
        sg = super(MidonetPluginV2, self).create_security_group(
            context, security_group, default_sg)

        try:
            # Process the MidoNet side
            self.client.create_port_group(tenant_id,
                                          _sg_port_group_name(sg["id"]))
            chain_names = _sg_chain_names(sg["id"])
            chains = {}
            for direction, chain_name in chain_names.iteritems():
                c = self.client.create_chain(tenant_id, chain_name)
                chains[direction] = c

            # Create all the rules for this SG.  Only accept rules are created
            for r in sg['security_group_rules']:
                self._create_accept_chain_rule(context, r,
                                               chain=chains[r['direction']])
        except Exception:
            LOG.error(_("Failed to create MidoNet resources for sg %(sg)r"),
                      {"sg": sg})
            with excutils.save_and_reraise_exception():
                with context.session.begin(subtransactions=True):
                    sg = self._get_security_group(context, sg["id"])
                    context.session.delete(sg)

        LOG.debug(_("MidonetPluginV2.create_security_group exiting: sg=%r"),
                  sg)
        return sg

    def delete_security_group(self, context, id):
        """Delete chains for Neutron security group."""
        LOG.debug(_("MidonetPluginV2.delete_security_group called: id=%s"), id)

        with context.session.begin(subtransactions=True):
            sg = super(MidonetPluginV2, self).get_security_group(context, id)
            if not sg:
                raise ext_sg.SecurityGroupNotFound(id=id)

            if sg["name"] == 'default' and not context.is_admin:
                raise ext_sg.SecurityGroupCannotRemoveDefault()

            sg_id = sg['id']
            filters = {'security_group_id': [sg_id]}
            if super(MidonetPluginV2, self)._get_port_security_group_bindings(
                    context, filters):
                raise ext_sg.SecurityGroupInUse(id=sg_id)

            # Delete MidoNet Chains and portgroup for the SG
            tenant_id = sg['tenant_id']
            self.client.delete_chains_by_names(
                tenant_id, _sg_chain_names(sg["id"]).values())

            self.client.delete_port_group_by_name(
                tenant_id, _sg_port_group_name(sg["id"]))

            super(MidonetPluginV2, self).delete_security_group(context, id)

    def create_security_group_rule(self, context, security_group_rule):
        """Create a security group rule

        Create a security group rule in the Neutron DB and corresponding
        MidoNet resources in its data store.
        """
        LOG.debug(_("MidonetPluginV2.create_security_group_rule called: "
                    "security_group_rule=%(security_group_rule)r"),
                  {'security_group_rule': security_group_rule})

        with context.session.begin(subtransactions=True):
            rule = super(MidonetPluginV2, self).create_security_group_rule(
                context, security_group_rule)

            self._create_accept_chain_rule(context, rule)

            LOG.debug(_("MidonetPluginV2.create_security_group_rule exiting: "
                        "rule=%r"), rule)
            return rule

    def delete_security_group_rule(self, context, sg_rule_id):
        """Delete a security group rule

        Delete a security group rule from the Neutron DB and corresponding
        MidoNet resources from its data store.
        """
        LOG.debug(_("MidonetPluginV2.delete_security_group_rule called: "
                    "sg_rule_id=%s"), sg_rule_id)
        with context.session.begin(subtransactions=True):
            rule = super(MidonetPluginV2, self).get_security_group_rule(
                context, sg_rule_id)

            if not rule:
                raise ext_sg.SecurityGroupRuleNotFound(id=sg_rule_id)

            sg = self._get_security_group(context,
                                          rule["security_group_id"])
            chain_name = _sg_chain_names(sg["id"])[rule["direction"]]
            self.client.remove_rules_by_property(rule["tenant_id"], chain_name,
                                                 OS_SG_RULE_KEY,
                                                 str(rule["id"]))
            super(MidonetPluginV2, self).delete_security_group_rule(
                context, sg_rule_id)

    def _add_chain_rule(self, chain, action, **kwargs):

        nw_proto = kwargs.get("nw_proto")
        src_addr = kwargs.pop("src_addr", None)
        dst_addr = kwargs.pop("dst_addr", None)
        src_port_from = kwargs.pop("src_port_from", None)
        src_port_to = kwargs.pop("src_port_to", None)
        dst_port_from = kwargs.pop("dst_port_from", None)
        dst_port_to = kwargs.pop("dst_port_to", None)

        # Convert to the keys and values that midonet client understands
        if src_addr:
            kwargs["nw_src_addr"], kwargs["nw_src_length"] = net_util.net_addr(
                src_addr)

        if dst_addr:
            kwargs["nw_dst_addr"], kwargs["nw_dst_length"] = net_util.net_addr(
                dst_addr)

        kwargs["tp_src"] = {"start": src_port_from, "end": src_port_to}

        kwargs["tp_dst"] = {"start": dst_port_from, "end": dst_port_to}

        if nw_proto == 1:  # ICMP
            # Overwrite port fields regardless of the direction
            kwargs["tp_src"] = {"start": src_port_from, "end": src_port_from}
            kwargs["tp_dst"] = {"start": dst_port_to, "end": dst_port_to}

        return self.client.add_chain_rule(chain, action=action, **kwargs)
