first commit
This commit is contained in:
414
.venv/bin/hetznerctl
Executable file
414
.venv/bin/hetznerctl
Executable file
@@ -0,0 +1,414 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user