From 84dd1af404a948fc4adf5b44a70317eb2d7b04a5 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 30 Jun 2018 13:24:30 +0300 Subject: Change: name hetznertool2 to hetznertool We don't need the old one anymore. --- hetznertool2 | 573 ----------------------------------------------------------- 1 file changed, 573 deletions(-) delete mode 100755 hetznertool2 (limited to 'hetznertool2') diff --git a/hetznertool2 b/hetznertool2 deleted file mode 100755 index 8b22cb5..0000000 --- a/hetznertool2 +++ /dev/null @@ -1,573 +0,0 @@ -#!/usr/bin/env python3 - - -import argparse -import copy -import os -import subprocess -import sys -import tempfile - -import yaml - - -CONFIG_FILENAME = os.path.expanduser('~/.config/hetznertool/hetznertool2.yaml') - - -expect_script='''\ -if {[llength $argv] != 2} { - puts stderr "Usage: $argv0 name token - exit 2 -} - -lassign $argv name token - -log_user 0 -spawn hcloud context create $name -expect { - "Token:" { - send "$token\n" - exp_continue - } - timeout {close} -} -lassign [wait] wait_pid spawn_id exec_rc wait_code childkilled -# couldn't exec pesign -if {$exec_rc != 0} { - exit 1 -} -# killed by signal (e.g. timeout) -if {$childkilled == "CHILDKILLED"} { - exit 1 -} -# 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))) - - def main(self): - 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.load(CONFIG_FILENAME) - return config - - -class Config: - - def __init__(self): - self.config_vars = [] - self.profiles = {} - - def dump(self): - for profile in self.profiles: - for name in self.profiles[profile]: - print(profile, name, repr(self.get(profile, name))) - - def load(self, filename): - with open(filename) as f: - obj = yaml.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)) - for name in obj[profile]: - if name not in self.config_vars: - raise Exception( - 'Profile {} has extra variable {}'.format( - profile, name)) - self.set(profile, name, obj[profile][name]) - - def add(self, name): - assert name not in self.config_vars - self.config_vars.append(name) - - def get(self, profile, 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 - } - - def set(self, profile, name, value): - if profile not in self.profiles: - self.profiles[profile] = {} - self.profiles[profile][name] = value - - -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.update(args) - func = args['func'] - func(profile) - - def create_parser(self): - parser = argparse.ArgumentParser( - 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) - - 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') - - servers = factory.add_parser('list') - - contexts = factory.add_parser('contexts') - - 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') - - 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) - create.set_defaults(func=Create().run) - delete.set_defaults(func=Delete().run) - contexts.set_defaults(func=ListContexts().run) - create_context.set_defaults(func=CreateContext().run) - zonefile.set_defaults(func=Zonefile().run) - - return parser - - def usage(self, args): - raise Exception('Missing operation') - - -class Zone: - - def __init__(self, domain): - self.domain = domain - self.servers = [] - - def servername(self, context, server): - return '{}-{}'.format(context, server) - - def dnsname(self, context, server): - return '{}.{}'.format(self.servername(context, server), self.domain) - - def add(self, context, server, addr): - self.servers.append((context, server, addr)) - - def __iter__(self): - return iter(self.servers) - - -class ZoneWriter: - - def __init__(self, zonedir, zonefile, ns1): - self.zonedir = zonedir - self.zonefile = zonefile - self.ns1 = ns1 - - def write(self, zone): - git = Git(self.zonedir) - if not git.isclean(): - raise Exception('git dir unclean: {}'.format(self.zonedir)) - - git.pull() - - pathname = os.path.join(self.zonedir, self.zonefile) - with open(pathname, 'w') as f: - self.write_zone(zone, f) - - if not git.isclean(): - git.add('.') - git.commit('update zone file') - git.push() - - def write_zone(self, zone, f): - raise NotImplementedError() - - -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)) - - -class ZoneWriterBind9(ZoneWriter): - - preamble = '''\ -$TTL 30 -$ORIGIN h.qvarnlabs.eu. - -@ IN SOA ns1.qvarnlabs.net. ops.qvarnlabs.com ({serial} 30 30 8640000 15 ) -@ 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)) - self.kick_bind9() - - def increment_serial(self): - 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: - f.write(str(serial)) - return serial - - def kick_bind9(self): - 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']) - - -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 - ] - for server in servers: - f.write('{} ansible_ssh_host={}\n'.format( - server, zone.dnsname(context, server))) - - -class Operation: - - def __init__(self): - self.hcloud = Hcloud() - - def run(self, profile): - raise NotImplementedError() - - def new_pass(self, profile): - return Pass(profile['pass-dir']) - - def create_hcloud_contexts(self, profile): - p = self.new_pass(profile) - pass_subdir = profile['pass-subdir'] - pass_contexts = p.listdir(pass_subdir) - hcloud_contexts = self.hcloud.list_contexts() - for context in pass_contexts: - if context not in hcloud_contexts: - token = p.get_token(pass_subdir, context) - self.hcloud.create_context(context, token) - - def update_zone_file(self, profile): - zonedir = profile['dnszone-dir'] - filename = profile['dnszone-file'] - ns1 = profile['ns1'] - - zone, contexts = self.create_zone(profile) - klass = self.get_zone_writer(profile) - writer = klass(zonedir, filename, ns1) - writer.write(zone) - - self.write_inventory(profile, zone, contexts) - - def create_zone(self, profile): - zone = Zone(profile['domain']) - - p = self.new_pass(profile) - 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) - - return zone, pass_contexts - - def get_zone_writer(self, profile): - styles = { - 'dns-api': ZoneWriterDnsApi, - 'qvarnlabs': ZoneWriterBind9, - } - - style = profile['style'] - if style not in styles: - 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.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'] - zone = Zone(domain) - for context in pass_contexts: - for server, addr in self.hcloud.list_servers(context): - print(zone.dnsname(context, server), addr) - - -class Create(Operation): - - def run(self, profile): - if profile['act']: - self.create_hcloud_contexts(profile) - - ssh_key = profile['ssh-key'] - context = profile['context'] - specfile = SpecFile(profile['specfile']) - zone = Zone(profile['domain']) - for server in specfile.servers(): - if profile['act']: - self.hcloud.create_server(context, ssh_key=ssh_key, **server) - print('created', zone.dnsname(context, server['name'])) - else: - print('pretending to create', zone.dnsname(context, server['name'])) - - if profile['act']: - self.update_zone_file(profile) - - -class Delete(Operation): - - def run(self, profile): - if profile['act']: - self.create_hcloud_contexts(profile) - - 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)) - self.hcloud.delete_server(context, server) - else: - print('pretend-deleting', zone.dnsname(context, server)) - - 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'] - 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'] - - git = Git(pass_dir) - if not git.isclean(): - raise Exception('pass word store is not clean') - git.pull() - - p = self.new_pass(profile) - p.create(pass_subdir, context, token) - - git.push() - - self.hcloud.create_context(context, token) - - -class Hcloud: - - def run(self, *args): - argv = ['hcloud'] + list(args) - output = subprocess.check_output(argv) - return output.decode('UTF-8').splitlines() - - def list_contexts(self): - lines = self.run('context', 'list') - return sorted(lines[1:]) - - def use_context(self, context): - self.run('context', 'use', context) - - def list_servers(self, context): - self.use_context(context) - servers = [] - 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): - 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, - ) - - def delete_server(self, context, name): - self.use_context(context) - self.run('server', 'delete', name) - - def create_context(self, name, token): - fd, filename = tempfile.mkstemp() - os.write(fd, expect_script.encode('UTF-8')) - os.close(fd) - - 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 - - def run(self, *args, **kwargs): - 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() - - def listdir(self, dirname): - names = [] - 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() - - def create(self, dirname, basename, value): - fd, filename = tempfile.mkstemp() - os.remove(filename) - 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) - - os.close(fd) - - -class Git: - - def __init__(self, dirname): - self.dirname = dirname - - def run(self, *args, **kwargs): - argv = ['git'] + list(args) - return subprocess.check_output(argv, cwd=self.dirname, **kwargs) - - def isclean(self): - bin = self.run('status', '--porcelain') - return bin == b'' - - def add(self, filename): - self.run('add', filename) - - def commit(self, msg): - self.run('commit', '-m', msg, '-q') - - def pull(self): - self.run('pull', '-q') - - def push(self): - 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']: - server = copy.deepcopy(server) - server.update(self.obj['defaults']) - server['server_type'] = server.pop('type') - servers.append(server) - return servers - - -HetznerTool().run() -- cgit v1.2.1