neutron/neutron/plugins/midonet/plugin.py

1268 lines
53 KiB
Python

# 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('network: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:
super(MidonetPluginV2, self).delete_network(context, id)
except Exception:
LOG.error(_('Failed to delete neutron db, while Midonet bridge=%r'
'had been deleted'), id)
raise
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 Nicira 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='network: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_db = fip_qry.filter_by(fixed_port_id=port_id).one()
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)