Turn off dnsmasq's DNS forwarding on public interfaces
[nodemanager.git] / plugins / planetstack-net.py
1 """
2 This plugin sets up dnsmasq and iptables to support the "Private-Nat"
3 and "Public" network models for OpenCloud.  It communicates with OvS
4 on the local node and Quantum to gather information about the virtual
5 interfaces instantiated by Quantum.  It uses this information to:
6
7 * add the Quantum-assigned IP address to the vif 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, forward_dns=True):
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         # Turn off forwarding DNS queries, only do DHCP
179         if forward_dns == False:
180             cmd.append('--port=0')
181
182         try:
183             logger.log('%s: starting dnsmasq on device %s' % (plugin, dev))
184             subprocess.check_call(cmd)
185         except:
186             logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev))
187             logger.log(' '.join(cmd))
188
189 def convert_ovs_output_to_dict(out):
190     decoded = json.loads(out.strip())
191     headings = decoded['headings']
192     data = decoded['data']
193
194     records = []
195     for rec in data:
196         mydict = {}
197         for i in range(0, len(headings) - 1):
198             if not isinstance(rec[i], list):
199                 mydict[headings[i]] = rec[i]
200             else:
201                 if rec[i][0] == 'set':
202                     mydict[headings[i]] = rec[i][1]
203                 elif rec[i][0] == 'map':
204                     newdict = {}
205                     for (key, value) in rec[i][1]:
206                         newdict[key] = value
207                     mydict[headings[i]] = newdict
208                 elif rec[i][0] == 'uuid':
209                     mydict['uuid'] = rec[i][1]
210         records.append(mydict)
211
212     return records
213
214
215 # Get a list of local VM interfaces and then query Quantum to get
216 # Port records for these interfaces.
217 def get_local_quantum_ports():
218     ports = []
219
220     # Get local information for VM interfaces from OvS
221     ovs_out = subprocess.check_output(['/usr/bin/ovs-vsctl', '-f', 'json', 'find', 
222                                        'Interface', 'external_ids:iface-id!="absent"'])
223     records = convert_ovs_output_to_dict(ovs_out)
224
225     if records:
226         # Extract Quantum Port IDs from OvS records
227         port_ids = []
228         for rec in records:
229             port_ids.append(rec['external_ids']['iface-id'])
230
231         # Get the full info on these ports from Quantum
232         quantum = client.Client(username=quantum_username,
233                                 password=quantum_password,
234                                 tenant_name=quantum_tenant_name,
235                                 auth_url=quantum_auth_url)
236         ports = quantum.list_ports(id=port_ids)['ports']
237
238     return ports
239
240
241 # Generate a dhcp-hostsfile for dnsmasq.  The purpose is to make sure
242 # that the IP address assigned by Quantum appears on NAT interface.
243 def write_dnsmasq_hostsfile(dev, ports, net_id):
244     logger.log("%s: Writing hostsfile for %s" % (plugin, dev))
245     f = open(get_hostsfile(dev), 'w')
246     for port in ports:
247         if port['network_id'] == net_id:
248             entry = "%s,%s\n" % (port['mac_address'], port['fixed_ips'][0]['ip_address'])
249             f.write(entry)
250             logger.log("%s: %s" % (plugin, entry))
251     f.close()
252
253     # Send SIGHUP to dnsmasq to make it re-read hostsfile
254     dnsmasq_sighup(dev)
255
256 # Set up iptables rules in the 'planetstack-net' chain based on
257 # the nat:forward_ports field in the Port record.
258 def set_up_port_forwarding(dev, ports):
259     for port in ports:
260         if port['network_id'] == nat_net_id:
261             for fw in port['nat:forward_ports']:
262                 ipaddr = port['fixed_ips'][0]['ip_address']
263                 protocol = fw['l4_protocol']
264                 fwport = fw['l4_port']
265                 # logger.log("%s: fwd port %s/%s to %s" % (plugin, protocol, fwport, ipaddr))
266
267                 unfilter_ipaddr(dev, ipaddr)
268                 # Shouldn't hardcode br-eth0 here.  Maybe use '-d <node IP address>'?
269                 add_iptables_rule('nat', plugin, ['-i', 'br-eth0', 
270                                                   '-p', protocol, '--dport', fwport,
271                                                   '-j', 'DNAT', '--to-destination', ipaddr])
272
273 def get_net_id_by_name(name):
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=name)
280     return net['networks'][0]['id']
281
282 def start():
283     global quantum_username
284     global quantum_password
285     global quantum_tenant_name
286
287     logger.log("%s: plugin starting up..." % plugin)
288
289     parser = ConfigParser()
290     parser.read("/etc/nova/nova.conf")
291     quantum_username = parser.get("DEFAULT", "quantum_admin_username")
292     quantum_password = parser.get("DEFAULT", "quantum_admin_password")
293     quantum_tenant_name = parser.get("DEFAULT", "quantum_admin_tenant_name")
294
295
296 def GetSlivers(data, config=None, plc=None):
297     global nat_net_id
298     global site_net_id
299
300     if not nat_net_id:
301         try:
302             nat_net_id = get_net_id_by_name(nat_net_name)
303             logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
304         except:
305             logger.log("%s: no network called %s..." % (plugin, nat_net_name))
306
307
308     # Fix me
309     # site_net_name = "devel"
310     site_net_name = "sharednet1"
311     if not site_net_id:
312         try:
313             site_net_id = get_net_id_by_name(site_net_name)
314             logger.log("%s: %s id is %s..." % (plugin, site_net_name, site_net_id))
315         except:
316             logger.log("%s: no network called %s..." % (plugin, site_net_name))
317
318     reset_iptables_chain()
319     ports = get_local_quantum_ports()
320
321     for interface in data['interfaces']:
322         try:
323             settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
324         except:
325             continue
326
327         tags = {}
328         for setting in settings:
329             tags[setting['tagname'].upper()] = setting['value']
330
331         if 'IFNAME' in tags:
332             dev = tags['IFNAME']
333         else:
334             # Skip devices that don't have names
335             logger.log('%s: Device has no name, skipping...' % plugin)
336             continue
337
338         logger.log('%s: Processing device %s' % (plugin, dev))
339
340         # Process Private-Nat networks
341         if 'NAT' in tags and nat_net_id:
342             add_iptables_masq(dev, interface)
343             write_dnsmasq_hostsfile(dev, ports, nat_net_id)
344             set_up_port_forwarding(dev, ports)
345             start_dnsmasq(dev, interface)
346
347         # Process Public networks
348         if interface['is_primary'] and site_net_id:
349             if 'OVS_BRIDGE' in tags:
350                 dev = tags['OVS_BRIDGE']
351             write_dnsmasq_hostsfile(dev, ports, site_net_id)
352             start_dnsmasq(dev, interface, forward_dns=False)
353