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:
7 * add the Quantum-assigned IP address to the vif via dnsmasq
8 * set up port forwarding rules through the NAT using iptables
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.
17 # system provided modules
18 import os, string, time, socket
19 from socket import inet_aton
20 import subprocess, signal
22 from ConfigParser import ConfigParser
24 # PlanetLab system modules
28 from quantumclient.v2_0 import client
33 plugin = "planetstack-net"
35 nat_net_name = "nat-net"
39 quantum_auth_url = "http://viccidev1:5000/v2.0/"
40 quantum_username = None
41 quantum_password = None
42 quantum_tenant_name = None
45 # Helper functions for converting to CIDR notation
46 def get_net_size(netmask):
49 binary_str += bin(int(octet))[2:].zfill(8)
50 return str(len(binary_str.rstrip('0')))
52 def to_cidr(ipaddr, netmask):
57 ipaddr = ipaddr.split('.')
58 netmask = netmask.split('.')
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)
63 def ipaddr_range(network, broadcast):
64 start = network.split('.')
65 end = broadcast.split('.')
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)
71 return '.'.join(start) + ',' + '.'.join(end)
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)
79 def add_iptables_rule(table, chain, args, pos = None):
80 iptargs = ['-t', table, '-C', chain] + args
82 run_iptables_cmd(iptargs)
85 iptargs = ['-t', table, '-I', chain, str(pos)] + args
89 run_iptables_cmd(iptargs)
91 logger.log('%s: FAILED to add iptables rule' % plugin)
93 def reset_iptables_chain():
95 # Flush the planetstack-nat chain
96 run_iptables_cmd(['-t', 'nat', '-F', plugin])
98 # Probably the chain doesn't exist, try creating it
99 run_iptables_cmd(['-t', 'nat', '-N', plugin])
101 add_iptables_rule('nat', 'PREROUTING', ['-j', plugin])
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'],
111 # Enable iptables MASQ for a device
112 def add_iptables_masq(dev, interface):
113 ipaddr = interface['ip']
114 netmask = interface['netmask']
118 cidr = to_cidr(ipaddr, netmask)
120 logger.log('%s: could not convert ipaddr %s and netmask %s to CIDR'
121 % (plugin, ipaddr, netmask))
124 args = ['-s', cidr, '!', '-d', cidr, '-j', 'MASQUERADE']
125 add_iptables_rule('nat', 'POSTROUTING', args)
127 def get_pidfile(dev):
128 return '/var/run/dnsmasq-%s.pid' % dev
130 def get_leasefile(dev):
131 return '/var/lib/dnsmasq/%s.leases' % dev
133 def get_hostsfile(dev):
134 return '/var/lib/dnsmasq/%s.hosts' % dev
136 # Check if dnsmasq already running
137 def dnsmasq_running(dev):
138 pidfile = get_pidfile(dev)
140 pid = open(pidfile, 'r').read().strip()
141 if os.path.exists('/proc/%s' % pid):
147 def dnsmasq_sighup(dev):
148 pidfile = get_pidfile(dev)
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))
155 logger.log("%s: Sending SIGHUP to dnsmasq FAILED on dev %s" % (plugin, dev))
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',
169 '--pid-file=%s' % get_pidfile(dev),
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']]
179 logger.log('%s: starting dnsmasq on device %s' % (plugin, dev))
180 subprocess.check_call(cmd)
182 logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev))
183 logger.log(' '.join(cmd))
185 def convert_ovs_output_to_dict(out):
186 decoded = json.loads(out.strip())
187 headings = decoded['headings']
188 data = decoded['data']
193 for i in range(0, len(headings) - 1):
194 if not isinstance(rec[i], list):
195 mydict[headings[i]] = rec[i]
197 if rec[i][0] == 'set':
198 mydict[headings[i]] = rec[i][1]
199 elif rec[i][0] == 'map':
201 for (key, value) in rec[i][1]:
203 mydict[headings[i]] = newdict
204 elif rec[i][0] == 'uuid':
205 mydict['uuid'] = rec[i][1]
206 records.append(mydict)
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():
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)
222 # Extract Quantum Port IDs from OvS records
225 port_ids.append(rec['external_ids']['iface-id'])
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']
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')
243 if port['network_id'] == net_id:
244 entry = "%s,%s\n" % (port['mac_address'], port['fixed_ips'][0]['ip_address'])
246 logger.log("%s: %s" % (plugin, entry))
249 # Send SIGHUP to dnsmasq to make it re-read hostsfile
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):
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))
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])
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)
275 net = quantum.list_networks(name=name)
276 return net['networks'][0]['id']
279 global quantum_username
280 global quantum_password
281 global quantum_tenant_name
283 logger.log("%s: plugin starting up..." % plugin)
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")
292 def GetSlivers(data, config=None, plc=None):
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))
301 logger.log("%s: no network called %s..." % (plugin, nat_net_name))
305 # site_net_name = "devel"
306 site_net_name = "sharednet1"
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))
312 logger.log("%s: no network called %s..." % (plugin, site_net_name))
314 reset_iptables_chain()
315 ports = get_local_quantum_ports()
317 for interface in data['interfaces']:
319 settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']})
324 for setting in settings:
325 tags[setting['tagname'].upper()] = setting['value']
330 # Skip devices that don't have names
331 logger.log('%s: Device has no name, skipping...' % plugin)
334 logger.log('%s: Processing device %s' % (plugin, dev))
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)
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)