dragonflow/dragonflow/controller/dnat_app.py

408 lines
16 KiB
Python

# Copyright (c) 2015 OpenStack Foundation.
# 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.
import collections
import netaddr
from neutron.agent.ovsdb.native import idlutils
from neutron_lib import constants as n_const
from oslo_config import cfg
from oslo_log import log
from oslo_service import loopingcall
from ryu.lib.packet import arp
from ryu.ofproto import ether
import six
from dragonflow._i18n import _
from dragonflow.controller.common.arp_responder import ArpResponder
from dragonflow.controller.common import constants as const
from dragonflow.controller.common import utils
from dragonflow.controller.df_base_app import DFlowApp
LOG = log.getLogger(__name__)
DF_DNAT_APP_OPTS = [
cfg.StrOpt('external_network_bridge',
default='br-ex',
help=_("Name of bridge used for external network traffic")),
cfg.StrOpt('int_peer_patch_port', default='patch-ex',
help=_("Peer patch port in integration bridge for external "
"bridge.")),
cfg.StrOpt('ex_peer_patch_port', default='patch-int',
help=_("Peer patch port in external bridge for integration "
"bridge.")),
cfg.IntOpt('send_arp_interval', default=5,
help=_("Polling interval for arp request in seconds"))
]
FIP_GW_RESOLVING_STATUS = 'resolving'
class DNATApp(DFlowApp):
def __init__(self, *args, **kwargs):
super(DNATApp, self).__init__(*args, **kwargs)
self.vswitch_api = kwargs['vswitch_api']
self.nb_api = kwargs['nb_api']
cfg.CONF.register_opts(DF_DNAT_APP_OPTS, group='df_dnat_app')
self.external_network_bridge = \
cfg.CONF.df_dnat_app.external_network_bridge
self.integration_bridge = cfg.CONF.df.integration_bridge
self.int_peer_patch_port = cfg.CONF.df_dnat_app.int_peer_patch_port
self.ex_peer_patch_port = cfg.CONF.df_dnat_app.ex_peer_patch_port
self.send_arp_interval = cfg.CONF.df_dnat_app.send_arp_interval
self.external_networks = collections.defaultdict(int)
self.local_floatingips = collections.defaultdict(str)
def switch_features_handler(self, ev):
self._init_external_bridge()
self._init_external_network_bridge_check()
self._install_output_to_physical_patch(self.external_ofport)
def _check_for_external_network_bridge_mac(self):
idl = self.vswitch_api.idl
if not idl:
return
interface = idlutils.row_by_value(
idl,
'Interface',
'name',
self.external_network_bridge,
None,
)
if not interface:
return
if not interface.mac_in_use[0]:
return
if interface.mac_in_use[0] == '00:00:00:00:00:00':
return
return interface.mac_in_use[0]
def _wait_for_external_network_bridge_mac(self):
mac = self._check_for_external_network_bridge_mac()
if not mac:
return
for key, floatingip in six.iteritems(self.local_floatingips):
self._install_dnat_egress_rules(floatingip, mac)
raise loopingcall.LoopingCallDone()
def _init_external_network_bridge_check(self):
"""Spawn a thread to check that br-ex is ready."""
periodic = loopingcall.FixedIntervalLoopingCall(
self._wait_for_external_network_bridge_mac)
periodic.start(interval=self.send_arp_interval)
def _init_external_bridge(self):
self.external_ofport = self.vswitch_api.create_patch_port(
self.integration_bridge,
self.ex_peer_patch_port,
self.int_peer_patch_port)
self.vswitch_api.create_patch_port(
self.external_network_bridge,
self.int_peer_patch_port,
self.ex_peer_patch_port)
def _increase_external_network_count(self, network_id):
self.external_networks[network_id] += 1
def _decrease_external_network_count(self, network_id):
self.external_networks[network_id] -= 1
def _get_external_network_count(self, network_id):
return self.external_networks[network_id]
def _is_first_external_network(self, network_id):
if self._get_external_network_count(network_id) == 0:
# check whether there are other networks
for key, val in six.iteritems(self.external_networks):
if key != network_id and val > 0:
return False
return True
return False
def _is_last_external_network(self, network_id):
if self._get_external_network_count(network_id) == 1:
# check whether there are other networks
for key, val in six.iteritems(self.external_networks):
if key != network_id and val > 0:
return False
return True
return False
def _get_match_arp_reply(self, arp_tpa, arp_spa, network_id=None):
parser = self.get_datapath().ofproto_parser
match = parser.OFPMatch()
match.set_dl_type(ether.ETH_TYPE_ARP)
match.set_arp_tpa(utils.ipv4_text_to_int(str(arp_tpa)))
match.set_arp_spa(utils.ipv4_text_to_int(str(arp_spa)))
match.set_arp_opcode(arp.ARP_REPLY)
if network_id is not None:
match.set_metadata(network_id)
return match
def _install_floatingip_arp_responder(self, floatingip):
# install floatingip arp responder flow rules
if netaddr.IPAddress(floatingip.get_ip_address()).version != 4:
return
ArpResponder(self.get_datapath(),
None,
floatingip.get_ip_address(),
floatingip.get_mac_address(),
const.INGRESS_NAT_TABLE).add()
def _remove_floatingip_arp_responder(self, floatingip):
# install floatingip arp responder flow rules
if netaddr.IPAddress(floatingip.get_ip_address()).version != 4:
return
ArpResponder(self.get_datapath(),
None,
floatingip.get_ip_address(),
floatingip.get_mac_address(),
const.INGRESS_NAT_TABLE).remove()
def _get_vm_port_info(self, floatingip):
lport = self.db_store.get_local_port(
floatingip.get_lport_id())
mac = lport.get_mac()
ip = lport.get_ip()
tunnel_key = lport.get_tunnel_key()
network_id = lport.get_external_value('local_network_id')
net_id = lport.get_lswitch_id()
segmentation_id = self.db_store.get_network_id(
net_id,
)
return (mac, ip, tunnel_key, network_id, segmentation_id)
def _install_dnat_ingress_rules(self, floatingip):
parser = self.get_datapath().ofproto_parser
ofproto = self.get_datapath().ofproto
match = parser.OFPMatch(eth_type=ether.ETH_TYPE_IP,
ipv4_dst=floatingip.get_ip_address())
vm_mac, vm_ip, vm_tunnel_key, network_id, _ = \
self._get_vm_port_info(floatingip)
fip_mac = floatingip.get_mac_address()
actions = [
parser.OFPActionSetField(eth_src=fip_mac),
parser.OFPActionSetField(eth_dst=vm_mac),
parser.OFPActionDecNwTtl(),
parser.OFPActionSetField(ipv4_dst=vm_ip),
parser.OFPActionSetField(reg7=vm_tunnel_key),
parser.OFPActionSetField(metadata=network_id)
]
action_inst = parser.OFPInstructionActions(
ofproto.OFPIT_APPLY_ACTIONS, actions)
goto_inst = parser.OFPInstructionGotoTable(
const.INGRESS_CONNTRACK_TABLE)
inst = [action_inst, goto_inst]
self.mod_flow(
self.get_datapath(),
inst=inst,
table_id=const.INGRESS_NAT_TABLE,
priority=const.PRIORITY_MEDIUM,
match=match)
def _remove_dnat_ingress_rules(self, floatingip):
parser = self.get_datapath().ofproto_parser
ofproto = self.get_datapath().ofproto
match = parser.OFPMatch(eth_type=ether.ETH_TYPE_IP,
ipv4_dst=floatingip.get_ip_address())
self.mod_flow(
self.get_datapath(),
command=ofproto.OFPFC_DELETE,
table_id=const.INGRESS_NAT_TABLE,
priority=const.PRIORITY_MEDIUM,
match=match)
def _get_dnat_egress_match(self, floatingip):
_, vm_ip, _, _, segmentation_id = self._get_vm_port_info(floatingip)
parser = self.get_datapath().ofproto_parser
match = parser.OFPMatch(eth_type=ether.ETH_TYPE_IP,
metadata=segmentation_id,
ipv4_src=vm_ip)
return match
def _install_dnat_egress_rules(self, floatingip, network_bridge_mac):
fip_mac = floatingip.get_mac_address()
fip_ip = floatingip.get_ip_address()
parser = self.get_datapath().ofproto_parser
ofproto = self.get_datapath().ofproto
match = self._get_dnat_egress_match(floatingip)
actions = [
parser.OFPActionSetField(eth_src=fip_mac),
parser.OFPActionSetField(eth_dst=network_bridge_mac),
parser.OFPActionSetField(ipv4_src=fip_ip)]
action_inst = parser.OFPInstructionActions(
ofproto.OFPIT_APPLY_ACTIONS, actions)
goto_inst = parser.OFPInstructionGotoTable(const.EGRESS_EXTERNAL_TABLE)
inst = [action_inst, goto_inst]
self.mod_flow(
self.get_datapath(),
inst=inst,
table_id=const.EGRESS_NAT_TABLE,
priority=const.PRIORITY_MEDIUM,
match=match)
self.update_floatingip_status(
floatingip, n_const.FLOATINGIP_STATUS_ACTIVE)
def _remove_dnat_egress_rules(self, floatingip):
ofproto = self.get_datapath().ofproto
match = self._get_dnat_egress_match(floatingip)
self.mod_flow(
self.get_datapath(),
command=ofproto.OFPFC_DELETE,
table_id=const.EGRESS_NAT_TABLE,
priority=const.PRIORITY_MEDIUM,
match=match)
def _install_egress_nat_rules(self, floatingip):
net = netaddr.IPNetwork(floatingip.get_external_cidr())
if net.version != 4:
return
match = self._get_dnat_egress_match(floatingip)
self.add_flow_go_to_table(self.get_datapath(),
const.L3_LOOKUP_TABLE,
const.PRIORITY_MEDIUM,
const.EGRESS_NAT_TABLE,
match=match)
mac = self._check_for_external_network_bridge_mac()
if mac:
self._install_dnat_egress_rules(floatingip, mac)
def _remove_egress_nat_rules(self, floatingip):
net = netaddr.IPNetwork(floatingip.get_external_cidr())
if net.version != 4:
return
ofproto = self.get_datapath().ofproto
match = self._get_dnat_egress_match(floatingip)
self.mod_flow(
self.get_datapath(),
command=ofproto.OFPFC_DELETE,
table_id=const.L3_LOOKUP_TABLE,
priority=const.PRIORITY_MEDIUM,
match=match)
self._remove_dnat_egress_rules(floatingip)
def _install_ingress_nat_rules(self, floatingip):
network_id = floatingip.get_floating_network_id()
# TODO(Fei Rao) check the network type
if self._is_first_external_network(network_id):
# if it is the first floating ip on this node, then
# install the common goto flow rule.
parser = self.get_datapath().ofproto_parser
match = parser.OFPMatch()
match.set_in_port(self.external_ofport)
self.add_flow_go_to_table(self.get_datapath(),
const.INGRESS_CLASSIFICATION_DISPATCH_TABLE,
const.PRIORITY_DEFAULT,
const.INGRESS_NAT_TABLE,
match=match)
self._install_floatingip_arp_responder(floatingip)
self._install_dnat_ingress_rules(floatingip)
self._increase_external_network_count(network_id)
def _remove_ingress_nat_rules(self, floatingip):
network_id = floatingip.get_floating_network_id()
if self._is_last_external_network(network_id):
# if it is the last floating ip on this node, then
# remove the common goto flow rule.
parser = self.get_datapath().ofproto_parser
ofproto = self.get_datapath().ofproto
match = parser.OFPMatch()
match.set_in_port(self.external_ofport)
self.mod_flow(
self.get_datapath(),
command=ofproto.OFPFC_DELETE,
table_id=const.INGRESS_CLASSIFICATION_DISPATCH_TABLE,
priority=const.PRIORITY_DEFAULT,
match=match)
self._remove_floatingip_arp_responder(floatingip)
self._remove_dnat_ingress_rules(floatingip)
self._decrease_external_network_count(network_id)
def update_floatingip_status(self, floatingip, status):
floatingip.update_fip_status(status)
self.nb_api.update_floatingip(id=floatingip.get_id(),
topic=floatingip.get_topic(),
notify=False,
status=status)
def associate_floatingip(self, floatingip):
self.local_floatingips[floatingip.get_id()] = floatingip
self._install_ingress_nat_rules(floatingip)
self._install_egress_nat_rules(floatingip)
def disassociate_floatingip(self, floatingip):
self.local_floatingips.pop(floatingip.get_id(), 0)
self.delete_floatingip(floatingip)
self.update_floatingip_status(
floatingip, n_const.FLOATINGIP_STATUS_DOWN)
def remove_local_port(self, lport):
port_id = lport.get_id()
ips_to_disassociate = [
fip for fip in six.itervalues(self.local_floatingips)
if fip.get_lport_id() == port_id]
for floatingip in ips_to_disassociate:
self.disassociate_floatingip(floatingip)
def update_bridge_port(self, lport):
port_name = lport.get_name()
if port_name != self.external_network_bridge:
return
mac = self._check_for_external_network_bridge_mac()
if not mac:
return
for key, floatingip in six.iteritems(self.local_floatingips):
self._install_dnat_egress_rules(floatingip, mac)
def delete_floatingip(self, floatingip):
self._remove_ingress_nat_rules(floatingip)
self._remove_egress_nat_rules(floatingip)
def update_logical_switch(self, lswitch):
fip_groups = self.db_store.check_and_update_floatingips(
lswitch)
if not fip_groups:
return
for fip_group in fip_groups:
fip, old_fip = fip_group
# save to df db
self.nb_api.update_floatingip(
id=fip.get_id(),
topic=fip.get_topic(),
notify=False,
external_gateway_ip=fip.get_external_gateway_ip())
def _install_output_to_physical_patch(self, ofport):
parser = self.get_datapath().ofproto_parser
ofproto = self.get_datapath().ofproto
actions = [parser.OFPActionOutput(ofport,
ofproto.OFPCML_NO_BUFFER)]
actions_inst = parser.OFPInstructionActions(
ofproto.OFPIT_APPLY_ACTIONS, actions)
inst = [actions_inst]
self.mod_flow(self.get_datapath(), inst=inst,
table_id=const.EGRESS_EXTERNAL_TABLE,
priority=const.PRIORITY_MEDIUM, match=None)