#!/home/joachim/git/hetzner/.venv/bin/python3 import sys import locale import warnings import argparse from os.path import expanduser from hetzner.robot import Robot import logging try: from ConfigParser import RawConfigParser except ImportError: from configparser import RawConfigParser def make_option(*args, **kwargs): return (args, kwargs) class SubCommand(object): command = None description = None long_description = None option_list = [] requires_robot = True def __init__(self, configfile): self.config = RawConfigParser() self.config.read(configfile) def putline(self, line): data = line + u"\n" try: sys.stdout.write(data) except UnicodeEncodeError: preferred = locale.getpreferredencoding() sys.stdout.write(data.encode(preferred, 'replace')) def execute(self, robot, parser, args): pass class Reboot(SubCommand): command = 'reboot' description = "Reboot a server" option_list = [ make_option('-m', '--method', dest='method', choices=['soft', 'hard', 'manual'], default='soft', help="The method to use for the reboot"), make_option('ip', metavar='IP', nargs='+', help="IP address of the server to reboot"), ] def execute(self, robot, parser, args): for ip in args.ip: server = robot.servers.get(ip) if server: server.reboot(args.method) class Rescue(SubCommand): command = 'rescue' description = "Activate rescue system" long_description = ("Reboot into rescue system, spawn a shell and" " after the shell is closed, reboot back into" " the normal system.") option_list = [ make_option('-p', '--patience', dest='patience', type=int, default=300, help=("The time to wait between subsequent" " reboot tries")), make_option('-m', '--manual', dest='manual', action='store_true', default=False, help=("If all reboot tries fail," " automatically send a support" " request")), make_option('-n', '--noshell', dest='noshell', action='store_true', default=False, help=("Don't drop into a shell, only print" " rescue password")), make_option('ip', metavar='IP', nargs='+', help="IP address of the server to put into rescue system"), ] def execute(self, robot, parser, args): for ip in args.ip: server = robot.servers.get(ip) if not server: continue kwargs = { 'patience': args.patience, 'manual': args.manual, } if args.noshell: server.rescue.observed_activate(**kwargs) msg = u"Password for {0}: {1}".format(server.ip, server.rescue.password) self.putline(msg) else: with warnings.catch_warnings(): warnings.simplefilter("ignore") server.rescue.shell(**kwargs) class SetName(SubCommand): command = "set-name" description = "Change the name of a server" option_list = [ make_option('ip', metavar='IP', help="IP address of the server"), make_option('name', metavar='NAME', nargs='?', default='', help="New name of the server"), ] def execute(self, robot, parser, args): robot.servers.get(args.ip).set_name(args.name) class ListServers(SubCommand): command = 'list' description = "List all servers" def execute(self, robot, parser, args): for server in robot.servers: info = { 'id': server.number, 'model': server.product } if server.name != "": info['name'] = server.name infolist = [u"{0}: {1}".format(key, val) for key, val in info.items()] self.putline(u"{0} ({1})".format(server.ip, u", ".join(infolist))) class ShowServer(SubCommand): command = 'show' description = "Show details about a server" option_list = [ make_option('ip', nargs='+', metavar='IP', help="IP address of the server"), ] def execute(self, robot, parser, args): for ip in args.ip: self.print_serverinfo(robot.servers.get(ip)) def print_line(self, key, val): self.putline(u"{0:<15}{1}".format(key + u":", val)) def print_serverinfo(self, server): info = [ ("Number", server.number), ("Main IP", server.ip), ("Name", server.name), ("Product", server.product), ("Data center", server.datacenter), ("Traffic", server.traffic), ("Status", server.status), ("Cancelled", server.cancelled), ("Paid until", server.paid_until), ] for key, val in info: self.print_line(key, val) for ip in server.ips: if ip.rdns.ptr is None: addr = ip.ip else: addr = u"{0} (rPTR: {1})".format(ip.ip, ip.rdns.ptr) self.print_line(u"IP address", addr) for net in server.subnets: addrtype = u"IPv6" if net.is_ipv6 else u"IPv4" addr = u"{0}/{1} ({2})".format(net.net_ip, net.mask, addrtype) self.print_line(u"Subnet", addr) self.print_line(u"Gateway", net.gateway) for rdns in server.rdns: rptr = u"{0} -> {1}".format(rdns.ip, rdns.ptr) self.print_line(u"Reverse PTR", rptr) class ReverseDNS(SubCommand): command = 'rdns' description = "List and set reverse DNS records" option_list = [ make_option('-s', '--set', dest='setptr', action='store_true', default=False, help="Set a new reverse PTR"), make_option('-d', '--delete', dest='delptr', action='store_true', default=False, help="Delete reverse PTR"), make_option('ip', metavar='IP', nargs='?', default=None, help="IP address of the server"), make_option('value', metavar='RPTR', nargs='?', default=None, help="New reverse record to set"), ] def execute(self, robot, parser, args): if args.ip is None: for rdns in robot.rdns: self.putline("{0} -> {1}".format(rdns.ip, rdns.ptr)) elif args.delptr: robot.rdns.get(args.ip).remove() elif args.setptr: if args.ip is None or args.value is None: parser.error("Need exactly two arguments: IP address and new" " reverse FQDN.") else: rdns = robot.rdns.get(args.ip) rdns.set(args.value) else: rdns = robot.rdns.get(args.ip) if rdns.ptr is None: self.putline("No reverse record set for {0}.".format(rdns.ip)) else: self.putline("{0} -> {1}".format(rdns.ip, rdns.ptr)) class Failover(SubCommand): command = 'failover' description = 'List and set failover IP addresses' option_list = [ make_option('-s', '--set', dest='setfailover', action='store_true', default=False, help="Assign failover IP address to server"), make_option('ip', nargs='?', default=None, help="Failover IP address to assign"), make_option('destination', nargs='?', default=None, help="IP address of new failover destination") ] def execute(self, robot, parser, args): if args.setfailover: errs = [] if not args.ip: errs.append("Error: you need to set the failover IP you" " want to assign. Option 'ip'") if not args.destination: errs.append("Error: you need to set the new destination of" " the failover IP. Option 'dest'") if len(errs) > 0: for err in errs: self.putline(err) else: failover = robot.failover.set(args.ip, args.destination) self.putline("Failover IP successfully assigned to new" " destination") self.putline(str(failover)) else: failovers = robot.failover.list() if len(failovers) > 0: self.putline("Found %s failover IPs" % len(failovers)) for failover in failovers.values(): self.putline(str(failover)) class Admin(SubCommand): command = 'admin' description = "Create/delete dedicated admin accounts" option_list = [ make_option('-C', '--create', dest='addadmin', action='store_true', default=False, help="Create admin account"), make_option('-d', '--delete', dest='deladmin', action='store_true', default=False, help="Delete admin account"), make_option('-p', '--password', dest='admpasswd', metavar='PASSWORD', help="Use this password instead of generating one"), make_option('ip', metavar='IP', nargs='+', default=None, help="IP address of the server"), ] def execute(self, robot, parser, args): for ip in args.ip: server = robot.servers.get(ip) if args.addadmin: login, passwd = server.admin.create(passwd=args.admpasswd) msg = "{0}: {1} -> {2}".format(server.ip, login, passwd) self.putline(msg) elif args.deladmin: server.admin.delete() else: if server.admin.exists: msg = "{0}: {1}".format(server.ip, server.admin.login) else: msg = "No admin account for {0}.".format(server.ip) self.putline(msg) class Config(SubCommand): command = 'config' description = "Get or set options" long_description = ("Set options by just using `config section.option" " value' or list options by not providing any" " arguments.") option_list = [ make_option('-d', '--delete', dest='delete', action='store_true', default=False, help="Delete an option"), make_option('name', nargs='?', help="Section and name of the option"), make_option('value', nargs='?', default=None, help="New value of the option"), ] requires_robot = False def execute(self, robot, parser, args): if args.name is None: for section in self.config.sections(): for key, value in self.config.items(section): self.putline("{0}.{1}={2!r}".format(section, key, value)) else: if '.' not in args.name: parser.error("Option name needs to be in the form" "
..") section, name = args.name.split('.', 1) if args.value is None: if not args.delete: parser.error("In order to delete/unset an option, please" " use -d.") self.config.remove_option(section, name) if len(self.config.options(section)) == 0: self.config.remove_section(section) else: if not self.config.has_section(section): self.config.add_section(section) self.config.set(section, name, args.value) with open(args.configfile, 'w') as fp: self.config.write(fp) def main(): subcommands = [ Config, Reboot, Rescue, SetName, ListServers, ShowServer, ReverseDNS, Admin, Failover, ] common_parser = argparse.ArgumentParser( description="Common options", add_help=False ) global_options = common_parser.add_argument_group(title="global options") global_options.add_argument('-c', '--config', dest='configfile', default='~/.hetznerrc', type=expanduser, help="The location of the configuration file") global_options.add_argument('--debug', action='store_true', help="Show debug output.") parser = argparse.ArgumentParser( description="Hetzner Robot commandline interface", prog='hetznerctl', formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[common_parser] ) subparsers = parser.add_subparsers( title="available commands", metavar="command", help="description", ) for cmd in subcommands: subparser = subparsers.add_parser( cmd.command, help=cmd.description, description=cmd.long_description, formatter_class=argparse.ArgumentDefaultsHelpFormatter, parents=[common_parser] ) for args, kwargs in cmd.option_list: subparser.add_argument(*args, **kwargs) subparser.set_defaults(cmdclass=cmd) args = parser.parse_args() logging.basicConfig(format='%(name)s: %(message)s', level=logging.DEBUG if args.debug else logging.INFO) if getattr(args, 'cmdclass', None) is None: parser.print_help() parser.exit(1) subcommand = args.cmdclass(args.configfile) if subcommand.requires_robot: if not subcommand.config.has_option('login', 'username') or \ not subcommand.config.has_option('login', 'password'): parser.error(( "You need to set a user and password in {0} in order to" " continue with this operation. You can do this using" " `hetznerctl config login.username ' and" " `hetznerctl config login.password '." ).format(args.configfile)) robot = Robot( subcommand.config.get('login', 'username'), subcommand.config.get('login', 'password'), ) else: robot = None subcommand.execute(robot, parser, args) if __name__ == '__main__': main()