From c03078428067e150dcdb9d15414209134c2cce76 Mon Sep 17 00:00:00 2001 From: pajowu Date: Tue, 25 Aug 2020 21:38:53 +0200 Subject: [PATCH 1/7] Add support for StorageBoxes --- hetzner/robot.py | 15 ++++++ hetzner/storagebox.py | 118 ++++++++++++++++++++++++++++++++++++++++++ hetznerctl | 22 ++++++++ 3 files changed, 155 insertions(+) create mode 100644 hetzner/storagebox.py diff --git a/hetzner/robot.py b/hetzner/robot.py index 47ec8f0..de25b07 100644 --- a/hetzner/robot.py +++ b/hetzner/robot.py @@ -17,6 +17,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 +428,24 @@ 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 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) diff --git a/hetzner/storagebox.py b/hetzner/storagebox.py new file mode 100644 index 0000000..b772276 --- /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): + result = self.conn.put('/storagebox/{0}/subaccount/{1}'.format(self.box_id_, self.username), + {'homedirectory': homedirectory, + 'samba': samba, + 'ssh': ssh, + 'external_reachability': external_reachability, + 'webdav': webdav, + 'readonly': readonly, + 'comment': comment}) + + return result + + def reset_password(self): + result = self.conn.post('/storagebox/{0}/subaccount/{1}/password'.format(self.box_id_, self.username), 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 95408a4..a6e0262 100755 --- a/hetznerctl +++ b/hetznerctl @@ -339,6 +339,27 @@ 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 = [u"{0}: {1}".format(key, val) + for key, val in info.items()] + + self.putline(u"{0} ({1})".format(storagebox.login, u", ".join(infolist))) + + + def main(): subcommands = [ Config, @@ -350,6 +371,7 @@ def main(): ReverseDNS, Admin, Failover, + ListStorageboxes ] common_parser = argparse.ArgumentParser( From cd3e4d7792dd06d44eb540bbc8109f957396bd4d Mon Sep 17 00:00:00 2001 From: pajowu Date: Sun, 4 Oct 2020 22:29:24 +0200 Subject: [PATCH 2/7] Apply suggestions from code review (Part 1) Co-authored-by: aszlig --- hetznerctl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hetznerctl b/hetznerctl index a6e0262..52f4a49 100755 --- a/hetznerctl +++ b/hetznerctl @@ -359,7 +359,6 @@ class ListStorageboxes(SubCommand): self.putline(u"{0} ({1})".format(storagebox.login, u", ".join(infolist))) - def main(): subcommands = [ Config, @@ -371,7 +370,7 @@ def main(): ReverseDNS, Admin, Failover, - ListStorageboxes + ListStorageboxes, ] common_parser = argparse.ArgumentParser( From a206ff7078244509f4697b80aa6a823ea5c0ad17 Mon Sep 17 00:00:00 2001 From: pajowu Date: Sun, 4 Oct 2020 22:29:34 +0200 Subject: [PATCH 3/7] Apply suggestions from code review (Part 2) --- hetzner/storagebox.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/hetzner/storagebox.py b/hetzner/storagebox.py index b772276..4bb157a 100644 --- a/hetzner/storagebox.py +++ b/hetzner/storagebox.py @@ -30,19 +30,19 @@ def update_info(self, result): self.comment = data['comment'] def update(self, homedirectory, samba, ssh, external_reachability, webdav, readonly, comment): - result = self.conn.put('/storagebox/{0}/subaccount/{1}'.format(self.box_id_, self.username), - {'homedirectory': homedirectory, - 'samba': samba, - 'ssh': ssh, - 'external_reachability': external_reachability, - 'webdav': webdav, - 'readonly': readonly, - 'comment': comment}) - - return result + 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): - result = self.conn.post('/storagebox/{0}/subaccount/{1}/password'.format(self.box_id_, self.username), None) + data = f'/storagebox/{self.box_id_}/subaccount/{self.username}/password' + result = self.conn.post(data, None) return result['password'] def delete(self): From b9d0ac571e31d8d7f47d102ada2df7e41fdfbc54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arkadiusz=20Dzi=C4=99giel?= Date: Mon, 5 Dec 2022 15:24:40 +0100 Subject: [PATCH 4/7] fill storagebox id for servers --- hetzner/server.py | 1 + 1 file changed, 1 insertion(+) 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" From 8e0ebb3291110a462ccc4ce17305b63280abdbb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arkadiusz=20Dzi=C4=99giel?= Date: Mon, 5 Dec 2022 16:25:11 +0100 Subject: [PATCH 5/7] add support for key endpoint --- hetzner/key.py | 34 ++++++++++++++++++++++++++++++++++ hetzner/robot.py | 16 ++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 hetzner/key.py 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 93ceddd..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: @@ -442,6 +444,19 @@ 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) @@ -449,3 +464,4 @@ def __init__(self, user, passwd): self.storageboxes = StorageBoxManager(self.conn) self.rdns = ReverseDNSManager(self.conn) self.failover = FailoverManager(self.conn, self.servers) + self.keys = KeysManager(self.conn) From 66aa3e51d51b33ed84fd17f902e0a2102edfc5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arkadiusz=20Dzi=C4=99giel?= Date: Mon, 5 Dec 2022 16:28:05 +0100 Subject: [PATCH 6/7] remove unneeded underscores --- hetzner/storagebox.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/hetzner/storagebox.py b/hetzner/storagebox.py index 4bb157a..438896a 100644 --- a/hetzner/storagebox.py +++ b/hetzner/storagebox.py @@ -6,9 +6,9 @@ class SubAccount(object): - def __init__(self, conn, box_id_, result): + def __init__(self, conn, box_id, result): self.conn = conn - self.box_id_ = box_id_ + self.box_id = box_id self.update_info(result) def update_info(self, result): @@ -30,7 +30,7 @@ def update_info(self, result): self.comment = data['comment'] def update(self, homedirectory, samba, ssh, external_reachability, webdav, readonly, comment): - path = f'/storagebox/{self.box_id_}/subaccount/{self.username}' + path = f'/storagebox/{self.box_id}/subaccount/{self.username}' data = {'homedirectory': homedirectory, 'samba': samba, 'ssh': ssh, @@ -41,24 +41,24 @@ def update(self, homedirectory, samba, ssh, external_reachability, webdav, reado return self.conn.put(path, data) def reset_password(self): - data = f'/storagebox/{self.box_id_}/subaccount/{self.username}/password' + 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)) + 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_): + def __init__(self, conn, box_id): self.conn = conn - self.box_id_ = box_id_ + 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_), + result = self.conn.post('/storagebox/{0}/subaccount'.format(self.box_id), {'homedirectory': homedirectory, 'samba': samba, 'ssh': ssh, @@ -70,10 +70,10 @@ def create(self, homedirectory, samba, ssh, external_reachability, webdav, reado return result def delete(self, username): - self.conn.delete('/storagebox/{0}/subaccount/{1}'.format(self.box_id_, 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_))]) + 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): From c3a80b333a318716d8e6697a500f609407a69259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arkadiusz=20Dzi=C4=99giel?= Date: Thu, 15 Dec 2022 19:28:20 +0100 Subject: [PATCH 7/7] remove unicode string markers --- hetznerctl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hetznerctl b/hetznerctl index 4fea0c0..a8851f3 100755 --- a/hetznerctl +++ b/hetznerctl @@ -356,10 +356,10 @@ class ListStorageboxes(SubCommand): if storagebox.name != "": info['name'] = storagebox.name - infolist = [u"{0}: {1}".format(key, val) + infolist = ["{0}: {1}".format(key, val) for key, val in info.items()] - self.putline(u"{0} ({1})".format(storagebox.login, u", ".join(infolist))) + self.putline("{0} ({1})".format(storagebox.login, ", ".join(infolist))) def main():