291 lines
10 KiB
Python
291 lines
10 KiB
Python
# 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.
|
|
|
|
from typing import List
|
|
from typing import Dict
|
|
|
|
import re
|
|
from xml.etree import ElementTree as ET # noqa
|
|
|
|
from libcloud.common.base import ConnectionUserAndKey
|
|
from libcloud.common.base import XmlResponse
|
|
|
|
|
|
# API HOST to connect
|
|
API_HOST = 'durabledns.com'
|
|
|
|
|
|
def _schema_builder(urn_nid, method, attributes):
|
|
"""
|
|
Return a xml schema used to do an API request.
|
|
|
|
:param urn_nid: API urn namespace id.
|
|
:type urn_nid: type: ``str``
|
|
|
|
:param method: API method.
|
|
:type method: type: ``str``
|
|
|
|
:param attributes: List of attributes to include.
|
|
:type attributes: ``list`` of ``str``
|
|
|
|
rtype: :class:`Element`
|
|
"""
|
|
soap = ET.Element(
|
|
'soap:Body',
|
|
{'xmlns:m': "https://durabledns.com/services/dns/%s" % method}
|
|
)
|
|
urn = ET.SubElement(soap, 'urn:%s:%s' % (urn_nid, method))
|
|
# Attributes specification
|
|
for attribute in attributes:
|
|
ET.SubElement(urn, 'urn:%s:%s' % (urn_nid, attribute))
|
|
return soap
|
|
|
|
|
|
SCHEMA_BUILDER_MAP = {
|
|
'list_zones': {
|
|
'urn_nid': 'listZoneswsdl',
|
|
'method': 'listZones',
|
|
'attributes': ['apiuser', 'apikey']
|
|
},
|
|
'list_records': {
|
|
'urn_nid': 'listRecordswsdl',
|
|
'method': 'listRecords',
|
|
'attributes': ['apiuser', 'apikey', 'zonename']
|
|
},
|
|
'get_zone': {
|
|
'urn_nid': 'getZonewsdl',
|
|
'method': 'getZone',
|
|
'attributes': ['apiuser', 'apikey', 'zonename']
|
|
},
|
|
'get_record': {
|
|
'urn_nid': 'getRecordwsdl',
|
|
'method': 'getRecord',
|
|
'attributes': ['apiuser', 'apikey', 'zonename', 'recordid']
|
|
},
|
|
'create_zone': {
|
|
'urn_nid': 'createZonewsdl',
|
|
'method': 'createZone',
|
|
'attributes': ['apiuser', 'apikey', 'zonename', 'ns', 'mbox',
|
|
'refresh', 'retry', 'expire', 'minimum', 'ttl',
|
|
'xfer', 'update_acl']
|
|
},
|
|
'create_record': {
|
|
'urn_nid': 'createRecordwsdl',
|
|
'method': 'createRecord',
|
|
'attributes': ['apiuser', 'apikey', 'zonename', 'name', 'type',
|
|
'data', 'aux', 'ttl', 'ddns_enabled']
|
|
},
|
|
'update_zone': {
|
|
'urn_nid': 'updateZonewsdl',
|
|
'method': 'updateZone',
|
|
'attributes': ['apiuser', 'apikey', 'zonename', 'ns', 'mbox',
|
|
'refresh', 'retry', 'expire', 'minimum', 'ttl',
|
|
'xfer', 'update_acl']
|
|
},
|
|
'update_record': {
|
|
'urn_nid': 'updateRecordwsdl',
|
|
'method': 'updateRecord',
|
|
'attributes': ['apiuser', 'apikey', 'zonename', 'id', 'name', 'aux',
|
|
'data', 'ttl', 'ddns_enabled']
|
|
},
|
|
'delete_zone': {
|
|
'urn_nid': 'deleteZonewsdl',
|
|
'method': 'deleteZone',
|
|
'attributes': ['apiuser', 'apikey', 'zonename']
|
|
},
|
|
'delete_record': {
|
|
'urn_nid': 'deleteRecordwsdl',
|
|
'method': 'deleteRecord',
|
|
'attributes': ['apiuser', 'apikey', 'zonename', 'id']
|
|
}
|
|
}
|
|
|
|
|
|
class DurableDNSException(Exception):
|
|
|
|
def __init__(self, code, message):
|
|
self.code = code
|
|
self.message = message
|
|
self.args = (code, message)
|
|
|
|
def __str__(self):
|
|
return "%s %s" % (self.code, self.message)
|
|
|
|
def __repr__(self):
|
|
return "DurableDNSException %s %s" % (self.code, self.message)
|
|
|
|
|
|
class DurableResponse(XmlResponse):
|
|
|
|
errors = [] # type: List[Dict]
|
|
objects = [] # type: List[Dict]
|
|
|
|
def __init__(self, response, connection):
|
|
super(DurableResponse, self).__init__(response=response,
|
|
connection=connection)
|
|
|
|
self.objects, self.errors = self.parse_body_and_error()
|
|
if self.errors:
|
|
raise self._make_excp(self.errors[0])
|
|
|
|
def parse_body_and_error(self):
|
|
"""
|
|
Used to parse body from httplib.HttpResponse object.
|
|
"""
|
|
objects = []
|
|
errors = []
|
|
error_dict = {}
|
|
extra = {}
|
|
zone_dict = {}
|
|
record_dict = {}
|
|
xml_obj = self.parse_body()
|
|
|
|
# pylint: disable=no-member
|
|
envelop_body = list(xml_obj)[0]
|
|
method_resp = list(envelop_body)[0]
|
|
# parse the xml_obj
|
|
# handle errors
|
|
if 'Fault' in method_resp.tag:
|
|
fault = [fault for fault in list(method_resp)
|
|
if fault.tag == 'faultstring'][0]
|
|
error_dict['ERRORMESSAGE'] = fault.text.strip()
|
|
error_dict['ERRORCODE'] = self.status
|
|
errors.append(error_dict)
|
|
|
|
# parsing response from listZonesResponse
|
|
if 'listZonesResponse' in method_resp.tag:
|
|
answer = list(method_resp)[0]
|
|
for element in answer:
|
|
zone_dict['id'] = list(element)[0].text
|
|
objects.append(zone_dict)
|
|
# reset the zone_dict
|
|
zone_dict = {}
|
|
# parse response from listRecordsResponse
|
|
if 'listRecordsResponse' in method_resp.tag:
|
|
answer = list(method_resp)[0]
|
|
for element in answer:
|
|
for child in list(element):
|
|
if child.tag == 'id':
|
|
record_dict['id'] = child.text.strip()
|
|
objects.append(record_dict)
|
|
# reset the record_dict for later usage
|
|
record_dict = {}
|
|
# parse response from getZoneResponse
|
|
if 'getZoneResponse' in method_resp.tag:
|
|
for child in list(method_resp):
|
|
if child.tag == 'origin':
|
|
zone_dict['id'] = child.text.strip()
|
|
zone_dict['domain'] = child.text.strip()
|
|
elif child.tag == 'ttl':
|
|
zone_dict['ttl'] = int(child.text.strip())
|
|
elif child.tag == 'retry':
|
|
extra['retry'] = int(child.text.strip())
|
|
elif child.tag == 'expire':
|
|
extra['expire'] = int(child.text.strip())
|
|
elif child.tag == 'minimum':
|
|
extra['minimum'] = int(child.text.strip())
|
|
else:
|
|
if child.text:
|
|
extra[child.tag] = child.text.strip()
|
|
else:
|
|
extra[child.tag] = ''
|
|
zone_dict['extra'] = extra
|
|
objects.append(zone_dict)
|
|
# parse response from getRecordResponse
|
|
if 'getRecordResponse' in method_resp.tag:
|
|
answer = list(method_resp)[0]
|
|
for child in list(method_resp):
|
|
if child.tag == 'id' and child.text:
|
|
record_dict['id'] = child.text.strip()
|
|
elif child.tag == 'name' and child.text:
|
|
record_dict['name'] = child.text.strip()
|
|
elif child.tag == 'type' and child.text:
|
|
record_dict['type'] = child.text.strip()
|
|
elif child.tag == 'data' and child.text:
|
|
record_dict['data'] = child.text.strip()
|
|
elif child.tag == 'aux' and child.text:
|
|
record_dict['aux'] = child.text.strip()
|
|
elif child.tag == 'ttl' and child.text:
|
|
record_dict['ttl'] = child.text.strip()
|
|
if not record_dict:
|
|
error_dict['ERRORMESSAGE'] = 'Record does not exist'
|
|
error_dict['ERRORCODE'] = 404
|
|
errors.append(error_dict)
|
|
objects.append(record_dict)
|
|
record_dict = {}
|
|
if 'createZoneResponse' in method_resp.tag:
|
|
answer = list(method_resp)[0]
|
|
if answer.tag == 'return' and answer.text:
|
|
record_dict['id'] = answer.text.strip()
|
|
objects.append(record_dict)
|
|
# catch Record does not exists error when deleting record
|
|
if 'deleteRecordResponse' in method_resp.tag:
|
|
answer = list(method_resp)[0]
|
|
if 'Record does not exists' in answer.text.strip():
|
|
errors.append({'ERRORMESSAGE': answer.text.strip(),
|
|
'ERRORCODE': self.status})
|
|
# parse response in createRecordResponse
|
|
if 'createRecordResponse' in method_resp.tag:
|
|
answer = list(method_resp)[0]
|
|
record_dict['id'] = answer.text.strip()
|
|
objects.append(record_dict)
|
|
record_dict = {}
|
|
|
|
return (objects, errors)
|
|
|
|
def parse_body(self):
|
|
# A problem arise in the api response because there are undeclared
|
|
# xml namespaces. In order to fix that at the moment, we use the
|
|
# _fix_response method to clean up since we won't always have lxml
|
|
# library.
|
|
self._fix_response()
|
|
body = super(DurableResponse, self).parse_body()
|
|
return body
|
|
|
|
def success(self):
|
|
"""
|
|
Used to determine if the request was successful.
|
|
"""
|
|
return len(self.errors) == 0
|
|
|
|
def _make_excp(self, error):
|
|
return DurableDNSException(error['ERRORCODE'], error['ERRORMESSAGE'])
|
|
|
|
def _fix_response(self):
|
|
items = re.findall('<ns1:.+ xmlns:ns1="">', self.body, flags=0)
|
|
for item in items:
|
|
parts = item.split(' ')
|
|
prefix = parts[0].replace('<', '').split(':')[1]
|
|
new_item = "<" + prefix + ">"
|
|
close_tag = "</" + parts[0].replace('<', '') + ">"
|
|
new_close_tag = "</" + prefix + ">"
|
|
self.body = self.body.replace(item, new_item)
|
|
self.body = self.body.replace(close_tag, new_close_tag)
|
|
|
|
|
|
class DurableConnection(ConnectionUserAndKey):
|
|
host = API_HOST
|
|
responseCls = DurableResponse
|
|
|
|
def add_default_params(self, params):
|
|
params['user_id'] = self.user_id
|
|
params['key'] = self.key
|
|
return params
|
|
|
|
def add_default_headers(self, headers):
|
|
headers['Content-Type'] = 'text/xml'
|
|
headers['Content-Encoding'] = 'gzip; charset=ISO-8859-1'
|
|
return headers
|