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):
70 args = ['-t', table, '-C', chain] + args
72 run_iptables_cmd(args)
76 run_iptables_cmd(args)
78 logger.log('%s: FAILED to add iptables rule' % plugin)
80 def reset_iptables_chain():
82 # Flush the planetstack-nat chain
83 run_iptables_cmd(['-t', 'nat', '-F', plugin])
85 # Probably the chain doesn't exist, try creating it
86 run_iptables_cmd(['-t', 'nat', '-N', plugin])
88 add_iptables_rule('nat', 'PREROUTING', ['-j', plugin])
90 # Enable iptables MASQ for a device
91 def add_iptables_masq(dev, interface):
92 ipaddr = interface['ip']
93 netmask = interface['netmask']
97 cidr = to_cidr(ipaddr, netmask)
99 logger.log('%s: could not convert ipaddr %s and netmask %s to CIDR'
100 % (plugin, ipaddr, netmask))
103 args = ['-s', cidr, '!', '-d', cidr, '-j', 'MASQUERADE']
104 add_iptables_rule('nat', 'POSTROUTING', args)
106 def get_pidfile(dev):
107 return '/var/run/dnsmasq-%s.pid' % dev
109 def get_leasefile(dev):
110 return '/var/lib/dnsmasq/%s.leases' % dev
112 def get_hostsfile(dev):
113 return '/var/lib/dnsmasq/%s.hosts' % dev
115 # Check if dnsmasq already running
116 def dnsmasq_running(dev):
117 pidfile = get_pidfile(dev)
119 pid = open(pidfile, 'r').read().strip()
120 if os.path.exists('/proc/%s' % pid):
126 def dnsmasq_sighup(dev):
127 pidfile = get_pidfile(dev)
129 pid = open(pidfile, 'r').read().strip()
130 if os.path.exists('/proc/%s' % pid):
131 os.kill(int(pid), signal.SIGHUP)
132 logger.log("%s: Sent SIGHUP to dnsmasq on dev %s" % (plugin, dev))
134 logger.log("%s: Sending SIGHUP to dnsmasq FAILED on dev %s" % (plugin, dev))
136 # Enable dnsmasq for this interface
137 def start_dnsmasq(dev, interface):
138 if not dnsmasq_running(dev):
140 logger.log('%s: starting dnsmasq on device %s' % (plugin, dev))
141 iprange = ipaddr_range(interface['network'], interface['broadcast'])
142 logger.log('%s: IP range: %s' % (plugin, iprange))
143 subprocess.check_call(['/usr/sbin/dnsmasq',
148 '--pid-file=%s' % get_pidfile(dev),
150 '--interface=%s' % dev,
151 '--dhcp-range=%s,120' % iprange,
152 '--dhcp-leasefile=%s' % get_leasefile(dev),
153 '--dhcp-hostsfile=%s' % get_hostsfile(dev),
154 '--dhcp-no-override'])
156 logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev))
158 nat_net_name = "nat-net"
160 quantum_auth_url = "http://viccidev1:5000/v2.0/"
161 quantum_username = None
162 quantum_password = None
163 quantum_tenant_name = None
165 def convert_ovs_output_to_dict(out):
166 decoded = json.loads(out.strip())
167 headings = decoded['headings']
168 data = decoded['data']
173 for i in range(0, len(headings) - 1):
174 if not isinstance(rec[i], list):
175 mydict[headings[i]] = rec[i]
177 if rec[i][0] == 'set':
178 mydict[headings[i]] = rec[i][1]
179 elif rec[i][0] == 'map':
181 for (key, value) in rec[i][1]:
183 mydict[headings[i]] = newdict
184 elif rec[i][0] == 'uuid':
185 mydict['uuid'] = rec[i][1]
186 records.append(mydict)
190 # Do all processing associated with Quantum ports. It first gets a
191 # list of local VM interfaces and then queries Quantum to get Port
192 # records for these interfaces. Then for all interfaces on the NAT
193 # network it does the following:
195 # 1) Generates a dhcp-hostsfile for dnsmasq. The purpose is to make
196 # sure that the IP address assigned by Quantum appears on NAT
199 # 2) Sets up iptables rules in the 'planetstack-net' chain based on
200 # the nat:forward_ports field in the Port record.
202 def process_quantum_ports(dev):
203 # Get local information for VM interfaces from OvS
204 ovs_out = subprocess.check_output(['/usr/bin/ovs-vsctl', '-f', 'json', 'find',
205 'Interface', 'external_ids:iface-id!="absent"'])
206 records = convert_ovs_output_to_dict(ovs_out)
208 # Extract Quantum Port IDs from OvS records
211 port_ids.append(rec['external_ids']['iface-id'])
213 # Get the full info on these ports from Quantum
214 quantum = client.Client(username=quantum_username,
215 password=quantum_password,
216 tenant_name=quantum_tenant_name,
217 auth_url=quantum_auth_url)
218 ports = quantum.list_ports(id=port_ids)
219 # logger.log("%s: %s" % (plugin, ports))
221 # Write relevant entries to dnsmasq hostsfile
222 logger.log("%s: Writing hostsfile for %s" % (plugin, dev))
223 f = open(get_hostsfile(dev), 'w')
224 for port in ports['ports']:
225 if port['network_id'] == nat_net_id:
226 entry = "%s,%s\n" % (port['mac_address'], port['fixed_ips'][0]['ip_address'])
228 logger.log("%s: %s" % (plugin, entry))
231 # Send SIGHUP to dnsmasq to make it re-read hostsfile
234 # Set up iptables rules for port forwarding
235 for port in ports['ports']:
236 if port['network_id'] == nat_net_id:
237 for fw in port['nat:forward_ports']:
238 ipaddr = port['fixed_ips'][0]['ip_address']
239 protocol = fw['l4_protocol']
240 fwport = fw['l4_port']
241 # logger.log("%s: fwd port %s/%s to %s" % (plugin, protocol, fwport, ipaddr))
242 add_iptables_rule('nat', plugin, ['-i', 'br-eth0',
243 '-p', protocol, '--dport', fwport,
244 '-j', 'DNAT', '--to-destination', ipaddr])
247 global quantum_username
248 global quantum_password
249 global quantum_tenant_name
252 logger.log("%s: plugin starting up..." % plugin)
254 parser = ConfigParser()
255 parser.read("/etc/nova/nova.conf")
256 quantum_username = parser.get("DEFAULT", "quantum_admin_username")
257 quantum_password = parser.get("DEFAULT", "quantum_admin_password")
258 quantum_tenant_name = parser.get("DEFAULT", "quantum_admin_tenant_name")
260 quantum = client.Client(username=quantum_username,
261 password=quantum_password,
262 tenant_name=quantum_tenant_name,
263 auth_url=quantum_auth_url)
265 net = quantum.list_networks(name=nat_net_name)
266 nat_net_id = net['networks'][0]['id']
268 logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
270 def GetSlivers(data, config=None, plc=None):
271 reset_iptables_chain()
272 for interface in data['interfaces']:
274 settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
279 for setting in settings:
280 tags[setting['tagname'].upper()] = setting['value']
285 # Skip devices that don't have names
286 logger.log('%s: Device has no name, skipping...' % plugin)
289 logger.log('%s: Processing device %s' % (plugin, dev))
292 add_iptables_masq(dev, interface)
293 process_quantum_ports(dev)
294 start_dnsmasq(dev, interface)