first commit

This commit is contained in:
2026-02-16 17:41:03 +00:00
commit 3f15490a0d
1055 changed files with 194272 additions and 0 deletions

414
.venv/bin/hetznerctl Executable file
View 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()