""" # 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()