summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2018-05-17 18:36:36 +0300
committerLars Wirzenius <liw@liw.fi>2018-05-17 18:36:36 +0300
commit5c27e37e753fc735e87bca1948ed6085c248d910 (patch)
tree6f013b9e4e80f1a7333f0876b4272752804e350b
parentdff8a6721ea6cd04d1cba259d4111d03d26833f6 (diff)
downloadhetznertool-5c27e37e753fc735e87bca1948ed6085c248d910.tar.gz
Add: hetznertool
-rwxr-xr-xhetznertool230
1 files changed, 230 insertions, 0 deletions
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()