From 5c27e37e753fc735e87bca1948ed6085c248d910 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Thu, 17 May 2018 18:36:36 +0300 Subject: Add: hetznertool --- hetznertool | 230 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100755 hetznertool diff --git a/hetznertool b/hetznertool new file mode 100755 index 0000000..60a3c08 --- /dev/null +++ b/hetznertool @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 + +import argparse +import copy +import os +import subprocess + +import yaml + + +CONFIG_FILENAME = os.path.expanduser('~/.config/hetznertool/hetznertool.yaml') + + +default_config = { + 'dnszone-dir': os.path.expanduser('~/qvarnlabs/code/dnszone'), + 'dnszone-file': 'db.hetzner', + 'ansible-inventory-dir': '.', + 'ssh-key': None, +} + + +def main(): + config = read_config() + parser = create_parser(config) + args = vars(parser.parse_args()) + func = args['func'] + func(args) + + +def hcloud(*args): + argv = ['hcloud'] + list(args) + output = subprocess.check_output(argv) + return output.decode('UTF-8').splitlines() + + +def use_context(context): + hcloud('context', 'use', context) + + +def list_contexts(): + lines = hcloud('context', 'list') + assert len(lines) > 0 + assert lines[0] == "NAME" + del lines[0] # This is just the title line, says "NAME" + return lines + + +def list_servers(): + lines = hcloud('server', 'list') + assert len(lines) > 0 + del lines[0] # This is just the title line + return [parse_server_line(line) for line in lines] + + +def parse_server_line(line): + words = line.split() + return { + 'name': words[1], + 'ipv4': words[3], + } + + +def dns_name(context, name): + return '{}-{}.h.qvarnlabs.eu'.format(context, name) + + +class ServerSpecification(dict): + + def from_dict(self, as_dict): + self.clear() + self.update(copy.deepcopy(as_dict)) + + def from_yaml(self, stream): + self.from_dict(yaml.safe_load(stream)) + + def from_file(self, filename): + with open(filename) as f: + self.from_yaml(f) + + def _hosts(self): + return self.get('hosts', []) + + def _defaults(self): + return copy.deepcopy(self.get('defaults', {})) + + def _get(self, name): + hosts = self._hosts() + for host in hosts: + if host.get('name') == name: + return copy.deepcopy(host) + + def get_servers(self): + return [host['name'] for host in self._hosts()] + + def get_server(self, name): + server = self._defaults() + server.update(self._get(name)) + return server + + + +def create_func(args): + spec = ServerSpecification() + spec.from_file(args['specfile']) + + use_context(args['context']) + for name in spec.get_servers(): + server = spec.get_server(name) + print('creating {} in {}'.format(name, args['context'])) + hcloud( + 'server', 'create', + '--name', name, + '--image', server['image'], + '--type', server['type'], + '--ssh-key', args['ssh_key'], + ) + update_zone_file(args) + + +def list_func(args): + contexts = list_contexts() + for context in contexts: + use_context(context) + for info in list_servers(): + domain = dns_name(context, info['name']) + print(domain, info['ipv4']) + + +def delete_func(args): + use_context(args['context']) + for info in list_servers(): + domain = dns_name(args['context'], info['name']) + print( + 'deleting {} ({} in {})'.format( + domain, info['name'], args['context'])) + hcloud('server', 'delete', info['name']) + + +def update_zone_file(args): + dirname = args['dnszone_dir'] + basename = args['dnszone_file'] + filename = os.path.join(dirname, basename) + serial_name = os.path.join(dirname, 'serial') + + print('Updating zone file {}'.format(filename)) + + subprocess.check_call(['git', 'pull'], cwd=dirname) + + serial = int(open(serial_name).readline().strip()) + serial += 1 + + with open(filename, 'w') as f: + write_zone(f, serial) + + open(serial_name, 'w').write('{}\n'.format(serial)) + + subprocess.check_call( + ['git', 'commit', '-m', 'automatic zone update', basename, 'serial'], + cwd=dirname) + + subprocess.check_call(['git', 'push'], cwd=dirname) + + +def write_zone(stream, serial): + stream.write(''' +$TTL 30 +$ORIGIN dev.qvarnlabs.eu. + +@ IN SOA ns1.qvarnlabs.net. ops.qvarnlabs.com ( + {} 30 30 8640000 15 ) + +@ IN NS ns1.qvarnlabs.net. +@ IN NS ns2.qvarnlabs.net. + +'''.format(serial)) + + for context in list_contexts(): + use_context(context) + for info in list_servers(): + domain = dns_name(context, info['name']) + stream.write('{} IN A {}\n'.format(domain, info['ipv4'])) + + +def read_config(): + config = copy.deepcopy(default_config) + filename = CONFIG_FILENAME + if os.path.exists(filename): + with open(filename) as f: + config.update(yaml.safe_load(f)) + return config + + +def create_parser(config): + parser = argparse.ArgumentParser( + description='Manage VMs in Hetzner Cloud for QvarnLabs.') + + factory = parser.add_subparsers() + + create = factory.add_parser('create') + create.add_argument( + '--ssh-key', + metavar='KEYNAME', + required=True, + help='create VM so it allow login via ssh key uploaded as KEYNAME') + create.add_argument( + 'context', help='hcloud context to create VMs in') + create.add_argument( + 'specfile', help='file to read VM specifications from') + create.add_argument( + '--dnszone-dir', default=config['dnszone-dir'], + metavar='DIR', help='write DNS zone directory into DIR') + create.add_argument( + '--dnszone-file', default=config['dnszone-file'], + metavar='FILE', help='write DNS zone directory into FILE') + + servers = factory.add_parser('list') + + delete = factory.add_parser('delete') + delete.add_argument( + 'context', help='hcloud context to delete all VMs from') + + create.set_defaults(func=create_func) + servers.set_defaults(func=list_func) + delete.set_defaults(func=delete_func) + + return parser + + +if __name__ == '__main__': + main() -- cgit v1.2.1