From 79110a528ceae5bf18e4dd941b2b844f2dc2e4f8 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Tue, 17 Apr 2018 01:03:16 +0200 Subject: [PATCH] Initial commit --- genconfdrv | 317 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100755 genconfdrv diff --git a/genconfdrv b/genconfdrv new file mode 100755 index 0000000..825a17b --- /dev/null +++ b/genconfdrv @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from __future__ import print_function + +import argparse +#import fs +#from fs import tempfs, path +import fs.tempfs +import fs.path +import ipaddress +import json +import os +import subprocess +import sys +import uuid + +__VERSION__ = '0.1' + + +class ConfigDrive: + def __init__(self, genisoimage='/usr/bin/genisoimage', verbose=False): + self._tmpfs = None + self._genisoimage = genisoimage + self._user_data = {} # {"system_info": {"default_user": None}} + self._interfaces = [] + self._pubkeys = [] + self._verbose = verbose + + self._added_resolv_module_call = False + + if not os.path.exists(genisoimage): + print("Error: %s does not exist, no genisoimage found!" % genisoimage, file=sys.stderr) + sys.exit(1) + + self.open() + + def set_hostname(self, hostname): + self._hostname = hostname + + def conf_network(self, interface, address=None, gateway=None): + if not address and gateway: + raise ValueError("You cannot define a gateway, but supply no address") + + if not self._interfaces: + self._interfaces.extend([ + "auto lo", + "iface lo inet loopback", + ]) + + if address: + if "/" not in address: + raise ValueError("IP Interface is not a subnet") + address = ipaddress.ip_interface(address) + method = "static" + else: + method = "dhcp" + + self._interfaces.extend([ + "", + "auto %s" % interface, + "iface %s inet%s %s" % (interface, "" if not address or address.version == 4 else "6", method), + ]) + + if address: + self._interfaces.append(" address %s" % address) + + if gateway: + self._interfaces.append(" gateway %s" % str(ipaddress.ip_address(gateway))) + + def conf_resolve(self, resolvers): + if not self._hostname: + raise ValueError("Please set a hostname before calling this function") + + for n, resolver in enumerate(resolvers, 1): + try: + ipaddress.ip_address(resolver) + except ValueError as e: + print("Nameserver argument %s: %s" % (n, e), file=sys.stderr) + sys.exit(1) + + self._user_data["manage_resolv_conf"] = True + self._user_data["resolv_conf"] = { + "nameservers": resolvers, + } + + if "." in self._hostname: + self._user_data["domain"] = ".".join(self._hostname.split(".")[1:]) + + # debian, by default, does not call the cc_resolv_conf module + + if not self._added_resolv_module_call: + self.add_command("cloud-init single --name cc_resolv_conf", True) + self._added_resolv_module_call = True + + def add_user(self, name, keys=None, gecos=None, sudo=False, password=None): + if "users" not in self._user_data: + self._user_data["users"] = [] + + user = { + "name": name, + "shell": "/bin/bash", + "home": "/home/%s" % name + } + + if keys: + if type(keys) == str: + keys = [keys] + + user["ssh_authorized_keys"] = keys + + if gecos: + user["gecos"] = gecos + + if sudo: + user["sudo"] = "ALL=(ALL) NOPASSWD:ALL" + + if password: + raise NotImplementedError("crypt, salt, $6$ something") + + self._user_data["users"].append(user) + + def add_fp(self, path, fp): + self.add_text(path, fp.read()) + + def add_text(self, path, content): + dir_path = fs.path.dirname(path) + if dir_path and not self._tmpfs.exists(dir_path): + self._tmpfs.makedirs(dir_path) + + + self._tmpfs.settext(path, content) + if self._verbose: + print(" >>", path) + print(content) + print() + + def open(self): + if not self._tmpfs: + self._tmpfs = fs.tempfs.TempFS("genconfdrv", auto_clean=True) + + def _write_metadata(self): + if not self._hostname: + raise ValueError("No hostname set") + + meta_data = { + # "availability_zone": "cat", + "files": [], + "hostname": self._hostname, + "name": self._hostname.split(".")[0], + #"meta": { + # "role": "webservers", + # "essential": False, + #} + "uuid": str(uuid.uuid4()), + } + + if self._interfaces: + # add the source-directory to interfaces + self._interfaces.extend([ + "", + "source-directory /etc/network/interfaces.d/", + "", + ]) + + meta_data["files"].append({"content_path": "/content/0000", "path": "/etc/network/interfaces"}) + self.add_text("/openstack/content/0000", "\n".join(self._interfaces)) + + meta_data["files"].append({"content_path": "/content/0001", "path": "/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg"}) + self.add_text("/openstack/content/0001", "network: {config: disabled}") + + #meta_data["files"].append({"content_path": "/content/0002", "path": "/etc/cloud/cloud.cfg.d/10-resolv-conf.cfg"}) + #self.add_text("/openstack/content/0002", "cloud_init_modules:\n - resolv-conf\n") + + if self._pubkeys: + meta_data["public_keys"] = {} + for n, key in enumerate(self._pubkeys): + meta_data["public_keys"]["key-%02d" % n] = key + + self.add_text("/openstack/latest/meta_data.json", json.dumps(meta_data, indent=4)) + + def enable_upgrades(self): + self._user_data["package_update"] = True + self._user_data["package_upgrade"] = True + + def add_command(self, command, boot=True): + key = "bootcmd" if boot else "runcmd" + if key not in self._user_data: + self._user_data[key] = [] + + self._user_data[key].append(command) + + def add_pubkey(self, pubkey): + self._pubkeys.append(pubkey) + + def set_password(self, user, password): + if "chpasswd" not in self._user_data: + self._user_data["chpasswd"] = {} + self._user_data["chpasswd"]["list"] = "" + #self._user_data["chpasswd"]["list"] = [] + + #self._user_data["chpasswd"]["list"].append("%s:%s" % (user, password)) + self._user_data["chpasswd"]["list"] += "%s:%s\n" % (user, password) + + def _write_userdata(self): + self.add_text("/openstack/latest/user_data", "#cloud-config\n" + json.dumps(self._user_data, indent=4)) + + def write_iso(self, path): + self._write_metadata() + self._write_userdata() + + p = subprocess.Popen([self._genisoimage, + "-J", "-r", "-q", + "-V", "config-2", + "-publisher", "seba-genconfdrv" + "-l", "-ldots", "-allow-lowercase", "-allow-multidot", + "-input-charset", "utf-8", + "-o", path, + self._tmpfs.getsyspath(""), + ]) + + p.wait() + + def close(self): + if self._tmpfs: + self._tmpfs.close() + +# defaults for testing +# cfgdrv.set_hostname("foo.someserver.de") +# cfgdrv.conf_network("ens3", "172.23.0.4/24", "172.23.0.1") +# cfgdrv.conf_resolve(["1.1.1.1", "8.8.8.8"]) +# cfgdrv.enable_upgrades() +# cfgdrv.add_command("rm -rf /home/debian/; userdel debian; groupdel debian", True) +# cfgdrv.add_command("cloud-init single --name cc_resolv_conf", True) +# cfgdrv.add_command("rm -f /etc/network/interfaces.d/eth*.cfg", True) +# cfgdrv.add_command("sed -rni '/^([^#]|## template)/p' /etc/cloud/templates/sources.list.*.tmpl; rm /etc/apt/sources.list.d/*", True) +# #cfgdrv.add_command("(whoami; date) > /root/bleep", False) +# cfgdrv.add_pubkey("ssh-rsa bleep foo") +# cfgdrv.set_password("root", "kitteh") + + +def main(): + parser = argparse.ArgumentParser() + + parser.add_argument("-H", "--hostname", required=True, help="Hostname") + parser.add_argument("-o", "--output", required=True, help="Path to write iso to") + parser.add_argument("-n", "--nameservers", "--ns", default=["1.1.1.1", "8.8.8.8"], nargs="+", help="Nameservers") + parser.add_argument("-i", "--networks", "--net", default=[], nargs="+") + parser.add_argument("-u", "--disable-upgrades", action="store_true", default=False) + parser.add_argument("-v", "--verbose", action="store_true", default=False) + parser.add_argument("--no-debian-cleanup", "--ndc", action="store_true", default=False) + parser.add_argument("--set-root-password", "--srp", default=None) + parser.add_argument("-a", "--add-user", default=[], nargs="+", help="Add users, format is username:key?:sudo?:gecos?:password?, sudo is a bool, key is either an ssh key or a path to an ssh key") + + + args = parser.parse_args() + + cfgdrv = None + try: + cfgdrv = ConfigDrive(verbose=args.verbose) + + cfgdrv.set_hostname(args.hostname) + + for net in args.networks: + net = net.split(":") + cfgdrv.conf_network(*net) + + if args.nameservers: + cfgdrv.conf_resolve(args.nameservers) + + if not args.disable_upgrades: + cfgdrv.enable_upgrades() + + if not args.no_debian_cleanup: + cfgdrv.add_command("rm -f /etc/network/interfaces.d/eth*.cfg", True) + cfgdrv.add_command("sed -rni '/^([^#]|## template)/p' /etc/cloud/templates/sources.list.*.tmpl; rm /etc/apt/sources.list.d/*", True) + cfgdrv.add_command("sed -rni '/^([^#]|## template)/p' /etc/resolv.conf /etc/cloud/templates/resolv.conf.tmpl", True) + + + if args.set_root_password: + cfgdrv.set_password("root", args.set_root_password) + + if args.add_user: + for user in args.add_user: + user = user.split(":") + # user key sudo gecos password + if len(user) < 2: + parser.error("Missing key parameter for user") + + keys = "" + if len(user) >= 2: + if user[1].startswith("ssh-"): + keys = [user[1]] + else: + with open(os.path.expanduser(user[1])) as keyfile: + keys = keyfile.read().split("\n") + + sudo = True + if len(user) >= 3: + sudo = user[2] not in (False, 0, "0", "no", "false", "False") + + gecos = None + if len(user) >= 4: + gecos = user[3] + + password = None + if len(user) >= 5: + password = user[4] + cfgdrv.add_user(user[0], keys, sudo=sudo, gecos=gecos, password=password) + + if args.output: + cfgdrv.write_iso(args.output) + finally: + if cfgdrv: + cfgdrv.close() + +if __name__ == '__main__': + main()