Unfilter NAT internal IP address so it can accept packets from outside
[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, pos = None):
70     iptargs = ['-t', table, '-C',  chain] + args
71     try:
72         run_iptables_cmd(iptargs)
73     except:
74         if pos:
75             iptargs = ['-t', table, '-I', chain, str(pos)] + args
76         else:
77             iptargs[2] = '-A'
78         try:
79             run_iptables_cmd(iptargs)
80         except:
81             logger.log('%s: FAILED to add iptables rule' % plugin)
82
83 def reset_iptables_chain():
84     try:
85         # Flush the planetstack-nat chain
86         run_iptables_cmd(['-t', 'nat', '-F', plugin])
87     except:
88         # Probably the chain doesn't exist, try creating it
89         run_iptables_cmd(['-t', 'nat', '-N', plugin]) 
90
91     add_iptables_rule('nat', 'PREROUTING', ['-j', plugin]) 
92
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'], 
99                       pos = 1)
100     
101 # Enable iptables MASQ for a device
102 def add_iptables_masq(dev, interface):
103     ipaddr = interface['ip']
104     netmask = interface['netmask']
105     
106     cidr = None
107     try:
108         cidr = to_cidr(ipaddr, netmask)
109     except:
110         logger.log('%s: could not convert ipaddr %s and netmask %s to CIDR' 
111                    % (plugin, ipaddr, netmask))
112         return
113     
114     args = ['-s',  cidr, '!',  '-d',  cidr, '-j', 'MASQUERADE']
115     add_iptables_rule('nat', 'POSTROUTING', args)
116
117 def get_pidfile(dev):
118     return '/var/run/dnsmasq-%s.pid' % dev
119
120 def get_leasefile(dev):
121     return '/var/lib/dnsmasq/%s.leases' % dev
122
123 def get_hostsfile(dev):
124     return '/var/lib/dnsmasq/%s.hosts' % dev
125
126 # Check if dnsmasq already running
127 def dnsmasq_running(dev):
128     pidfile = get_pidfile(dev)
129     try:
130         pid = open(pidfile, 'r').read().strip()
131         if os.path.exists('/proc/%s' % pid):
132             return True
133     except:
134         pass
135     return False
136
137 def dnsmasq_sighup(dev):
138     pidfile = get_pidfile(dev)
139     try:
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))
144     except:
145         logger.log("%s: Sending SIGHUP to dnsmasq FAILED on dev %s" % (plugin, dev))
146
147 # Enable dnsmasq for this interface
148 def start_dnsmasq(dev, interface):
149     if not dnsmasq_running(dev):
150         try:
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', 
155                                    '--strict-order', 
156                                    '--bind-interfaces',
157                                    '--local=//',  
158                                    '--domain-needed',  
159                                    '--pid-file=%s' % get_pidfile(dev), 
160                                    '--conf-file=',
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'])
166         except:
167             logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev))
168
169 nat_net_name = "nat-net"
170 nat_net_id = None
171 quantum_auth_url = "http://viccidev1:5000/v2.0/"
172 quantum_username = None
173 quantum_password = None
174 quantum_tenant_name = None
175
176 def convert_ovs_output_to_dict(out):
177     decoded = json.loads(out.strip())
178     headings = decoded['headings']
179     data = decoded['data']
180
181     records = []
182     for rec in data:
183         mydict = {}
184         for i in range(0, len(headings) - 1):
185             if not isinstance(rec[i], list):
186                 mydict[headings[i]] = rec[i]
187             else:
188                 if rec[i][0] == 'set':
189                     mydict[headings[i]] = rec[i][1]
190                 elif rec[i][0] == 'map':
191                     newdict = {}
192                     for (key, value) in rec[i][1]:
193                         newdict[key] = value
194                     mydict[headings[i]] = newdict
195                 elif rec[i][0] == 'uuid':
196                     mydict['uuid'] = rec[i][1]
197         records.append(mydict)
198
199     return records
200
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:
205 #
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
208 # interface.
209 #
210 # 2) Sets up iptables rules in the 'planetstack-net' chain based on 
211 # the nat:forward_ports field in the Port record.  
212
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)
218
219     # Extract Quantum Port IDs from OvS records
220     port_ids = []
221     for rec in records:
222         port_ids.append(rec['external_ids']['iface-id'])
223
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))
231
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'])
238             f.write(entry)
239             logger.log("%s: %s" % (plugin, entry))
240     f.close()
241
242     # Send SIGHUP to dnsmasq to make it re-read hostsfile
243     dnsmasq_sighup(dev)
244
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))
253
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])
259             
260 def start():
261     global quantum_username
262     global quantum_password
263     global quantum_tenant_name
264     global nat_net_id
265
266     logger.log("%s: plugin starting up..." % plugin)
267
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")
273
274     quantum = client.Client(username=quantum_username, 
275                             password=quantum_password,
276                             tenant_name=quantum_tenant_name,
277                             auth_url=quantum_auth_url)
278
279     net = quantum.list_networks(name=nat_net_name)
280     nat_net_id = net['networks'][0]['id']
281
282     logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
283
284 def GetSlivers(data, config=None, plc=None):
285     reset_iptables_chain()
286     for interface in data['interfaces']:
287         try:
288             settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
289         except:
290             continue
291
292         tags = {}
293         for setting in settings:
294             tags[setting['tagname'].upper()] = setting['value']
295
296         if 'IFNAME' in tags:
297             dev = tags['IFNAME']
298         else:
299             # Skip devices that don't have names
300             logger.log('%s: Device has no name, skipping...' % plugin)
301             continue
302
303         logger.log('%s: Processing device %s' % (plugin, dev))
304         
305         if 'NAT' in tags:
306             add_iptables_masq(dev, interface)
307             process_quantum_ports(dev)
308             start_dnsmasq(dev, interface)