Refactored, added support for port forwarding
[nodemanager.git] / plugins / planetstack-net.py
1 """
2 This plugin sets up the NAT interfaces for PlanetStack. It processes
3 each interface that has a 'nat' tag set.
4
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
9
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.
15 """
16
17 # system provided modules
18 import os, string, time, socket
19 from socket import inet_aton
20 import subprocess, signal
21 import json
22 from ConfigParser import ConfigParser
23
24 # PlanetLab system modules
25 import sioc, plnet
26
27 # Quantum modules
28 from quantumclient.v2_0 import client
29
30 # local modules
31 import logger
32
33 plugin = "planetstack-net"
34
35 # Helper functions for converting to CIDR notation
36 def get_net_size(netmask):
37     binary_str = ''
38     for octet in netmask:
39         binary_str += bin(int(octet))[2:].zfill(8)
40     return str(len(binary_str.rstrip('0')))
41
42 def to_cidr(ipaddr, netmask):
43     # validate input
44     inet_aton(ipaddr)
45     inet_aton(netmask)
46
47     ipaddr = ipaddr.split('.')
48     netmask = netmask.split('.')
49
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)
52
53 def ipaddr_range(network, broadcast):
54     start = network.split('.')
55     end = broadcast.split('.')
56
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)
60
61     return '.'.join(start) + ',' + '.'.join(end)
62
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)
68     
69 def add_iptables_rule(table, chain, args):
70     args = ['-t', table, '-C',  chain] + args
71     try:
72         run_iptables_cmd(args)
73     except:
74         args[2] = '-A'
75         try:
76             run_iptables_cmd(args)
77         except:
78             logger.log('%s: FAILED to add iptables rule' % plugin)
79
80 def reset_iptables_chain():
81     try:
82         # Flush the planetstack-nat chain
83         run_iptables_cmd(['-t', 'nat', '-F', plugin])
84     except:
85         # Probably the chain doesn't exist, try creating it
86         run_iptables_cmd(['-t', 'nat', '-N', plugin]) 
87
88     add_iptables_rule('nat', 'PREROUTING', ['-j', plugin]) 
89
90 # Enable iptables MASQ for a device
91 def add_iptables_masq(dev, interface):
92     ipaddr = interface['ip']
93     netmask = interface['netmask']
94     
95     cidr = None
96     try:
97         cidr = to_cidr(ipaddr, netmask)
98     except:
99         logger.log('%s: could not convert ipaddr %s and netmask %s to CIDR' 
100                    % (plugin, ipaddr, netmask))
101         return
102     
103     args = ['-s',  cidr, '!',  '-d',  cidr, '-j', 'MASQUERADE']
104     add_iptables_rule('nat', 'POSTROUTING', args)
105
106 def get_pidfile(dev):
107     return '/var/run/dnsmasq-%s.pid' % dev
108
109 def get_leasefile(dev):
110     return '/var/lib/dnsmasq/%s.leases' % dev
111
112 def get_hostsfile(dev):
113     return '/var/lib/dnsmasq/%s.hosts' % dev
114
115 # Check if dnsmasq already running
116 def dnsmasq_running(dev):
117     pidfile = get_pidfile(dev)
118     try:
119         pid = open(pidfile, 'r').read().strip()
120         if os.path.exists('/proc/%s' % pid):
121             return True
122     except:
123         pass
124     return False
125
126 def dnsmasq_sighup(dev):
127     pidfile = get_pidfile(dev)
128     try:
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))
133     except:
134         logger.log("%s: Sending SIGHUP to dnsmasq FAILED on dev %s" % (plugin, dev))
135
136 # Enable dnsmasq for this interface
137 def start_dnsmasq(dev, interface):
138     if not dnsmasq_running(dev):
139         try:
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', 
144                                    '--strict-order', 
145                                    '--bind-interfaces',
146                                    '--local=//',  
147                                    '--domain-needed',  
148                                    '--pid-file=%s' % get_pidfile(dev), 
149                                    '--conf-file=',
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'])
155         except:
156             logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev))
157
158 nat_net_name = "nat-net"
159 nat_net_id = None
160 quantum_auth_url = "http://viccidev1:5000/v2.0/"
161 quantum_username = None
162 quantum_password = None
163 quantum_tenant_name = None
164
165 def convert_ovs_output_to_dict(out):
166     decoded = json.loads(out.strip())
167     headings = decoded['headings']
168     data = decoded['data']
169
170     records = []
171     for rec in data:
172         mydict = {}
173         for i in range(0, len(headings) - 1):
174             if not isinstance(rec[i], list):
175                 mydict[headings[i]] = rec[i]
176             else:
177                 if rec[i][0] == 'set':
178                     mydict[headings[i]] = rec[i][1]
179                 elif rec[i][0] == 'map':
180                     newdict = {}
181                     for (key, value) in rec[i][1]:
182                         newdict[key] = value
183                     mydict[headings[i]] = newdict
184                 elif rec[i][0] == 'uuid':
185                     mydict['uuid'] = rec[i][1]
186         records.append(mydict)
187
188     return records
189
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:
194 #
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
197 # interface.
198 #
199 # 2) Sets up iptables rules in the 'planetstack-net' chain based on 
200 # the nat:forward_ports field in the Port record.  
201
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)
207
208     # Extract Quantum Port IDs from OvS records
209     port_ids = []
210     for rec in records:
211         port_ids.append(rec['external_ids']['iface-id'])
212
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))
220
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'])
227             f.write(entry)
228             logger.log("%s: %s" % (plugin, entry))
229     f.close()
230
231     # Send SIGHUP to dnsmasq to make it re-read hostsfile
232     dnsmasq_sighup(dev)
233
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])
245             
246 def start():
247     global quantum_username
248     global quantum_password
249     global quantum_tenant_name
250     global nat_net_id
251
252     logger.log("%s: plugin starting up..." % plugin)
253
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")
259
260     quantum = client.Client(username=quantum_username, 
261                             password=quantum_password,
262                             tenant_name=quantum_tenant_name,
263                             auth_url=quantum_auth_url)
264
265     net = quantum.list_networks(name=nat_net_name)
266     nat_net_id = net['networks'][0]['id']
267
268     logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
269
270 def GetSlivers(data, config=None, plc=None):
271     reset_iptables_chain()
272     for interface in data['interfaces']:
273         try:
274             settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
275         except:
276             continue
277
278         tags = {}
279         for setting in settings:
280             tags[setting['tagname'].upper()] = setting['value']
281
282         if 'IFNAME' in tags:
283             dev = tags['IFNAME']
284         else:
285             # Skip devices that don't have names
286             logger.log('%s: Device has no name, skipping...' % plugin)
287             continue
288
289         logger.log('%s: Processing device %s' % (plugin, dev))
290         
291         if 'NAT' in tags:
292             add_iptables_masq(dev, interface)
293             process_quantum_ports(dev)
294             start_dnsmasq(dev, interface)