2 This plugin sets up dnsmasq and iptables to support the "Private-Nat"
3 and "Public" network models for OpenCloud. It communicates with OvS
4 on the local node and Quantum to gather information about the virtual
5 interfaces instantiated by Quantum. It uses this information to:
7 * add the Quantum-assigned IP address to the vif via dnsmasq
8 * set up port forwarding rules through the NAT using iptables
10 The iptables configuration uses a chain called 'planetstack-net' to
11 hold the port forwarding rules. This is called from the PREROUTING
12 chain of the nat table. The chain is flushed and rebuilt every time
13 the plugin runs to avoid stale rules. This plugin also sets up the
14 MASQ rule in the POSTROUTING chain.
17 # system provided modules
18 import os, string, time, socket
19 from socket import inet_aton
20 import subprocess, signal
22 from ConfigParser import ConfigParser
24 # PlanetLab system modules
28 from quantumclient.v2_0 import client
33 plugin = "planetstack-net"
35 nat_net_name = "nat-net"
39 quantum_auth_url = None
40 quantum_username = None
41 quantum_password = None
42 quantum_tenant_name = None
45 # Helper functions for converting to CIDR notation
46 def get_net_size(netmask):
49 binary_str += bin(int(octet))[2:].zfill(8)
50 return str(len(binary_str.rstrip('0')))
52 def to_cidr(ipaddr, netmask):
57 ipaddr = ipaddr.split('.')
58 netmask = netmask.split('.')
60 net_start = [str(int(ipaddr[x]) & int(netmask[x])) for x in range(0,4)]
61 return '.'.join(net_start) + '/' + get_net_size(netmask)
63 def ipaddr_range(network, broadcast):
64 start = network.split('.')
65 end = broadcast.split('.')
67 # Assume interface always claims the first address in the block
68 start[3] = str(int(start[3]) + 2)
69 end[3] = str(int(end[3]) - 1)
71 return '.'.join(start) + ',' + '.'.join(end)
73 # Should possibly be using python-iptables for this stuff
74 def run_iptables_cmd(args):
75 cmd = ['/sbin/iptables'] + args
76 logger.log('%s: %s' % (plugin, ' '.join(cmd)))
77 subprocess.check_call(cmd)
79 def add_iptables_rule(table, chain, args, pos = None):
80 iptargs = ['-t', table, '-C', chain] + args
82 run_iptables_cmd(iptargs)
85 iptargs = ['-t', table, '-I', chain, str(pos)] + args
89 run_iptables_cmd(iptargs)
91 logger.log('%s: FAILED to add iptables rule' % plugin)
93 def reset_iptables_chain():
95 # Flush the planetstack-nat chain
96 run_iptables_cmd(['-t', 'nat', '-F', plugin])
98 # Probably the chain doesn't exist, try creating it
99 run_iptables_cmd(['-t', 'nat', '-N', plugin])
101 add_iptables_rule('nat', 'PREROUTING', ['-j', plugin])
103 # Nova blocks packets from external addresses by default.
104 # This is hacky but it gets around the issue.
105 def unfilter_ipaddr(dev, ipaddr):
106 add_iptables_rule(table = 'filter',
107 chain = 'nova-compute-sg-fallback',
108 args = ['-d', ipaddr, '-j', 'ACCEPT'],
111 # Enable iptables MASQ for a device
112 def add_iptables_masq(dev, interface):
113 ipaddr = interface['ip']
114 netmask = interface['netmask']
118 cidr = to_cidr(ipaddr, netmask)
120 logger.log('%s: could not convert ipaddr %s and netmask %s to CIDR'
121 % (plugin, ipaddr, netmask))
124 args = ['-s', cidr, '!', '-d', cidr, '-j', 'MASQUERADE']
125 add_iptables_rule('nat', 'POSTROUTING', args)
127 def get_pidfile(dev):
128 return '/var/run/dnsmasq-%s.pid' % dev
130 def get_leasefile(dev):
131 return '/var/lib/dnsmasq/%s.leases' % dev
133 def get_hostsfile(dev):
134 return '/var/lib/dnsmasq/%s.hosts' % dev
136 # Check if dnsmasq already running
137 def dnsmasq_running(dev):
138 pidfile = get_pidfile(dev)
140 pid = open(pidfile, 'r').read().strip()
141 if os.path.exists('/proc/%s' % pid):
147 def dnsmasq_sighup(dev):
148 pidfile = get_pidfile(dev)
150 pid = open(pidfile, 'r').read().strip()
151 if os.path.exists('/proc/%s' % pid):
152 os.kill(int(pid), signal.SIGHUP)
153 logger.log("%s: Sent SIGHUP to dnsmasq on dev %s" % (plugin, dev))
155 logger.log("%s: Sending SIGHUP to dnsmasq FAILED on dev %s" % (plugin, dev))
157 # Enable dnsmasq for this interface.
158 # It's possible that we could get by with a single instance of dnsmasq running on
159 # all devices but I haven't tried it.
160 def start_dnsmasq(dev, interface, forward_dns=True):
161 if not dnsmasq_running(dev):
162 # The '--dhcp-range=<IP addr>,static' argument to dnsmasq ensures that it only
163 # hands out IP addresses to clients listed in the hostsfile
164 cmd = ['/usr/sbin/dnsmasq',
169 '--pid-file=%s' % get_pidfile(dev),
171 '--interface=%s' % dev,
172 '--except-interface=lo',
173 '--dhcp-leasefile=%s' % get_leasefile(dev),
174 '--dhcp-hostsfile=%s' % get_hostsfile(dev),
175 '--dhcp-no-override',
176 '--dhcp-range=%s,static' % interface['ip']]
178 # Turn off forwarding DNS queries, only do DHCP
179 if forward_dns == False:
180 cmd.append('--port=0')
183 logger.log('%s: starting dnsmasq on device %s' % (plugin, dev))
184 subprocess.check_call(cmd)
186 logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev))
187 logger.log(' '.join(cmd))
189 def convert_ovs_output_to_dict(out):
190 decoded = json.loads(out.strip())
191 headings = decoded['headings']
192 data = decoded['data']
197 for i in range(0, len(headings) - 1):
198 if not isinstance(rec[i], list):
199 mydict[headings[i]] = rec[i]
201 if rec[i][0] == 'set':
202 mydict[headings[i]] = rec[i][1]
203 elif rec[i][0] == 'map':
205 for (key, value) in rec[i][1]:
207 mydict[headings[i]] = newdict
208 elif rec[i][0] == 'uuid':
209 mydict['uuid'] = rec[i][1]
210 records.append(mydict)
215 # Get a list of local VM interfaces and then query Quantum to get
216 # Port records for these interfaces.
217 def get_local_quantum_ports():
220 # Get local information for VM interfaces from OvS
221 ovs_out = subprocess.check_output(['/usr/bin/ovs-vsctl', '-f', 'json', 'find',
222 'Interface', 'external_ids:iface-id!="absent"'])
223 records = convert_ovs_output_to_dict(ovs_out)
226 # Extract Quantum Port IDs from OvS records
229 port_ids.append(rec['external_ids']['iface-id'])
231 # Get the full info on these ports from Quantum
232 quantum = client.Client(username=quantum_username,
233 password=quantum_password,
234 tenant_name=quantum_tenant_name,
235 auth_url=quantum_auth_url)
236 ports = quantum.list_ports(id=port_ids)['ports']
241 # Generate a dhcp-hostsfile for dnsmasq. The purpose is to make sure
242 # that the IP address assigned by Quantum appears on NAT interface.
243 def write_dnsmasq_hostsfile(dev, ports, net_id):
244 logger.log("%s: Writing hostsfile for %s" % (plugin, dev))
245 f = open(get_hostsfile(dev), 'w')
247 if port['network_id'] == net_id:
248 entry = "%s,%s\n" % (port['mac_address'], port['fixed_ips'][0]['ip_address'])
250 logger.log("%s: %s" % (plugin, entry))
253 # Send SIGHUP to dnsmasq to make it re-read hostsfile
256 # Set up iptables rules in the 'planetstack-net' chain based on
257 # the nat:forward_ports field in the Port record.
258 def set_up_port_forwarding(dev, ports):
260 if port['network_id'] == nat_net_id and port['nat:forward_ports']:
261 for fw in port['nat:forward_ports']:
262 ipaddr = port['fixed_ips'][0]['ip_address']
263 protocol = fw['l4_protocol']
264 fwport = fw['l4_port']
265 # logger.log("%s: fwd port %s/%s to %s" % (plugin, protocol, fwport, ipaddr))
267 unfilter_ipaddr(dev, ipaddr)
268 # Shouldn't hardcode br-eth0 here. Maybe use '-d <node IP address>'?
269 add_iptables_rule('nat', plugin, ['-i', 'br-eth0',
270 '-p', protocol, '--dport', fwport,
271 '-j', 'DNAT', '--to-destination', ipaddr])
273 def get_net_id_by_name(name):
274 quantum = client.Client(username=quantum_username,
275 password=quantum_password,
276 tenant_name=quantum_tenant_name,
277 auth_url=quantum_auth_url)
279 net = quantum.list_networks(name=name)
280 return net['networks'][0]['id']
283 global quantum_username
284 global quantum_password
285 global quantum_tenant_name
286 global quantum_auth_url
288 logger.log("%s: plugin starting up..." % plugin)
290 parser = ConfigParser()
291 parser.read("/etc/nova/nova.conf")
292 quantum_username = parser.get("DEFAULT", "quantum_admin_username")
293 quantum_password = parser.get("DEFAULT", "quantum_admin_password")
294 quantum_tenant_name = parser.get("DEFAULT", "quantum_admin_tenant_name")
295 quantum_auth_url = parser.get("DEFAULT", "quantum_admin_auth_url")
297 def GetSlivers(data, config=None, plc=None):
303 nat_net_id = get_net_id_by_name(nat_net_name)
304 logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
306 logger.log("%s: no network called %s..." % (plugin, nat_net_name))
310 # site_net_name = "devel"
311 site_net_name = "sharednet1"
314 site_net_id = get_net_id_by_name(site_net_name)
315 logger.log("%s: %s id is %s..." % (plugin, site_net_name, site_net_id))
317 logger.log("%s: no network called %s..." % (plugin, site_net_name))
319 reset_iptables_chain()
320 ports = get_local_quantum_ports()
322 for interface in data['interfaces']:
324 settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
329 for setting in settings:
330 tags[setting['tagname'].upper()] = setting['value']
335 # Skip devices that don't have names
336 logger.log('%s: Device has no name, skipping...' % plugin)
339 logger.log('%s: Processing device %s' % (plugin, dev))
341 # Process Private-Nat networks
342 if 'NAT' in tags and nat_net_id:
343 add_iptables_masq(dev, interface)
344 write_dnsmasq_hostsfile(dev, ports, nat_net_id)
345 set_up_port_forwarding(dev, ports)
346 start_dnsmasq(dev, interface)
348 # Process Public networks
349 if interface['is_primary'] and site_net_id:
350 if 'OVS_BRIDGE' in tags:
351 dev = tags['OVS_BRIDGE']
352 write_dnsmasq_hostsfile(dev, ports, site_net_id)
353 start_dnsmasq(dev, interface, forward_dns=False)