From 05702ecd1a642ffc1d44201af7f24d6b69f8bbf6 Mon Sep 17 00:00:00 2001 From: Ian Southam Date: Wed, 6 Aug 2014 15:34:15 +0200 Subject: [PATCH] Added guest network configuration Added iptables config (adds but no deletes yet) Apache config (VPC GN) DNSMasq config (VPX GN) --- .../debian/config/opt/cloud/bin/configure.py | 338 ++++++++++++++++-- .../debian/config/opt/cloud/bin/merge.py | 2 + 2 files changed, 301 insertions(+), 39 deletions(-) diff --git a/systemvm/patches/debian/config/opt/cloud/bin/configure.py b/systemvm/patches/debian/config/opt/cloud/bin/configure.py index 714ba525f17..5cee321e8b4 100755 --- a/systemvm/patches/debian/config/opt/cloud/bin/configure.py +++ b/systemvm/patches/debian/config/opt/cloud/bin/configure.py @@ -22,6 +22,8 @@ import subprocess import logging import re import time +import shutil +import os.path class CsHelper: """ General helper functions @@ -59,6 +61,60 @@ class CsHelper: result = p.communicate()[0] return result.splitlines() + def service(self, name, op): + self.execute("service %s %s" % (name, op)) + logging.info("Service %s %s" % (name, op)) + + def copy_if_needed(self, src, dest): + """ Copy a file if the destination does not already exist + """ + if os.path.isfile(dest): + return + try: + shutil.copy2(src, dest) + except IOError: + logging.Error("Could not copy %s to %s" % (src, dest)) + else: + logging.info("Copied %s to %s" % (src, dest)) + +class CsFile: + """ File editors """ + + def __init__(self, filename): + self.filename = filename + self.changed = False + self.load() + + def load(self): + self.new_config = [] + for line in open(self.filename): + self.new_config.append(line) + logging.debug("Reading file %s" % self.filename) + + def isChanged(self): + return self.changed + + def commit(self): + if not self.changed: + return + handle = open(self.filename, "w+") + for line in self.new_config: + handle.write(line) + handle.close() + logging.info("Wrote edited file %s" % self.filename) + + def search(self, search, replace): + found = False + logging.debug("Searching for %s and replacing with %s" % (search, replace)) + for index, line in enumerate(self.new_config): + if re.search(search, line): + found = True + if not replace in line: + self.changed = True + self.new_config[index] = replace + "\n" + if not found: + self.new_config.append(replace + "\n") + self.changed = True class CsRule: """ Manage iprules @@ -85,7 +141,7 @@ class CsRule: return False -class csRoute: +class CsRoute: """ Manage routes """ def __init__(self, dev): @@ -102,24 +158,29 @@ class csRoute: CsHelper().execute("ip route flush table %s" % (self.table)) CsHelper().execute("ip route flush cache") - def add(self, address): + def add(self, address, method = "add"): # ip route show dev eth1 table Table_eth1 10.0.2.0/24 - # sudo ip route add default via $defaultGwIP table $tableName proto static - cmd = "dev %s table %s %s" % (self.dev, self.table, address['network']) - self.addifmissing(cmd) + if(method == "add"): + cmd = "dev %s table %s %s" % (self.dev, self.table, address['network']) + self.set_route(cmd, method) - def addifmissing(self, cmd): + def set_route(self, cmd, method = "add"): """ Add a route is it is not already defined """ found = False for i in CsHelper().execute("ip route show " + cmd): found = True - if not found: + if not found and method == "add": logging.info("Add " + cmd) cmd = "ip route add " + cmd - CsHelper().execute(cmd) + elif found and method == "delete": + logging.info("Delete " + cmd) + cmd = "ip route delete " + cmd + else: + return + CsHelper().execute(cmd) -class csRpsrfs: +class CsRpsrfs: """ Configure rpsrfs if there is more than one cpu """ def __init__(self, dev): @@ -156,6 +217,86 @@ class csRpsrfs: return count +class CsApp: + def __init__(self, ip): + self.dev = ip.getDevice() + self.ip = ip.getAddress()['public_ip'] + self.domain = "domain.local" + self.type = ip.get_type() + if self.type == "guest": + gn = CsGuestNetwork(self.dev) + self.domain = gn.getDomain() + +class CsApache(CsApp): + """ Set up Apache """ + + def setup(self): + CsHelper().copy_if_needed("/etc/apache2/vhostexample.conf", "/etc/apache2/conf.d/vhost%s.conf" % self.dev) + + file = CsFile("/etc/apache2/conf.d/vhost%s.conf" % (self.dev)) + file.search("", "\t" % (self.ip)) + file.search("", "\t" % (self.ip)) + file.search("", "\t" % (self.ip)) + file.search("Listen .*:80", "Listen %s:80" % (self.ip)) + file.search("Listen .*:443", "Listen %s:443" % (self.ip)) + file.search("ServerName.*", "\tServerName vhost%s.cloudinternal.com" % (self.dev)) + file.commit() + +class CsDnsmasq(CsApp): + """ Set up dnsmasq """ + + def add_firewall_rules(self, method): + """ Add the necessary firewall rules + This is problamatic because the current logic cannot delete them + (In a convergence model) + + We will need to store some state about what "used" to be there + """ + firewall = CsIpTables(self.dev) + + cmds = "-A INPUT -i %s -p udp -m udp --dport 67 -j %s" + slist = [ self.dev, "ACCEPT" ] + firewall.change_rule("", slist, cmds) + + cmds = "-A INPUT -i %s -d %s -p %s -m %s --dport %s -j %s" + slist = [ self.dev, self.ip, "udp", "udp", "53", "ACCEPT" ] + firewall.change_rule("", slist, cmds) + + cmds = "-A INPUT -i %s -d %s -p %s -m %s --dport %s -j %s" + slist = [ self.dev, self.ip, "tcp", "tcp", "53", "ACCEPT" ] + firewall.change_rule("", slist, cmds) + + def configure_server(self, method = "add"): + file = CsFile("/etc/dnsmasq.d/cloud.conf") + file.search("dhcp-range=interface:%s" % self.dev, \ + "dhcp-range=interface:%s,set:interface-%s,%s,static" % (self.dev, self.dev, self.ip)) + file.search("dhcp-option=tag:interface-%s," % self.dev, \ + "dhcp-option=tag:interface-%s,15,%s" % (self.dev, self.domain)) + file.commit() + + if file.isChanged(): + CsHelper().service("dnsmasq", "restart") + + +class CsGuestNetwork: + def __init__(self, device): + self.data = {} + db = dataBag() + db.setKey("guestnetwork") + db.load() + dbag = db.getDataBag() + for dev in dbag: + if dev == "id": + continue + if dev == device: + self.data = dbag[dev][0] + + def getDomain(self): + if 'domain_name' in self.data: + return self.data['domain_name'] + else: + return "cloudnine.internal" + class CsDevice: """ Configure Network Devices """ def __init__(self, dev): @@ -168,6 +309,13 @@ class CsDevice: self.tableNo = dev[3] self.table = "Table_%s" % (dev) + def configure_rp(self): + """ + Configure Reverse Path Filtering + """ + filename = "/proc/sys/net/ipv4/conf/%s/rp_filter" % (self.dev) + CsHelper().updatefile(filename, "1\n", "w") + def buildlist(self): """ List all available network devices on the system @@ -182,30 +330,6 @@ class CsDevice: continue self.devlist.append(vals[0]) - def set_connmark(self): - """ Set connmark for device """ - if not self.has_connmark(): - cmd="-A PREROUTING -i %s -m state --state NEW -j CONNMARK --set-mark %s" \ - % (self.dev, self.tableNo) - CsHelper().execute("iptables -t mangle %s" % (cmd)) - logging.error("Set connmark for device %s (Table %s)", self.dev, self.tableNo) - - def has_connmark(self): - cmd = "iptables-save -t mangle" - for line in CsHelper().execute(cmd): - if not "PREROUTING" in line: - continue - if not "state" in line: - continue - if not "CONNMARK" in line: - continue - if not "set-xmark" in line: - continue - if not self.dev in line: - continue - return True - return False - def waitForDevice(self): """ Wait up to 15 seconds for a device to become available """ @@ -229,9 +353,108 @@ class CsDevice: if " DOWN " in i: cmd2 = "ip link set %s up" % (self.dev) CsHelper().execute(cmd2) - self.set_connmark() + CsIpTables(self.dev).set_connmark() +class CsIpTables: + + """ Utility class + All the bits and pieces needed for iptables operations + """ + def __init__(self, dev): + self.dev = dev + self.tableNo = dev[3] + self.table = "Table_%s" % (dev) + self.devChain = "ACL_INBOUND_%s" % (dev) + + def set_connmark(self, method = "add"): + """ Set connmark for device """ + slist = ["PREROUTING", self.dev, "state", "NEW", "CONNMARK", self.tableNo] + if method == "add": + cmds="-A %s -i %s -m %s --state %s -j %s --set-mark 0x%s" + else: + cmds="-D %s -i %s -m %s --state %s -j %s --set-mark 0x%s" + self.change_rule("mangle", slist, cmds) + + def set_accept(self, table, method = "add"): + """ Add an accept rule + First version - very simple - once I find out what the patterns + are this will be refactored + """ + slist = [ self.devChain, "ACCEPT" ] + if method == "add": + cmds = "-A %s -j %s" + else: + cmds = "-D %s -j %s" + self.change_rule( table, slist, cmds) + + def set_forward(self, ip, method = "add"): + """ set a forward to the device chain + takes a CsIP object """ + slist = [ "FORWARD", self.dev, ip['network'], self.devChain ] + if method == "add": + cmds = "-A %s -o %s -d %s -j %s" + else: + cmds = "-A %s -o %s -d %s -j %s" + self.change_rule('mangle', slist, cmds) + + def set_drop(self, method = "add"): + """ Ensure the last rule is drop """ + slist = [ self.devChain, "DROP" ] + if method == "add": + cmds="-A %s -j %s" + else: + cmds="-D %s -j %s" + self.change_rule('', slist, cmds) + + def set_static_nat(self, ip, method = "add"): + """ Add static nat to a device/ip combination + Takes a CsIp object as its parameter + """ + slist = ["POSTROUTING", ip['network'], self.dev, "SNAT", ip['public_ip']] + if method == "add": + cmds ="-A %s -s %s -o %s -j %s --to-source %s" + else: + cmds ="-D %s -s %s -o %s -j %s --to-source %s" + self.change_rule('nat', slist, cmds) + + def set_preroute(self, ip, method): + slist = [ "PREROUTING", "NEW", self.dev, ip['network'], ip['public_ip'], self.devChain ] + cmds = "-A %s -m state --state %s -i %s -s %s ! -d %s -j %s" + if method == "add": + self.change_rule('mangle', slist, cmds) + + def change_rule(self, table, slist, cmds): + cmd = '' + if not self.has_rule(table, slist): + if table != '': + cmd = "-t %s " % table + cmd += cmds % tuple(slist) + CsHelper().execute("iptables %s" % (cmd)) + logging.info("iptables %s", cmd) + + def set_chain(self, table, method): + """ Create a chain if it does not already exist """ + slist = [ self.devChain ] + cmd = '' + if not self.has_rule(table, slist): + if table != '': + cmd = "-t %s " % table + cmd += "-N %s" % tuple(slist) + CsHelper().execute("iptables %s" % (cmd)) + logging.info("iptables %s", cmd) + + def has_rule(self, table, list): + """ Check if a particular rule exists """ + cmd = "iptables-save " + if table != "": + cmd += "-t %s" % (table) + for line in CsHelper().execute(cmd): + matches = len([i for i in list if i in line]) + if(matches == len(list)): + return True + return False + class CsIP: def __init__(self, dev): @@ -243,6 +466,8 @@ class CsIP: def setAddress(self, address): self.address = address + def getAddress(self): + return self.address def configure(self): logging.info("Configuring address %s on device %s", self.ip(), self.dev) @@ -252,13 +477,44 @@ class CsIP: def post_configure(self): """ The steps that must be done after a device is configured """ - route = csRoute(self.dev) + route = CsRoute(self.dev) route.routeTable() CsRule(self.dev).addMark() CsDevice(self.dev).setUp() self.arpPing() - route.add(self.address) - csRpsrfs(self.dev).enable() + CsRpsrfs(self.dev).enable() + self.post_config_change("add") + + def get_type(self): + """ Return the type of the IP + guest + control + public + """ + if "nw_type" in self.address: + return self.address['nw_type'] + return "unknown" + + def post_config_change(self, method): + route = CsRoute(self.dev) + route.routeTable() + route.add(self.address, method) + # On deletion nw_type will no longer be known + if self.get_type() in [ "guest" ]: + CsDevice(self.dev).configure_rp() + CsIpTables(self.dev).set_static_nat(self.address, method) + CsIpTables(self.dev).set_chain('', method) + CsIpTables(self.dev).set_chain('mangle', method) + CsIpTables(self.dev).set_accept('mangle', method) + CsIpTables(self.dev).set_forward(self.address, method) + CsIpTables(self.dev).set_drop(method) + CsIpTables(self.dev).set_preroute(self.address, method) + dns = CsDnsmasq(self) + dns.add_firewall_rules("add") + dns.configure_server() + app = CsApache(self) + app.setup() + route.flush() def list(self): @@ -278,6 +534,9 @@ class CsIP: def ip(self): return str(self.address['cidr']) + def getDevice(self): + return self.dev + def hasIP(self, ip): return ip in self.address.values() @@ -312,9 +571,10 @@ class CsIP: cmd = "ip addr del dev %s %s" % (self.dev, ip) subprocess.call(cmd, shell=True) logging.info("Removed address %s from device %s", ip, self.dev) - + self.post_config_change("delete") def main(argv): + logging.basicConfig(filename='/var/log/cloud.log', level=logging.DEBUG, format='%(asctime)s %(message)s') @@ -333,7 +593,7 @@ def main(argv): continue ip = CsIP(dev) for address in dbag[dev]: - csRoute(dev).add(address) + CsRoute(dev).add(address) ip.setAddress(address) if ip.configured(): logging.info("Address %s on device %s already configured", ip.ip(), dev) diff --git a/systemvm/patches/debian/config/opt/cloud/bin/merge.py b/systemvm/patches/debian/config/opt/cloud/bin/merge.py index d61dc363965..b8a53ce2d55 100755 --- a/systemvm/patches/debian/config/opt/cloud/bin/merge.py +++ b/systemvm/patches/debian/config/opt/cloud/bin/merge.py @@ -84,6 +84,8 @@ class updateDataBag: dp['nw_type'] = 'guest' qf = loadQueueFile() qf.load({ 'ip_address' : [ dp ], 'type' : 'ips'}) + if 'domain_name' not in d.keys() or d['domain_name'] == '': + d['domain_name'] = "cloudnine.internal" return cs_guestnetwork.merge(dbag, self.qFile.data) def processIP(self, dbag):