Improve comments
[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):
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
211 # Get a list of local VM interfaces and then query Quantum to get
212 # Port records for these interfaces.
213 def get_local_quantum_ports():
214     ports = []
215
216     # Get local information for VM interfaces from OvS
217     ovs_out = subprocess.check_output(['/usr/bin/ovs-vsctl', '-f', 'json', 'find', 
218                                        'Interface', 'external_ids:iface-id!="absent"'])
219     records = convert_ovs_output_to_dict(ovs_out)
220
221     if records:
222         # Extract Quantum Port IDs from OvS records
223         port_ids = []
224         for rec in records:
225             port_ids.append(rec['external_ids']['iface-id'])
226
227         # Get the full info on these ports from Quantum
228         quantum = client.Client(username=quantum_username,
229                                 password=quantum_password,
230                                 tenant_name=quantum_tenant_name,
231                                 auth_url=quantum_auth_url)
232         ports = quantum.list_ports(id=port_ids)['ports']
233
234     return ports
235
236
237 # Generate a dhcp-hostsfile for dnsmasq.  The purpose is to make sure
238 # that the IP address assigned by Quantum appears on NAT interface.
239 def write_dnsmasq_hostsfile(dev, ports, net_id):
240     logger.log("%s: Writing hostsfile for %s" % (plugin, dev))
241     f = open(get_hostsfile(dev), 'w')
242     for port in ports:
243         if port['network_id'] == net_id:
244             entry = "%s,%s\n" % (port['mac_address'], port['fixed_ips'][0]['ip_address'])
245             f.write(entry)
246             logger.log("%s: %s" % (plugin, entry))
247     f.close()
248
249     # Send SIGHUP to dnsmasq to make it re-read hostsfile
250     dnsmasq_sighup(dev)
251
252 # Set up iptables rules in the 'planetstack-net' chain based on
253 # the nat:forward_ports field in the Port record.
254 def set_up_port_forwarding(dev, ports):
255     for port in ports:
256         if port['network_id'] == nat_net_id:
257             for fw in port['nat:forward_ports']:
258                 ipaddr = port['fixed_ips'][0]['ip_address']
259                 protocol = fw['l4_protocol']
260                 fwport = fw['l4_port']
261                 # logger.log("%s: fwd port %s/%s to %s" % (plugin, protocol, fwport, ipaddr))
262
263                 unfilter_ipaddr(dev, ipaddr)
264                 # Shouldn't hardcode br-eth0 here.  Maybe use '-d <node IP address>'?
265                 add_iptables_rule('nat', plugin, ['-i', 'br-eth0', 
266                                                   '-p', protocol, '--dport', fwport,
267                                                   '-j', 'DNAT', '--to-destination', ipaddr])
268
269 def get_net_id_by_name(name):
270     quantum = client.Client(username=quantum_username,
271                             password=quantum_password,
272                             tenant_name=quantum_tenant_name,
273                             auth_url=quantum_auth_url)
274
275     net = quantum.list_networks(name=name)
276     return net['networks'][0]['id']
277
278 def start():
279     global quantum_username
280     global quantum_password
281     global quantum_tenant_name
282
283     logger.log("%s: plugin starting up..." % plugin)
284
285     parser = ConfigParser()
286     parser.read("/etc/nova/nova.conf")
287     quantum_username = parser.get("DEFAULT", "quantum_admin_username")
288     quantum_password = parser.get("DEFAULT", "quantum_admin_password")
289     quantum_tenant_name = parser.get("DEFAULT", "quantum_admin_tenant_name")
290
291
292 def GetSlivers(data, config=None, plc=None):
293     global nat_net_id
294     global site_net_id
295
296     if not nat_net_id:
297         try:
298             nat_net_id = get_net_id_by_name(nat_net_name)
299             logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
300         except:
301             logger.log("%s: no network called %s..." % (plugin, nat_net_name))
302
303
304     # Fix me
305     # site_net_name = "devel"
306     site_net_name = "sharednet1"
307     if not site_net_id:
308         try:
309             site_net_id = get_net_id_by_name(site_net_name)
310             logger.log("%s: %s id is %s..." % (plugin, site_net_name, site_net_id))
311         except:
312             logger.log("%s: no network called %s..." % (plugin, site_net_name))
313
314     reset_iptables_chain()
315     ports = get_local_quantum_ports()
316
317     for interface in data['interfaces']:
318         try:
319             settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
320         except:
321             continue
322
323         tags = {}
324         for setting in settings:
325             tags[setting['tagname'].upper()] = setting['value']
326
327         if 'IFNAME' in tags:
328             dev = tags['IFNAME']
329         else:
330             # Skip devices that don't have names
331             logger.log('%s: Device has no name, skipping...' % plugin)
332             continue
333
334         logger.log('%s: Processing device %s' % (plugin, dev))
335
336         # Process Private-Nat networks
337         if 'NAT' in tags and nat_net_id:
338             add_iptables_masq(dev, interface)
339             write_dnsmasq_hostsfile(dev, ports, nat_net_id)
340             set_up_port_forwarding(dev, ports)
341             start_dnsmasq(dev, interface)
342
343         # Process Public networks
344         if interface['is_primary'] and site_net_id:
345             if 'OVS_BRIDGE' in tags:
346                 dev = tags['OVS_BRIDGE']
347             write_dnsmasq_hostsfile(dev, ports, site_net_id)
348             start_dnsmasq(dev, interface)
349