415 lines
14 KiB
Python
Executable File
415 lines
14 KiB
Python
Executable File
#!/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>.")
|
|
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 <your-robot-username>' and"
|
|
" `hetznerctl config login.password <your-robot-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()
|