blob: 84099a87b222d217140b537c0f13d79c0d458ffd [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Gandi Live driver base classes
"""
import json
from libcloud.utils.py3 import httplib
from libcloud.common.base import JsonResponse, ConnectionKey
from libcloud.common.types import ProviderError
__all__ = [
"API_HOST",
"GandiLiveBaseError",
"JsonParseError",
"ResourceNotFoundError",
"InvalidRequestError",
"ResourceConflictError",
"GandiLiveResponse",
"GandiLiveConnection",
"BaseGandiLiveDriver",
]
API_HOST = "dns.api.gandi.net"
class GandiLiveBaseError(ProviderError):
"""
Exception class for Gandi Live driver
"""
pass
class JsonParseError(GandiLiveBaseError):
pass
# Example:
# {
# "code": 404,
# "message": "Unknown zone",
# "object": "LocalizedHTTPNotFound",
# "cause": "Not Found"
# }
class ResourceNotFoundError(GandiLiveBaseError):
pass
# Example:
# {
# "code": 400,
# "message": "zone or zone_uuid must be set",
# "object": "HTTPBadRequest",
# "cause": "No zone set.",
# "errors": [
# {
# "location": "body",
# "name": "zone_uuid",
# "description": "\"FAKEUUID\" is not a UUID"
# }
# ]
# }
class InvalidRequestError(GandiLiveBaseError):
pass
# Examples:
# {
# "code": 409,
# "message": "Zone Testing already exists",
# "object": "HTTPConflict",
# "cause": "Duplicate Entry"
# }
# {
# "code": 409,
# "message": "The domain example.org already exists",
# "object": "HTTPConflict",
# "cause": "Duplicate Entry"
# }
# {
# "code": 409,
# "message": "This zone is still used by 1 domains",
# "object": "HTTPConflict",
# "cause": "In use"
# }
class ResourceConflictError(GandiLiveBaseError):
pass
class GandiLiveResponse(JsonResponse):
"""
A Base Gandi Live Response class to derive from.
"""
def success(self):
"""
Determine if our request was successful.
For the Gandi Live response class, tag all responses as successful and
raise appropriate Exceptions from parse_body.
:return: C{True}
"""
return True
def parse_body(self):
"""
Parse the JSON response body, or raise exceptions as appropriate.
:return: JSON dictionary
:rtype: ``dict``
"""
json_error = False
try:
body = json.loads(self.body)
except Exception:
# If there is both a JSON parsing error and an unsuccessful http
# response (like a 404), we want to raise the http error and not
# the JSON one, so don't raise JsonParseError here.
body = self.body
json_error = True
# Service does not appear to return HTTP 202 Accepted for anything.
valid_http_codes = [
httplib.OK,
httplib.CREATED,
]
if self.status in valid_http_codes:
if json_error:
raise JsonParseError(body, self.status)
else:
return body
elif self.status == httplib.NO_CONTENT:
# Parse error for empty body is acceptable, but a non-empty body
# is not.
if len(body) > 0:
msg = '"No Content" response contained content'
raise GandiLiveBaseError(msg, self.status)
else:
return {}
elif self.status == httplib.NOT_FOUND:
message = self._get_error(body, json_error)
raise ResourceNotFoundError(message, self.status)
elif self.status == httplib.BAD_REQUEST:
message = self._get_error(body, json_error)
raise InvalidRequestError(message, self.status)
elif self.status == httplib.CONFLICT:
message = self._get_error(body, json_error)
raise ResourceConflictError(message, self.status)
else:
message = self._get_error(body, json_error)
raise GandiLiveBaseError(message, self.status)
# Errors are not described at all in Gandi's official documentation.
# It appears when an error arises, a JSON object is returned along with
# an HTTP 4xx class code. The object is structured as:
# {
# code: <code>,
# object: <object>,
# message: <message>,
# cause: <cause>,
# errors: [
# {
# location: <error-location>,
# name: <error-name>,
# description: <error-description>
# }
# ]
# }
# where
# <code> is a number equal to the HTTP response status code
# <object> is a string with some internal name for the status code
# <message> is a string detailing what the problem is
# <cause> is a string that comes from a set of succinct problem summaries
# errors is optional; if present:
# <error-location> is a string for which part of the request to look in
# <error-name> is a string naming the parameter
# <error-description> is a string detailing what the problem is
# Here we ignore object and combine message and cause along with an error
# if one or more exists.
def _get_error(self, body, json_error):
"""
Get the error code and message from a JSON response.
Incorporate the first error if there are multiple errors.
:param body: The body of the JSON response dictionary
:type body: ``dict``
:return: String containing error message
:rtype: ``str``
"""
if not json_error and "cause" in body:
message = "{}: {}".format(body["cause"], body["message"])
if "errors" in body:
err = body["errors"][0]
message = "{} ({} in {}: {})".format(
message,
err.get("location"),
err.get("name"),
err.get("description"),
)
else:
message = body
return message
class GandiLiveConnection(ConnectionKey):
"""
Connection class for the Gandi Live driver
"""
responseCls = GandiLiveResponse
host = API_HOST
def add_default_headers(self, headers):
"""
Returns default headers as a dictionary.
"""
headers["Content-Type"] = "application/json"
headers["X-Api-Key"] = self.key
return headers
def encode_data(self, data):
"""Encode data to JSON"""
return json.dumps(data)
class BaseGandiLiveDriver:
"""
Gandi Live base driver
"""
connectionCls = GandiLiveConnection
name = "GandiLive"