From 65ac217539fc93dd595bd2790d22acd0e9604293 Mon Sep 17 00:00:00 2001 From: Andy Bavier Date: Mon, 9 Sep 2013 16:40:05 -0400 Subject: [PATCH] PlanetStack networking plugin --- plugins/planetstack-net.py | 245 +++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 plugins/planetstack-net.py diff --git a/plugins/planetstack-net.py b/plugins/planetstack-net.py new file mode 100644 index 0000000..b567674 --- /dev/null +++ b/plugins/planetstack-net.py @@ -0,0 +1,245 @@ +# system provided modules +import os, string, time, socket +from socket import inet_aton +import subprocess, signal +import json +from ConfigParser import ConfigParser + +# PlanetLab system modules +import sioc, plnet + +# Quantum modules +from quantumclient.v2_0 import client + +# local modules +import logger + +plugin = "planetstack-net" + +# Helper functions for converting to CIDR notation +def get_net_size(netmask): + binary_str = '' + for octet in netmask: + binary_str += bin(int(octet))[2:].zfill(8) + return str(len(binary_str.rstrip('0'))) + +def to_cidr(ipaddr, netmask): + # validate input + inet_aton(ipaddr) + inet_aton(netmask) + + ipaddr = ipaddr.split('.') + netmask = netmask.split('.') + + net_start = [str(int(ipaddr[x]) & int(netmask[x])) for x in range(0,4)] + return '.'.join(net_start) + '/' + get_net_size(netmask) + +def ipaddr_range(network, broadcast): + start = network.split('.') + end = broadcast.split('.') + + # Assume interface always claims the first address in the block + start[3] = str(int(start[3]) + 2) + end[3] = str(int(end[3]) - 1) + + return '.'.join(start) + ',' + '.'.join(end) + +# Enable iptables MASQ for a device +def add_iptables_masq(dev, interface): + ipaddr = interface['ip'] + netmask = interface['netmask'] + + cidr = None + try: + cidr = to_cidr(ipaddr, netmask) + except: + logger.log('%s: could not convert ipaddr %s and netmask %s to CIDR' + % (plugin, ipaddr, netmask)) + return + + cmd = ['/sbin/iptables', '-t', 'nat', '-C', 'POSTROUTING', '-s', cidr, + '!', '-d', cidr, '-j', 'MASQUERADE'] + try: + logger.log('%s: checking if NAT iptables rule present for device %s' % (plugin, dev)) + subprocess.check_call(cmd) + except: + logger.log('%s: adding NAT iptables NAT for device %s' % (plugin, dev)) + cmd[3] = '-A' + try: + subprocess.check_call(cmd) + except: + logger.log('%s: FAILED to add NAT iptables rule for device %s' % (plugin, dev)) + +def get_pidfile(dev): + return '/var/run/dnsmasq-%s.pid' % dev + +def get_leasefile(dev): + return '/var/lib/dnsmasq/%s.leases' % dev + +def get_hostsfile(dev): + return '/var/lib/dnsmasq/%s.hosts' % dev + +# Check if dnsmasq already running +def dnsmasq_running(dev): + pidfile = get_pidfile(dev) + try: + pid = open(pidfile, 'r').read().strip() + if os.path.exists('/proc/%s' % pid): + return True + except: + pass + return False + +def dnsmasq_sighup(dev): + pidfile = get_pidfile(dev) + try: + pid = open(pidfile, 'r').read().strip() + if os.path.exists('/proc/%s' % pid): + os.kill(int(pid), signal.SIGHUP) + logger.log("%s: Sent SIGHUP to dnsmasq on dev %s" % (plugin, dev)) + except: + logger.log("%s: Sending SIGHUP to dnsmasq FAILED on dev %s" % (plugin, dev)) + +# Enable dnsmasq for this interface +def start_dnsmasq(dev, interface): + if not dnsmasq_running(dev): + try: + logger.log('%s: starting dnsmasq on device %s' % (plugin, dev)) + iprange = ipaddr_range(interface['network'], interface['broadcast']) + logger.log('%s: IP range: %s' % (plugin, iprange)) + subprocess.check_call(['/usr/sbin/dnsmasq', + '--strict-order', + '--bind-interfaces', + '--local=//', + '--domain-needed', + '--pid-file=%s' % get_pidfile(dev), + '--conf-file=', + '--interface=%s' % dev, + '--dhcp-range=%s,120' % iprange, + '--dhcp-leasefile=%s' % get_leasefile(dev), + '--dhcp-hostsfile=%s' % get_hostsfile(dev), + '--dhcp-no-override']) + except: + logger.log('%s: FAILED to start dnsmasq for device %s' % (plugin, dev)) + +nat_net_name = "nat-net" +nat_net_id = None +quantum_auth_url = "http://viccidev1:5000/v2.0/" +quantum_username = None +quantum_password = None +quantum_tenant_name = None + +def convert_ovs_output_to_dict(out): + decoded = json.loads(out.strip()) + headings = decoded['headings'] + data = decoded['data'] + + records = [] + for rec in data: + mydict = {} + for i in range(0, len(headings) - 1): + if not isinstance(rec[i], list): + mydict[headings[i]] = rec[i] + else: + if rec[i][0] == 'set': + mydict[headings[i]] = rec[i][1] + elif rec[i][0] == 'map': + newdict = {} + for (key, value) in rec[i][1]: + newdict[key] = value + mydict[headings[i]] = newdict + elif rec[i][0] == 'uuid': + mydict['uuid'] = rec[i][1] + records.append(mydict) + + return records + +# Generate a dhcp-hostsfile for dnsmasq for all the local interfaces +# on the NAT network. The purpose is to make sure that the IP address +# assigned by Quantum appears on NAT interfaces. +# +# Workflow: +# - Get list of local VM interfaces +# - Query Quantum to get port information for these interfaces +# - Throw away all those not belonging to nat-net network +# - Write the MAC addr and IPs to the dhcp-hostsfile file (and log it) + +def write_hostsfile(dev): + # Get local information for VM interfaces from OvS + ovs_out = subprocess.check_output(['/usr/bin/ovs-vsctl', '-f', 'json', 'find', + 'Interface', 'external_ids:iface-id!="absent"']) + records = convert_ovs_output_to_dict(ovs_out) + + # Extract Quantum Port IDs from OvS records + port_ids = [] + for rec in records: + port_ids.append(rec['external_ids']['iface-id']) + + # Get the full info on these ports from Quantum + quantum = client.Client(username=quantum_username, + password=quantum_password, + tenant_name=quantum_tenant_name, + auth_url=quantum_auth_url) + ports = quantum.list_ports(id=port_ids) + + # Write relevant entries to dnsmasq hostsfile + logger.log("%s: Writing hostsfile for %s" % (plugin, dev)) + f = open(get_hostsfile(dev), 'w') + for port in ports['ports']: + if port['network_id'] == nat_net_id: + entry = "%s,%s\n" % (port['mac_address'], port['fixed_ips'][0]['ip_address']) + f.write(entry) + logger.log("%s: %s" % (plugin, entry)) + f.close() + + # Send SIGHUP to dnsmasq to make it re-read hostsfile + dnsmasq_sighup(dev) + +def start(): + global quantum_username + global quantum_password + global quantum_tenant_name + global nat_net_id + + logger.log("%s: plugin starting up..." % plugin) + + parser = ConfigParser() + parser.read("/etc/nova/nova.conf") + quantum_username = parser.get("DEFAULT", "quantum_admin_username") + quantum_password = parser.get("DEFAULT", "quantum_admin_password") + quantum_tenant_name = parser.get("DEFAULT", "quantum_admin_tenant_name") + + quantum = client.Client(username=quantum_username, + password=quantum_password, + tenant_name=quantum_tenant_name, + auth_url=quantum_auth_url) + + net = quantum.list_networks(name=nat_net_name) + nat_net_id = net['networks'][0]['id'] + + logger.log("%s: %s id is %s..." % (plugin, nat_net_name, nat_net_id)) + +def GetSlivers(data, config=None, plc=None): + for interface in data['interfaces']: + try: + settings = plc.GetInterfaceTags({'interface_tag_id': interface['interface_tag_ids']}) + except: + continue + + tags = {} + for setting in settings: + tags[setting['tagname'].upper()] = setting['value'] + + if 'IFNAME' in tags: + dev = tags['IFNAME'] + else: + # Skip devices that don't have names + logger.log('%s: Device has no name, skipping...' % plugin) + continue + + logger.log('%s: Processing device %s' % (plugin, dev)) + + if 'NAT' in tags: + add_iptables_masq(dev, interface) + write_hostsfile(dev) + start_dnsmasq(dev, interface) -- 2.47.0