+"""
+This plugin sets up dnsmasq and iptables to support the "Private-Nat"
+and "Public" network models for OpenCloud. It communicates with OvS
+on the local node and Quantum to gather information about the virtual
+interfaces instantiated by Quantum. It uses this information to:
+
+* add the Quantum-assigned IP address to the vif via dnsmasq
+* set up port forwarding rules through the NAT using iptables
+
+The iptables configuration uses a chain called 'planetstack-net' to
+hold the port forwarding rules. This is called from the PREROUTING
+chain of the nat table. The chain is flushed and rebuilt every time
+the plugin runs to avoid stale rules. This plugin also sets up the
+MASQ rule in the POSTROUTING chain.
+"""
+
# system provided modules
import os, string, time, socket
from socket import inet_aton
plugin = "planetstack-net"
+nat_net_name = "nat-net"
+nat_net_id = None
+site_net_id = None
+
+quantum_auth_url = None
+quantum_username = None
+quantum_password = None
+quantum_tenant_name = None
+
+
# Helper functions for converting to CIDR notation
def get_net_size(netmask):
binary_str = ''
return '.'.join(start) + ',' + '.'.join(end)
+# Should possibly be using python-iptables for this stuff
+def run_iptables_cmd(args):
+ cmd = ['/sbin/iptables'] + args
+ logger.log('%s: %s' % (plugin, ' '.join(cmd)))
+ subprocess.check_call(cmd)
+
+def add_iptables_rule(table, chain, args, pos = None):
+ iptargs = ['-t', table, '-C', chain] + args
+ try:
+ run_iptables_cmd(iptargs)
+ except:
+ if pos:
+ iptargs = ['-t', table, '-I', chain, str(pos)] + args
+ else:
+ iptargs[2] = '-A'
+ try:
+ run_iptables_cmd(iptargs)
+ except:
+ logger.log('%s: FAILED to add iptables rule' % plugin)
+
+def reset_iptables_chain():
+ try:
+ # Flush the planetstack-nat chain
+ run_iptables_cmd(['-t', 'nat', '-F', plugin])
+ except:
+ # Probably the chain doesn't exist, try creating it
+ run_iptables_cmd(['-t', 'nat', '-N', plugin])
+
+ add_iptables_rule('nat', 'PREROUTING', ['-j', plugin])
+
+# Nova blocks packets from external addresses by default.
+# This is hacky but it gets around the issue.
+def unfilter_ipaddr(dev, ipaddr):
+ add_iptables_rule(table = 'filter',
+ chain = 'nova-compute-sg-fallback',
+ args = ['-d', ipaddr, '-j', 'ACCEPT'],
+ pos = 1)
+
# Enable iptables MASQ for a device
def add_iptables_masq(dev, interface):
ipaddr = interface['ip']
% (plugin, ipaddr, netmask))
return
- cmd = ['/sbin/iptables', '-t', 'nat', '-C', 'POSTROUTING', '-s', cidr,
- '!', '-d', cidr, '-j', 'MASQUERADE']
- try:
- logger.log('%s: checking if NAT iptables rule present for device %s' % (plugin, dev))
- subprocess.check_call(cmd)
- except:
- logger.log('%s: adding NAT iptables NAT for device %s' % (plugin, dev))
- cmd[3] = '-A'
- try:
- subprocess.check_call(cmd)
- except:
- logger.log('%s: FAILED to add NAT iptables rule for device %s' % (plugin, dev))
+ args = ['-s', cidr, '!', '-d', cidr, '-j', 'MASQUERADE']
+ add_iptables_rule('nat', 'POSTROUTING', args)
def get_pidfile(dev):
return '/var/run/dnsmasq-%s.pid' % dev
except:
logger.log("%s: Sending SIGHUP to dnsmasq FAILED on dev %s" % (plugin, dev))
-# Enable dnsmasq for this interface
-def start_dnsmasq(dev, interface):
+# Enable dnsmasq for this interface.
+# It's possible that we could get by with a single instance of dnsmasq running on
+# all devices but I haven't tried it.
+def start_dnsmasq(dev, interface, forward_dns=True):
if not dnsmasq_running(dev):
+ # The '--dhcp-range=<IP addr>,static' argument to dnsmasq ensures that it only
+ # hands out IP addresses to clients listed in the hostsfile
+ cmd = ['/usr/sbin/dnsmasq',
+ '--strict-order',
+ '--bind-interfaces',
+ '--local=//',
+ '--domain-needed',
+ '--pid-file=%s' % get_pidfile(dev),
+ '--conf-file=',
+ '--interface=%s' % dev,
+ '--except-interface=lo',
+ '--dhcp-leasefile=%s' % get_leasefile(dev),
+ '--dhcp-hostsfile=%s' % get_hostsfile(dev),
+ '--dhcp-no-override',
+ '--dhcp-range=%s,static' % interface['ip']]
+
+ # Turn off forwarding DNS queries, only do DHCP
+ if forward_dns == False:
+ cmd.append('--port=0')
+
try:
logger.log('%s: starting dnsmasq on device %s' % (plugin, dev))
- iprange = ipaddr_range(interface['network'], interface['broadcast'])
- logger.log('%s: IP range: %s' % (plugin, iprange))
- subprocess.check_call(['/usr/sbin/dnsmasq',
- '--strict-order',
- '--bind-interfaces',
- '--local=//',
- '--domain-needed',
- '--pid-file=%s' % get_pidfile(dev),
- '--conf-file=',
- '--interface=%s' % dev,
- '--dhcp-range=%s,120' % iprange,
- '--dhcp-leasefile=%s' % get_leasefile(dev),
- '--dhcp-hostsfile=%s' % get_hostsfile(dev),
- '--dhcp-no-override'])
+ subprocess.check_call(cmd)
except:
logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev))
-
-nat_net_name = "nat-net"
-nat_net_id = None
-quantum_auth_url = "http://viccidev1:5000/v2.0/"
-quantum_username = None
-quantum_password = None
-quantum_tenant_name = None
+ logger.log(' '.join(cmd))
def convert_ovs_output_to_dict(out):
decoded = json.loads(out.strip())
return records
-# Generate a dhcp-hostsfile for dnsmasq for all the local interfaces
-# on the NAT network. The purpose is to make sure that the IP address
-# assigned by Quantum appears on NAT interfaces.
-#
-# Workflow:
-# - Get list of local VM interfaces
-# - Query Quantum to get port information for these interfaces
-# - Throw away all those not belonging to nat-net network
-# - Write the MAC addr and IPs to the dhcp-hostsfile file (and log it)
-
-def write_hostsfile(dev):
+
+# Get a list of local VM interfaces and then query Quantum to get
+# Port records for these interfaces.
+def get_local_quantum_ports():
+ ports = []
+
# Get local information for VM interfaces from OvS
ovs_out = subprocess.check_output(['/usr/bin/ovs-vsctl', '-f', 'json', 'find',
'Interface', 'external_ids:iface-id!="absent"'])
records = convert_ovs_output_to_dict(ovs_out)
- # Extract Quantum Port IDs from OvS records
- port_ids = []
- for rec in records:
- port_ids.append(rec['external_ids']['iface-id'])
+ if records:
+ # Extract Quantum Port IDs from OvS records
+ port_ids = []
+ for rec in records:
+ port_ids.append(rec['external_ids']['iface-id'])
- # Get the full info on these ports from Quantum
- quantum = client.Client(username=quantum_username,
- password=quantum_password,
- tenant_name=quantum_tenant_name,
- auth_url=quantum_auth_url)
- ports = quantum.list_ports(id=port_ids)
+ # Get the full info on these ports from Quantum
+ quantum = client.Client(username=quantum_username,
+ password=quantum_password,
+ tenant_name=quantum_tenant_name,
+ auth_url=quantum_auth_url)
+ ports = quantum.list_ports(id=port_ids)['ports']
+
+ return ports
- # Write relevant entries to dnsmasq hostsfile
+
+# Generate a dhcp-hostsfile for dnsmasq. The purpose is to make sure
+# that the IP address assigned by Quantum appears on NAT interface.
+def write_dnsmasq_hostsfile(dev, ports, net_id):
logger.log("%s: Writing hostsfile for %s" % (plugin, dev))
f = open(get_hostsfile(dev), 'w')
- for port in ports['ports']:
- if port['network_id'] == nat_net_id:
+ for port in ports:
+ if port['network_id'] == net_id:
entry = "%s,%s\n" % (port['mac_address'], port['fixed_ips'][0]['ip_address'])
f.write(entry)
logger.log("%s: %s" % (plugin, entry))
# Send SIGHUP to dnsmasq to make it re-read hostsfile
dnsmasq_sighup(dev)
+# Set up iptables rules in the 'planetstack-net' chain based on
+# the nat:forward_ports field in the Port record.
+def set_up_port_forwarding(dev, ports):
+ for port in ports:
+ if port['network_id'] == nat_net_id and port['nat:forward_ports']:
+ for fw in port['nat:forward_ports']:
+ ipaddr = port['fixed_ips'][0]['ip_address']
+ protocol = fw['l4_protocol']
+ fwport = fw['l4_port']
+ # logger.log("%s: fwd port %s/%s to %s" % (plugin, protocol, fwport, ipaddr))
+
+ unfilter_ipaddr(dev, ipaddr)
+ # Shouldn't hardcode br-eth0 here. Maybe use '-d <node IP address>'?
+ add_iptables_rule('nat', plugin, ['-i', 'br-eth0',
+ '-p', protocol, '--dport', fwport,
+ '-j', 'DNAT', '--to-destination', ipaddr])
+
+def get_net_id_by_name(name):
+ quantum = client.Client(username=quantum_username,
+ password=quantum_password,
+ tenant_name=quantum_tenant_name,
+ auth_url=quantum_auth_url)
+
+ net = quantum.list_networks(name=name)
+ return net['networks'][0]['id']
+
def start():
global quantum_username
global quantum_password
global quantum_tenant_name
- global nat_net_id
+ global quantum_auth_url
logger.log("%s: plugin starting up..." % plugin)
quantum_username = parser.get("DEFAULT", "quantum_admin_username")
quantum_password = parser.get("DEFAULT", "quantum_admin_password")
quantum_tenant_name = parser.get("DEFAULT", "quantum_admin_tenant_name")
+ quantum_auth_url = parser.get("DEFAULT", "quantum_admin_auth_url")
- quantum = client.Client(username=quantum_username,
- password=quantum_password,
- tenant_name=quantum_tenant_name,
- auth_url=quantum_auth_url)
+def GetSlivers(data, config=None, plc=None):
+ global nat_net_id
+ global site_net_id
- net = quantum.list_networks(name=nat_net_name)
- nat_net_id = net['networks'][0]['id']
+ if not nat_net_id:
+ try:
+ nat_net_id = get_net_id_by_name(nat_net_name)
+ logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
+ except:
+ logger.log("%s: no network called %s..." % (plugin, nat_net_name))
- logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
-def GetSlivers(data, config=None, plc=None):
+ # Fix me
+ # site_net_name = "devel"
+ site_net_name = "sharednet1"
+ if not site_net_id:
+ try:
+ site_net_id = get_net_id_by_name(site_net_name)
+ logger.log("%s: %s id is %s..." % (plugin, site_net_name, site_net_id))
+ except:
+ logger.log("%s: no network called %s..." % (plugin, site_net_name))
+
+ reset_iptables_chain()
+ ports = get_local_quantum_ports()
+
for interface in data['interfaces']:
try:
settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
continue
logger.log('%s: Processing device %s' % (plugin, dev))
-
- if 'NAT' in tags:
+
+ # Process Private-Nat networks
+ if 'NAT' in tags and nat_net_id:
add_iptables_masq(dev, interface)
- write_hostsfile(dev)
+ write_dnsmasq_hostsfile(dev, ports, nat_net_id)
+ set_up_port_forwarding(dev, ports)
start_dnsmasq(dev, interface)
+
+ # Process Public networks
+ if interface['is_primary'] and site_net_id:
+ if 'OVS_BRIDGE' in tags:
+ dev = tags['OVS_BRIDGE']
+ write_dnsmasq_hostsfile(dev, ports, site_net_id)
+ start_dnsmasq(dev, interface, forward_dns=False)
+