510 lines
16 KiB
Python
510 lines
16 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.
|
|
|
|
__all__ = [
|
|
'GoDaddyDNSDriver'
|
|
]
|
|
|
|
try:
|
|
import simplejson as json
|
|
except Exception:
|
|
import json
|
|
|
|
from libcloud.common.base import ConnectionKey, JsonResponse
|
|
from libcloud.common.types import LibcloudError
|
|
from libcloud.utils.py3 import httplib
|
|
from libcloud.dns.types import Provider, RecordType, RecordDoesNotExistError
|
|
from libcloud.dns.base import DNSDriver, Zone, Record
|
|
|
|
API_HOST = 'api.godaddy.com'
|
|
VALID_RECORD_EXTRA_PARAMS = ['prio', 'ttl']
|
|
|
|
|
|
class GoDaddyDNSException(LibcloudError):
|
|
def __init__(self, code, message):
|
|
self.code = code
|
|
self.message = message
|
|
self.args = (code, message)
|
|
|
|
def __str__(self):
|
|
return self.__repr__()
|
|
|
|
def __repr__(self):
|
|
return ('<GoDaddyDNSException in %s: %s>' % (self.code, self.message))
|
|
|
|
|
|
class GoDaddyDNSResponse(JsonResponse):
|
|
valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED,
|
|
httplib.NO_CONTENT]
|
|
|
|
def parse_body(self):
|
|
if not self.body:
|
|
return None
|
|
# json.loads doesn't like the regex expressions used in godaddy schema
|
|
self.body = self.body.replace('\\.', '\\\\.')
|
|
|
|
data = json.loads(self.body)
|
|
return data
|
|
|
|
def parse_error(self):
|
|
data = self.parse_body()
|
|
raise GoDaddyDNSException(code=data['code'], message=data['message'])
|
|
|
|
def success(self):
|
|
return self.status in self.valid_response_codes
|
|
|
|
|
|
class GoDaddyDNSConnection(ConnectionKey):
|
|
responseCls = GoDaddyDNSResponse
|
|
host = API_HOST
|
|
|
|
allow_insecure = False
|
|
|
|
def __init__(self, key, secret, secure=True, shopper_id=None, host=None,
|
|
port=None, url=None, timeout=None,
|
|
proxy_url=None, backoff=None, retry_delay=None):
|
|
super(GoDaddyDNSConnection, self).__init__(
|
|
key,
|
|
secure=secure, host=host,
|
|
port=port, url=url,
|
|
timeout=timeout,
|
|
proxy_url=proxy_url,
|
|
backoff=backoff,
|
|
retry_delay=retry_delay)
|
|
self.key = key
|
|
self.secret = secret
|
|
self.shopper_id = shopper_id
|
|
|
|
def add_default_headers(self, headers):
|
|
if self.shopper_id is not None:
|
|
headers['X-Shopper-Id'] = self.shopper_id
|
|
headers['Content-type'] = 'application/json'
|
|
headers['Authorization'] = "sso-key %s:%s" % \
|
|
(self.key, self.secret)
|
|
return headers
|
|
|
|
|
|
class GoDaddyDNSDriver(DNSDriver):
|
|
"""
|
|
A driver for GoDaddy DNS.
|
|
|
|
This is for customers of GoDaddy
|
|
who wish to purchase, update existing domains
|
|
and manage records for DNS zones owned by GoDaddy NS servers.
|
|
"""
|
|
type = Provider.GODADDY
|
|
name = 'GoDaddy DNS'
|
|
website = 'https://www.godaddy.com/'
|
|
connectionCls = GoDaddyDNSConnection
|
|
|
|
RECORD_TYPE_MAP = {
|
|
RecordType.A: 'A',
|
|
RecordType.AAAA: 'AAAA',
|
|
RecordType.CNAME: 'CNAME',
|
|
RecordType.MX: 'MX',
|
|
RecordType.NS: 'SPF',
|
|
RecordType.SRV: 'SRV',
|
|
RecordType.TXT: 'TXT',
|
|
}
|
|
|
|
def __init__(self, shopper_id, key, secret,
|
|
secure=True, host=None, port=None):
|
|
"""
|
|
Instantiate a new `GoDaddyDNSDriver`
|
|
|
|
:param shopper_id: Your customer ID or shopper ID with GoDaddy
|
|
:type shopper_id: ``str``
|
|
|
|
:param key: Your access key from developer.godaddy.com
|
|
:type key: ``str``
|
|
|
|
:param secret: Your access key secret
|
|
:type secret: ``str``
|
|
"""
|
|
self.shopper_id = shopper_id
|
|
super(GoDaddyDNSDriver, self).__init__(key=key, secret=secret,
|
|
secure=secure,
|
|
host=host, port=port,
|
|
shopper_id=str(shopper_id))
|
|
|
|
def list_zones(self):
|
|
"""
|
|
Return a list of zones (purchased domains)
|
|
|
|
:return: ``list`` of :class:`Zone`
|
|
"""
|
|
result = self.connection.request(
|
|
'/v1/domains/').object
|
|
zones = self._to_zones(result)
|
|
return zones
|
|
|
|
def list_records(self, zone):
|
|
"""
|
|
Return a list of records for the provided zone.
|
|
|
|
:param zone: Zone to list records for.
|
|
:type zone: :class:`Zone`
|
|
|
|
:return: ``list`` of :class:`Record`
|
|
"""
|
|
result = self.connection.request(
|
|
'/v1/domains/%s/records' % (zone.domain)).object
|
|
records = self._to_records(items=result, zone=zone)
|
|
return records
|
|
|
|
def create_record(self, name, zone, type, data, extra=None):
|
|
"""
|
|
Create a new record.
|
|
|
|
:param name: Record name without the domain name (e.g. www).
|
|
Note: If you want to create a record for a base domain
|
|
name, you should specify empty string ('') for this
|
|
argument.
|
|
:type name: ``str``
|
|
|
|
:param zone: Zone where the requested record is created.
|
|
:type zone: :class:`Zone`
|
|
|
|
:param type: DNS record type (A, AAAA, ...).
|
|
:type type: :class:`RecordType`
|
|
|
|
:param data: Data for the record (depends on the record type).
|
|
:type data: ``str``
|
|
|
|
:param extra: Extra attributes (driver specific). (optional)
|
|
:type extra: ``dict``
|
|
|
|
:rtype: :class:`Record`
|
|
"""
|
|
new_record = self._format_record(name, type, data, extra)
|
|
self.connection.request(
|
|
'/v1/domains/%s/records' % (zone.domain), method='PATCH',
|
|
data=json.dumps([new_record]))
|
|
id = self._get_id_of_record(name, type)
|
|
return Record(
|
|
id=id, name=name,
|
|
type=type, data=data,
|
|
zone=zone, driver=self,
|
|
ttl=new_record['ttl'],
|
|
extra=extra)
|
|
|
|
def update_record(self, record, name, type, data, extra=None):
|
|
"""
|
|
Update an existing record.
|
|
|
|
:param record: Record to update.
|
|
:type record: :class:`Record`
|
|
|
|
:param name: Record name without the domain name (e.g. www).
|
|
Note: If you want to create a record for a base domain
|
|
name, you should specify empty string ('') for this
|
|
argument.
|
|
:type name: ``str``
|
|
|
|
:param type: DNS record type (A, AAAA, ...).
|
|
:type type: :class:`RecordType`
|
|
|
|
:param data: Data for the record (depends on the record type).
|
|
:type data: ``str``
|
|
|
|
:param extra: (optional) Extra attributes (driver specific).
|
|
:type extra: ``dict``
|
|
|
|
:rtype: :class:`Record`
|
|
"""
|
|
new_record = self._format_record(name, type, data, extra)
|
|
self.connection.request(
|
|
'/v1/domains/%s/records/%s/%s' % (record.zone.domain,
|
|
record.type,
|
|
record.name),
|
|
method='PUT',
|
|
data=json.dumps([new_record]))
|
|
id = self._get_id_of_record(name, type)
|
|
return Record(
|
|
id=id, name=name,
|
|
type=type, data=data,
|
|
zone=record.zone, driver=self,
|
|
ttl=new_record['ttl'],
|
|
extra=extra)
|
|
|
|
def get_record(self, zone_id, record_id):
|
|
"""
|
|
Return a Record instance.
|
|
|
|
:param zone_id: ID of the required zone
|
|
:type zone_id: ``str``
|
|
|
|
:param record_id: ID of the required record
|
|
:type record_id: ``str``
|
|
|
|
:rtype: :class:`Record`
|
|
"""
|
|
parts = record_id.split(':')
|
|
result = self.connection.request(
|
|
'/v1/domains/%s/records/%s/%s' % (
|
|
zone_id,
|
|
parts[1],
|
|
parts[0])).object
|
|
if len(result) == 0:
|
|
raise RecordDoesNotExistError(record_id,
|
|
driver=self,
|
|
record_id=record_id)
|
|
return self._to_record(result[0],
|
|
self.get_zone(zone_id))
|
|
|
|
def get_zone(self, zone_id):
|
|
"""
|
|
Get a zone (by domain)
|
|
|
|
:param zone_id: The domain, not the ID
|
|
:type zone_id: ``str``
|
|
|
|
:rtype: :class:`Zone`
|
|
"""
|
|
result = self.connection.request(
|
|
'/v1/domains/%s/' % zone_id).object
|
|
zone = self._to_zone(result)
|
|
return zone
|
|
|
|
def delete_zone(self, zone):
|
|
"""
|
|
Delete a zone.
|
|
|
|
Note: This will CANCEL a purchased domain
|
|
|
|
:param zone: Zone to delete.
|
|
:type zone: :class:`Zone`
|
|
|
|
:rtype: ``bool``
|
|
"""
|
|
self.connection.request(
|
|
'/v1/domains/%s' % (zone.domain),
|
|
method='DELETE')
|
|
# no error means ok
|
|
return True
|
|
|
|
def ex_check_availability(self, domain, for_transfer=False):
|
|
"""
|
|
Check the availability of the domain
|
|
|
|
:param domain: the domain name e.g. wazzlewobbleflooble.com
|
|
:type domain: ``str``
|
|
|
|
:param for_transfer: Check if domain is available for transfer
|
|
:type for_transfer: ``bool``
|
|
|
|
:rtype: `list` of :class:`GoDaddyAvailability`
|
|
"""
|
|
result = self.connection.request(
|
|
'/v1/domains/available',
|
|
method='GET',
|
|
params={
|
|
'domain': domain,
|
|
'forTransfer': str(for_transfer)
|
|
}
|
|
).object
|
|
return GoDaddyAvailability(
|
|
domain=result['domain'],
|
|
available=result['available'],
|
|
price=result['price'],
|
|
currency=result['currency'],
|
|
period=result['period']
|
|
)
|
|
|
|
def ex_list_tlds(self):
|
|
"""
|
|
List available TLDs for sale
|
|
|
|
:rtype: ``list`` of :class:`GoDaddyTLD`
|
|
"""
|
|
result = self.connection.request(
|
|
'/v1/domains/tlds',
|
|
method='GET'
|
|
).object
|
|
return self._to_tlds(result)
|
|
|
|
def ex_get_purchase_schema(self, tld):
|
|
"""
|
|
Get the schema that needs completing to purchase a new domain
|
|
Use this in conjunction with ex_purchase_domain
|
|
|
|
:param tld: The top level domain e.g com, eu, uk
|
|
:type tld: ``str``
|
|
|
|
:rtype: `dict` the JSON Schema
|
|
"""
|
|
result = self.connection.request(
|
|
'/v1/domains/purchase/schema/%s' % tld,
|
|
method='GET'
|
|
).object
|
|
return result
|
|
|
|
def ex_get_agreements(self, tld, privacy=True):
|
|
"""
|
|
Get the legal agreements for a tld
|
|
Use this in conjunction with ex_purchase_domain
|
|
|
|
:param tld: The top level domain e.g com, eu, uk
|
|
:type tld: ``str``
|
|
|
|
:rtype: `dict` the JSON Schema
|
|
"""
|
|
result = self.connection.request(
|
|
'/v1/domains/agreements',
|
|
params={
|
|
'tlds': tld,
|
|
'privacy': str(privacy)
|
|
},
|
|
method='GET'
|
|
).object
|
|
agreements = []
|
|
for item in result:
|
|
agreements.append(
|
|
GoDaddyLegalAgreement(
|
|
agreement_key=item['agreementKey'],
|
|
title=item['title'],
|
|
url=item['url'],
|
|
content=item['content']))
|
|
return agreements
|
|
|
|
def ex_purchase_domain(self, purchase_request):
|
|
"""
|
|
Purchase a domain with GoDaddy
|
|
|
|
:param purchase_request: The completed document
|
|
from ex_get_purchase_schema
|
|
:type purchase_request: ``dict``
|
|
|
|
:rtype: :class:`GoDaddyDomainPurchaseResponse` Your order
|
|
"""
|
|
result = self.connection.request(
|
|
'/v1/domains/purchase',
|
|
data=purchase_request,
|
|
method='POST'
|
|
).object
|
|
return GoDaddyDomainPurchaseResponse(
|
|
order_id=result['orderId'],
|
|
item_count=result['itemCount'],
|
|
total=result['total'],
|
|
currency=result['currency']
|
|
)
|
|
|
|
def _format_record(self, name, type, data, extra):
|
|
if extra is None:
|
|
extra = {}
|
|
new_record = {}
|
|
if type == RecordType.SRV:
|
|
new_record = {
|
|
'type': type,
|
|
'name': name,
|
|
'data': data,
|
|
'priority': 1,
|
|
'ttl': extra.get('ttl', 5),
|
|
'service': extra.get('service', ''),
|
|
'protocol': extra.get('protocol', ''),
|
|
'port': extra.get('port', ''),
|
|
'weight': extra.get('weight', '1')
|
|
}
|
|
else:
|
|
new_record = {
|
|
'type': type,
|
|
'name': name,
|
|
'data': data,
|
|
'ttl': extra.get('ttl', 5)
|
|
}
|
|
if type == RecordType.MX:
|
|
new_record['priority'] = 1
|
|
return new_record
|
|
|
|
def _to_zones(self, items):
|
|
zones = []
|
|
for item in items:
|
|
zones.append(self._to_zone(item))
|
|
return zones
|
|
|
|
def _to_zone(self, item):
|
|
extra = {"expires": item['expires']}
|
|
zone = Zone(id=item['domainId'], domain=item['domain'],
|
|
type='master', ttl=None,
|
|
driver=self, extra=extra)
|
|
return zone
|
|
|
|
def _to_records(self, items, zone=None):
|
|
records = []
|
|
|
|
for item in items:
|
|
records.append(self._to_record(item=item, zone=zone))
|
|
return records
|
|
|
|
def _to_record(self, item, zone=None):
|
|
ttl = item['ttl']
|
|
type = self._string_to_record_type(item['type'])
|
|
name = item['name']
|
|
id = self._get_id_of_record(name, type)
|
|
record = Record(id=id, name=name,
|
|
type=type, data=item['data'],
|
|
zone=zone, driver=self,
|
|
ttl=ttl)
|
|
return record
|
|
|
|
def _to_tlds(self, items):
|
|
tlds = []
|
|
for item in items:
|
|
tlds.append(self._to_tld(item))
|
|
return tlds
|
|
|
|
def _to_tld(self, item):
|
|
return GoDaddyTLD(
|
|
name=item['name'],
|
|
tld_type=item['type']
|
|
)
|
|
|
|
def _get_id_of_record(self, name, type):
|
|
return '%s:%s' % (name, type)
|
|
|
|
def _ex_connection_class_kwargs(self):
|
|
return {'shopper_id': self.shopper_id}
|
|
|
|
|
|
class GoDaddyAvailability(object):
|
|
def __init__(self, domain, available, price, currency, period):
|
|
self.domain = domain
|
|
self.available = bool(available)
|
|
# currency comes in micro-units, convert to dollars.
|
|
self.price = float(price) / 1000000
|
|
self.currency = currency
|
|
self.period = int(period)
|
|
|
|
|
|
class GoDaddyTLD(object):
|
|
def __init__(self, name, tld_type):
|
|
self.name = name
|
|
self.type = tld_type
|
|
|
|
|
|
class GoDaddyDomainPurchaseResponse(object):
|
|
def __init__(self, order_id, item_count, total, currency):
|
|
self.order_id = order_id
|
|
self.item_count = item_count
|
|
self.total = total
|
|
self.current = currency
|
|
|
|
|
|
class GoDaddyLegalAgreement(object):
|
|
def __init__(self, agreement_key, title, url, content):
|
|
self.agreement_key = agreement_key
|
|
self.title = title
|
|
self.url = url
|
|
self.content = content
|