2 This plugin sets up the NAT interfaces for PlanetStack. It processes
3 each interface that has a 'nat' tag set.
5 It communicates with OvS on the local node and Quantum to gather
6 information about devices. It uses this information to:
7 * add the Quantum-assigned IP address to the interface 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 = "http://viccidev1:5000/v2.0/"
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):
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']]
179 logger.log('%s: starting dnsmasq on device %s' % (plugin, dev))
180 subprocess.check_call(cmd)
182 logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev))
183 logger.log(' '.join(cmd))
185 def convert_ovs_output_to_dict(out):
186 decoded = json.loads(out.strip())
187 headings = decoded['headings']
188 data = decoded['data']
193 for i in range(0, len(headings) - 1):
194 if not isinstance(rec[i], list):
195 mydict[headings[i]] = rec[i]
197 if rec[i][0] == 'set':
198 mydict[headings[i]] = rec[i][1]
199 elif rec[i][0] == 'map':
201 for (key, value) in rec[i][1]:
203 mydict[headings[i]] = newdict
204 elif rec[i][0] == 'uuid':
205 mydict['uuid'] = rec[i][1]
206 records.append(mydict)
210 # Do all processing associated with Quantum ports. It first gets a
211 # list of local VM interfaces and then queries Quantum to get Port
212 # records for these interfaces. Then for all interfaces on the NAT
213 # network it does the following:
215 # 1) Generates a dhcp-hostsfile for dnsmasq. The purpose is to make
216 # sure that the IP address assigned by Quantum appears on NAT
219 # 2) Sets up iptables rules in the 'planetstack-net' chain based on
220 # the nat:forward_ports field in the Port record.
222 def get_local_quantum_ports():
225 # Get local information for VM interfaces from OvS
226 ovs_out = subprocess.check_output(['/usr/bin/ovs-vsctl', '-f', 'json', 'find',
227 'Interface', 'external_ids:iface-id!="absent"'])
228 records = convert_ovs_output_to_dict(ovs_out)
231 # Extract Quantum Port IDs from OvS records
234 port_ids.append(rec['external_ids']['iface-id'])
236 # Get the full info on these ports from Quantum
237 quantum = client.Client(username=quantum_username,
238 password=quantum_password,
239 tenant_name=quantum_tenant_name,
240 auth_url=quantum_auth_url)
241 ports = quantum.list_ports(id=port_ids)['ports']
246 def write_dnsmasq_hostsfile(dev, ports, net_id):
247 # Write relevant entries to dnsmasq hostsfile
248 logger.log("%s: Writing hostsfile for %s" % (plugin, dev))
249 f = open(get_hostsfile(dev), 'w')
251 if port['network_id'] == net_id:
252 entry = "%s,%s\n" % (port['mac_address'], port['fixed_ips'][0]['ip_address'])
254 logger.log("%s: %s" % (plugin, entry))
257 # Send SIGHUP to dnsmasq to make it re-read hostsfile
260 def set_up_port_forwarding(dev, ports):
261 # Set up iptables rules for port forwarding
263 if port['network_id'] == nat_net_id:
264 for fw in port['nat:forward_ports']:
265 ipaddr = port['fixed_ips'][0]['ip_address']
266 protocol = fw['l4_protocol']
267 fwport = fw['l4_port']
268 # logger.log("%s: fwd port %s/%s to %s" % (plugin, protocol, fwport, ipaddr))
270 unfilter_ipaddr(dev, ipaddr)
271 # Shouldn't hardcode br-eth0 here. Maybe use '-d <node IP address>'?
272 add_iptables_rule('nat', plugin, ['-i', 'br-eth0',
273 '-p', protocol, '--dport', fwport,
274 '-j', 'DNAT', '--to-destination', ipaddr])
276 def get_net_id_by_name(name):
277 quantum = client.Client(username=quantum_username,
278 password=quantum_password,
279 tenant_name=quantum_tenant_name,
280 auth_url=quantum_auth_url)
282 net = quantum.list_networks(name=name)
283 return net['networks'][0]['id']
286 global quantum_username
287 global quantum_password
288 global quantum_tenant_name
290 logger.log("%s: plugin starting up..." % plugin)
292 parser = ConfigParser()
293 parser.read("/etc/nova/nova.conf")
294 quantum_username = parser.get("DEFAULT", "quantum_admin_username")
295 quantum_password = parser.get("DEFAULT", "quantum_admin_password")
296 quantum_tenant_name = parser.get("DEFAULT", "quantum_admin_tenant_name")
299 def GetSlivers(data, config=None, plc=None):
305 nat_net_id = get_net_id_by_name(nat_net_name)
306 logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
308 logger.log("%s: no network called %s..." % (plugin, nat_net_name))
312 # site_net_name = "devel"
313 site_net_name = "sharednet1"
316 site_net_id = get_net_id_by_name(site_net_name)
317 logger.log("%s: %s id is %s..." % (plugin, site_net_name, site_net_id))
319 logger.log("%s: no network called %s..." % (plugin, site_net_name))
321 reset_iptables_chain()
322 ports = get_local_quantum_ports()
324 for interface in data['interfaces']:
326 settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
331 for setting in settings:
332 tags[setting['tagname'].upper()] = setting['value']
337 # Skip devices that don't have names
338 logger.log('%s: Device has no name, skipping...' % plugin)
341 logger.log('%s: Processing device %s' % (plugin, dev))
343 # Process Private-Nat networks
344 if 'NAT' in tags and nat_net_id:
345 add_iptables_masq(dev, interface)
346 write_dnsmasq_hostsfile(dev, ports, nat_net_id)
347 set_up_port_forwarding(dev, ports)
348 start_dnsmasq(dev, interface)
350 # Process Public networks
351 if interface['is_primary'] and site_net_id:
352 if 'OVS_BRIDGE' in tags:
353 dev = tags['OVS_BRIDGE']
354 write_dnsmasq_hostsfile(dev, ports, site_net_id)
355 start_dnsmasq(dev, interface)