#!/usr/bin/env python # Copyright (c) 2014-2016, NORDUnet A/S. # See LICENSE for licensing information. import argparse import sys import readconfig import re import base64 from datetime import datetime class Symbol(str): pass class Binary(str): pass clean_string = re.compile(r'^[-.:_/A-Za-z0-9 ]*$') clean_symbol = re.compile(r'^[_A-Za-z0-9]*$') def quote_erlang_string(s): if clean_string.match(s): return '"' + s + '"' else: return "[" + ",".join([str(ord(c)) for c in s]) + "]" def quote_erlang_symbol(s): if clean_symbol.match(s): return s elif clean_string.match(s): return "'" + s + "'" else: print >>sys.stderr, "Cannot generate symbol", s sys.exit(1) def gen_erlang(term, level=1): indent = " " * level separator = ",\n" + indent if isinstance(term, Symbol): return quote_erlang_symbol(term) elif isinstance(term, Binary): return "<<" + ",".join([str(ord(c)) for c in term]) + ">>" elif isinstance(term, basestring): return quote_erlang_string(term) elif isinstance(term, int): return str(term) elif isinstance(term, tuple): tuplecontents = [gen_erlang(e, level=level+1) for e in term] if "\n" not in "".join(tuplecontents): separator = ", " return "{" + separator.join(tuplecontents) + "}" elif isinstance(term, list): listcontents = [gen_erlang(e, level=level+1) for e in term] return "[" + separator.join(listcontents) + "]" else: print "unknown type", type(term) sys.exit(1) saslconfig = [(Symbol("sasl_error_logger"), Symbol("false")), (Symbol("errlog_type"), Symbol("error")), (Symbol("error_logger_mf_dir"), "sasl_log"), (Symbol("error_logger_mf_maxbytes"), 10485760), (Symbol("error_logger_mf_maxfiles"), 10), ] def parse_address(address): parsed_address = address.split(":") if len(parsed_address) != 2: print >>sys.stderr, "Invalid address format", address sys.exit(1) return (parsed_address[0], int(parsed_address[1])) def get_node_config(nodename, config): nodetype = [] nodeconfig = {} for t in ["frontendnodes", "storagenodes", "signingnodes", "mergenodes"]: for node in config[t]: if node["name"] == nodename: nodetype.append(t) nodeconfig[t] = node if len(nodetype) == 0: print >>sys.stderr, "Cannot find config for node", nodename sys.exit(1) if len(nodetype) >= 2 and set(nodetype) != set(["frontendnodes", "storagenodes"]): print >>sys.stderr, "Node type unsupported:", nodetype sys.exit(1) return (set(nodetype), nodeconfig) def get_address(bind_address, nodeconfig): if bind_address: (host, port) = parse_address(bind_address) else: (_, port) = parse_address(nodeconfig["address"]) host = "0.0.0.0" return (host, port) def gen_http_servers(nodetype, nodeconfig, bind_addresses, bind_publicaddress, bind_publichttpaddress): http_servers = [] https_servers = [] if "frontendnodes" in nodetype and "mergenodes" in nodetype: print >>sys.stderr, "cannot have both frontend node and merge node at the same time", nodetype sys.exit(1) if "frontendnodes" in nodetype: (host, port) = get_address(bind_addresses["frontend"], nodeconfig["frontendnodes"]) if bind_publicaddress: (publichost, publicport) = parse_address(bind_publicaddress) else: (_, publicport) = parse_address(nodeconfig["frontendnodes"]["publicaddress"]) publichost = "0.0.0.0" if bind_publichttpaddress: (publichttphost, publichttpport) = parse_address(bind_publichttpaddress) http_servers.append((Symbol("external_http_api"), publichttphost, publichttpport, Symbol("v1"))) https_servers.append((Symbol("external_https_api"), publichost, publicport, Symbol("v1"))) https_servers.append((Symbol("frontend_https_api"), host, port, Symbol("frontend"))) if "storagenodes" in nodetype: (host, port) = get_address(bind_addresses["storage"], nodeconfig["storagenodes"]) https_servers.append((Symbol("storage_https_api"), host, port, Symbol("storage"))) if "signingnodes" in nodetype: (host, port) = get_address(bind_addresses["signing"], nodeconfig["signingnodes"]) https_servers.append((Symbol("signing_https_api"), host, port, Symbol("signing"))) if "mergenodes" in nodetype: (host, port) = get_address(bind_addresses["merge"], nodeconfig["mergenodes"]) https_servers.append((Symbol("frontend_https_api"), host, port, Symbol("frontend"))) if nodetype - set(["frontendnodes", "storagenodes", "signingnodes", "mergenodes"]): print >>sys.stderr, "unknown nodetype", nodetype sys.exit(1) return (http_servers, https_servers) def allowed_clients_frontend(mergenodenames, primarymergenodename): return [ ("/plop/v1/frontend/sendentry", mergenodenames), ("/plop/v1/frontend/sendlog", mergenodenames), ("/plop/v1/frontend/publish-sth", [primarymergenodename]), ("/plop/v1/frontend/verify-entries", [primarymergenodename]), ("/plop/v1/frontend/currentposition", mergenodenames), ("/plop/v1/frontend/missingentries", mergenodenames), ] def allowed_clients_mergesecondary(primarymergenodename): return [ ("/plop/v1/merge/sendentry", [primarymergenodename]), ("/plop/v1/merge/sendlog", [primarymergenodename]), ("/plop/v1/merge/verifyroot", [primarymergenodename]), ("/plop/v1/merge/verifiedsize", [primarymergenodename]), ("/plop/v1/merge/setverifiedsize", [primarymergenodename]), ("/plop/v1/merge/missingentries", [primarymergenodename]), ] def allowed_clients_public(): noauth = Symbol("noauth") return [ ("/ct/v1/add-chain", noauth), ("/ct/v1/add-pre-chain", noauth), ("/ct/v1/get-sth", noauth), ("/ct/v1/get-sth-consistency", noauth), ("/ct/v1/get-proof-by-hash", noauth), ("/ct/v1/get-entries", noauth), ("/ct/v1/get-entry-and-proof", noauth), ("/ct/v1/get-roots", noauth), ] def allowed_clients_signing(frontendnodenames, primarymergenodename): return [ ("/plop/v1/signing/sct", frontendnodenames), ("/plop/v1/signing/sth", [primarymergenodename]), ] def allowed_clients_storage(frontendnodenames, mergenodenames): return [ ("/plop/v1/storage/sendentry", frontendnodenames), ("/plop/v1/storage/entrycommitted", frontendnodenames), ("/plop/v1/storage/fetchnewentries", mergenodenames), ("/plop/v1/storage/getentry", mergenodenames), ] def allowed_servers_frontend(signingnodenames, storagenodenames): return [ ("/plop/v1/storage/sendentry", storagenodenames), ("/plop/v1/storage/entrycommitted", storagenodenames), ("/plop/v1/signing/sct", signingnodenames), ] def allowed_servers_primarymerge(frontendnodenames, secondarymergenames, signingnodenames): return [ ("/plop/v1/frontend/verify-entries", frontendnodenames), ("/plop/v1/frontend/sendlog", frontendnodenames), ("/plop/v1/frontend/sendentry", frontendnodenames), ("/plop/v1/frontend/publish-sth", frontendnodenames), ("/plop/v1/merge/verifiedsize", secondarymergenames), ("/plop/v1/merge/verifyroot", secondarymergenames), ("/plop/v1/merge/setverifiedsize", secondarymergenames), ("/plop/v1/merge/sendlog", secondarymergenames), ("/plop/v1/merge/sendentry", secondarymergenames), ("/plop/v1/signing/sth", signingnodenames), ] def parse_ratelimit_expression(expression): if expression == "none": return Symbol("none") parts = expression.split(" ") if not (len(parts) == 3 and parts[1] == 'per' and parts[2] in ["second", "minute", "hour"]): print >>sys.stderr, "Ratelimit expressions must have the format \" per second|minute|hour\" or \"none\"" sys.exit(1) return (int(parts[0]), Symbol(parts[2])) def parse_ratelimit((type, description)): descriptions = [parse_ratelimit_expression(s.strip()) for s in description.split(",")] if len(descriptions) != 1: print >>sys.stderr, "%s: Only one ratelimit expression supported right now" % (type,) return (Symbol(type), descriptions) def api_keys(config): return [(node["nodename"], Binary(base64.b64decode(node["publickey"]))) for node in config["apikeys"]] def gen_config(nodename, config, localconfig): print "generating config for", nodename paths = localconfig["paths"] apikeys = api_keys(config) bind_addresses = { "frontend": localconfig.get("frontendaddresses", {}).get(nodename), "storage": localconfig.get("storageaddresses", {}).get(nodename), "signing": localconfig.get("signingaddresses", {}).get(nodename), "merge": localconfig.get("mergeaddresses", {}).get(nodename), } bind_publicaddress = localconfig.get("ctapiaddresses", {}).get(nodename) bind_publichttpaddress = localconfig.get("publichttpaddresses", {}).get(nodename) options = localconfig.get("options", []) configfile = open(paths["configdir"] + "/" + nodename + ".config", "w") print >>configfile, "%% catlfish configuration file (-*- erlang -*-)" plopcontrolfilename = nodename + ".plopcontrol" plopconfigfilename = paths["configdir"] + "/" + nodename + ".plopconfig" plopconfigfile = open(plopconfigfilename, "w") print >>plopconfigfile, "%% plop configuration file (-*- erlang -*-)" (nodetype, nodeconfig) = get_node_config(nodename, config) if nodename == config["primarymergenode"]: (http_servers, https_servers) = [], [] else: (http_servers, https_servers) = gen_http_servers(nodetype, nodeconfig, bind_addresses, bind_publicaddress, bind_publichttpaddress=bind_publichttpaddress) catlfishconfig = [] plopconfig = [] reloadableplopconfig = [] if nodetype & set(["frontendnodes", "mergenodes"]): catlfishconfig.append((Symbol("known_roots_path"), localconfig["paths"]["knownroots"])) if "frontendnodes" in nodetype: if "sctcaching" in options: catlfishconfig.append((Symbol("sctcache_root_path"), paths["db"] + "sctcache/")) if "ratelimits" in localconfig: ratelimits = map(parse_ratelimit, localconfig["ratelimits"].items()) catlfishconfig.append((Symbol("ratelimits"), ratelimits)) catlfishconfig += [ (Symbol("https_servers"), https_servers), (Symbol("http_servers"), http_servers), (Symbol("https_certfile"), paths["https_certfile"]), (Symbol("https_keyfile"), paths["https_keyfile"]), ] catlfishconfig.append((Symbol("mmd"), config["mmd"])) lagerconfig = [ (Symbol("handlers"), [ (Symbol("lager_console_backend"), Symbol("info")), (Symbol("lager_file_backend"), [(Symbol("file"), nodename + "-error.log"), (Symbol("level"), Symbol("error"))]), (Symbol("lager_file_backend"), [(Symbol("file"), nodename + "-debug.log"), (Symbol("level"), Symbol("debug"))]), (Symbol("lager_file_backend"), [(Symbol("file"), nodename + "-console.log"), (Symbol("level"), Symbol("info"))]), ]) ] plopconfig += [ (Symbol("https_cacertfile"), paths["https_cacertfile"]), (Symbol("https_cacert_fingerprint"), Binary(base64.b16decode(config["cafingerprint"]))), ] if "dbbackend" in localconfig: dbbackend = localconfig["dbbackend"] if dbbackend not in ("fsdb", "permdb"): print >>sys.stderr, "DB backend not recognized:", dbbackend sys.exit(1) plopconfig += [ (Symbol("db_backend"), Symbol(dbbackend)), ] if dbbackend == "permdb" and len(localconfig["localnodes"]) != 1: print >>sys.stderr, "When using permdb, all services have to be in the same node" sys.exit(1) #print "nodetype", ", ".join(nodetype) if nodetype & set(["frontendnodes", "storagenodes"]): plopconfig += [ (Symbol("entry_root_path"), paths["db"] + "certentries"), (Symbol("entryhash_root_path"), paths["db"] + "entryhash"), (Symbol("indexforhash_root_path"), paths["db"] + "certindex"), ] if "frontendnodes" in nodetype: plopconfig += [ (Symbol("index_path"), paths["db"] + "index"), (Symbol("sth_path"), paths["db"] + "sth"), (Symbol("sendsth_verified_path"), paths["db"] + "sendsth-verified"), (Symbol("entryhash_from_entry"), (Symbol("catlfish"), Symbol("entryhash_from_entry"))), ] if "storagenodes" in nodetype: plopconfig += [ (Symbol("newentries_path"), paths["db"] + "newentries"), (Symbol("lastverifiednewentry_path"), paths["db"] + "lastverifiednewentry"), ] if nodetype & set(["frontendnodes", "mergenodes"]): plopconfig += [ (Symbol("verify_entry"), (Symbol("catlfish"), Symbol("verify_entry"))), ] if "mergenodes" in nodetype: plopconfig += [ (Symbol("verifiedsize_path"), paths["mergedb"] + "/verifiedsize"), (Symbol("index_path"), paths["mergedb"] + "/logorder"), (Symbol("entry_root_path"), paths["mergedb"] + "/chains"), ] signingnodenames = [node["name"] for node in config["signingnodes"]] signingnodeaddresses = ["https://%s/plop/v1/signing/" % node["address"] for node in config["signingnodes"]] mergenodenames = [node["name"] for node in config["mergenodes"]] primarymergenodename = config["primarymergenode"] storagenodeaddresses = ["https://%s/plop/v1/storage/" % node["address"] for node in config["storagenodes"]] frontendnodenames = [node["name"] for node in config["frontendnodes"]] frontendnodeaddresses = ["https://%s/plop/v1/frontend/" % node["address"] for node in config["frontendnodes"]] allowed_clients = [] allowed_servers = [] services = set() if "frontendnodes" in nodetype: storagenodenames = [node["name"] for node in config["storagenodes"]] reloadableplopconfig.append((Symbol("storage_nodes"), storagenodeaddresses)) reloadableplopconfig.append((Symbol("storage_nodes_quorum"), config["storage-quorum-size"])) services.add(Symbol("ht")) allowed_clients += allowed_clients_frontend(mergenodenames, primarymergenodename) allowed_clients += allowed_clients_public() allowed_servers += allowed_servers_frontend(signingnodenames, storagenodenames) if "storagenodes" in nodetype: allowed_clients += allowed_clients_storage(frontendnodenames, mergenodenames) if "signingnodes" in nodetype: allowed_clients += allowed_clients_signing(frontendnodenames, primarymergenodename) services = [Symbol("sign")] if "mergenodes" in nodetype: reloadableplopconfig.append((Symbol("storage_nodes"), storagenodeaddresses)) reloadableplopconfig.append((Symbol("storage_nodes_quorum"), config["storage-quorum-size"])) services.add(Symbol("ht")) if nodename == primarymergenodename: mergesecondarynames = [node["name"] for node in config["mergenodes"] if node["name"] != primarymergenodename] mergesecondaryaddresses = ["https://%s/plop/v1/merge/" % node["address"] for node in config["mergenodes"] if node["name"] != primarymergenodename] merge = localconfig["merge"] plopconfig.append((Symbol("db_backend_opt"), [(Symbol("write_flag"), Symbol("read"))])) plopconfig.append((Symbol("merge_delay"), merge["min-delay"])) plopconfig.append((Symbol("merge_dist_winsize"), merge["dist-window-size"])) plopconfig.append((Symbol("merge_dist_sendlog_chunksize"), merge["dist-sendlog-chunksize"])) plopconfig.append((Symbol("merge_dist_sendentries_chunksize"), merge["dist-sendentries-chunksize"])) reloadableplopconfig.append((Symbol("merge_backup_winsize"), merge["backup-window-size"])) reloadableplopconfig.append((Symbol("merge_backup_sendlog_chunksize"), merge["backup-sendlog-chunksize"])) reloadableplopconfig.append((Symbol("merge_backup_sendentries_chunksize"), merge["backup-sendentries-chunksize"])) plopconfig.append((Symbol("frontend_nodes"), frontendnodeaddresses)) reloadableplopconfig.append((Symbol("merge_secondaries"), zip(mergesecondarynames, mergesecondaryaddresses))) plopconfig.append((Symbol("sth_path"), paths["mergedb"] + "/sth")) plopconfig.append((Symbol("fetched_path"), paths["mergedb"] + "/fetched")) plopconfig.append((Symbol("verified_path"), paths["mergedb"] + "/verified")) plopconfig.append((Symbol("minsize_path"), paths["mergedb"] + "/minsize")) reloadableplopconfig.append((Symbol("signing_nodes"), signingnodeaddresses)) reloadableplopconfig.append((Symbol("backup_quorum"), config["backup-quorum-size"])) allowed_servers += allowed_servers_primarymerge(frontendnodenames, mergesecondarynames, signingnodenames) else: allowed_clients += allowed_clients_mergesecondary(primarymergenodename) plopconfig += [ (Symbol("services"), list(services)), ] if "signingnodes" in nodetype: hsm = localconfig.get("hsm") if "logprivatekey" in paths: plopconfig.append((Symbol("log_private_key"), paths["logprivatekey"])) if hsm: plopconfig.append((Symbol("hsm"), [hsm.get("library"), str(hsm.get("slot")), "ecdsa", hsm.get("label"), hsm.get("pin")])) if not ("logprivatekey" in paths or hsm): print >>sys.stderr, "Neither logprivatekey nor hsm configured for signing node", nodename sys.exit(1) plopconfig += [ (Symbol("log_public_key"), Binary(base64.b64decode(config["logpublickey"]))), (Symbol("own_key"), (nodename, "%s/%s-private.pem" % (paths["privatekeys"], nodename))), ] if "frontendnodes" in nodetype: reloadableplopconfig.append((Symbol("signing_nodes"), signingnodeaddresses)) plopconfig += [ (Symbol("plopconfig"), plopconfigfilename), (Symbol("plopcontrol"), plopcontrolfilename), ] reloadableplopconfig += [ (Symbol("allowed_clients"), list(allowed_clients)), (Symbol("allowed_servers"), list(allowed_servers)), (Symbol("apikeys"), apikeys), (Symbol("version"), config["version"]), ] erlangconfig = [ (Symbol("sasl"), saslconfig), (Symbol("catlfish"), catlfishconfig), (Symbol("lager"), lagerconfig), (Symbol("plop"), plopconfig), ] print >>configfile, gen_erlang(erlangconfig) + ".\n" print >>plopconfigfile, gen_erlang(reloadableplopconfig) + ".\n" configfile.close() plopconfigfile.close() def gen_testmakefile(config, testmakefile, machines, shellvars=False): configfile = open(testmakefile, "w") print >>configfile, "#", testmakefile, "generated by", sys.argv[0], datetime.now() frontendnodenames = set([node["name"] for node in config["frontendnodes"]]) storagenodenames = set([node["name"] for node in config["storagenodes"]]) signingnodenames = set([node["name"] for node in config["signingnodes"]]) mergenodenames = set([node["name"] for node in config["mergenodes"]]) mergesecondarynodenames = set([node["name"] for node in config["mergenodes"] if node["name"] != config["primarymergenode"]]) frontendnodeaddresses = [node["publicaddress"] for node in config["frontendnodes"]] storagenodeaddresses = [node["address"] for node in config["storagenodes"]] signingnodeaddresses = [node["address"] for node in config["signingnodes"]] mergesecondarynodeaddresses = [node["address"] for node in config["mergenodes"] if node["name"] != config["primarymergenode"]] delimiter = '"' if shellvars else '' print >>configfile, "NODES=" + delimiter + " ".join(frontendnodenames|storagenodenames|signingnodenames|mergenodenames) + delimiter print >>configfile, "FRONTENDNODES=" + delimiter + " ".join(['%s:%s' % (name, 'catlfish') for name in frontendnodenames]) + delimiter print >>configfile, "STORAGENODES=" + delimiter + " ".join(['%s:%s' % (name, 'catlfish') for name in storagenodenames if name not in frontendnodenames]) + delimiter print >>configfile, "SIGNINGNODES=" + delimiter + " ".join(['%s:%s' % (name, 'catlfish') for name in signingnodenames]) + delimiter print >>configfile, "MERGESECONDARYNODES=" + delimiter + " ".join(['%s:%s' % (name, 'catlfish') for name in mergesecondarynodenames]) + delimiter print >>configfile, "MERGEPRIMARYNODE=" '%s:%s' % (config["primarymergenode"], 'merge') print >>configfile, "MACHINES=" + delimiter + " ".join([str(e) for e in range(1, machines+1)]) + delimiter print >>configfile, "TESTURLS=" + delimiter + " ".join(frontendnodeaddresses+storagenodeaddresses+signingnodeaddresses+mergesecondarynodeaddresses) + delimiter print >>configfile, "BASEURL=" + delimiter + config["baseurl"] + delimiter configfile.close() def printnodenames(config): frontendnodenames = set([node["name"] for node in config["frontendnodes"]]) storagenodenames = set([node["name"] for node in config["storagenodes"]]) signingnodenames = set([node["name"] for node in config["signingnodes"]]) mergenodenames = set([node["name"] for node in config["mergenodes"]]) print " ".join(frontendnodenames|storagenodenames|signingnodenames|mergenodenames) def main(): parser = argparse.ArgumentParser(description="") parser.add_argument('--config', help="System configuration", required=True) parser.add_argument('--localconfig', help="Local configuration") parser.add_argument("--testmakefile", metavar="file", help="Generate makefile variables for test") parser.add_argument("--testshellvars", metavar="file", help="Generate shell variable file for test") parser.add_argument("--getnodenames", action='store_true', help="Get list of node names") parser.add_argument("--machines", type=int, metavar="n", help="Number of machines") args = parser.parse_args() if args.testmakefile and args.machines: config = readconfig.read_config(args.config) gen_testmakefile(config, args.testmakefile, args.machines) elif args.testshellvars and args.machines: config = readconfig.read_config(args.config) gen_testmakefile(config, args.testshellvars, args.machines, shellvars=True) elif args.getnodenames: config = readconfig.read_config(args.config) printnodenames(config) elif args.localconfig: localconfig = readconfig.read_config(args.localconfig) config = readconfig.verify_and_read_config(args.config, localconfig["logadminkey"]) localnodes = localconfig["localnodes"] for localnode in localnodes: gen_config(localnode, config, localconfig) else: print >>sys.stderr, "Nothing to do" sys.exit(1) main()