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 # Helper functions for converting to CIDR notation
36 def get_net_size(netmask):
39 binary_str += bin(int(octet))[2:].zfill(8)
40 return str(len(binary_str.rstrip('0')))
42 def to_cidr(ipaddr, netmask):
47 ipaddr = ipaddr.split('.')
48 netmask = netmask.split('.')
50 net_start = [str(int(ipaddr[x]) & int(netmask[x])) for x in range(0,4)]
51 return '.'.join(net_start) + '/' + get_net_size(netmask)
53 def ipaddr_range(network, broadcast):
54 start = network.split('.')
55 end = broadcast.split('.')
57 # Assume interface always claims the first address in the block
58 start[3] = str(int(start[3]) + 2)
59 end[3] = str(int(end[3]) - 1)
61 return '.'.join(start) + ',' + '.'.join(end)
63 # Should possibly be using python-iptables for this stuff
64 def run_iptables_cmd(args):
65 cmd = ['/sbin/iptables'] + args
66 logger.log('%s: %s' % (plugin, ' '.join(cmd)))
67 subprocess.check_call(cmd)
69 def add_iptables_rule(table, chain, args, pos = None):
70 iptargs = ['-t', table, '-C', chain] + args
72 run_iptables_cmd(iptargs)
75 iptargs = ['-t', table, '-I', chain, str(pos)] + args
79 run_iptables_cmd(iptargs)
81 logger.log('%s: FAILED to add iptables rule' % plugin)
83 def reset_iptables_chain():
85 # Flush the planetstack-nat chain
86 run_iptables_cmd(['-t', 'nat', '-F', plugin])
88 # Probably the chain doesn't exist, try creating it
89 run_iptables_cmd(['-t', 'nat', '-N', plugin])
91 add_iptables_rule('nat', 'PREROUTING', ['-j', plugin])
93 # Nova blocks packets from external addresses by default.
94 # This is hacky but it gets around the issue.
95 def unfilter_ipaddr(dev, ipaddr):
96 add_iptables_rule(table = 'filter',
97 chain = 'nova-compute-sg-fallback',
98 args = ['-d', ipaddr, '-j', 'ACCEPT'],
101 # Enable iptables MASQ for a device
102 def add_iptables_masq(dev, interface):
103 ipaddr = interface['ip']
104 netmask = interface['netmask']
108 cidr = to_cidr(ipaddr, netmask)
110 logger.log('%s: could not convert ipaddr %s and netmask %s to CIDR'
111 % (plugin, ipaddr, netmask))
114 args = ['-s', cidr, '!', '-d', cidr, '-j', 'MASQUERADE']
115 add_iptables_rule('nat', 'POSTROUTING', args)
117 def get_pidfile(dev):
118 return '/var/run/dnsmasq-%s.pid' % dev
120 def get_leasefile(dev):
121 return '/var/lib/dnsmasq/%s.leases' % dev
123 def get_hostsfile(dev):
124 return '/var/lib/dnsmasq/%s.hosts' % dev
126 # Check if dnsmasq already running
127 def dnsmasq_running(dev):
128 pidfile = get_pidfile(dev)
130 pid = open(pidfile, 'r').read().strip()
131 if os.path.exists('/proc/%s' % pid):
137 def dnsmasq_sighup(dev):
138 pidfile = get_pidfile(dev)
140 pid = open(pidfile, 'r').read().strip()
141 if os.path.exists('/proc/%s' % pid):
142 os.kill(int(pid), signal.SIGHUP)
143 logger.log("%s: Sent SIGHUP to dnsmasq on dev %s" % (plugin, dev))
145 logger.log("%s: Sending SIGHUP to dnsmasq FAILED on dev %s" % (plugin, dev))
147 # Enable dnsmasq for this interface
148 def start_dnsmasq(dev, interface):
149 if not dnsmasq_running(dev):
151 logger.log('%s: starting dnsmasq on device %s' % (plugin, dev))
152 iprange = ipaddr_range(interface['network'], interface['broadcast'])
153 logger.log('%s: IP range: %s' % (plugin, iprange))
154 subprocess.check_call(['/usr/sbin/dnsmasq',
159 '--pid-file=%s' % get_pidfile(dev),
161 '--interface=%s' % dev,
162 '--dhcp-range=%s,120' % iprange,
163 '--dhcp-leasefile=%s' % get_leasefile(dev),
164 '--dhcp-hostsfile=%s' % get_hostsfile(dev),
165 '--dhcp-no-override'])
167 logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev))
169 nat_net_name = "nat-net"
171 quantum_auth_url = "http://viccidev1:5000/v2.0/"
172 quantum_username = None
173 quantum_password = None
174 quantum_tenant_name = None
176 def convert_ovs_output_to_dict(out):
177 decoded = json.loads(out.strip())
178 headings = decoded['headings']
179 data = decoded['data']
184 for i in range(0, len(headings) - 1):
185 if not isinstance(rec[i], list):
186 mydict[headings[i]] = rec[i]
188 if rec[i][0] == 'set':
189 mydict[headings[i]] = rec[i][1]
190 elif rec[i][0] == 'map':
192 for (key, value) in rec[i][1]:
194 mydict[headings[i]] = newdict
195 elif rec[i][0] == 'uuid':
196 mydict['uuid'] = rec[i][1]
197 records.append(mydict)
201 # Do all processing associated with Quantum ports. It first gets a
202 # list of local VM interfaces and then queries Quantum to get Port
203 # records for these interfaces. Then for all interfaces on the NAT
204 # network it does the following:
206 # 1) Generates a dhcp-hostsfile for dnsmasq. The purpose is to make
207 # sure that the IP address assigned by Quantum appears on NAT
210 # 2) Sets up iptables rules in the 'planetstack-net' chain based on
211 # the nat:forward_ports field in the Port record.
213 def process_quantum_ports(dev):
214 # Get local information for VM interfaces from OvS
215 ovs_out = subprocess.check_output(['/usr/bin/ovs-vsctl', '-f', 'json', 'find',
216 'Interface', 'external_ids:iface-id!="absent"'])
217 records = convert_ovs_output_to_dict(ovs_out)
219 # Extract Quantum Port IDs from OvS records
222 port_ids.append(rec['external_ids']['iface-id'])
224 # Get the full info on these ports from Quantum
225 quantum = client.Client(username=quantum_username,
226 password=quantum_password,
227 tenant_name=quantum_tenant_name,
228 auth_url=quantum_auth_url)
229 ports = quantum.list_ports(id=port_ids)
230 # logger.log("%s: %s" % (plugin, ports))
232 # Write relevant entries to dnsmasq hostsfile
233 logger.log("%s: Writing hostsfile for %s" % (plugin, dev))
234 f = open(get_hostsfile(dev), 'w')
235 for port in ports['ports']:
236 if port['network_id'] == nat_net_id:
237 entry = "%s,%s\n" % (port['mac_address'], port['fixed_ips'][0]['ip_address'])
239 logger.log("%s: %s" % (plugin, entry))
242 # Send SIGHUP to dnsmasq to make it re-read hostsfile
245 # Set up iptables rules for port forwarding
246 for port in ports['ports']:
247 if port['network_id'] == nat_net_id:
248 for fw in port['nat:forward_ports']:
249 ipaddr = port['fixed_ips'][0]['ip_address']
250 protocol = fw['l4_protocol']
251 fwport = fw['l4_port']
252 # logger.log("%s: fwd port %s/%s to %s" % (plugin, protocol, fwport, ipaddr))
254 unfilter_ipaddr(dev, ipaddr)
255 # Shouldn't hardcode br-eth0 here. Maybe use '-d <node IP address>'?
256 add_iptables_rule('nat', plugin, ['-i', 'br-eth0',
257 '-p', protocol, '--dport', fwport,
258 '-j', 'DNAT', '--to-destination', ipaddr])
261 global quantum_username
262 global quantum_password
263 global quantum_tenant_name
266 logger.log("%s: plugin starting up..." % plugin)
268 parser = ConfigParser()
269 parser.read("/etc/nova/nova.conf")
270 quantum_username = parser.get("DEFAULT", "quantum_admin_username")
271 quantum_password = parser.get("DEFAULT", "quantum_admin_password")
272 quantum_tenant_name = parser.get("DEFAULT", "quantum_admin_tenant_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=nat_net_name)
280 nat_net_id = net['networks'][0]['id']
282 logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
284 def GetSlivers(data, config=None, plc=None):
285 reset_iptables_chain()
286 for interface in data['interfaces']:
288 settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
293 for setting in settings:
294 tags[setting['tagname'].upper()] = setting['value']
299 # Skip devices that don't have names
300 logger.log('%s: Device has no name, skipping...' % plugin)
303 logger.log('%s: Processing device %s' % (plugin, dev))
306 add_iptables_masq(dev, interface)
307 process_quantum_ports(dev)
308 start_dnsmasq(dev, interface)