From 025c35a5fd48a204ce2ba31637a1f3b658d0fe25 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Wed, 17 Nov 2021 09:48:22 +0200 Subject: fix: formatting Sponsored-by: author --- hetznertool | 300 ++++++++++++++++++++++++++++-------------------------------- 1 file changed, 139 insertions(+), 161 deletions(-) diff --git a/hetznertool b/hetznertool index cb3ba2b..90f41d4 100755 --- a/hetznertool +++ b/hetznertool @@ -11,10 +11,10 @@ import tempfile import yaml -CONFIG_FILENAME = os.path.expanduser('~/.config/hetznertool/hetznertool.yaml') +CONFIG_FILENAME = os.path.expanduser("~/.config/hetznertool/hetznertool.yaml") -expect_script='''\ +expect_script = """\ if {[llength $argv] != 2} { puts stderr "Usage: $argv0 name token exit 2 @@ -42,41 +42,39 @@ if {$childkilled == "CHILDKILLED"} { } # all good? exit $wait_code -''' +""" class HetznerTool: - def run(self): try: self.main() except Exception as e: - sys.stderr.write('ERROR: {}\n'.format(str(e))) + sys.stderr.write("ERROR: {}\n".format(str(e))) def main(self): - os.environ['LC_ALL'] = 'C' + os.environ["LC_ALL"] = "C" config = self.create_config() parser = Parser(config) parser.execute() def create_config(self): config = Config() - config.add('ssh-key') - config.add('pass-dir') - config.add('pass-subdir') - config.add('ns1') - config.add('domain') - config.add('dnszone-dir') - config.add('dnszone-file') - config.add('ansible-inventory-dir') - config.add('style') + config.add("ssh-key") + config.add("pass-dir") + config.add("pass-subdir") + config.add("ns1") + config.add("domain") + config.add("dnszone-dir") + config.add("dnszone-file") + config.add("ansible-inventory-dir") + config.add("style") config.load(CONFIG_FILENAME) return config class Config: - def __init__(self): self.config_vars = [] self.profiles = {} @@ -88,18 +86,18 @@ class Config: def load(self, filename): with open(filename) as f: - obj = yaml.load(f) + obj = yaml.safe_load(f) for profile in obj: for name in self.config_vars: if name not in obj[profile]: raise Exception( - 'Profile {} is missing variable {}'.format( - profile, name)) + "Profile {} is missing variable {}".format(profile, name) + ) for name in obj[profile]: if name not in self.config_vars: raise Exception( - 'Profile {} has extra variable {}'.format( - profile, name)) + "Profile {} has extra variable {}".format(profile, name) + ) self.set(profile, name, obj[profile][name]) def add(self, name): @@ -107,13 +105,10 @@ class Config: self.config_vars.append(name) def get(self, profile, name): - return self.profiles.get(profile).get(name, '') + return self.profiles.get(profile).get(name, "") def get_profile(self, profile): - return { - name: self.get(profile, name) - for name in self.config_vars - } + return {name: self.get(profile, name) for name in self.config_vars} def set(self, profile, name, value): if profile not in self.profiles: @@ -122,50 +117,45 @@ class Config: class Parser: - def __init__(self, config): self.config = config def execute(self): parser = self.create_parser() args = vars(parser.parse_args()) - profile = self.config.get_profile(args['profile']) + profile = self.config.get_profile(args["profile"]) profile.update(args) - func = args['func'] + func = args["func"] func(profile) def create_parser(self): parser = argparse.ArgumentParser( - description='Manage VMs in Hetzner Cloud, with DNS updates.') + description="Manage VMs in Hetzner Cloud, with DNS updates." + ) parser.add_argument( - '-n', '--no-act', dest='act', default=True, action='store_false') - parser.add_argument( - '-p', '--profile', dest='profile', required=True) + "-n", "--no-act", dest="act", default=True, action="store_false" + ) + parser.add_argument("-p", "--profile", dest="profile", required=True) factory = parser.add_subparsers() - create = factory.add_parser('create') - create.add_argument( - 'context', help='hcloud context to create VMs in') - create.add_argument( - 'specfile', help='file to read VM specifications from') + create = factory.add_parser("create") + create.add_argument("context", help="hcloud context to create VMs in") + create.add_argument("specfile", help="file to read VM specifications from") - servers = factory.add_parser('list') + servers = factory.add_parser("list") - contexts = factory.add_parser('contexts') + contexts = factory.add_parser("contexts") - zonefile = factory.add_parser('zonefile') + zonefile = factory.add_parser("zonefile") - create_context = factory.add_parser('create-context') - create_context.add_argument( - 'context', help='create new hcloud context') - create_context.add_argument( - 'token', help='token for hcloud context') + create_context = factory.add_parser("create-context") + create_context.add_argument("context", help="create new hcloud context") + create_context.add_argument("token", help="token for hcloud context") - delete = factory.add_parser('delete') - delete.add_argument( - 'context', help='hcloud context to delete all VMs from') + delete = factory.add_parser("delete") + delete.add_argument("context", help="hcloud context to delete all VMs from") parser.set_defaults(func=self.usage) servers.set_defaults(func=List().run) @@ -178,20 +168,19 @@ class Parser: return parser def usage(self, args): - raise Exception('Missing operation') + raise Exception("Missing operation") class Zone: - def __init__(self, domain): self.domain = domain self.servers = [] def servername(self, context, server): - return '{}-{}'.format(context, server) + return "{}-{}".format(context, server) def dnsname(self, context, server): - return '{}.{}'.format(self.servername(context, server), self.domain) + return "{}.{}".format(self.servername(context, server), self.domain) def add(self, context, server, addr): self.servers.append((context, server, addr)) @@ -201,7 +190,6 @@ class Zone: class ZoneWriter: - def __init__(self, zonedir, zonefile, ns1): self.zonedir = zonedir self.zonefile = zonefile @@ -210,17 +198,17 @@ class ZoneWriter: def write(self, zone): git = Git(self.zonedir) if not git.isclean(): - raise Exception('git dir unclean: {}'.format(self.zonedir)) + raise Exception("git dir unclean: {}".format(self.zonedir)) git.pull() pathname = os.path.join(self.zonedir, self.zonefile) - with open(pathname, 'w') as f: + with open(pathname, "w") as f: self.write_zone(zone, f) if not git.isclean(): - git.add('.') - git.commit('update zone file') + git.add(".") + git.commit("update zone file") git.push() def write_zone(self, zone, f): @@ -228,16 +216,15 @@ class ZoneWriter: class ZoneWriterDnsApi(ZoneWriter): - def write_zone(self, zone, f): for context, server, addr in zone: name = zone.dnsname(context, server) - f.write('+{}:{}:60\n'.format(name, addr)) + f.write("+{}:{}:60\n".format(name, addr)) class ZoneWriterBind9(ZoneWriter): - preamble = '''\ + preamble = """\ $TTL 30 $ORIGIN h.qvarnlabs.eu. @@ -245,54 +232,51 @@ $ORIGIN h.qvarnlabs.eu. @ IN NS ns1.qvarnlabs.net. @ IN NS ns2.qvarnlabs.net. -''' +""" def write_zone(self, zone, f): serial = self.increment_serial() f.write(self.preamble.format(serial=serial)) for context, server, addr in zone: name = zone.servername(context, server) - f.write('{} IN A {}\n'.format(name, addr)) + f.write("{} IN A {}\n".format(name, addr)) f.flush() self.kick_bind9() def increment_serial(self): - filename = os.path.join(self.zonedir, 'serial') + filename = os.path.join(self.zonedir, "serial") with open(filename) as f: serial = int(f.readline().strip()) serial += 1 - with open(filename, 'w') as f: + with open(filename, "w") as f: f.write(str(serial)) return serial def kick_bind9(self): - target = '{}:/etc/bind/{}'.format(self.ns1, self.zonefile) + target = "{}:/etc/bind/{}".format(self.ns1, self.zonefile) filename = os.path.join(self.zonedir, self.zonefile) - subprocess.check_call(['scp', '-q', filename, target]) - subprocess.check_call(['ssh', self.ns1, 'systemctl', 'reload', 'bind9']) + subprocess.check_call(["scp", "-q", filename, target]) + subprocess.check_call(["ssh", self.ns1, "systemctl", "reload", "bind9"]) class Inventory: - def __init__(self, dirname): self.dirname = dirname def write(self, zone, contexts): for context in contexts: - filename = os.path.join(self.dirname, 'hosts.{}'.format(context)) - with open(filename, 'w') as f: - servers = [ - server - for ctx, server, _ in zone - if ctx == context - ] + filename = os.path.join(self.dirname, "hosts.{}".format(context)) + with open(filename, "w") as f: + servers = [server for ctx, server, _ in zone if ctx == context] for server in servers: - f.write('{} ansible_ssh_host={}\n'.format( - server, zone.dnsname(context, server))) + f.write( + "{} ansible_ssh_host={}\n".format( + server, zone.dnsname(context, server) + ) + ) class Operation: - def __init__(self): self.hcloud = Hcloud() @@ -300,11 +284,11 @@ class Operation: raise NotImplementedError() def new_pass(self, profile): - return Pass(profile['pass-dir']) + return Pass(profile["pass-dir"]) def create_hcloud_contexts(self, profile): p = self.new_pass(profile) - pass_subdir = profile['pass-subdir'] + pass_subdir = profile["pass-subdir"] pass_contexts = p.listdir(pass_subdir) hcloud_contexts = self.hcloud.list_contexts() for context in pass_contexts: @@ -313,9 +297,9 @@ class Operation: self.hcloud.create_context(context, token) def update_zone_file(self, profile): - zonedir = profile['dnszone-dir'] - filename = profile['dnszone-file'] - ns1 = profile['ns1'] + zonedir = profile["dnszone-dir"] + filename = profile["dnszone-file"] + ns1 = profile["ns1"] zone, contexts = self.create_zone(profile) klass = self.get_zone_writer(profile) @@ -325,10 +309,10 @@ class Operation: self.write_inventory(profile, zone, contexts) def create_zone(self, profile): - zone = Zone(profile['domain']) + zone = Zone(profile["domain"]) p = self.new_pass(profile) - pass_contexts = p.listdir(profile['pass-subdir']) + pass_contexts = p.listdir(profile["pass-subdir"]) for context in pass_contexts: for server, addr in self.hcloud.list_servers(context): zone.add(context, server, addr) @@ -337,34 +321,32 @@ class Operation: def get_zone_writer(self, profile): styles = { - 'dns-api': ZoneWriterDnsApi, - 'qvarnlabs': ZoneWriterBind9, + "dns-api": ZoneWriterDnsApi, + "qvarnlabs": ZoneWriterBind9, } - style = profile['style'] + style = profile["style"] if style not in styles: - raise Exception('unknown zone file style: {}'.format(style)) + raise Exception("unknown zone file style: {}".format(style)) return styles[style] def write_inventory(self, profile, zone, contexts): - inventory = Inventory(profile['ansible-inventory-dir']) + inventory = Inventory(profile["ansible-inventory-dir"]) inventory.write(zone, contexts) class Zonefile(Operation): - def run(self, profile): self.update_zone_file(profile) class List(Operation): - def run(self, profile): self.create_hcloud_contexts(profile) p = self.new_pass(profile) - pass_contexts = p.listdir(profile['pass-subdir']) - domain = profile['domain'] + pass_contexts = p.listdir(profile["pass-subdir"]) + domain = profile["domain"] zone = Zone(domain) for context in pass_contexts: for server, addr in self.hcloud.list_servers(context): @@ -372,66 +354,62 @@ class List(Operation): class Create(Operation): - def run(self, profile): - if profile['act']: + if profile["act"]: self.create_hcloud_contexts(profile) - ssh_key = profile['ssh-key'] - context = profile['context'] - specfile = SpecFile(profile['specfile']) - zone = Zone(profile['domain']) + ssh_key = profile["ssh-key"] + context = profile["context"] + specfile = SpecFile(profile["specfile"]) + zone = Zone(profile["domain"]) for server in specfile.servers(): - if profile['act']: + if profile["act"]: self.hcloud.create_server(context, ssh_key=ssh_key, **server) - print('created', zone.dnsname(context, server['name'])) + print("created", zone.dnsname(context, server["name"])) else: - print('pretending to create', zone.dnsname(context, server['name'])) + print("pretending to create", zone.dnsname(context, server["name"])) - if profile['act']: + if profile["act"]: self.update_zone_file(profile) class Delete(Operation): - def run(self, profile): - if profile['act']: + if profile["act"]: self.create_hcloud_contexts(profile) - ssh_key = profile['ssh-key'] - context = profile['context'] - zone = Zone(profile['domain']) + ssh_key = profile["ssh-key"] + context = profile["context"] + zone = Zone(profile["domain"]) for server, _ in self.hcloud.list_servers(context): - if profile['act']: - print('deleting', zone.dnsname(context, server)) + if profile["act"]: + print("deleting", zone.dnsname(context, server)) self.hcloud.delete_server(context, server) else: - print('pretend-deleting', zone.dnsname(context, server)) + print("pretend-deleting", zone.dnsname(context, server)) - if profile['act']: + if profile["act"]: self.update_zone_file(profile) class ListContexts(Operation): - def run(self, profile): p = self.new_pass(profile) - pass_subdir = profile['pass-subdir'] + pass_subdir = profile["pass-subdir"] for context in p.listdir(pass_subdir): print(context) class CreateContext(Operation): - def run(self, profile): - context = profile['context'] - token = profile['token'] - pass_dir = profile['pass-dir'] - pass_subdir = profile['pass-subdir'] + context = profile["context"] + token = profile["token"] + pass_dir = profile["pass-dir"] + pass_subdir = profile["pass-subdir"] git = Git(pass_dir) if not git.isclean(): - raise Exception('pass word store is not clean') + raise Exception("pass word store is not clean") git.pull() p = self.new_pass(profile) @@ -443,131 +421,131 @@ class CreateContext(Operation): class Hcloud: - def run(self, *args): - argv = ['hcloud'] + list(args) + argv = ["hcloud"] + list(args) output = subprocess.check_output(argv) - return output.decode('UTF-8').splitlines() + return output.decode("UTF-8").splitlines() def list_contexts(self): - lines = self.run('context', 'list') + lines = self.run("context", "list") return sorted(lines[1:]) def use_context(self, context): - self.run('context', 'use', context) + self.run("context", "use", context) def list_servers(self, context): self.use_context(context) servers = [] - lines = self.run('server', 'list') + lines = self.run("server", "list") for line in lines[1:]: words = line.split() servers.append((words[1], words[3])) return servers - def create_server( - self, context, name, image=None, ssh_key=None, server_type=None): + def create_server(self, context, name, image=None, ssh_key=None, server_type=None): assert image is not None assert ssh_key is not None assert server_type is not None self.use_context(context) self.run( - 'server', 'create', - '--name', name, - '--ssh-key', ssh_key, - '--image', image, - '--type', server_type, + "server", + "create", + "--name", + name, + "--ssh-key", + ssh_key, + "--image", + image, + "--type", + server_type, ) def delete_server(self, context, name): self.use_context(context) - self.run('server', 'delete', name) + self.run("server", "delete", name) def create_context(self, name, token): fd, filename = tempfile.mkstemp() - os.write(fd, expect_script.encode('UTF-8')) + os.write(fd, expect_script.encode("UTF-8")) os.close(fd) - argv = ['expect', '-f', filename, name, token] + argv = ["expect", "-f", filename, name, token] subprocess.check_output(argv) class Pass: - def __init__(self, pwdir): self.env = dict(os.environ) - self.env['PASSWORD_STORE_DIR'] = pwdir + self.env["PASSWORD_STORE_DIR"] = pwdir def run(self, *args, **kwargs): - argv = ['pass'] + list(args) + argv = ["pass"] + list(args) return subprocess.check_output(argv, env=self.env, **kwargs) def run_lines(self, *args): text = self.run(*args) - return text.decode('UTF-8').splitlines() + return text.decode("UTF-8").splitlines() def listdir(self, dirname): names = [] - for line in self.run_lines('show', dirname): + for line in self.run_lines("show", dirname): words = line.split() if len(words) == 2: names.append(words[1]) return sorted(names) def get_token(self, dirname, basename): - bin = self.run('show', '{}/{}'.format(dirname, basename)) - return bin.decode('UTF-8').strip() + bin = self.run("show", "{}/{}".format(dirname, basename)) + return bin.decode("UTF-8").strip() def create(self, dirname, basename, value): fd, filename = tempfile.mkstemp() os.remove(filename) - os.write(fd, value.encode('UTF-8')) + os.write(fd, value.encode("UTF-8")) os.lseek(fd, 0, os.SEEK_SET) pathname = os.path.join(dirname, basename) - self.run('insert', '-m', pathname, stdin=fd) + self.run("insert", "-m", pathname, stdin=fd) os.close(fd) class Git: - def __init__(self, dirname): self.dirname = dirname def run(self, *args, **kwargs): - argv = ['git'] + list(args) + argv = ["git"] + list(args) return subprocess.check_output(argv, cwd=self.dirname, **kwargs) def isclean(self): - bin = self.run('status', '--porcelain') - return bin == b'' + bin = self.run("status", "--porcelain") + return bin == b"" def add(self, filename): - self.run('add', filename) + self.run("add", filename) def commit(self, msg): - self.run('commit', '-m', msg, '-q') - + self.run("commit", "-m", msg, "-q") + def pull(self): - self.run('pull', '-q') - + self.run("pull", "-q") + def push(self): - self.run('push', '-q') + self.run("push", "-q") class SpecFile: - def __init__(self, filename): with open(filename) as f: self.obj = yaml.safe_load(f) def servers(self): servers = [] - for server in self.obj['hosts']: + for server in self.obj["hosts"]: server = copy.deepcopy(server) - server.update(self.obj['defaults']) - server['server_type'] = server.pop('type') + server.update(self.obj["defaults"]) + server["server_type"] = server.pop("type") servers.append(server) return servers -- cgit v1.2.1