Merge branch 'planetstack' of ssh://git.planet-lab.org/git/nodemanager into planetstack
[nodemanager.git] / net.py
1 """network configuration"""
2
3 # system provided modules
4 import os, string, time, socket
5 from socket import inet_aton
6
7 # PlanetLab system modules
8 import sioc, plnet
9
10 # local modules
11 import plnode.bwlimit as bwlimit
12 import logger, iptables, tools
13 import subprocess
14
15 # we can't do anything without a network
16 priority=1
17
18 dev_default = tools.get_default_if()
19
20
21 def start():
22     logger.log("net: plugin starting up...")
23
24 def GetSlivers(data, config, plc):
25     # added by caglar
26     # band-aid for short period as old API returns networks instead of interfaces
27     global KEY_NAME
28     KEY_NAME = "interfaces"
29     #################
30
31     logger.verbose("net: GetSlivers called.")
32     if not 'interfaces' in data:
33         # added by caglar
34         # band-aid for short period as old API returns networks instead of interfaces
35         # logger.log_missing_data('net.GetSlivers','interfaces')
36         # return
37         if not 'networks' in data:
38             logger.log_missing_data('net.GetSlivers','interfaces')
39             return
40         else:
41             KEY_NAME = "networks"
42         ##################
43
44     plnet.InitInterfaces(logger, plc, data)
45     
46     """
47     if 'OVERRIDES' in dir(config):
48         if config.OVERRIDES.get('net_max_rate') == '-1':
49             logger.log("net: Slice and node BW Limits disabled.")
50             if len(bwlimit.tc("class show dev %s" % dev_default)):
51                 logger.verbose("net: *** DISABLING NODE BW LIMITS ***")
52                 bwlimit.stop()
53         else:
54             InitNodeLimit(data)
55             InitI2(plc, data)
56     else:
57         InitNodeLimit(data)
58         InitI2(plc, data)
59     InitNAT(plc, data)
60     """
61     
62     InitPlanetStack(plc, data)
63
64
65 def InitNodeLimit(data):
66
67     # query running network interfaces
68     devs = sioc.gifconf()
69     ips = dict(zip(devs.values(), devs.keys()))
70     macs = {}
71     for dev in devs:
72         macs[sioc.gifhwaddr(dev).lower()] = dev
73
74     for interface in data[KEY_NAME]:
75         # Get interface name preferably from MAC address, falling
76         # back on IP address.
77         hwaddr=interface['mac']
78         if hwaddr <> None: hwaddr=hwaddr.lower()
79         if hwaddr in macs:
80             dev = macs[interface['mac']]
81         elif interface['ip'] in ips:
82             dev = ips[interface['ip']]
83         else:
84             logger.log('net: %s: no such interface with address %s/%s' % (interface['hostname'], interface['ip'], interface['mac']))
85             continue
86
87         # Get current node cap
88         try:
89             old_bwlimit = bwlimit.get_bwcap(dev)
90         except:
91             old_bwlimit = None
92
93         # Get desired node cap
94         if interface['bwlimit'] is None or interface['bwlimit'] < 0:
95             new_bwlimit = bwlimit.bwmax
96         else:
97             new_bwlimit = interface['bwlimit']
98
99         if old_bwlimit != new_bwlimit:
100             # Reinitialize bandwidth limits
101             bwlimit.init(dev, new_bwlimit)
102
103             # XXX This should trigger an rspec refresh in case
104             # some previously invalid sliver bwlimit is now valid
105             # again, or vice-versa.
106
107 def InitI2(plc, data):
108     if not 'groups' in data: return
109
110     if "Internet2" in data['groups']:
111         logger.log("net: This is an Internet2 node.  Setting rules.")
112         i2nodes = []
113         i2nodeids = plc.GetNodeGroups(["Internet2"])[0]['node_ids']
114         for node in plc.GetInterfaces({"node_id": i2nodeids}, ["ip"]):
115             # Get the IPs
116             i2nodes.append(node['ip'])
117         # this will create the set if it doesn't already exist
118         # and add IPs that don't exist in the set rather than
119         # just recreateing the set.
120         bwlimit.exempt_init('Internet2', i2nodes)
121
122         # set the iptables classification rule if it doesnt exist.
123         cmd = '-A POSTROUTING -m set --set Internet2 dst -j CLASSIFY --set-class 0001:2000 --add-mark'
124         rules = []
125         ipt = os.popen("/sbin/iptables-save")
126         for line in ipt.readlines(): rules.append(line.strip(" \n"))
127         ipt.close()
128         if cmd not in rules:
129             logger.verbose("net:  Adding iptables rule for Internet2")
130             os.popen("/sbin/iptables -t mangle " + cmd)
131
132 def InitNAT(plc, data):
133
134     # query running network interfaces
135     devs = sioc.gifconf()
136     ips = dict(zip(devs.values(), devs.keys()))
137     macs = {}
138     for dev in devs:
139         macs[sioc.gifhwaddr(dev).lower()] = dev
140
141     ipt = iptables.IPTables()
142     for interface in data[KEY_NAME]:
143         # Get interface name preferably from MAC address, falling
144         # back on IP address.
145         hwaddr=interface['mac']
146         if hwaddr <> None: hwaddr=hwaddr.lower()
147         if hwaddr in macs:
148             dev = macs[interface['mac']]
149         elif interface['ip'] in ips:
150             dev = ips[interface['ip']]
151         else:
152             logger.log('net: %s: no such interface with address %s/%s' % (interface['hostname'], interface['ip'], interface['mac']))
153             continue
154
155         try:
156             settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
157         except:
158             continue
159
160         for setting in settings:
161             if setting['category'].upper() != 'FIREWALL':
162                 continue
163             if setting['name'].upper() == 'EXTERNAL':
164                 # Enable NAT for this interface
165                 ipt.add_ext(dev)
166             elif setting['name'].upper() == 'INTERNAL':
167                 ipt.add_int(dev)
168             elif setting['name'].upper() == 'PF': # XXX Uglier code is hard to find...
169                 for pf in setting['value'].split("\n"):
170                     fields = {}
171                     for field in pf.split(","):
172                         (key, val) = field.split("=", 2)
173                         fields[key] = val
174                     if 'new_dport' not in fields:
175                         fields['new_dport'] = fields['dport']
176                     if 'source' not in fields:
177                         fields['source'] = "0.0.0.0/0"
178                     ipt.add_pf(fields)
179     ipt.commit()
180
181 # Helper functions for converting to CIDR notation
182 def get_net_size(netmask):
183     binary_str = ''
184     for octet in netmask:
185         binary_str += bin(int(octet))[2:].zfill(8)
186     return str(len(binary_str.rstrip('0')))
187
188 def to_cidr(ipaddr, netmask):
189     # validate input
190     inet_aton(ipaddr)
191     inet_aton(netmask)
192
193     ipaddr = ipaddr.split('.')
194     netmask = netmask.split('.')
195
196     net_start = [str(int(ipaddr[x]) & int(netmask[x])) for x in range(0,4)]
197     return '.'.join(net_start) + '/' + get_net_size(netmask)
198
199 def ipaddr_range(network, broadcast):
200     start = network.split('.')
201     end = broadcast.split('.')
202
203     # Assume interface always claims the first address in the block
204     start[3] = str(int(start[3]) + 2)
205     end[3] = str(int(end[3]) - 1)
206
207     return '.'.join(start) + ',' + '.'.join(end)
208
209 def InitPlanetStack(plc, data):
210
211     for interface in data[KEY_NAME]:
212         try:
213             settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
214         except:
215             continue
216
217         tags = {}
218         for setting in settings:
219             tags[setting['tagname'].upper()] = setting['value']
220
221         if 'IFNAME' in tags:
222             dev = tags['IFNAME']
223         else:
224             # Skip devices that don't have names
225             logger.log('net:InitPlanetStack: Device has no name, skipping...')
226             continue
227
228         logger.log('net:InitPlanetStack: Processing device %s' % dev)
229         
230         if 'NAT' in tags:
231             # Enable iptables MASQ on this device
232             # Right now the subnet is hardcoded, should instead use interface's subnet
233             ipaddr = interface['ip']
234             netmask = interface['netmask']
235
236             if (ipaddr and netmask):
237                 try:
238                     cidr = to_cidr(ipaddr, netmask)
239                 except:
240                     logger.log('net:InitPlanetStack: could not convert ipaddr %s and netmask %s to CIDR' % (ipaddr, netmask))
241
242             if cidr:
243                 cmd = ['/sbin/iptables', '-t', 'nat', '-C',  'POSTROUTING', '-s',  cidr, 
244                    '!',  '-d',  cidr, '-j', 'MASQUERADE']
245                 try:
246                     logger.log('net:InitPlanetStack: checking if NAT iptables rule present for device %s' % dev)
247                     subprocess.check_call(cmd)
248                 except:
249                     logger.log('net:InitPlanetStack: adding NAT iptables NAT for device %s' % dev)
250                     cmd[3] = '-A'
251                     try:
252                         subprocess.check_call(cmd)
253                     except:
254                         logger.log('net:InitPlanetStack: failed to add NAT iptables rule for device %s' % dev)
255                 
256             # Enable dnsmasq for this interface
257             # Check if dnsmasq already running
258             start_dnsmasq = True
259             pidfile = '/var/run/dnsmasq-%s.pid' % dev
260             try:
261                 pid = open(pidfile, 'r').read().strip()
262                 if os.path.exists('/proc/%s' % pid):
263                     start_dnsmasq = False
264                     logger.log('net:InitPlanetStack: dnsmasq already running on device %s' % dev)
265             except:
266                 pass
267
268             if start_dnsmasq:
269                 try:
270                     logger.log('net:InitPlanetStack: starting dnsmasq on device %s' % dev)
271                     iprange = ipaddr_range(interface['network'], interface['broadcast'])
272                     logger.log('net:InitPlanetStack: IP range: %s' % iprange)
273                     subprocess.check_call(['/usr/sbin/dnsmasq', 
274                                            '--strict-order', 
275                                            '--bind-interfaces',
276                                            '--local=//',  
277                                            '--domain-needed',  
278                                            '--pid-file=%s' % pidfile, 
279                                            '--conf-file=',
280                                            '--interface=%s' % dev, 
281                                            '--dhcp-range=%s' % iprange, 
282                                            '--dhcp-leasefile=/var/lib/dnsmasq/%s.leases' % dev, 
283                                            '--dhcp-no-override'])
284                 
285                 except:
286                     logger.log('net:InitPlanetStack: failed to start dnsmasq for device %s' % dev)