537 lines
18 KiB
Python
537 lines
18 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.
|
|
"""
|
|
World Wide DNS Driver
|
|
"""
|
|
|
|
__all__ = [
|
|
'WorldWideDNSDriver'
|
|
]
|
|
|
|
import re
|
|
|
|
from libcloud.common.types import LibcloudError
|
|
from libcloud.common.worldwidedns import WorldWideDNSConnection
|
|
from libcloud.dns.types import Provider, RecordType
|
|
from libcloud.dns.types import ZoneDoesNotExistError
|
|
from libcloud.dns.types import RecordError
|
|
from libcloud.dns.types import RecordDoesNotExistError
|
|
from libcloud.dns.base import DNSDriver, Zone, Record
|
|
|
|
|
|
MAX_RECORD_ENTRIES = 40 # Maximum record entries for zone
|
|
|
|
|
|
class WorldWideDNSError(LibcloudError):
|
|
|
|
def __repr__(self):
|
|
return ("<WorldWideDNSError in " +
|
|
repr(self.driver) +
|
|
" " +
|
|
repr(self.value) + ">")
|
|
|
|
|
|
class WorldWideDNSDriver(DNSDriver):
|
|
type = Provider.WORLDWIDEDNS
|
|
name = 'World Wide DNS'
|
|
website = 'https://www.worldwidedns.net/'
|
|
connectionCls = WorldWideDNSConnection
|
|
|
|
RECORD_TYPE_MAP = {
|
|
RecordType.MX: 'MX',
|
|
RecordType.CNAME: 'CNAME',
|
|
RecordType.A: 'A',
|
|
RecordType.NS: 'NS',
|
|
RecordType.SRV: 'SRV',
|
|
RecordType.TXT: 'TXT',
|
|
}
|
|
|
|
def __init__(self, key, secret=None, reseller_id=None, secure=True,
|
|
host=None, port=None, **kwargs):
|
|
"""
|
|
:param key: API key or username to used (required)
|
|
:type key: ``str``
|
|
|
|
:param secret: Secret password to be used (required)
|
|
:type secret: ``str``
|
|
|
|
:param reseller_id: Reseller ID for reseller accounts
|
|
:type reseller_id: ``str``
|
|
|
|
:param secure: Whether to use HTTPS or HTTP. Note: Some providers
|
|
only support HTTPS, and it is on by default.
|
|
:type secure: ``bool``
|
|
|
|
:param host: Override hostname used for connections.
|
|
:type host: ``str``
|
|
|
|
:param port: Override port used for connections.
|
|
:type port: ``int``
|
|
|
|
:return: ``None``
|
|
"""
|
|
super(WorldWideDNSDriver, self).__init__(key=key, secret=secret,
|
|
secure=secure, host=host,
|
|
port=port, **kwargs)
|
|
self.reseller_id = reseller_id
|
|
|
|
def list_zones(self):
|
|
"""
|
|
Return a list of zones.
|
|
|
|
:return: ``list`` of :class:`Zone`
|
|
|
|
For more info, please see:
|
|
https://www.worldwidedns.net/dns_api_protocol_list.asp
|
|
or
|
|
https://www.worldwidedns.net/dns_api_protocol_list_reseller.asp
|
|
"""
|
|
action = '/api_dns_list.asp'
|
|
if self.reseller_id is not None:
|
|
action = '/api_dns_list_reseller.asp'
|
|
zones = self.connection.request(action)
|
|
if len(zones.body) == 0:
|
|
return []
|
|
else:
|
|
return self._to_zones(zones.body)
|
|
|
|
def iterate_records(self, zone):
|
|
"""
|
|
Return a generator to iterate over records for the provided zone.
|
|
|
|
:param zone: Zone to list records for.
|
|
:type zone: :class:`Zone`
|
|
|
|
:rtype: ``generator`` of :class:`Record`
|
|
"""
|
|
records = self._to_records(zone)
|
|
for record in records:
|
|
yield record
|
|
|
|
def get_zone(self, zone_id):
|
|
"""
|
|
Return a Zone instance.
|
|
|
|
:param zone_id: ID of the required zone
|
|
:type zone_id: ``str``
|
|
|
|
:rtype: :class:`Zone`
|
|
"""
|
|
zones = self.list_zones()
|
|
zone = [zone for zone in zones if zone.id == zone_id]
|
|
if len(zone) == 0:
|
|
raise ZoneDoesNotExistError(driver=self,
|
|
value="The zone doesn't exists",
|
|
zone_id=zone_id)
|
|
return zone[0]
|
|
|
|
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 number of the required record.
|
|
:type record_id: ``str``
|
|
|
|
:rtype: :class:`Record`
|
|
"""
|
|
zone = self.get_zone(zone_id)
|
|
try:
|
|
if int(record_id) not in range(1, MAX_RECORD_ENTRIES + 1):
|
|
raise RecordDoesNotExistError(value="Record doesn't exists",
|
|
driver=zone.driver,
|
|
record_id=record_id)
|
|
except ValueError:
|
|
raise WorldWideDNSError(
|
|
value="Record id should be a string number", driver=self)
|
|
subdomain = zone.extra.get('S%s' % record_id)
|
|
type = zone.extra.get('T%s' % record_id)
|
|
data = zone.extra.get('D%s' % record_id)
|
|
record = self._to_record(record_id, subdomain, type, data, zone)
|
|
return record
|
|
|
|
def update_zone(self, zone, domain, type='master', ttl=None, extra=None,
|
|
ex_raw=False):
|
|
"""
|
|
Update an existing zone.
|
|
|
|
:param zone: Zone to update.
|
|
:type zone: :class:`Zone`
|
|
|
|
:param domain: Zone domain name (e.g. example.com)
|
|
:type domain: ``str``
|
|
|
|
:param type: Zone type (master / slave).
|
|
:type type: ``str``
|
|
|
|
:param ttl: TTL for new records. (optional)
|
|
:type ttl: ``int``
|
|
|
|
:param extra: Extra attributes (driver specific) (optional). Values not
|
|
specified such as *SECURE*, *IP*, *FOLDER*, *HOSTMASTER*,
|
|
*REFRESH*, *RETRY* and *EXPIRE* will be kept as already
|
|
is. The same will be for *S(1 to 40)*, *T(1 to 40)* and
|
|
*D(1 to 40)* if not in raw mode and for *ZONENS* and
|
|
*ZONEDATA* if it is.
|
|
:type extra: ``dict``
|
|
|
|
:param ex_raw: Mode we use to do the update using zone file or not.
|
|
:type ex_raw: ``bool``
|
|
|
|
:rtype: :class:`Zone`
|
|
|
|
For more info, please see
|
|
https://www.worldwidedns.net/dns_api_protocol_list_domain.asp
|
|
or
|
|
https://www.worldwidedns.net/dns_api_protocol_list_domain_raw.asp
|
|
or
|
|
https://www.worldwidedns.net/dns_api_protocol_list_domain_reseller.asp
|
|
or
|
|
https://www.worldwidedns.net/dns_api_protocol_list_domain_raw_reseller.asp
|
|
"""
|
|
if extra is not None:
|
|
not_specified = [key for key in zone.extra.keys() if key not in
|
|
extra.keys()]
|
|
else:
|
|
not_specified = zone.extra.keys()
|
|
|
|
if ttl is None:
|
|
ttl = zone.ttl
|
|
|
|
params = {'DOMAIN': domain,
|
|
'TTL': ttl}
|
|
|
|
for key in not_specified:
|
|
params[key] = zone.extra[key]
|
|
if extra is not None:
|
|
params.update(extra)
|
|
if ex_raw:
|
|
action = '/api_dns_modify_raw.asp'
|
|
if self.reseller_id is not None:
|
|
action = '/api_dns_modify_raw_reseller.asp'
|
|
method = 'POST'
|
|
else:
|
|
action = '/api_dns_modify.asp'
|
|
if self.reseller_id is not None:
|
|
action = '/api_dns_modify_reseller.asp'
|
|
method = 'GET'
|
|
response = self.connection.request(action, params=params, # noqa
|
|
method=method)
|
|
zone = self.get_zone(zone.id)
|
|
return zone
|
|
|
|
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 (MX, CNAME, A, NS, SRV, TXT).
|
|
:type type: :class:`RecordType`
|
|
|
|
:param data: Data for the record (depends on the record type).
|
|
:type data: ``str``
|
|
|
|
:param extra: Contains 'entry' Entry position (1 thru 40)
|
|
:type extra: ``dict``
|
|
|
|
:rtype: :class:`Record`
|
|
"""
|
|
if (extra is None) or ('entry' not in extra):
|
|
raise WorldWideDNSError(value="You must enter 'entry' parameter",
|
|
driver=self)
|
|
record_id = extra.get('entry')
|
|
if name == '':
|
|
name = '@'
|
|
if type not in self.RECORD_TYPE_MAP:
|
|
raise RecordError(value="Record type is not allowed",
|
|
driver=record.zone.driver,
|
|
record_id=name)
|
|
zone = record.zone
|
|
extra = {'S%s' % record_id: name,
|
|
'T%s' % record_id: type,
|
|
'D%s' % record_id: data}
|
|
zone = self.update_zone(zone, zone.domain, extra=extra)
|
|
record = self.get_record(zone.id, record_id)
|
|
return record
|
|
|
|
def create_zone(self, domain, type='master', ttl=None, extra=None):
|
|
"""
|
|
Create a new zone.
|
|
|
|
:param domain: Zone domain name (e.g. example.com)
|
|
:type domain: ``str``
|
|
|
|
:param type: Zone type (master / slave).
|
|
:type type: ``str``
|
|
|
|
:param ttl: TTL for new records. (optional)
|
|
:type ttl: ``int``
|
|
|
|
:param extra: Extra attributes (driver specific). (optional). Possible
|
|
parameter in here should be *DYN* which values should be
|
|
1 for standart and 2 for dynamic. Default is 1.
|
|
:type extra: ``dict``
|
|
|
|
:rtype: :class:`Zone`
|
|
|
|
For more info, please see
|
|
https://www.worldwidedns.net/dns_api_protocol_new_domain.asp
|
|
or
|
|
https://www.worldwidedns.net/dns_api_protocol_new_domain_reseller.asp
|
|
"""
|
|
if type == 'master':
|
|
_type = 0
|
|
elif type == 'slave':
|
|
_type = 1
|
|
if extra:
|
|
dyn = extra.get('DYN') or 1
|
|
else:
|
|
dyn = 1
|
|
params = {'DOMAIN': domain,
|
|
'TYPE': _type}
|
|
action = '/api_dns_new_domain.asp'
|
|
if self.reseller_id is not None:
|
|
params['DYN'] = dyn
|
|
action = '/api_dns_new_domain_reseller.asp'
|
|
self.connection.request(action, params=params)
|
|
zone = self.get_zone(domain)
|
|
if ttl is not None:
|
|
zone = self.update_zone(zone, zone.domain, ttl=ttl)
|
|
return zone
|
|
|
|
def create_record(self, name, zone, type, data, extra=None):
|
|
"""
|
|
Create a new record.
|
|
|
|
We can create 40 record per domain. If all slots are full, we can
|
|
replace one of them by choosing a specific entry in ``extra`` argument.
|
|
|
|
: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 (MX, CNAME, A, NS, SRV, TXT).
|
|
:type type: :class:`RecordType`
|
|
|
|
:param data: Data for the record (depends on the record type).
|
|
:type data: ``str``
|
|
|
|
:param extra: Contains 'entry' Entry position (1 thru 40)
|
|
:type extra: ``dict``
|
|
|
|
:rtype: :class:`Record`
|
|
"""
|
|
if (extra is None) or ('entry' not in extra):
|
|
# If no entry is specified, we look for an available one. If all
|
|
# are full, raise error.
|
|
record_id = self._get_available_record_entry(zone)
|
|
if not record_id:
|
|
raise WorldWideDNSError(value="All record entries are full",
|
|
driver=zone.driver)
|
|
else:
|
|
record_id = extra.get('entry')
|
|
if name == '':
|
|
name = '@'
|
|
if type not in self.RECORD_TYPE_MAP:
|
|
raise RecordError(value="Record type is not allowed",
|
|
driver=zone.driver,
|
|
record_id=record_id)
|
|
extra = {'S%s' % record_id: name,
|
|
'T%s' % record_id: type,
|
|
'D%s' % record_id: data}
|
|
zone = self.update_zone(zone, zone.domain, extra=extra)
|
|
record = self.get_record(zone.id, record_id)
|
|
return record
|
|
|
|
def delete_zone(self, zone):
|
|
"""
|
|
Delete a zone.
|
|
|
|
Note: This will delete all the records belonging to this zone.
|
|
|
|
:param zone: Zone to delete.
|
|
:type zone: :class:`Zone`
|
|
|
|
:rtype: ``bool``
|
|
|
|
For more information, please see
|
|
https://www.worldwidedns.net/dns_api_protocol_delete_domain.asp
|
|
or
|
|
https://www.worldwidedns.net/dns_api_protocol_delete_domain_reseller.asp
|
|
"""
|
|
params = {'DOMAIN': zone.domain}
|
|
action = '/api_dns_delete_domain.asp'
|
|
if self.reseller_id is not None:
|
|
action = '/api_dns_delete_domain_reseller.asp'
|
|
response = self.connection.request(action, params=params)
|
|
return response.success()
|
|
|
|
def delete_record(self, record):
|
|
"""
|
|
Delete a record.
|
|
|
|
:param record: Record to delete.
|
|
:type record: :class:`Record`
|
|
|
|
:rtype: ``bool``
|
|
"""
|
|
zone = record.zone
|
|
for index in range(MAX_RECORD_ENTRIES):
|
|
if record.name == zone.extra['S%s' % (index + 1)]:
|
|
entry = index + 1
|
|
break
|
|
extra = {'S%s' % entry: '',
|
|
'T%s' % entry: 'NONE',
|
|
'D%s' % entry: ''}
|
|
self.update_zone(zone, zone.domain, extra=extra)
|
|
return True
|
|
|
|
def ex_view_zone(self, domain, name_server):
|
|
"""
|
|
View zone file from a name server
|
|
|
|
:param domain: Domain name.
|
|
:type domain: ``str``
|
|
|
|
:param name_server: Name server to check. (1, 2 or 3)
|
|
:type name_server: ``int``
|
|
|
|
:rtype: ``str``
|
|
|
|
For more info, please see:
|
|
https://www.worldwidedns.net/dns_api_protocol_viewzone.asp
|
|
or
|
|
https://www.worldwidedns.net/dns_api_protocol_viewzone_reseller.asp
|
|
"""
|
|
params = {'DOMAIN': domain,
|
|
'NS': name_server}
|
|
action = '/api_dns_viewzone.asp'
|
|
if self.reseller_id is not None:
|
|
action = '/api_dns_viewzone_reseller.asp'
|
|
response = self.connection.request(action, params=params)
|
|
return response.object
|
|
|
|
def ex_transfer_domain(self, domain, user_id):
|
|
"""
|
|
This command will allow you, if you are a reseller, to change the
|
|
userid on a domain name to another userid in your account ONLY if that
|
|
new userid is already created.
|
|
|
|
:param domain: Domain name.
|
|
:type domain: ``str``
|
|
|
|
:param user_id: The new userid to connect to the domain name.
|
|
:type user_id: ``str``
|
|
|
|
:rtype: ``bool``
|
|
|
|
For more info, please see:
|
|
https://www.worldwidedns.net/dns_api_protocol_transfer.asp
|
|
"""
|
|
if self.reseller_id is None:
|
|
raise WorldWideDNSError("This is not a reseller account",
|
|
driver=self)
|
|
params = {'DOMAIN': domain,
|
|
'NEW_ID': user_id}
|
|
response = self.connection.request('/api_dns_transfer.asp',
|
|
params=params)
|
|
return response.success()
|
|
|
|
def _get_available_record_entry(self, zone):
|
|
"""Return an available entry to store a record."""
|
|
entries = zone.extra
|
|
for entry in range(1, MAX_RECORD_ENTRIES + 1):
|
|
subdomain = entries.get('S%s' % entry)
|
|
_type = entries.get('T%s' % entry)
|
|
data = entries.get('D%s' % entry)
|
|
if not any([subdomain, _type, data]):
|
|
return entry
|
|
return None
|
|
|
|
def _to_zones(self, data):
|
|
domain_list = re.split('\r?\n', data)
|
|
zones = []
|
|
for line in domain_list:
|
|
zone = self._to_zone(line)
|
|
zones.append(zone)
|
|
|
|
return zones
|
|
|
|
def _to_zone(self, line):
|
|
data = line.split('\x1f')
|
|
name = data[0]
|
|
if data[1] == "P":
|
|
type = "master"
|
|
domain_data = self._get_domain_data(name)
|
|
resp_lines = re.split('\r?\n', domain_data.body)
|
|
soa_block = resp_lines[:6]
|
|
zone_data = resp_lines[6:]
|
|
extra = {'HOSTMASTER': soa_block[0], 'REFRESH': soa_block[1],
|
|
'RETRY': soa_block[2], 'EXPIRE': soa_block[3],
|
|
'SECURE': soa_block[5]}
|
|
ttl = soa_block[4]
|
|
for line in range(MAX_RECORD_ENTRIES):
|
|
line_data = zone_data[line].split('\x1f')
|
|
extra['S%s' % (line + 1)] = line_data[0]
|
|
_type = line_data[1]
|
|
extra['T%s' % (line + 1)] = _type if _type != 'NONE' else ''
|
|
try:
|
|
extra['D%s' % (line + 1)] = line_data[2]
|
|
except IndexError:
|
|
extra['D%s' % (line + 1)] = ''
|
|
elif data[1] == 'S':
|
|
type = 'slave'
|
|
extra = {}
|
|
ttl = 0
|
|
return Zone(id=name, domain=name, type=type,
|
|
ttl=ttl, driver=self, extra=extra)
|
|
|
|
def _get_domain_data(self, name):
|
|
params = {'DOMAIN': name}
|
|
data = self.connection.request('/api_dns_list_domain.asp',
|
|
params=params)
|
|
return data
|
|
|
|
def _to_records(self, zone):
|
|
records = []
|
|
for record_id in range(1, MAX_RECORD_ENTRIES + 1):
|
|
subdomain = zone.extra['S%s' % (record_id)]
|
|
type = zone.extra['T%s' % (record_id)]
|
|
data = zone.extra['D%s' % (record_id)]
|
|
if subdomain and type and data:
|
|
record = self._to_record(
|
|
record_id, subdomain, type, data, zone)
|
|
records.append(record)
|
|
return records
|
|
|
|
def _to_record(self, _id, subdomain, type, data, zone):
|
|
return Record(id=_id, name=subdomain, type=type, data=data, zone=zone,
|
|
driver=zone.driver)
|