Repository: libcloud Updated Branches: refs/heads/trunk 91c69db05 -> e136292a9
[Libcloud-732] Add implementation for World Wide DNS provider Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/4eed2b2d Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/4eed2b2d Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/4eed2b2d Branch: refs/heads/trunk Commit: 4eed2b2d16b48b0ff0993eb152d5ebfba0398ace Parents: 1bf0f47 Author: Alejandro Pereira <[email protected]> Authored: Tue Aug 18 08:27:10 2015 -0300 Committer: Alejandro Pereira <[email protected]> Committed: Mon Sep 7 18:05:18 2015 -0300 ---------------------------------------------------------------------- CHANGES.rst | 4 + docs/dns/drivers/worldwidedns.rst | 22 + .../dns/worldwidedns/instantiate_driver.py | 10 + libcloud/common/worldwidedns.py | 195 +++++++ libcloud/dns/drivers/worldwidedns.py | 502 +++++++++++++++++++ libcloud/dns/providers.py | 2 + libcloud/dns/types.py | 1 + .../test/dns/fixtures/worldwidedns/api_dns_list | 1 + .../worldwidedns/api_dns_list_domain_asp | 46 ++ .../api_dns_list_domain_asp_CREATE_RECORD | 46 ++ .../api_dns_list_domain_asp_DELETE_RECORD | 46 ++ .../api_dns_list_domain_asp_UPDATE_RECORD | 46 ++ .../api_dns_list_domain_asp_UPDATE_ZONE | 46 ++ libcloud/test/dns/test_worldwidedns.py | 339 +++++++++++++ libcloud/test/secrets.py-dist | 1 + 15 files changed, 1307 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/CHANGES.rst ---------------------------------------------------------------------- diff --git a/CHANGES.rst b/CHANGES.rst index 3fc74a7..1ff36e2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -60,6 +60,10 @@ Loadbalancer DNS ~~~ +- Add new driver for WorldWideDNS service. + (GITHUB-566, LIBCLOUD-732) + [Alejandro Pereira] + - Add new driver for AuroraDNS service. (GITHUB-562, LIBCLOUD-735) [Wido den Hollander] http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/docs/dns/drivers/worldwidedns.rst ---------------------------------------------------------------------- diff --git a/docs/dns/drivers/worldwidedns.rst b/docs/dns/drivers/worldwidedns.rst new file mode 100644 index 0000000..c9a24a8 --- /dev/null +++ b/docs/dns/drivers/worldwidedns.rst @@ -0,0 +1,22 @@ +World Wide DNS Driver Documentation +===================================== + +`WorldWideDNS`_ provides Primary DNS services, Secondary DNS services, and +Dynamic DNS hosting services for thousands of businesses, website hosting, +website developers, and consumers. Resellers can completely private +label / white label DNS services with custom name servers. + +Instantiating the driver +------------------------------------- + +.. literalinclude:: /examples/dns/worldwidedns/instantiate_driver.py + :language: python + +API Docs +-------- + +.. autoclass:: libcloud.dns.drivers.worldwidedns.WorldWideDNSDriver + :members: + :inherited-members: + +.. _`WorldWideDNS`: https://www.worldwidedns.net/ http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/docs/examples/dns/worldwidedns/instantiate_driver.py ---------------------------------------------------------------------- diff --git a/docs/examples/dns/worldwidedns/instantiate_driver.py b/docs/examples/dns/worldwidedns/instantiate_driver.py new file mode 100644 index 0000000..53bc1c9 --- /dev/null +++ b/docs/examples/dns/worldwidedns/instantiate_driver.py @@ -0,0 +1,10 @@ +from libcloud.dns.types import Provider +from libcloud.dns.providers import get_driver + +cls = get_driver(Provider.WORLDWIDEDNS) + +# Normal account +driver = cls('username', 'apikey') + +# Reseller account +driver = cls('username', 'apikey', reseller_id='reseller_id') http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/common/worldwidedns.py ---------------------------------------------------------------------- diff --git a/libcloud/common/worldwidedns.py b/libcloud/common/worldwidedns.py new file mode 100644 index 0000000..1c02a12 --- /dev/null +++ b/libcloud/common/worldwidedns.py @@ -0,0 +1,195 @@ +# 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. +import re + +from libcloud.common.base import ConnectionUserAndKey +from libcloud.common.base import Response +from libcloud.common.types import ProviderError + + +OK_CODES = ['200', '211', '212', '213'] +ERROR_CODES = ['401', '403', '405', '406', '407', '408', '409', '410', '411', + '412', '413', '414', '450', '451'] + + +class WorldWideDNSException(ProviderError): + def __init__(self, value, http_code, code, driver=None): + self.code = code + super(WorldWideDNSException, self).__init__(value, http_code, driver) + + +class SuspendedAccount(WorldWideDNSException): + def __init__(self, http_code, driver=None): + value = "Login ID you supplied is SUSPENDED, you need to renew" + \ + " your account" + super(SuspendedAccount, self).__init__(value, http_code, 401, + driver) + + +class LoginOrPasswordNotMatch(WorldWideDNSException): + def __init__(self, http_code, driver=None): + value = "Login ID and/or Password you supplied is not on file or" + \ + " does not match" + super(LoginOrPasswordNotMatch, self).__init__(value, http_code, 403, + driver) + + +class NonExistentDomain(WorldWideDNSException): + def __init__(self, http_code, driver=None): + value = "Domain name supplied is not in your account" + super(NonExistentDomain, self).__init__(value, http_code, 405, + driver) + + +class CouldntRemoveDomain(WorldWideDNSException): + def __init__(self, http_code, driver=None): + value = "Error occured removing domain from name server, try again" + super(CouldntRemoveDomain, self).__init__(value, http_code, 406, + driver) + + +class LimitExceeded(WorldWideDNSException): + def __init__(self, http_code, driver=None): + value = "Your limit was exceeded, you need to upgrade your account" + super(LimitExceeded, self).__init__(value, http_code, 407, + driver) + + +class ExistentDomain(WorldWideDNSException): + def __init__(self, http_code, driver=None): + value = "Domain already exists on our servers" + super(ExistentDomain, self).__init__(value, http_code, 408, + driver) + + +class DomainBanned(WorldWideDNSException): + def __init__(self, http_code, driver=None): + value = "Domain is listed in DNSBL and is banned from our servers" + super(DomainBanned, self).__init__(value, http_code, 409, + driver) + + +class InvalidDomainName(WorldWideDNSException): + def __init__(self, http_code, driver=None): + value = "Invalid domain name" + super(InvalidDomainName, self).__init__(value, http_code, 410, + driver) + + +class ErrorOnReloadInNameServer(WorldWideDNSException): + def __init__(self, server, http_code, driver=None): + if server == 1: + value = "Name server #1 kicked an error on reload, contact support" + code = 411 + elif server == 2: + value = "Name server #2 kicked an error on reload, contact support" + code = 412 + elif server == 3: + value = "Name server #3 kicked an error on reload, contact support" + code = 413 + super(ErrorOnReloadInNameServer, self).__init__(value, http_code, code, + driver) + + +class NewUserNotValid(WorldWideDNSException): + def __init__(self, http_code, driver=None): + value = "New userid is not valid" + super(NewUserNotValid, self).__init__(value, http_code, 414, + driver) + + +class CouldntReachNameServer(WorldWideDNSException): + def __init__(self, http_code, driver=None): + value = "Couldn't reach the name server, try again later" + super(CouldntReachNameServer, self).__init__(value, http_code, 450, + driver) + + +class NoZoneFile(WorldWideDNSException): + def __init__(self, http_code, driver=None): + value = "No zone file in the name server queried" + super(NoZoneFile, self).__init__(value, http_code, 451, + driver) + + +ERROR_CODE_TO_EXCEPTION_CLS = { + '401': SuspendedAccount, + '403': LoginOrPasswordNotMatch, + '405': NonExistentDomain, + '406': CouldntRemoveDomain, + '407': LimitExceeded, + '408': ExistentDomain, + '409': DomainBanned, + '410': InvalidDomainName, + '411': ErrorOnReloadInNameServer, + '412': ErrorOnReloadInNameServer, + '413': ErrorOnReloadInNameServer, + '414': NewUserNotValid, + '450': CouldntReachNameServer, + '451': NoZoneFile, +} + + +class WorldWideDNSResponse(Response): + + def parse_body(self): + """ + Parse response body. + + :return: Parsed body. + :rtype: ``str`` + """ + if self._code_response(self.body): + codes = re.split('\r?\n', self.body) + for code in codes: + if code in OK_CODES: + continue + elif code in ERROR_CODES: + exception = ERROR_CODE_TO_EXCEPTION_CLS.get(code) + if code in ['411', '412', '413']: + server = int(code[2]) + raise exception(server, self.status) + raise exception(self.status) + return self.body + + def _code_response(self, body): + """ + Checks if the response body contains code status. + + :rtype: ``bool`` + """ + available_response_codes = OK_CODES + ERROR_CODES + codes = re.split('\r?\n', body) + if codes[0] in available_response_codes: + return True + return False + + +class WorldWideDNSConnection(ConnectionUserAndKey): + host = 'www.worldwidedns.net' + responseCls = WorldWideDNSResponse + + def add_default_params(self, params): + """ + Add parameters that are necessary for every request + + This method adds ``NAME`` and ``PASSWORD`` to + the request. + """ + params["NAME"] = self.user_id + params["PASSWORD"] = self.key + if hasattr(self, 'reseller_id'): + params["ID"] = self.reseller_id + return params http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/dns/drivers/worldwidedns.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/drivers/worldwidedns.py b/libcloud/dns/drivers/worldwidedns.py new file mode 100644 index 0000000..d5fdd5a --- /dev/null +++ b/libcloud/dns/drivers/worldwidedns.py @@ -0,0 +1,502 @@ +# 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: Weither 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 of the required record + :type record_id: ``str`` + + :rtype: :class:`Record` + """ + zone = self.get_zone(zone_id) + r_entry = [i for i in range(MAX_RECORD_ENTRIES) if + zone.extra.get('S%s' % i) == record_id] + if not r_entry: + raise RecordDoesNotExistError(value="Record doesn't exists", + driver=zone.driver, + record_id=record_id) + entry = r_entry[0] + type = zone.extra.get('T%s' % entry) + data = zone.extra.get('D%s' % entry) + record = self._to_record(record_id, type, data, zone) + return record + + def update_zone(self, zone, domain, type='master', ttl=None, extra=None, + ex_raw=False): + """ + Update en 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, ex_entry): + """ + 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 ex_entry: Entry position (1 thru 40) + :type ex_entry: ``int`` + + :rtype: :class:`Record` + """ + 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' % ex_entry: name, + 'T%s' % ex_entry: type, + 'D%s' % ex_entry: data} + zone = self.update_zone(zone, zone.domain, extra=extra) + record = self.get_record(zone.id, name) + 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, ex_entry): + """ + 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 (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 ex_entry: Entry position (1 thru 40) + :type ex_entry: ``int`` + + :rtype: :class:`Record` + """ + if name == '': + name = '@' + if type not in self.RECORD_TYPE_MAP: + raise RecordError(value="Record type is not allowed", + driver=zone.driver, + record_id=name) + extra = {'S%s' % ex_entry: name, + 'T%s' % ex_entry: type, + 'D%s' % ex_entry: data} + zone = self.update_zone(zone, zone.domain, extra=extra) + record = self.get_record(zone.id, name) + 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 _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] + extra['T%s' % (line + 1)] = line_data[1] + 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 entry in range(MAX_RECORD_ENTRIES): + subdomain = zone.extra['S%s' % (entry + 1)] + type = zone.extra['T%s' % (entry + 1)] + data = zone.extra['D%s' % (entry + 1)] + if subdomain and type and data: + record = self._to_record(subdomain, type, data, zone) + records.append(record) + return records + + def _to_record(self, subdomain, type, data, zone): + return Record(subdomain, subdomain, type, data, zone, zone.driver) http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/dns/providers.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/providers.py b/libcloud/dns/providers.py index aef0aa7..a3e4af3 100644 --- a/libcloud/dns/providers.py +++ b/libcloud/dns/providers.py @@ -37,6 +37,8 @@ DRIVERS = { ('libcloud.dns.drivers.softlayer', 'SoftLayerDNSDriver'), Provider.DIGITAL_OCEAN: ('libcloud.dns.drivers.digitalocean', 'DigitalOceanDNSDriver'), + Provider.WORLDWIDEDNS: + ('libcloud.dns.drivers.worldwidedns', 'WorldWideDNSDriver'), # Deprecated Provider.RACKSPACE_US: ('libcloud.dns.drivers.rackspace', 'RackspaceUSDNSDriver'), http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/dns/types.py ---------------------------------------------------------------------- diff --git a/libcloud/dns/types.py b/libcloud/dns/types.py index ad2b34a..ab81976 100644 --- a/libcloud/dns/types.py +++ b/libcloud/dns/types.py @@ -39,6 +39,7 @@ class Provider(object): SOFTLAYER = 'softlayer' DIGITAL_OCEAN = 'digitalocean' AURORADNS = 'auroradns' + WORLDWIDEDNS = 'worldwidedns' # Deprecated RACKSPACE_US = 'rackspace_us' http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/test/dns/fixtures/worldwidedns/api_dns_list ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/worldwidedns/api_dns_list b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list new file mode 100644 index 0000000..004f582 --- /dev/null +++ b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list @@ -0,0 +1 @@ +niteowebsponsoredthisone.comP \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp new file mode 100644 index 0000000..f58f2d1 --- /dev/null +++ b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp @@ -0,0 +1,46 @@ +hostmaster.niteowebsponsoredthisone.com +21600 +10800 +604800 +43200 + +wwwA0.0.0.0 +@A0.0.0.0 +@MX10 niteowebsponsoredthisone.com +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_CREATE_RECORD ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_CREATE_RECORD b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_CREATE_RECORD new file mode 100644 index 0000000..1f0e876 --- /dev/null +++ b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_CREATE_RECORD @@ -0,0 +1,46 @@ +hostmaster.niteowebsponsoredthisone.com +21600 +10800 +604800 +43200 + +wwwA0.0.0.0 +domain2A0.0.0.2 +@MX10 niteowebsponsoredthisone.com +domain4A0.0.0.4 +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_DELETE_RECORD ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_DELETE_RECORD b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_DELETE_RECORD new file mode 100644 index 0000000..5a40168 --- /dev/null +++ b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_DELETE_RECORD @@ -0,0 +1,46 @@ +hostmaster.niteowebsponsoredthisone.com +21600 +10800 +604800 +43200 + +wwwA0.0.0.0 +NONE +@MX10 niteowebsponsoredthisone.com +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_UPDATE_RECORD ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_UPDATE_RECORD b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_UPDATE_RECORD new file mode 100644 index 0000000..c469527 --- /dev/null +++ b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_UPDATE_RECORD @@ -0,0 +1,46 @@ +hostmaster.niteowebsponsoredthisone.com +21600 +10800 +604800 +43200 + +domain1A0.0.0.1 +@A0.0.0.0 +@MX10 niteowebsponsoredthisone.com +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_UPDATE_ZONE ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_UPDATE_ZONE b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_UPDATE_ZONE new file mode 100644 index 0000000..4e1be63 --- /dev/null +++ b/libcloud/test/dns/fixtures/worldwidedns/api_dns_list_domain_asp_UPDATE_ZONE @@ -0,0 +1,46 @@ +mail.niteowebsponsoredthisone.com +21600 +10800 +604800 +3800 + +wwwA0.0.0.0 +@A0.0.0.0 +@MX10 niteowebsponsoredthisone.com +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE +NONE \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/test/dns/test_worldwidedns.py ---------------------------------------------------------------------- diff --git a/libcloud/test/dns/test_worldwidedns.py b/libcloud/test/dns/test_worldwidedns.py new file mode 100644 index 0000000..8ea966e --- /dev/null +++ b/libcloud/test/dns/test_worldwidedns.py @@ -0,0 +1,339 @@ +# 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 + +import sys +import unittest + +from libcloud.utils.py3 import httplib + +from libcloud.dns.types import RecordType, ZoneDoesNotExistError +from libcloud.dns.types import RecordDoesNotExistError +from libcloud.dns.drivers.worldwidedns import WorldWideDNSDriver +from libcloud.common.worldwidedns import NonExistentDomain +from libcloud.common.worldwidedns import InvalidDomainName + +from libcloud.test import MockHttp +from libcloud.test.file_fixtures import DNSFileFixtures +from libcloud.test.secrets import DNS_PARAMS_WORLDWIDEDNS + + +class WorldWideDNSTests(unittest.TestCase): + def setUp(self): + WorldWideDNSDriver.connectionCls.conn_classes = ( + None, WorldWideDNSMockHttp) + WorldWideDNSMockHttp.type = None + self.driver = WorldWideDNSDriver(*DNS_PARAMS_WORLDWIDEDNS) + + def assertHasKeys(self, dictionary, keys): + for key in keys: + self.assertTrue(key in dictionary, 'key "%s" not in dictionary' % + (key)) + + def test_list_record_types(self): + record_types = self.driver.list_record_types() + self.assertEqual(len(record_types), 6) + self.assertTrue(RecordType.A in record_types) + self.assertTrue(RecordType.CNAME in record_types) + self.assertTrue(RecordType.MX in record_types) + self.assertTrue(RecordType.TXT in record_types) + self.assertTrue(RecordType.SRV in record_types) + self.assertTrue(RecordType.NS in record_types) + + def test_list_zones_success(self): + zones = self.driver.list_zones() + self.assertEqual(len(zones), 1) + + zone = zones[0] + self.assertEqual(zone.id, 'niteowebsponsoredthisone.com') + self.assertEqual(zone.type, 'master') + self.assertEqual(zone.domain, 'niteowebsponsoredthisone.com') + self.assertEqual(zone.ttl, '43200') + self.assertHasKeys(zone.extra, ['HOSTMASTER', 'REFRESH', 'RETRY', + 'EXPIRE', 'SECURE', 'S1', 'T1', 'D1', + 'S2', 'T2', 'D2', 'S3', 'T3', 'D3']) + + def test_list_records_success(self): + zone = self.driver.list_zones()[0] + records = self.driver.list_records(zone=zone) + self.assertEqual(len(records), 3) + + www = records[0] + self.assertEqual(www.id, 'www') + self.assertEqual(www.name, 'www') + self.assertEqual(www.type, RecordType.A) + self.assertEqual(www.data, '0.0.0.0') + self.assertEqual(www.extra, {}) + + def test_list_records_zone_does_not_exist(self): + WorldWideDNSMockHttp.type = 'ZONE_DOES_NOT_EXIST' + try: + zone = self.driver.list_zones()[0] + self.driver.list_records(zone=zone) + except NonExistentDomain: + e = sys.exc_info()[1] + self.assertEqual(e.code, 405) + else: + self.fail('Exception was not thrown') + + def test_get_zone_success(self): + zone = self.driver.get_zone(zone_id='niteowebsponsoredthisone.com') + self.assertEqual(zone.id, 'niteowebsponsoredthisone.com') + self.assertEqual(zone.type, 'master') + self.assertEqual(zone.domain, 'niteowebsponsoredthisone.com') + self.assertEqual(zone.ttl, '43200') + self.assertHasKeys(zone.extra, ['HOSTMASTER', 'REFRESH', 'RETRY', + 'EXPIRE', 'SECURE', 'S1', 'T1', 'D1', + 'S2', 'T2', 'D2', 'S3', 'T3', 'D3']) + + def test_get_zone_does_not_exist(self): + WorldWideDNSMockHttp.type = 'GET_ZONE_DOES_NOT_EXIST' + try: + self.driver.get_zone(zone_id='unexistentzone') + except ZoneDoesNotExistError: + e = sys.exc_info()[1] + self.assertEqual(e.zone_id, 'unexistentzone') + else: + self.fail('Exception was not thrown') + + def test_get_record_success(self): + record = self.driver.get_record(zone_id='niteowebsponsoredthisone.com', + record_id='www') + self.assertEqual(record.id, 'www') + self.assertEqual(record.name, 'www') + self.assertEqual(record.type, RecordType.A) + self.assertEqual(record.data, '0.0.0.0') + self.assertEqual(record.extra, {}) + + def test_get_record_zone_does_not_exist(self): + try: + self.driver.get_record(zone_id='unexistentzone', + record_id='3585100') + except ZoneDoesNotExistError: + e = sys.exc_info()[1] + self.assertEqual(e.zone_id, 'unexistentzone') + else: + self.fail('Exception was not thrown') + + def test_get_record_record_does_not_exist(self): + try: + self.driver.get_record(zone_id='niteowebsponsoredthisone.com', + record_id='3585100') + except RecordDoesNotExistError: + e = sys.exc_info()[1] + self.assertEqual(e.record_id, '3585100') + else: + self.fail('Exception was not thrown') + + def test_create_zone_success(self): + zone = self.driver.create_zone(domain='niteowebsponsoredthisone.com', + type='master') + self.assertEqual(zone.id, 'niteowebsponsoredthisone.com') + self.assertEqual(zone.domain, 'niteowebsponsoredthisone.com') + self.assertEqual(zone.ttl, '43200') + self.assertEqual(zone.type, 'master') + + def test_create_zone_validaton_error(self): + WorldWideDNSMockHttp.type = 'VALIDATION_ERROR' + + try: + self.driver.create_zone(domain='foo.%.com', type='master', + ttl=None, extra=None) + except InvalidDomainName: + e = sys.exc_info()[1] + self.assertEqual(e.code, 410) + else: + self.fail('Exception was not thrown') + + def test_update_zone_success(self): + zone = self.driver.list_zones()[0] + WorldWideDNSMockHttp.type = 'UPDATE_ZONE' + updated_zone = self.driver.update_zone(zone=zone, + domain='niteowebsponsoredthisone.com', # noqa + ttl=3800, + extra={'HOSTMASTER': + 'mail.niteowebsponsoredthisone.com'}) # noqa + + self.assertEqual(zone.extra['HOSTMASTER'], + 'hostmaster.niteowebsponsoredthisone.com') + + self.assertEqual(updated_zone.id, zone.id) + self.assertEqual(updated_zone.domain, 'niteowebsponsoredthisone.com') + self.assertEqual(updated_zone.type, zone.type) + self.assertEqual(updated_zone.ttl, '3800') + self.assertEqual(updated_zone.extra['HOSTMASTER'], + 'mail.niteowebsponsoredthisone.com') + self.assertEqual(updated_zone.extra['REFRESH'], zone.extra['REFRESH']) + self.assertEqual(updated_zone.extra['RETRY'], zone.extra['RETRY']) + self.assertEqual(updated_zone.extra['EXPIRE'], zone.extra['EXPIRE']) + self.assertEqual(updated_zone.extra['SECURE'], zone.extra['SECURE']) + self.assertEqual(updated_zone.extra['S1'], zone.extra['S1']) + self.assertEqual(updated_zone.extra['T1'], zone.extra['T1']) + self.assertEqual(updated_zone.extra['D1'], zone.extra['D1']) + self.assertEqual(updated_zone.extra['S2'], zone.extra['S2']) + self.assertEqual(updated_zone.extra['T2'], zone.extra['T2']) + self.assertEqual(updated_zone.extra['D2'], zone.extra['D2']) + self.assertEqual(updated_zone.extra['S3'], zone.extra['S3']) + self.assertEqual(updated_zone.extra['T3'], zone.extra['T3']) + self.assertEqual(updated_zone.extra['D3'], zone.extra['D3']) + + def test_create_record_success(self): + zone = self.driver.list_zones()[0] + WorldWideDNSMockHttp.type = 'CREATE_RECORD' + record = self.driver.create_record(name='domain4', zone=zone, + type=RecordType.A, data='0.0.0.4', + ex_entry=4) + + self.assertEqual(record.id, 'domain4') + self.assertEqual(record.name, 'domain4') + self.assertNotEqual(record.zone.extra.get('S4'), zone.extra.get('S4')) + self.assertNotEqual(record.zone.extra.get('D4'), zone.extra.get('D4')) + self.assertEqual(record.type, RecordType.A) + self.assertEqual(record.data, '0.0.0.4') + + def test_update_record_success(self): + zone = self.driver.list_zones()[0] + record = self.driver.get_record(zone.id, 'www') + WorldWideDNSMockHttp.type = 'UPDATE_RECORD' + record = self.driver.update_record(record=record, name='domain1', + type=RecordType.A, data='0.0.0.1', + ex_entry=1) + + self.assertEqual(record.id, 'domain1') + self.assertEqual(record.name, 'domain1') + self.assertNotEqual(record.zone.extra.get('S1'), zone.extra.get('S1')) + self.assertNotEqual(record.zone.extra.get('D1'), zone.extra.get('D1')) + self.assertEqual(record.type, RecordType.A) + self.assertEqual(record.data, '0.0.0.1') + + def test_delete_zone_success(self): + zone = self.driver.list_zones()[0] + status = self.driver.delete_zone(zone=zone) + self.assertTrue(status) + + def test_delete_zone_does_not_exist(self): + zone = self.driver.list_zones()[0] + + WorldWideDNSMockHttp.type = 'ZONE_DOES_NOT_EXIST' + + try: + self.driver.delete_zone(zone=zone) + except NonExistentDomain: + e = sys.exc_info()[1] + self.assertEqual(e.code, 405) + else: + self.fail('Exception was not thrown') + + def test_delete_record_success(self): + zone = self.driver.list_zones()[0] + records = self.driver.list_records(zone=zone) + self.assertEqual(len(records), 3) + record = records[1] + WorldWideDNSMockHttp.type = 'DELETE_RECORD' + status = self.driver.delete_record(record=record) + self.assertTrue(status) + zone = self.driver.list_zones()[0] + records = self.driver.list_records(zone=zone) + self.assertEqual(len(records), 2) + + +class WorldWideDNSMockHttp(MockHttp): + fixtures = DNSFileFixtures('worldwidedns') + + def _api_dns_list_asp(self, method, url, body, headers): + body = self.fixtures.load('api_dns_list') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_dns_list_domain_asp(self, method, url, body, headers): + body = self.fixtures.load('api_dns_list_domain_asp') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_dns_list_asp_ZONE_DOES_NOT_EXIST(self, method, url, body, + headers): + return (httplib.OK, '405', {}, httplib.responses[httplib.OK]) + + def _api_dns_list_asp_GET_ZONE_DOES_NOT_EXIST(self, method, url, body, + headers): + return (httplib.OK, '', {}, httplib.responses[httplib.OK]) + + def _api_dns_new_domain_asp(self, method, url, body, headers): + return (httplib.OK, '200', {}, httplib.responses[httplib.OK]) + + def _api_dns_new_domain_asp_VALIDATION_ERROR(self, method, url, body, + headers): + return (httplib.OK, '410', {}, httplib.responses[httplib.OK]) + + def _api_dns_modify_asp(self, method, url, body, headers): + return (httplib.OK, '211\r\n212\r\n213', {}, + httplib.responses[httplib.OK]) + + def _api_dns_list_asp_UPDATE_ZONE(self, method, url, body, headers): + body = self.fixtures.load('api_dns_list') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_dns_modify_asp_UPDATE_ZONE(self, method, url, body, headers): + return (httplib.OK, '211\r\n212\r\n213', {}, + httplib.responses[httplib.OK]) + + def _api_dns_list_domain_asp_UPDATE_ZONE(self, method, url, body, + headers): + body = self.fixtures.load('api_dns_list_domain_asp_UPDATE_ZONE') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_dns_list_asp_CREATE_RECORD(self, method, url, body, headers): + body = self.fixtures.load('api_dns_list') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_dns_modify_asp_CREATE_RECORD(self, method, url, body, headers): + return (httplib.OK, '211\r\n212\r\n213', {}, + httplib.responses[httplib.OK]) + + def _api_dns_list_domain_asp_CREATE_RECORD(self, method, url, body, + headers): + body = self.fixtures.load('api_dns_list_domain_asp_CREATE_RECORD') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_dns_list_asp_UPDATE_RECORD(self, method, url, body, headers): + body = self.fixtures.load('api_dns_list') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_dns_modify_asp_UPDATE_RECORD(self, method, url, body, headers): + return (httplib.OK, '211\r\n212\r\n213', {}, + httplib.responses[httplib.OK]) + + def _api_dns_list_domain_asp_UPDATE_RECORD(self, method, url, body, + headers): + body = self.fixtures.load('api_dns_list_domain_asp_UPDATE_RECORD') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_dns_delete_domain_asp(self, method, url, body, headers): + return (httplib.OK, '200', {}, httplib.responses[httplib.OK]) + + def _api_dns_delete_domain_asp_ZONE_DOES_NOT_EXIST(self, method, url, body, + headers): + return (httplib.OK, '405', {}, httplib.responses[httplib.OK]) + + def _api_dns_list_asp_DELETE_RECORD(self, method, url, body, headers): + body = self.fixtures.load('api_dns_list') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_dns_modify_asp_DELETE_RECORD(self, method, url, body, headers): + return (httplib.OK, '200', {}, httplib.responses[httplib.OK]) + + def _api_dns_list_domain_asp_DELETE_RECORD(self, method, url, body, + headers): + body = self.fixtures.load('api_dns_list_domain_asp_DELETE_RECORD') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + +if __name__ == '__main__': + sys.exit(unittest.main()) http://git-wip-us.apache.org/repos/asf/libcloud/blob/4eed2b2d/libcloud/test/secrets.py-dist ---------------------------------------------------------------------- diff --git a/libcloud/test/secrets.py-dist b/libcloud/test/secrets.py-dist index 3ebcac3..982add0 100644 --- a/libcloud/test/secrets.py-dist +++ b/libcloud/test/secrets.py-dist @@ -72,3 +72,4 @@ DNS_PARAMS_ROUTE53 = ('access_id', 'secret') DNS_GANDI = ('user', ) DNS_PARAMS_GOOGLE = ('email_address', 'key') DNS_KEYWORD_PARAMS_GOOGLE = {'project': 'project_name'} +DNS_PARAMS_WORLDWIDEDNS = ('user', 'key')
