1a686d4cbf6f7e1111a97efd40531660521b417e
[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 nat_net_name = "nat-net"
36 nat_net_id = None
37 site_net_id = None
38
39 quantum_auth_url = "http://viccidev1:5000/v2.0/"
40 quantum_username = None
41 quantum_password = None
42 quantum_tenant_name = None
43
44
45 # Helper functions for converting to CIDR notation
46 def get_net_size(netmask):
47     binary_str = ''
48     for octet in netmask:
49         binary_str += bin(int(octet))[2:].zfill(8)
50     return str(len(binary_str.rstrip('0')))
51
52 def to_cidr(ipaddr, netmask):
53     # validate input
54     inet_aton(ipaddr)
55     inet_aton(netmask)
56
57     ipaddr = ipaddr.split('.')
58     netmask = netmask.split('.')
59
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)
62
63 def ipaddr_range(network, broadcast):
64     start = network.split('.')
65     end = broadcast.split('.')
66
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)
70
71     return '.'.join(start) + ',' + '.'.join(end)
72
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)
78     
79 def add_iptables_rule(table, chain, args, pos = None):
80     iptargs = ['-t', table, '-C',  chain] + args
81     try:
82         run_iptables_cmd(iptargs)
83     except:
84         if pos:
85             iptargs = ['-t', table, '-I', chain, str(pos)] + args
86         else:
87             iptargs[2] = '-A'
88         try:
89             run_iptables_cmd(iptargs)
90         except:
91             logger.log('%s: FAILED to add iptables rule' % plugin)
92
93 def reset_iptables_chain():
94     try:
95         # Flush the planetstack-nat chain
96         run_iptables_cmd(['-t', 'nat', '-F', plugin])
97     except:
98         # Probably the chain doesn't exist, try creating it
99         run_iptables_cmd(['-t', 'nat', '-N', plugin]) 
100
101     add_iptables_rule('nat', 'PREROUTING', ['-j', plugin]) 
102
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'], 
109                       pos = 1)
110     
111 # Enable iptables MASQ for a device
112 def add_iptables_masq(dev, interface):
113     ipaddr = interface['ip']
114     netmask = interface['netmask']
115     
116     cidr = None
117     try:
118         cidr = to_cidr(ipaddr, netmask)
119     except:
120         logger.log('%s: could not convert ipaddr %s and netmask %s to CIDR' 
121                    % (plugin, ipaddr, netmask))
122         return
123     
124     args = ['-s',  cidr, '!',  '-d',  cidr, '-j', 'MASQUERADE']
125     add_iptables_rule('nat', 'POSTROUTING', args)
126
127 def get_pidfile(dev):
128     return '/var/run/dnsmasq-%s.pid' % dev
129
130 def get_leasefile(dev):
131     return '/var/lib/dnsmasq/%s.leases' % dev
132
133 def get_hostsfile(dev):
134     return '/var/lib/dnsmasq/%s.hosts' % dev
135
136 # Check if dnsmasq already running
137 def dnsmasq_running(dev):
138     pidfile = get_pidfile(dev)
139     try:
140         pid = open(pidfile, 'r').read().strip()
141         if os.path.exists('/proc/%s' % pid):
142             return True
143     except:
144         pass
145     return False
146
147 def dnsmasq_sighup(dev):
148     pidfile = get_pidfile(dev)
149     try:
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))
154     except:
155         logger.log("%s: Sending SIGHUP to dnsmasq FAILED on dev %s" % (plugin, dev))
156
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',
165                '--strict-order',
166                '--bind-interfaces',
167                '--local=//',
168                '--domain-needed',
169                '--pid-file=%s' % get_pidfile(dev),
170                '--conf-file=',
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']]
177
178         try:
179             logger.log('%s: starting dnsmasq on device %s' % (plugin, dev))
180             subprocess.check_call(cmd)
181         except:
182             logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev))
183             logger.log(' '.join(cmd))
184
185 def convert_ovs_output_to_dict(out):
186     decoded = json.loads(out.strip())
187     headings = decoded['headings']
188     data = decoded['data']
189
190     records = []
191     for rec in data:
192         mydict = {}
193         for i in range(0, len(headings) - 1):
194             if not isinstance(rec[i], list):
195                 mydict[headings[i]] = rec[i]
196             else:
197                 if rec[i][0] == 'set':
198                     mydict[headings[i]] = rec[i][1]
199                 elif rec[i][0] == 'map':
200                     newdict = {}
201                     for (key, value) in rec[i][1]:
202                         newdict[key] = value
203                     mydict[headings[i]] = newdict
204                 elif rec[i][0] == 'uuid':
205                     mydict['uuid'] = rec[i][1]
206         records.append(mydict)
207
208     return records
209
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:
214 #
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
217 # interface.
218 #
219 # 2) Sets up iptables rules in the 'planetstack-net' chain based on 
220 # the nat:forward_ports field in the Port record.  
221
222 def get_local_quantum_ports():
223     ports = []
224
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)
229
230     if records:
231         # Extract Quantum Port IDs from OvS records
232         port_ids = []
233         for rec in records:
234             port_ids.append(rec['external_ids']['iface-id'])
235
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']
242
243     return ports
244
245
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')
250     for port in ports:
251         if port['network_id'] == net_id:
252             entry = "%s,%s\n" % (port['mac_address'], port['fixed_ips'][0]['ip_address'])
253             f.write(entry)
254             logger.log("%s: %s" % (plugin, entry))
255     f.close()
256
257     # Send SIGHUP to dnsmasq to make it re-read hostsfile
258     dnsmasq_sighup(dev)
259
260 def set_up_port_forwarding(dev, ports):
261     # Set up iptables rules for port forwarding
262     for port in ports:
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))
269
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])
275
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)
281
282     net = quantum.list_networks(name=name)
283     return net['networks'][0]['id']
284
285 def start():
286     global quantum_username
287     global quantum_password
288     global quantum_tenant_name
289
290     logger.log("%s: plugin starting up..." % plugin)
291
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")
297
298
299 def GetSlivers(data, config=None, plc=None):
300     global nat_net_id
301     global site_net_id
302
303     if not nat_net_id:
304         try:
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))
307         except:
308             logger.log("%s: no network called %s..." % (plugin, nat_net_name))
309
310
311     # Fix me
312     # site_net_name = "devel"
313     site_net_name = "sharednet1"
314     if not site_net_id:
315         try:
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))
318         except:
319             logger.log("%s: no network called %s..." % (plugin, site_net_name))
320
321     reset_iptables_chain()
322     ports = get_local_quantum_ports()
323
324     for interface in data['interfaces']:
325         try:
326             settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
327         except:
328             continue
329
330         tags = {}
331         for setting in settings:
332             tags[setting['tagname'].upper()] = setting['value']
333
334         if 'IFNAME' in tags:
335             dev = tags['IFNAME']
336         else:
337             # Skip devices that don't have names
338             logger.log('%s: Device has no name, skipping...' % plugin)
339             continue
340
341         logger.log('%s: Processing device %s' % (plugin, dev))
342
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)
349
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)
356