From 0fc6aa22ca6f646430323533de2c514f94294930 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 23 Jun 2020 10:28:21 -0400 Subject: [PATCH 1/2] baseapi: create a ResponseError to grant access to the failing response --- packet/baseapi.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packet/baseapi.py b/packet/baseapi.py index 3e01d9e..c7c68c2 100644 --- a/packet/baseapi.py +++ b/packet/baseapi.py @@ -19,6 +19,21 @@ def cause(self): return self._cause +class ResponseError(Error): + def __init__(self, resp, data, exception=None): + if not data: + msg = "(empty response)" + elif "errors" in data: + msg = ", ".join(data["errors"]) + super().__init__("Error {0}: {1}".format(resp.status_code, msg), exception) + self._response = resp + + @property + def response(self): + """The Requests response which failed""" + return self._response + + class JSONReadError(Error): pass @@ -84,17 +99,12 @@ def call_api(self, method, type="GET", params=None): # noqa data = resp.content # pragma: no cover if not resp.ok: # pragma: no cover - msg = data - if not data: - msg = "(empty response)" - elif "errors" in data: - msg = ", ".join(data["errors"]) - raise Error("Error {0}: {1}".format(resp.status_code, msg)) + raise ResponseError(resp, data) try: resp.raise_for_status() except requests.HTTPError as e: # pragma: no cover - raise Error("Error {0}: {1}".format(resp.status_code, resp.reason), e) + raise ResponseError(resp, data, e) self.meta = None try: From fbaf181b65aa6ae39f7a4919f1396418834c6592 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 23 Jun 2020 10:58:43 -0400 Subject: [PATCH 2/2] ssh keys: retry on 404 DEBUG:packet.baseapi:POST https://api.packet.net/projects/86d5d066-b891-4608-af55-a481aa2c0094/ssh-keys {'key': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/w0nE8ezh9UX88pT3BLtn9Sx2dFZcDSR1+tv4oBjSj', 'label': 'grahamc-test'} {'X-Auth-Token': 'TOKEN', 'X-Consumer-Token': None, 'Content-Type': 'application/json'} DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.packet.net:443 DEBUG:urllib3.connectionpool:https://api.packet.net:443 "POST /projects/86d5d066-b891-4608-af55-a481aa2c0094/ssh-keys HTTP/1.1" 404 24 DEBUG:packet.baseapi:POST https://api.packet.net/projects/86d5d066-b891-4608-af55-a481aa2c0094/ssh-keys {'key': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/w0nE8ezh9UX88pT3BLtn9Sx2dFZcDSR1+tv4oBjSj', 'label': 'grahamc-test'} {'X-Auth-Token': 'TOKEN', 'X-Consumer-Token': None, 'Content-Type': 'application/json'} DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.packet.net:443 DEBUG:urllib3.connectionpool:https://api.packet.net:443 "POST /projects/86d5d066-b891-4608-af55-a481aa2c0094/ssh-keys HTTP/1.1" 422 33 DEBUG:packet.baseapi:GET https://api.packet.net/ssh-keys {} {'X-Auth-Token': 'TOKEN', 'X-Consumer-Token': None, 'Content-Type': 'application/json'} DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.packet.net:443 DEBUG:urllib3.connectionpool:https://api.packet.net:443 "GET /ssh-keys HTTP/1.1" 200 None SSHKey: 3088ab9e-af4d-4f5a-b301-94d90384b2f9 --- packet/Manager.py | 46 +++++++++++++++++++++++++++++++++++++++++----- packet/__init__.py | 1 + 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/packet/Manager.py b/packet/Manager.py index 92999bb..bf36b5a 100644 --- a/packet/Manager.py +++ b/packet/Manager.py @@ -3,6 +3,7 @@ from packet.Vlan import Vlan from .baseapi import BaseAPI from .baseapi import Error as PacketError +from .baseapi import ResponseError from .Batch import Batch from .Plan import Plan from .Device import Device @@ -197,11 +198,46 @@ def create_ssh_key(self, label, public_key): return SSHKey(data, self) def create_project_ssh_key(self, project_id, label, public_key): - params = {"key": public_key, "label": label} - data = self.call_api( - "projects/%s/ssh-keys" % project_id, type="POST", params=params - ) - return SSHKey(data, self) + """ + Successfully creating an SSH key with a Project API Token results + in a 404 from the API. If we get a 404, we try the request again. + + If the request actually failed with a 404, we will get another 404 + which we raise. + + If the request actually succeeded, we will get a 422. In this case, + we will try to list all the keys and find the SSHKey we just + received. + + Customer Report Reference: TUVD-0107-UIKB + """ + + def issue_req(): + try: + params = {"key": public_key, "label": label} + data = self.call_api( + "projects/%s/ssh-keys" % project_id, type="POST", params=params + ) + return SSHKey(data, self) + except ResponseError as e: + if e.response.status_code == 422: + # Try to pluck the SSH key from the listing API + keys = [ + key + for key in self.list_ssh_keys() + if key.key.strip() == public_key.strip() + ] + if len(keys) == 1: + return keys.pop() + raise + + try: + return issue_req() + except ResponseError as e: + if e.response.status_code == 404: + return issue_req() + else: + raise def list_volumes(self, project_id, params={}): params["include"] = "facility,attachments.device" diff --git a/packet/__init__.py b/packet/__init__.py index 86864fc..1fba8c6 100644 --- a/packet/__init__.py +++ b/packet/__init__.py @@ -26,3 +26,4 @@ from .Organization import Organization # noqa from .Provider import Provider # noqa from .baseapi import Error # noqa +from .baseapi import ResponseError # noqa