3c8b8ce101
Fixes bug 1042104 The fix follows the patch that was done on the DHCP agent to enable the user to configure the usage of namespaces. In the event that namspaces are disabled the agent is limited to running only one router. The agent needs to define the router_id that is supported. The process in this case is: 1. create router 2. start agent with router id Change-Id: I2a71dc009c5aea285ff9f903b3faa99b0c9f820f
479 lines
20 KiB
Python
479 lines
20 KiB
Python
"""
|
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
#
|
|
# Copyright 2012 Nicira Networks, Inc. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
#
|
|
# @author: Dan Wendlandt, Nicira, Inc
|
|
#
|
|
"""
|
|
|
|
import logging
|
|
import sys
|
|
import time
|
|
|
|
import netaddr
|
|
|
|
from quantum.agent.common import config
|
|
from quantum.agent.linux import interface
|
|
from quantum.agent.linux import ip_lib
|
|
from quantum.agent.linux import iptables_manager
|
|
from quantum.agent.linux import utils as linux_utils
|
|
from quantum.db import l3_db
|
|
from quantum.openstack.common import cfg
|
|
from quantum.openstack.common import importutils
|
|
from quantumclient.v2_0 import client
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
NS_PREFIX = 'qrouter-'
|
|
INTERNAL_DEV_PREFIX = 'qr-'
|
|
EXTERNAL_DEV_PREFIX = 'qgw-'
|
|
|
|
|
|
class RouterInfo(object):
|
|
|
|
def __init__(self, router_id, root_helper, use_namespaces):
|
|
self.router_id = router_id
|
|
self.ex_gw_port = None
|
|
self.internal_ports = []
|
|
self.floating_ips = []
|
|
self.root_helper = root_helper
|
|
self.use_namespaces = use_namespaces
|
|
|
|
self.iptables_manager = iptables_manager.IptablesManager(
|
|
root_helper=root_helper,
|
|
#FIXME(danwent): use_ipv6=True,
|
|
namespace=self.ns_name())
|
|
|
|
def ns_name(self):
|
|
if self.use_namespaces:
|
|
return NS_PREFIX + self.router_id
|
|
|
|
|
|
class L3NATAgent(object):
|
|
|
|
OPTS = [
|
|
cfg.StrOpt('admin_user'),
|
|
cfg.StrOpt('admin_password'),
|
|
cfg.StrOpt('admin_tenant_name'),
|
|
cfg.StrOpt('auth_url'),
|
|
cfg.StrOpt('auth_strategy', default='keystone'),
|
|
cfg.StrOpt('auth_region'),
|
|
cfg.StrOpt('root_helper', default='sudo'),
|
|
cfg.StrOpt('external_network_bridge', default='br-ex',
|
|
help="Name of bridge used for external network traffic."),
|
|
cfg.StrOpt('interface_driver',
|
|
help="The driver used to manage the virtual interface."),
|
|
cfg.IntOpt('polling_interval',
|
|
default=3,
|
|
help="The time in seconds between state poll requests."),
|
|
cfg.StrOpt('metadata_ip', default='',
|
|
help="IP address used by Nova metadata server."),
|
|
cfg.IntOpt('metadata_port',
|
|
default=8775,
|
|
help="TCP Port used by Nova metadata server."),
|
|
#FIXME(danwent): not currently used
|
|
cfg.BoolOpt('send_arp_for_ha',
|
|
default=True,
|
|
help="Send gratuitious ARP when router IP is configured"),
|
|
cfg.BoolOpt('use_namespaces', default=True,
|
|
help="Allow overlapping IP."),
|
|
cfg.StrOpt('router_id', default='',
|
|
help="If namespaces is disabled, the l3 agent can only"
|
|
" confgure a router that has the matching router ID.")
|
|
]
|
|
|
|
def __init__(self, conf):
|
|
self.conf = conf
|
|
self.router_info = {}
|
|
|
|
if not conf.interface_driver:
|
|
LOG.error(_('You must specify an interface driver'))
|
|
sys.exit(1)
|
|
try:
|
|
self.driver = importutils.import_object(conf.interface_driver,
|
|
conf)
|
|
except:
|
|
LOG.exception(_("Error importing interface driver '%s'"
|
|
% conf.interface_driver))
|
|
sys.exit(1)
|
|
|
|
self.polling_interval = conf.polling_interval
|
|
|
|
if not ip_lib.device_exists(self.conf.external_network_bridge):
|
|
raise Exception("external network bridge '%s' does not exist"
|
|
% self.conf.external_network_bridge)
|
|
|
|
self.qclient = client.Client(
|
|
username=self.conf.admin_user,
|
|
password=self.conf.admin_password,
|
|
tenant_name=self.conf.admin_tenant_name,
|
|
auth_url=self.conf.auth_url,
|
|
auth_strategy=self.conf.auth_strategy,
|
|
auth_region=self.conf.auth_region
|
|
)
|
|
|
|
self._destroy_all_router_namespaces()
|
|
|
|
def _destroy_all_router_namespaces(self):
|
|
"""Destroy all router namespaces on the host to eliminate
|
|
all stale linux devices, iptables rules, and namespaces.
|
|
"""
|
|
root_ip = ip_lib.IPWrapper(self.conf.root_helper)
|
|
for ns in root_ip.get_namespaces(self.conf.root_helper):
|
|
if ns.startswith(NS_PREFIX):
|
|
try:
|
|
self._destroy_router_namespace(ns)
|
|
except:
|
|
LOG.exception("couldn't delete namespace '%s'" % ns)
|
|
|
|
def _destroy_router_namespace(self, namespace):
|
|
ns_ip = ip_lib.IPWrapper(self.conf.root_helper,
|
|
namespace=namespace)
|
|
for d in ns_ip.get_devices():
|
|
if d.name.startswith(INTERNAL_DEV_PREFIX):
|
|
# device is on default bridge
|
|
self.driver.unplug(d.name)
|
|
elif d.name.startswith(EXTERNAL_DEV_PREFIX):
|
|
self.driver.unplug(d.name,
|
|
bridge=self.conf.external_network_bridge)
|
|
if self.conf.use_namespaces:
|
|
ns_ip.netns.delete(namespace)
|
|
|
|
def _create_router_namespace(self, ri):
|
|
ip_wrapper_root = ip_lib.IPWrapper(self.conf.root_helper)
|
|
ip_wrapper_root.netns.add(ri.ns_name())
|
|
|
|
ip_wrapper = ip_lib.IPWrapper(self.conf.root_helper,
|
|
namespace=ri.ns_name())
|
|
ip_wrapper.netns.execute(['sysctl', '-w', 'net.ipv4.ip_forward=1'])
|
|
|
|
def daemon_loop(self):
|
|
#TODO(danwent): this simple diff logic does not handle if
|
|
# details of a router port (e.g., IP, mac) are changed behind
|
|
# our back. Will fix this properly with update notifications.
|
|
|
|
while True:
|
|
try:
|
|
self.do_single_loop()
|
|
except:
|
|
LOG.exception("Error running l3_nat daemon_loop")
|
|
|
|
time.sleep(self.polling_interval)
|
|
|
|
def do_single_loop(self):
|
|
prev_router_ids = set(self.router_info)
|
|
cur_router_ids = set()
|
|
|
|
# identify and update new or modified routers
|
|
for r in self.qclient.list_routers()['routers']:
|
|
#FIXME(danwent): handle admin state
|
|
|
|
# If namespaces are disabled, only process the router associated
|
|
# with the configured agent id.
|
|
if (self.conf.use_namespaces or
|
|
r['id'] == self.conf.router_id):
|
|
cur_router_ids.add(r['id'])
|
|
else:
|
|
continue
|
|
if r['id'] not in self.router_info:
|
|
self.router_info[r['id']] = RouterInfo(
|
|
r['id'], self.conf.root_helper, self.conf.use_namespaces)
|
|
if self.conf.use_namespaces:
|
|
self._create_router_namespace(self.router_info[r['id']])
|
|
|
|
ri = self.router_info[r['id']]
|
|
self.process_router(ri)
|
|
|
|
# identify and remove routers that no longer exist
|
|
for router_id in prev_router_ids - cur_router_ids:
|
|
ri = self.router_info[router_id]
|
|
del self.router_info[router_id]
|
|
self._destroy_router_namespace(ri.ns_name())
|
|
prev_router_ids = cur_router_ids
|
|
|
|
def _set_subnet_info(self, port):
|
|
ips = port['fixed_ips']
|
|
if not ips:
|
|
raise Exception("Router port %s has no IP address" % port['id'])
|
|
if len(ips) > 1:
|
|
LOG.error("Ignoring multiple IPs on router port %s" % port['id'])
|
|
port['subnet'] = self.qclient.show_subnet(
|
|
ips[0]['subnet_id'])['subnet']
|
|
prefixlen = netaddr.IPNetwork(port['subnet']['cidr']).prefixlen
|
|
port['ip_cidr'] = "%s/%s" % (ips[0]['ip_address'], prefixlen)
|
|
|
|
def process_router(self, ri):
|
|
|
|
ex_gw_port = self._get_ex_gw_port(ri)
|
|
|
|
internal_ports = self.qclient.list_ports(
|
|
device_id=ri.router_id,
|
|
device_owner=l3_db.DEVICE_OWNER_ROUTER_INTF)['ports']
|
|
|
|
existing_port_ids = set([p['id'] for p in ri.internal_ports])
|
|
current_port_ids = set([p['id'] for p in internal_ports
|
|
if p['admin_state_up']])
|
|
new_ports = [p for p in internal_ports if
|
|
p['id'] in current_port_ids and
|
|
p['id'] not in existing_port_ids]
|
|
old_ports = [p for p in ri.internal_ports if
|
|
p['id'] not in current_port_ids]
|
|
|
|
for p in new_ports:
|
|
self._set_subnet_info(p)
|
|
ri.internal_ports.append(p)
|
|
self.internal_network_added(ri, ex_gw_port,
|
|
p['network_id'], p['id'],
|
|
p['ip_cidr'], p['mac_address'])
|
|
|
|
for p in old_ports:
|
|
ri.internal_ports.remove(p)
|
|
self.internal_network_removed(ri, ex_gw_port, p['id'],
|
|
p['ip_cidr'])
|
|
|
|
internal_cidrs = [p['ip_cidr'] for p in ri.internal_ports]
|
|
|
|
if ex_gw_port and not ri.ex_gw_port:
|
|
self._set_subnet_info(ex_gw_port)
|
|
self.external_gateway_added(ri, ex_gw_port, internal_cidrs)
|
|
elif not ex_gw_port and ri.ex_gw_port:
|
|
self.external_gateway_removed(ri, ri.ex_gw_port,
|
|
internal_cidrs)
|
|
|
|
if ri.ex_gw_port or ex_gw_port:
|
|
self.process_router_floating_ips(ri, ex_gw_port)
|
|
|
|
ri.ex_gw_port = ex_gw_port
|
|
|
|
def process_router_floating_ips(self, ri, ex_gw_port):
|
|
floating_ips = self.qclient.list_floatingips(
|
|
router_id=ri.router_id)['floatingips']
|
|
existing_floating_ip_ids = set([fip['id'] for fip in ri.floating_ips])
|
|
cur_floating_ip_ids = set([fip['id'] for fip in floating_ips])
|
|
|
|
id_to_fixed_map = {}
|
|
|
|
for fip in floating_ips:
|
|
if fip['port_id']:
|
|
if fip['id'] not in existing_floating_ip_ids:
|
|
ri.floating_ips.append(fip)
|
|
self.floating_ip_added(ri, ex_gw_port,
|
|
fip['floating_ip_address'],
|
|
fip['fixed_ip_address'])
|
|
|
|
# store to see if floatingip was remapped
|
|
id_to_fixed_map[fip['id']] = fip['fixed_ip_address']
|
|
|
|
floating_ip_ids_to_remove = (existing_floating_ip_ids -
|
|
cur_floating_ip_ids)
|
|
for fip in ri.floating_ips:
|
|
if fip['id'] in floating_ip_ids_to_remove:
|
|
ri.floating_ips.remove(fip)
|
|
self.floating_ip_removed(ri, ri.ex_gw_port,
|
|
fip['floating_ip_address'],
|
|
fip['fixed_ip_address'])
|
|
else:
|
|
# handle remapping of a floating IP
|
|
cur_fixed_ip = id_to_fixed_map[fip['id']]
|
|
existing_fixed_ip = fip['fixed_ip_address']
|
|
if (cur_fixed_ip and existing_fixed_ip and
|
|
cur_fixed_ip != existing_fixed_ip):
|
|
floating_ip = fip['floating_ip_address']
|
|
self.floating_ip_removed(ri, ri.ex_gw_port,
|
|
floating_ip, existing_fixed_ip)
|
|
self.floating_ip_added(ri, ri.ex_gw_port,
|
|
floating_ip, cur_fixed_ip)
|
|
|
|
def _get_ex_gw_port(self, ri):
|
|
ports = self.qclient.list_ports(
|
|
device_id=ri.router_id,
|
|
device_owner=l3_db.DEVICE_OWNER_ROUTER_GW)['ports']
|
|
if not ports:
|
|
return None
|
|
elif len(ports) == 1:
|
|
return ports[0]
|
|
else:
|
|
LOG.error("Ignoring multiple gateway ports for router %s"
|
|
% ri.router_id)
|
|
|
|
def get_internal_device_name(self, port_id):
|
|
return (INTERNAL_DEV_PREFIX + port_id)[:self.driver.DEV_NAME_LEN]
|
|
|
|
def get_external_device_name(self, port_id):
|
|
return (EXTERNAL_DEV_PREFIX + port_id)[:self.driver.DEV_NAME_LEN]
|
|
|
|
def external_gateway_added(self, ri, ex_gw_port, internal_cidrs):
|
|
|
|
interface_name = self.get_external_device_name(ex_gw_port['id'])
|
|
ex_gw_ip = ex_gw_port['fixed_ips'][0]['ip_address']
|
|
if not ip_lib.device_exists(interface_name,
|
|
root_helper=self.conf.root_helper,
|
|
namespace=ri.ns_name()):
|
|
self.driver.plug(ex_gw_port['network_id'],
|
|
ex_gw_port['id'], interface_name,
|
|
ex_gw_port['mac_address'],
|
|
bridge=self.conf.external_network_bridge,
|
|
namespace=ri.ns_name(),
|
|
prefix=EXTERNAL_DEV_PREFIX)
|
|
self.driver.init_l3(interface_name, [ex_gw_port['ip_cidr']],
|
|
namespace=ri.ns_name())
|
|
|
|
gw_ip = ex_gw_port['subnet']['gateway_ip']
|
|
if ex_gw_port['subnet']['gateway_ip']:
|
|
cmd = ['route', 'add', 'default', 'gw', gw_ip]
|
|
ip_wrapper = ip_lib.IPWrapper(self.conf.root_helper,
|
|
namespace=ri.ns_name())
|
|
if self.conf.use_namespaces:
|
|
ip_wrapper.netns.execute(cmd)
|
|
|
|
for (c, r) in self.external_gateway_filter_rules():
|
|
ri.iptables_manager.ipv4['filter'].add_rule(c, r)
|
|
for (c, r) in self.external_gateway_nat_rules(ex_gw_ip,
|
|
internal_cidrs,
|
|
interface_name):
|
|
ri.iptables_manager.ipv4['nat'].add_rule(c, r)
|
|
ri.iptables_manager.apply()
|
|
|
|
def external_gateway_removed(self, ri, ex_gw_port, internal_cidrs):
|
|
|
|
interface_name = self.get_external_device_name(ex_gw_port['id'])
|
|
if ip_lib.device_exists(interface_name,
|
|
root_helper=self.conf.root_helper,
|
|
namespace=ri.ns_name()):
|
|
self.driver.unplug(interface_name)
|
|
|
|
ex_gw_ip = ex_gw_port['fixed_ips'][0]['ip_address']
|
|
for c, r in self.external_gateway_filter_rules():
|
|
ri.iptables_manager.ipv4['filter'].remove_rule(c, r)
|
|
for c, r in self.external_gateway_nat_rules(ex_gw_ip, internal_cidrs,
|
|
interface_name):
|
|
ri.iptables_manager.ipv4['nat'].remove_rule(c, r)
|
|
ri.iptables_manager.apply()
|
|
|
|
def external_gateway_filter_rules(self):
|
|
rules = []
|
|
if self.conf.metadata_ip:
|
|
rules.append(('INPUT', '-s 0.0.0.0/0 -d %s '
|
|
'-p tcp -m tcp --dport %s '
|
|
'-j ACCEPT' %
|
|
(self.conf.metadata_ip, self.conf.metadata_port)))
|
|
return rules
|
|
|
|
def external_gateway_nat_rules(self, ex_gw_ip, internal_cidrs,
|
|
interface_name):
|
|
rules = [('POSTROUTING', '! -i %(interface_name)s '
|
|
'! -o %(interface_name)s -m conntrack ! '
|
|
'--ctstate DNAT -j ACCEPT' % locals())]
|
|
if self.conf.metadata_ip:
|
|
rules.append(('PREROUTING', '-s 0.0.0.0/0 -d 169.254.169.254/32 '
|
|
'-p tcp -m tcp --dport 80 -j DNAT '
|
|
'--to-destination %s:%s' %
|
|
(self.conf.metadata_ip, self.conf.metadata_port)))
|
|
for cidr in internal_cidrs:
|
|
rules.extend(self.internal_network_nat_rules(ex_gw_ip, cidr))
|
|
return rules
|
|
|
|
def internal_network_added(self, ri, ex_gw_port, network_id, port_id,
|
|
internal_cidr, mac_address):
|
|
interface_name = self.get_internal_device_name(port_id)
|
|
if not ip_lib.device_exists(interface_name,
|
|
root_helper=self.conf.root_helper,
|
|
namespace=ri.ns_name()):
|
|
self.driver.plug(network_id, port_id, interface_name, mac_address,
|
|
namespace=ri.ns_name(),
|
|
prefix=INTERNAL_DEV_PREFIX)
|
|
|
|
self.driver.init_l3(interface_name, [internal_cidr],
|
|
namespace=ri.ns_name())
|
|
|
|
if ex_gw_port:
|
|
ex_gw_ip = ex_gw_port['fixed_ips'][0]['ip_address']
|
|
for c, r in self.internal_network_nat_rules(ex_gw_ip,
|
|
internal_cidr):
|
|
ri.iptables_manager.ipv4['nat'].add_rule(c, r)
|
|
ri.iptables_manager.apply()
|
|
|
|
def internal_network_removed(self, ri, ex_gw_port, port_id, internal_cidr):
|
|
interface_name = self.get_internal_device_name(port_id)
|
|
if ip_lib.device_exists(interface_name,
|
|
root_helper=self.conf.root_helper,
|
|
namespace=ri.ns_name()):
|
|
self.driver.unplug(interface_name)
|
|
|
|
if ex_gw_port:
|
|
ex_gw_ip = ex_gw_port['fixed_ips'][0]['ip_address']
|
|
for c, r in self.internal_network_nat_rules(ex_gw_ip,
|
|
internal_cidr):
|
|
ri.iptables_manager.ipv4['nat'].remove_rule(c, r)
|
|
ri.iptables_manager.apply()
|
|
|
|
def internal_network_nat_rules(self, ex_gw_ip, internal_cidr):
|
|
rules = [('snat', '-s %s -j SNAT --to-source %s' %
|
|
(internal_cidr, ex_gw_ip))]
|
|
if self.conf.metadata_ip:
|
|
rules.append(('POSTROUTING', '-s %s -d %s/32 -j ACCEPT' %
|
|
(internal_cidr, self.conf.metadata_ip)))
|
|
return rules
|
|
|
|
def floating_ip_added(self, ri, ex_gw_port, floating_ip, fixed_ip):
|
|
ip_cidr = str(floating_ip) + '/32'
|
|
interface_name = self.get_external_device_name(ex_gw_port['id'])
|
|
device = ip_lib.IPDevice(interface_name, self.conf.root_helper,
|
|
namespace=ri.ns_name())
|
|
|
|
if not ip_cidr in [addr['cidr'] for addr in device.addr.list()]:
|
|
net = netaddr.IPNetwork(ip_cidr)
|
|
device.addr.add(net.version, ip_cidr, str(net.broadcast))
|
|
|
|
for chain, rule in self.floating_forward_rules(floating_ip, fixed_ip):
|
|
ri.iptables_manager.ipv4['nat'].add_rule(chain, rule)
|
|
ri.iptables_manager.apply()
|
|
|
|
def floating_ip_removed(self, ri, ex_gw_port, floating_ip, fixed_ip):
|
|
ip_cidr = str(floating_ip) + '/32'
|
|
net = netaddr.IPNetwork(ip_cidr)
|
|
interface_name = self.get_external_device_name(ex_gw_port['id'])
|
|
|
|
device = ip_lib.IPDevice(interface_name, self.conf.root_helper,
|
|
namespace=ri.ns_name())
|
|
device.addr.delete(net.version, ip_cidr)
|
|
|
|
for chain, rule in self.floating_forward_rules(floating_ip, fixed_ip):
|
|
ri.iptables_manager.ipv4['nat'].remove_rule(chain, rule)
|
|
ri.iptables_manager.apply()
|
|
|
|
def floating_forward_rules(self, floating_ip, fixed_ip):
|
|
return [('PREROUTING', '-d %s -j DNAT --to %s' %
|
|
(floating_ip, fixed_ip)),
|
|
('OUTPUT', '-d %s -j DNAT --to %s' %
|
|
(floating_ip, fixed_ip)),
|
|
('float-snat', '-s %s -j SNAT --to %s' %
|
|
(fixed_ip, floating_ip))]
|
|
|
|
|
|
def main():
|
|
conf = config.setup_conf()
|
|
conf.register_opts(L3NATAgent.OPTS)
|
|
conf.register_opts(interface.OPTS)
|
|
conf(sys.argv)
|
|
config.setup_logging(conf)
|
|
|
|
mgr = L3NATAgent(conf)
|
|
mgr.daemon_loop()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|