Bug fix
[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 = None
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 and port['nat:forward_ports']:
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     global quantum_auth_url
287
288     logger.log("%s: plugin starting up..." % plugin)
289
290     parser = ConfigParser()
291     parser.read("/etc/nova/nova.conf")
292     quantum_username = parser.get("DEFAULT", "quantum_admin_username")
293     quantum_password = parser.get("DEFAULT", "quantum_admin_password")
294     quantum_tenant_name = parser.get("DEFAULT", "quantum_admin_tenant_name")
295     quantum_auth_url = parser.get("DEFAULT", "quantum_admin_auth_url")
296
297 def GetSlivers(data, config=None, plc=None):
298     global nat_net_id
299     global site_net_id
300
301     if not nat_net_id:
302         try:
303             nat_net_id = get_net_id_by_name(nat_net_name)
304             logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
305         except:
306             logger.log("%s: no network called %s..." % (plugin, nat_net_name))
307
308
309     # Fix me
310     # site_net_name = "devel"
311     site_net_name = "sharednet1"
312     if not site_net_id:
313         try:
314             site_net_id = get_net_id_by_name(site_net_name)
315             logger.log("%s: %s id is %s..." % (plugin, site_net_name, site_net_id))
316         except:
317             logger.log("%s: no network called %s..." % (plugin, site_net_name))
318
319     reset_iptables_chain()
320     ports = get_local_quantum_ports()
321
322     for interface in data['interfaces']:
323         try:
324             settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
325         except:
326             continue
327
328         tags = {}
329         for setting in settings:
330             tags[setting['tagname'].upper()] = setting['value']
331
332         if 'IFNAME' in tags:
333             dev = tags['IFNAME']
334         else:
335             # Skip devices that don't have names
336             logger.log('%s: Device has no name, skipping...' % plugin)
337             continue
338
339         logger.log('%s: Processing device %s' % (plugin, dev))
340
341         # Process Private-Nat networks
342         if 'NAT' in tags and nat_net_id:
343             add_iptables_masq(dev, interface)
344             write_dnsmasq_hostsfile(dev, ports, nat_net_id)
345             set_up_port_forwarding(dev, ports)
346             start_dnsmasq(dev, interface)
347
348         # Process Public networks
349         if interface['is_primary'] and site_net_id:
350             if 'OVS_BRIDGE' in tags:
351                 dev = tags['OVS_BRIDGE']
352             write_dnsmasq_hostsfile(dev, ports, site_net_id)
353             start_dnsmasq(dev, interface, forward_dns=False)
354