diff --git a/hetzner/key.py b/hetzner/key.py new file mode 100644 index 0000000..13ae8a6 --- /dev/null +++ b/hetzner/key.py @@ -0,0 +1,34 @@ +import datetime + + +class Key: + name: str + fingerprint: str + size: int + data: str + created_at: datetime.datetime + + def __init__(self, conn, fingerprint: str = None, data: dict = None): + super(Key, self).__init__() + self._conn = conn + + if data is None: + self.fingerprint = fingerprint + else: + self.update_info(data) + + def update_info(self, result=None): + if result is None: + result = self._conn.request("get", f"/key/{self.fingerprint}") + + data = result["key"] + + for key in ("name", "fingerprint", "size", "data"): + setattr(self, key, data[key]) + self.created_at = datetime.datetime.strptime(data['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ') + + def rename(self, new_name: str): + return self._conn.request('POST', f"/key/{self.fingerprint}", {"name": new_name}) + + def __repr__(self): + return f"" diff --git a/hetzner/robot.py b/hetzner/robot.py index 0822101..8782058 100644 --- a/hetzner/robot.py +++ b/hetzner/robot.py @@ -5,6 +5,8 @@ from base64 import b64encode +from hetzner.key import Key + try: from httplib import BadStatusLine, ResponseNotReady except ImportError: @@ -17,6 +19,7 @@ from hetzner import WebRobotError, RobotError from hetzner.server import Server +from hetzner.storagebox import StorageBox from hetzner.rdns import ReverseDNSManager from hetzner.failover import FailoverManager from hetzner.util.http import ValidatedHTTPSConnection @@ -427,10 +430,38 @@ def get(self, ip): def __iter__(self): return iter([Server(self.conn, s) for s in self.conn.get('/server')]) +class StorageBoxManager(object): + def __init__(self, conn): + self.conn = conn + + def get(self, id_): + """ + Get storage boxes by providing its main id + """ + return StorageBox(self.conn, self.conn.get('/storagebox/{0}'.format(id_))) + + def __iter__(self): + return iter([StorageBox(self.conn, s) for s in self.conn.get('/storagebox')]) + + +class KeysManager(object): + def __init__(self, conn): + self._conn = conn + + def __iter__(self): + return iter([Key(self._conn, data=k) for k in self._conn.get('/key')]) + + def delete(self, fingerprint: str): + return self._conn.request('DELETE', f"/key/{fingerprint}") + + def add(self, name: str, data: str): + return Key(self._conn, data=self._conn.request('POST', "/key", {"name": name, "data": data})["key"]) class Robot(object): def __init__(self, user, passwd): self.conn = RobotConnection(user, passwd) self.servers = ServerManager(self.conn) + self.storageboxes = StorageBoxManager(self.conn) self.rdns = ReverseDNSManager(self.conn) self.failover = FailoverManager(self.conn, self.servers) + self.keys = KeysManager(self.conn) diff --git a/hetzner/server.py b/hetzner/server.py index 6f1c28f..0114e89 100644 --- a/hetzner/server.py +++ b/hetzner/server.py @@ -459,6 +459,7 @@ def update_info(self, result=None): self.status = data['status'] self.cancelled = data['cancelled'] self.paid_until = datetime.strptime(data['paid_until'], '%Y-%m-%d') + self.linked_storagebox = data['linked_storagebox'] def observed_reboot(self, *args, **kwargs): msg = ("Server.observed_reboot() is deprecated. Please use" diff --git a/hetzner/storagebox.py b/hetzner/storagebox.py new file mode 100644 index 0000000..438896a --- /dev/null +++ b/hetzner/storagebox.py @@ -0,0 +1,118 @@ +import logging + +from datetime import datetime + +__all__ = ['StorageBox', 'SubAccount', 'SubAccountManager'] + + +class SubAccount(object): + def __init__(self, conn, box_id, result): + self.conn = conn + self.box_id = box_id + self.update_info(result) + + def update_info(self, result): + """ + Update the information of the subaccount. + """ + data = result['subaccount'] + + self.username = data['username'] + self.accountid = data['accountid'] + self.server = data['server'] + self.homedirectory = data['homedirectory'] + self.samba = data['samba'] + self.ssh = data['ssh'] + self.external_reachability = data['external_reachability'] + self.webdav = data['webdav'] + self.readonly = data['readonly'] + self.createtime = datetime.strptime(data['createtime'], '%Y-%m-%d %H:%M:%S') + self.comment = data['comment'] + + def update(self, homedirectory, samba, ssh, external_reachability, webdav, readonly, comment): + path = f'/storagebox/{self.box_id}/subaccount/{self.username}' + data = {'homedirectory': homedirectory, + 'samba': samba, + 'ssh': ssh, + 'external_reachability': external_reachability, + 'webdav': webdav, + 'readonly': readonly, + 'comment': comment} + return self.conn.put(path, data) + + def reset_password(self): + data = f'/storagebox/{self.box_id}/subaccount/{self.username}/password' + result = self.conn.post(data, None) + return result['password'] + + def delete(self): + self.conn.delete('/storagebox/{0}/subaccount/{1}'.format(self.box_id, self.username)) + + def __repr__(self): + return "".format(self.username) + + +class SubAccountManager(object): + def __init__(self, conn, box_id): + self.conn = conn + self.box_id = box_id + + def create(self, homedirectory, samba, ssh, external_reachability, webdav, readonly, comment): + result = self.conn.post('/storagebox/{0}/subaccount'.format(self.box_id), + {'homedirectory': homedirectory, + 'samba': samba, + 'ssh': ssh, + 'external_reachability': external_reachability, + 'webdav': webdav, + 'readonly': readonly, + 'comment': comment}) + + return result + + def delete(self, username): + self.conn.delete('/storagebox/{0}/subaccount/{1}'.format(self.box_id, username)) + + def __iter__(self): + return iter([SubAccount(self.conn, self.box_id, s) for s in self.conn.get('/storagebox/{0}/subaccount'.format(self.box_id))]) + + +class StorageBox(object): + def __init__(self, conn, result): + self.conn = conn + self.update_info(result) + self.subaccounts = SubAccountManager(self.conn, self.id_) + self.logger = logging.getLogger("StorageBox #{0}".format(self.id_)) + + def update_info(self, result=None): + """ + Updates the information of the current SubAccount instance either by + sending a new GET request or by parsing the response given by result. + """ + if result is None: + result = self.conn.get('/storagebox/{0}'.format(self.id_)) + data = result['storagebox'] + + self.id_ = data['id'] + self.login = data['login'] + self.name = data['name'] + self.product = data['product'] + self.cancelled = data['cancelled'] + self.locked = data['locked'] + self.location = data['location'] + self.linked_server = data['linked_server'] + self.paid_until = datetime.strptime(data['paid_until'], '%Y-%m-%d') + if 'disk_quota' in data: + self.disk_quota = data['disk_quota'] + self.disk_usage = data['disk_usage'] + self.disk_usage_data = data['disk_usage_data'] + self.disk_usage_snapshots = data['disk_usage_snapshots'] + self.webdav = data['webdav'] + self.samba = data['samba'] + self.ssh = data['ssh'] + self.external_reachability = data['external_reachability'] + self.zfs = data['zfs'] + self.server = data['server'] + self.host_system = data['host_system'] + + def __repr__(self): + return "<{0} (#{1} {2})>".format(self.login, self.id_, self.product) diff --git a/hetznerctl b/hetznerctl index c7b8a1a..a8851f3 100755 --- a/hetznerctl +++ b/hetznerctl @@ -342,6 +342,26 @@ class Config(SubCommand): self.config.write(fp) +class ListStorageboxes(SubCommand): + command = 'list-storageboxes' + description = "List all storageboxes" + + def execute(self, robot, parser, args): + for storagebox in robot.storageboxes: + info = { + 'login': storagebox.login, + 'product': storagebox.product + } + + if storagebox.name != "": + info['name'] = storagebox.name + + infolist = ["{0}: {1}".format(key, val) + for key, val in info.items()] + + self.putline("{0} ({1})".format(storagebox.login, ", ".join(infolist))) + + def main(): subcommands = [ Config, @@ -353,6 +373,7 @@ def main(): ReverseDNS, Admin, Failover, + ListStorageboxes, ] common_parser = argparse.ArgumentParser(