#!/usr/bin/python # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import sys import os from merge import dataBag from pprint import pprint import subprocess import logging import re import time import shutil import os.path import CsHelper from CsNetfilter import CsNetfilters from fcntl import flock, LOCK_EX, LOCK_UN from CsDhcp import CsDhcp fw = [] class CsFile: """ File editors """ def __init__(self, filename): self.filename = filename self.changed = False self.load() def load(self): self.new_config = [] try: for line in open(self.filename): self.new_config.append(line) except IOError: logging.debug("File %s does not exist" % self.filename) return else: logging.debug("Reading file %s" % self.filename) def is_changed(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 line.lstrip().startswith("#"): continue 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 Supported Types: fwmark """ def __init__(self, dev): self.dev = dev self.tableNo = dev[3] self.table = "Table_%s" % (dev) def addMark(self): if not self.findMark(): cmd = "ip rule add fwmark %s table %s" % (self.tableNo, self.table) CsHelper.execute(cmd) logging.info("Added fwmark rule for %s" % (self.table)) def findMark(self): srch = "from all fwmark 0x%s lookup %s" % (self.tableNo, self.table) for i in CsHelper.execute("ip rule show"): if srch in i.strip(): return True return False class CsRoute: """ Manage routes """ def __init__(self, dev): self.dev = dev self.tableNo = dev[3] self.table = "Table_%s" % (dev) def routeTable(self): str = "%s %s" % (self.tableNo, self.table) filename = "/etc/iproute2/rt_tables" CsHelper.addifmissing(filename, str) def flush(self): CsHelper.execute("ip route flush table %s" % (self.table)) CsHelper.execute("ip route flush cache") def add(self, address, method = "add"): # ip route show dev eth1 table Table_eth1 10.0.2.0/24 if(method == "add"): cmd = "dev %s table %s %s" % (self.dev, self.table, address['network']) self.set_route(cmd, method) 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 and method == "add": logging.info("Add " + cmd) cmd = "ip route add " + cmd elif found and method == "delete": logging.info("Delete " + cmd) cmd = "ip route delete " + cmd else: return CsHelper.execute(cmd) class CsRpsrfs: """ Configure rpsrfs if there is more than one cpu """ def __init__(self, dev): self.dev = dev def enable(self): if not self.inKernel(): return cpus = self.cpus() if cpus < 2: return val = format((1 << cpus) - 1, "x") filename = "/sys/class/net/%s/queues/rx-0/rps_cpus" % (self.dev) CsHelper.updatefile(filename, val, "w+") CsHelper.updatefile("/proc/sys/net/core/rps_sock_flow_entries", "256", "w+") filename = "/sys/class/net/%s/queues/rx-0/rps_flow_cnt" % (self.dev) CsHelper.updatefile(filename, "256", "w+") logging.debug("rpsfr is configured for %s cpus" % (cpus)) def inKernel(self): try: open('/etc/rpsrfsenable') except IOError: logging.debug("rpsfr is not present in the kernel") return False else: logging.debug("rpsfr is present in the kernel") return True def cpus(self): count = 0 for line in open('/proc/cpuinfo'): if "processor" not in line: continue count += 1 if count < 2: logging.debug("Single CPU machine") return count class CsProcess(object): """ Manipulate processes """ def __init__(self, search): self.search = search def start(self, thru, background = ''): #if(background): #cmd = cmd + " &" logging.info("Started %s", " ".join(self.search)) os.system("%s %s %s" % (thru, " ".join(self.search), background)) def find(self): self.pid = [] for i in CsHelper.execute("ps aux"): items = len(self.search) proc = re.split("\s+", i)[items*-1:] matches = len([m for m in proc if m in self.search]) if matches == items: self.pid.append(re.split("\s+", i)[1]) return len(self.pid) > 0 class CsApp: def __init__(self, ip): self.dev = ip.getDevice() self.ip = ip.get_ip_address() self.type = ip.get_type() global fw class CsPasswdSvc(CsApp): """ nohup bash /opt/cloud/bin/vpc_passwd_server $ip >/dev/null 2>&1 & """ def setup(self): fw.append(["", "front", "-A INPUT -i %s -d %s/32 -p tcp -m tcp -m state --state NEW --dport 8080 -j ACCEPT" % (self.dev, self.ip) ]) proc = CsProcess(['/opt/cloud/bin/vpc_passwd_server', self.ip]) if not proc.find(): proc.start("/usr/bin/nohup", "2>&1 &") class CsApache(CsApp): """ Set up Apache """ def remove(self): file = "/etc/apache2/conf.d/vhost%s.conf" % self.dev if os.path.isfile(file): os.remove(file) CsHelper.service("apache2", "restart") 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() if file.is_changed(): CsHelper.service("apache2", "restart") fw.append(["", "front", "-A INPUT -i %s -d %s/32 -p tcp -m state -m tcp --state NEW --dport 80 -j ACCEPT" % (self.dev, self.ip) ]) class CsDnsmasq(CsApp): """ Set up dnsmasq """ def add_firewall_rules(self): """ Add the necessary firewall rules """ fw.append(["", "front", "-A INPUT -i %s -p udp -m udp --dport 67 -j ACCEPT" % self.dev ]) fw.append(["", "front", "-A INPUT -i %s -d %s/32 -p udp -m udp --dport 53 -j ACCEPT" % (self.dev, self.ip) ]) fw.append(["", "front", "-A INPUT -i %s -d %s/32 -p tcp -m tcp --dport 53 -j ACCEPT" % ( self.dev, self.ip ) ]) class CsDevice: """ Configure Network Devices """ def __init__(self, dev): self.devlist = [] self.dev = dev self.buildlist() self.table = '' self.tableNo = '' if dev != '': self.tableNo = dev[3] self.table = "Table_%s" % dev global fw 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 """ self.devlist = [] for line in open('/proc/net/dev'): vals = line.lstrip().split(':') if (not vals[0].startswith("eth")): continue self.devlist.append(vals[0]) def waitfordevice(self): """ Wait up to 15 seconds for a device to become available """ count = 0 while count < 15: if self.dev in self.devlist: return True time.sleep(1) count += 1 self.buildlist(); logging.error("Device %s cannot be configured - device was not found", self.dev) return False def list(self): return self.devlist def setUp(self): """ Ensure device is up """ cmd = "ip link show %s | grep 'state DOWN'" % self.dev for i in CsHelper.execute(cmd): if " DOWN " in i: cmd2 = "ip link set %s up" % self.dev CsHelper.execute(cmd2) cmd = "-A PREROUTING -i %s -m state --state NEW -j CONNMARK --set-xmark 0x%s/0xffffffff" % \ (self.dev, self.dev[3]) fw.append(["mangle", "", cmd]) class CsIP: def __init__(self, dev): self.dev = dev self.iplist = {} self.address = {} self.list() 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) cmd = "ip addr add dev %s %s brd +" % (self.dev, self.ip()) subprocess.call(cmd, shell=True) self.post_configure() def post_configure(self): """ The steps that must be done after a device is configured """ if not self.get_type() in [ "control" ]: route = CsRoute(self.dev) route.routeTable() CsRule(self.dev).addMark() CsDevice(self.dev).setUp() self.arpPing() 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 get_ip_address(self): """ Return ip address if known """ if "public_ip" in self.address: return self.address['public_ip'] 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" ]: devChain = "ACL_INBOUND_%s" % (self.dev) CsDevice(self.dev).configure_rp() fw.append(["nat", "", "-A POSTROUTING -s %s -o %s -j SNAT --to-source %s" % \ (self.address['network'], self.dev, self.address['public_ip']) ]) fw.append(["mangle", "", "-A %s -j ACCEPT" % devChain]) fw.append(["", "", "-A FORWARD -o %s -d %s -j %s" % (self.dev, self.address['network'], devChain) ]) fw.append(["", "", "-A %s -j DROP" % devChain]) fw.append(["mangle", "", "-A PREROUTING -m state --state NEW -i %s -s %s ! -d %s/32 -j %s" % \ (self.dev, self.address['network'], self.address['public_ip'], devChain) ]) dns = CsDnsmasq(self) dns.add_firewall_rules() app = CsApache(self) app.setup() pwdsvc = CsPasswdSvc(self).setup() route.flush() def list(self): self.iplist = {} cmd = ("ip addr show dev " + self.dev) for i in CsHelper.execute(cmd): vals = i.lstrip().split() if (vals[0] == 'inet'): self.iplist[vals[1]] = self.dev def configured(self): if self.address['cidr'] in self.iplist.keys(): return True return False def ip(self): return str(self.address['cidr']) def getDevice(self): return self.dev def hasIP(self, ip): return ip in self.address.values() def arpPing(self): cmd = "arping -c 1 -I %s -A -U -s %s %s" % (self.dev, self.address['public_ip'], self.address['public_ip']) CsHelper.execute(cmd) # Delete any ips that are configured but not in the bag def compare(self, bag): if len(self.iplist) > 0 and (not self.dev in bag.keys() or len(bag[self.dev]) == 0): # Remove all IPs on this device logging.info("Will remove all configured addresses on device %s", self.dev) self.delete("all") app = CsApache(self) app.remove() # This condition should not really happen but did :) # It means an apache file got orphaned after a guest network address was deleted if len(self.iplist) == 0 and (not self.dev in bag.keys() or len(bag[self.dev]) == 0): app = CsApache(self) app.remove() for ip in self.iplist: found = False if self.dev in bag.keys(): for address in bag[self.dev]: self.setAddress(address) if self.hasIP(ip): found = True if not found: self.delete(ip) def delete(self, ip): remove = [] if ip == "all": logging.info("Removing addresses from device %s", self.dev) remove = self.iplist.keys() else: remove.append(ip) for ip in remove: 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") class CsDataBag(object): def __init__(self, key): self.data = {} db = dataBag() db.setKey(key) db.load() self.dbag = db.getDataBag() global fw def get_bag(self): return self.dbag def process(self): pass class CsCmdLine(CsDataBag): """ Get cmdline config parameters """ def is_redundant(self): if "redundant" in self.dbag['config']: return self.dbag['config']['redundant'] == "true" return False class CsAcl(CsDataBag): """ Deal with Network acls """ class AclDevice(): """ A little class for each list of acls per device """ def __init__(self, obj): self.ingess = [] self.egress = [] self.device = obj['device'] self.ip = obj['nic_ip'] self.netmask= obj['nic_netmask'] self.cidr = "%s/%s" % (self.ip, self.netmask) if "ingress_rules" in obj.keys(): self.ingress = obj['ingress_rules'] if "egress_rules" in obj.keys(): self.egress = obj['egress_rules'] def create(self): self.process(self.ingress) self.process(self.egress) def process(self,rule_list): for i in rule_list: pprint(i) def process(self): for item in self.dbag: if item == "id": continue dev_obj = self.AclDevice(self.dbag[item]).create() class CsPassword(CsDataBag): """ Update the password cache A stupid step really as we should just rewrite the password server to use the databag """ cache = "/var/cache/cloud/passwords" def process(self): file = CsFile(self.cache) for item in self.dbag: if item == "id": continue self.__update(file, item, self.dbag[item]) file.commit() def __update(self, file, ip, password): file.search("%s=" % ip, "%s=%s" % (ip, password)) class CsVmMetadata(CsDataBag): def process(self): for ip in self.dbag: if ("id" == ip): continue logging.info("Processing metadata for %s" % ip) for item in self.dbag[ip]: folder = item[0] file = item[1] data = item[2] # process only valid data if folder != "userdata" and folder != "metadata": continue if file == "": continue self.__htaccess(ip, folder, file) if data == "": self.__deletefile(ip, folder, file) else: self.__createfile(ip, folder, file, data) def __deletefile(self, ip, folder, file): datafile = "/var/www/html/" + folder + "/" + ip + "/" + file if os.path.exists(datafile): os.remove(datafile) def __createfile(self, ip, folder, file, data): dest = "/var/www/html/" + folder + "/" + ip + "/" + file metamanifestdir = "/var/www/html/" + folder + "/" + ip metamanifest = metamanifestdir + "/meta-data" # base64 decode userdata if folder == "userdata" or folder == "user-data": if data is not None: data = base64.b64decode(data) fh = open(dest, "w") self.__exflock(fh) if data is not None: fh.write(data) else: fh.write("") self.__unflock(fh) fh.close() os.chmod(dest, 0644) if folder == "metadata" or folder == "meta-data": try: os.makedirs(metamanifestdir, 0755) except OSError as e: # error 17 is already exists, we do it this way for concurrency if e.errno != 17: print "failed to make directories " + metamanifestdir + " due to :" +e.strerror sys.exit(1) if os.path.exists(metamanifest): fh = open(metamanifest, "r+a") self.__exflock(fh) if not file in fh.read(): fh.write(file + '\n') self.__unflock(fh) fh.close() else: fh = open(metamanifest, "w") self.__exflock(fh) fh.write(file + '\n') self.__unflock(fh) fh.close() if os.path.exists(metamanifest): os.chmod(metamanifest, 0644) def __htaccess(self, ip, folder, file): entry = "RewriteRule ^" + file + "$ ../" + folder + "/%{REMOTE_ADDR}/" + file + " [L,NC,QSA]" htaccessFolder = "/var/www/html/latest" htaccessFile = htaccessFolder + "/.htaccess" try: os.mkdir(htaccessFolder,0755) except OSError as e: # error 17 is already exists, we do it this way for concurrency if e.errno != 17: print "failed to make directories " + htaccessFolder + " due to :" +e.strerror sys.exit(1) if os.path.exists(htaccessFile): fh = open(htaccessFile, "r+a") self.__exflock(fh) if not entry in fh.read(): fh.write(entry + '\n') self.__unflock(fh) fh.close() else: fh = open(htaccessFile, "w") self.__exflock(fh) fh.write("Options +FollowSymLinks\nRewriteEngine On\n\n") fh.write(entry + '\n') self.__unflock(fh) fh.close() entry="Options -Indexes\nOrder Deny,Allow\nDeny from all\nAllow from " + ip htaccessFolder = "/var/www/html/" + folder + "/" + ip htaccessFile = htaccessFolder+"/.htaccess" try: os.makedirs(htaccessFolder,0755) except OSError as e: # error 17 is already exists, we do it this way for sake of concurrency if e.errno != 17: print "failed to make directories " + htaccessFolder + " due to :" +e.strerror sys.exit(1) fh = open(htaccessFile, "w") self.__exflock(fh) fh.write(entry + '\n') self.__unflock(fh) fh.close() if folder == "metadata" or folder == "meta-data": entry = "RewriteRule ^meta-data/(.+)$ ../" + folder + "/%{REMOTE_ADDR}/$1 [L,NC,QSA]" htaccessFolder = "/var/www/html/latest" htaccessFile = htaccessFolder + "/.htaccess" fh = open(htaccessFile, "r+a") self.__exflock(fh) if not entry in fh.read(): fh.write(entry + '\n') entry = "RewriteRule ^meta-data/$ ../" + folder + "/%{REMOTE_ADDR}/meta-data [L,NC,QSA]" fh.seek(0) if not entry in fh.read(): fh.write(entry + '\n') self.__unflock(fh) fh.close() def __exflock(self, file): try: flock(file, LOCK_EX) except IOError as e: print "failed to lock file" + file.name + " due to : " + e.strerror sys.exit(1) #FIXME return True def __unflock(self, file): try: flock(file, LOCK_UN) except IOError: print "failed to unlock file" + file.name + " due to : " + e.strerror sys.exit(1) #FIXME return True class CsAddress(CsDataBag): def compare(self): for dev in CsDevice('').list(): ip = CsIP(dev) ip.compare(self.dbag) def process(self): for dev in self.dbag: if dev == "id": continue ip = CsIP(dev) for address in self.dbag[dev]: if not address["nw_type"] == "control": CsRoute(dev).add(address) ip.setAddress(address) if ip.configured(): logging.info("Address %s on device %s already configured", ip.ip(), dev) ip.post_configure() else: logging.info("Address %s on device %s not configured", ip.ip(), dev) if CsDevice(dev).waitfordevice(): ip.configure() def main(argv): logging.basicConfig(filename='/var/log/cloud.log', level=logging.DEBUG, format='%(asctime)s %(message)s') cl = CsCmdLine("cmdline") address = CsAddress("ips") address.compare() address.process() password = CsPassword("vmpassword") password.process() metadata = CsVmMetadata('vmdata') metadata.process() acls = CsAcl('networkacl') acls.process() nf = CsNetfilters() nf.compare(fw) dh = CsDataBag("dhcpentry") dhcp = CsDhcp(dh.get_bag(), cl) if __name__ == "__main__": main(sys.argv)