PlanetStack networking plugin
[nodemanager.git] / plugins / planetstack-net.py
1 # system provided modules
2 import os, string, time, socket
3 from socket import inet_aton
4 import subprocess, signal
5 import json
6 from ConfigParser import ConfigParser
7
8 # PlanetLab system modules
9 import sioc, plnet
10
11 # Quantum modules
12 from quantumclient.v2_0 import client
13
14 # local modules
15 import logger
16
17 plugin = "planetstack-net"
18
19 # Helper functions for converting to CIDR notation
20 def get_net_size(netmask):
21     binary_str = ''
22     for octet in netmask:
23         binary_str += bin(int(octet))[2:].zfill(8)
24     return str(len(binary_str.rstrip('0')))
25
26 def to_cidr(ipaddr, netmask):
27     # validate input
28     inet_aton(ipaddr)
29     inet_aton(netmask)
30
31     ipaddr = ipaddr.split('.')
32     netmask = netmask.split('.')
33
34     net_start = [str(int(ipaddr[x]) & int(netmask[x])) for x in range(0,4)]
35     return '.'.join(net_start) + '/' + get_net_size(netmask)
36
37 def ipaddr_range(network, broadcast):
38     start = network.split('.')
39     end = broadcast.split('.')
40
41     # Assume interface always claims the first address in the block
42     start[3] = str(int(start[3]) + 2)
43     end[3] = str(int(end[3]) - 1)
44
45     return '.'.join(start) + ',' + '.'.join(end)
46
47 # Enable iptables MASQ for a device
48 def add_iptables_masq(dev, interface):
49     ipaddr = interface['ip']
50     netmask = interface['netmask']
51     
52     cidr = None
53     try:
54         cidr = to_cidr(ipaddr, netmask)
55     except:
56         logger.log('%s: could not convert ipaddr %s and netmask %s to CIDR' 
57                    % (plugin, ipaddr, netmask))
58         return
59     
60     cmd = ['/sbin/iptables', '-t', 'nat', '-C',  'POSTROUTING', '-s',  cidr, 
61            '!',  '-d',  cidr, '-j', 'MASQUERADE']
62     try:
63         logger.log('%s: checking if NAT iptables rule present for device %s' % (plugin, dev))
64         subprocess.check_call(cmd)
65     except:
66         logger.log('%s: adding NAT iptables NAT for device %s' % (plugin, dev))
67         cmd[3] = '-A'
68         try:
69             subprocess.check_call(cmd)
70         except:
71             logger.log('%s: FAILED to add NAT iptables rule for device %s' % (plugin, dev))
72
73 def get_pidfile(dev):
74     return '/var/run/dnsmasq-%s.pid' % dev
75
76 def get_leasefile(dev):
77     return '/var/lib/dnsmasq/%s.leases' % dev
78
79 def get_hostsfile(dev):
80     return '/var/lib/dnsmasq/%s.hosts' % dev
81
82 # Check if dnsmasq already running
83 def dnsmasq_running(dev):
84     pidfile = get_pidfile(dev)
85     try:
86         pid = open(pidfile, 'r').read().strip()
87         if os.path.exists('/proc/%s' % pid):
88             return True
89     except:
90         pass
91     return False
92
93 def dnsmasq_sighup(dev):
94     pidfile = get_pidfile(dev)
95     try:
96         pid = open(pidfile, 'r').read().strip()
97         if os.path.exists('/proc/%s' % pid):
98             os.kill(int(pid), signal.SIGHUP)
99             logger.log("%s: Sent SIGHUP to dnsmasq on dev %s" % (plugin, dev))
100     except:
101         logger.log("%s: Sending SIGHUP to dnsmasq FAILED on dev %s" % (plugin, dev))
102
103 # Enable dnsmasq for this interface
104 def start_dnsmasq(dev, interface):
105     if not dnsmasq_running(dev):
106         try:
107             logger.log('%s: starting dnsmasq on device %s' % (plugin, dev))
108             iprange = ipaddr_range(interface['network'], interface['broadcast'])
109             logger.log('%s: IP range: %s' % (plugin, iprange))
110             subprocess.check_call(['/usr/sbin/dnsmasq', 
111                                    '--strict-order', 
112                                    '--bind-interfaces',
113                                    '--local=//',  
114                                    '--domain-needed',  
115                                    '--pid-file=%s' % get_pidfile(dev), 
116                                    '--conf-file=',
117                                    '--interface=%s' % dev, 
118                                    '--dhcp-range=%s,120' % iprange, 
119                                    '--dhcp-leasefile=%s' % get_leasefile(dev),
120                                    '--dhcp-hostsfile=%s' % get_hostsfile(dev),
121                                    '--dhcp-no-override'])
122         except:
123             logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev))
124
125 nat_net_name = "nat-net"
126 nat_net_id = None
127 quantum_auth_url = "http://viccidev1:5000/v2.0/"
128 quantum_username = None
129 quantum_password = None
130 quantum_tenant_name = None
131
132 def convert_ovs_output_to_dict(out):
133     decoded = json.loads(out.strip())
134     headings = decoded['headings']
135     data = decoded['data']
136
137     records = []
138     for rec in data:
139         mydict = {}
140         for i in range(0, len(headings) - 1):
141             if not isinstance(rec[i], list):
142                 mydict[headings[i]] = rec[i]
143             else:
144                 if rec[i][0] == 'set':
145                     mydict[headings[i]] = rec[i][1]
146                 elif rec[i][0] == 'map':
147                     newdict = {}
148                     for (key, value) in rec[i][1]:
149                         newdict[key] = value
150                     mydict[headings[i]] = newdict
151                 elif rec[i][0] == 'uuid':
152                     mydict['uuid'] = rec[i][1]
153         records.append(mydict)
154
155     return records
156
157 # Generate a dhcp-hostsfile for dnsmasq for all the local interfaces
158 # on the NAT network.  The purpose is to make sure that the IP address
159 # assigned by Quantum appears on NAT interfaces.
160 #
161 # Workflow:
162 # - Get list of local VM interfaces
163 # - Query Quantum to get port information for these interfaces
164 # - Throw away all those not belonging to nat-net network
165 # - Write the MAC addr and IPs to the dhcp-hostsfile file (and log it)
166
167 def write_hostsfile(dev):
168     # Get local information for VM interfaces from OvS
169     ovs_out = subprocess.check_output(['/usr/bin/ovs-vsctl', '-f', 'json', 'find', 
170                                        'Interface', 'external_ids:iface-id!="absent"'])
171     records = convert_ovs_output_to_dict(ovs_out)
172
173     # Extract Quantum Port IDs from OvS records
174     port_ids = []
175     for rec in records:
176         port_ids.append(rec['external_ids']['iface-id'])
177
178     # Get the full info on these ports from Quantum
179     quantum = client.Client(username=quantum_username, 
180                             password=quantum_password,
181                             tenant_name=quantum_tenant_name,
182                             auth_url=quantum_auth_url)
183     ports = quantum.list_ports(id=port_ids)
184
185     # Write relevant entries to dnsmasq hostsfile
186     logger.log("%s: Writing hostsfile for %s" % (plugin, dev))
187     f = open(get_hostsfile(dev), 'w')
188     for port in ports['ports']:
189         if port['network_id'] == nat_net_id:
190             entry = "%s,%s\n" % (port['mac_address'], port['fixed_ips'][0]['ip_address'])
191             f.write(entry)
192             logger.log("%s: %s" % (plugin, entry))
193     f.close()
194
195     # Send SIGHUP to dnsmasq to make it re-read hostsfile
196     dnsmasq_sighup(dev)
197
198 def start():
199     global quantum_username
200     global quantum_password
201     global quantum_tenant_name
202     global nat_net_id
203
204     logger.log("%s: plugin starting up..." % plugin)
205
206     parser = ConfigParser()
207     parser.read("/etc/nova/nova.conf")
208     quantum_username = parser.get("DEFAULT", "quantum_admin_username")
209     quantum_password = parser.get("DEFAULT", "quantum_admin_password")
210     quantum_tenant_name = parser.get("DEFAULT", "quantum_admin_tenant_name")
211
212     quantum = client.Client(username=quantum_username, 
213                             password=quantum_password,
214                             tenant_name=quantum_tenant_name,
215                             auth_url=quantum_auth_url)
216
217     net = quantum.list_networks(name=nat_net_name)
218     nat_net_id = net['networks'][0]['id']
219
220     logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id))
221
222 def GetSlivers(data, config=None, plc=None):
223     for interface in data['interfaces']:
224         try:
225             settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
226         except:
227             continue
228
229         tags = {}
230         for setting in settings:
231             tags[setting['tagname'].upper()] = setting['value']
232
233         if 'IFNAME' in tags:
234             dev = tags['IFNAME']
235         else:
236             # Skip devices that don't have names
237             logger.log('%s: Device has no name, skipping...' % plugin)
238             continue
239
240         logger.log('%s: Processing device %s' % (plugin, dev))
241         
242         if 'NAT' in tags:
243             add_iptables_masq(dev, interface)
244             write_hostsfile(dev)
245             start_dnsmasq(dev, interface)