Add APIs for managing TLDs

This change adds APIs for managing TLDs. Previously TLDs were in
2 files. With this change, the TLDs will be in the central's storage
database.

When the service starts for the first time, by default there are no
TLDs in the database.  When there are no TLDs in the database
the behavior is to not check for TLDs when creating domains.
Once a TLD is created in the database, the TLD checks are
enforced.

Change-Id: Ibc945af3b158355475f5c8ee7887ed4b32081337
This commit is contained in:
Vinod Mangalpally 2014-01-20 20:55:04 -06:00
parent 517cb0370c
commit 8666dde1ac
25 changed files with 926 additions and 7872 deletions

View File

@ -17,6 +17,7 @@ from designate.openstack.common import log as logging
from designate.api.v2.controllers import limits
from designate.api.v2.controllers import reverse
from designate.api.v2.controllers import schemas
from designate.api.v2.controllers import tlds
from designate.api.v2.controllers import zones
LOG = logging.getLogger(__name__)
@ -30,4 +31,5 @@ class RootController(object):
limits = limits.LimitsController()
schemas = schemas.SchemasController()
reverse = reverse.ReverseController()
tlds = tlds.TldsController()
zones = zones.ZonesController()

View File

@ -0,0 +1,126 @@
# Copyright (c) 2014 Rackspace Hosting
# All Rights Reserved.
#
# Licensed 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 pecan
from designate.openstack.common import log as logging
from designate import schema
from designate import utils
from designate.api.v2.controllers import rest
from designate.api.v2.views import tlds as tlds_view
from designate.central import rpcapi as central_rpcapi
LOG = logging.getLogger(__name__)
central_api = central_rpcapi.CentralAPI()
class TldsController(rest.RestController):
_view = tlds_view.TldsView()
_resource_schema = schema.Schema('v2', 'tld')
_collection_schema = schema.Schema('v2', 'tlds')
@pecan.expose(template='json:', content_type='application/json')
def get_one(self, tld_id):
""" Get Tld """
request = pecan.request
context = request.environ['context']
tld = central_api.get_tld(context, tld_id)
return self._view.show(context, request, tld)
@pecan.expose(template='json:', content_type='application/json')
def get_all(self, **params):
""" List Tlds """
request = pecan.request
context = request.environ['context']
# Extract the pagination params
#marker = params.pop('marker', None)
#limit = int(params.pop('limit', 30))
# Extract any filter params.
accepted_filters = ('name')
criterion = dict((k, params[k]) for k in accepted_filters
if k in params)
tlds = central_api.find_tlds(context, criterion)
return self._view.list(context, request, tlds)
@pecan.expose(template='json:', content_type='application/json')
def post_all(self):
""" Create Tld """
request = pecan.request
response = pecan.response
context = request.environ['context']
body = request.body_dict
# Validate the request conforms to the schema
self._resource_schema.validate(body)
# Convert from APIv2 -> Central format
values = self._view.load(context, request, body)
# Create the tld
tld = central_api.create_tld(context, values)
response.status_int = 201
response.headers['Location'] = self._view._get_resource_href(request,
tld)
# Prepare and return the response body
return self._view.show(context, request, tld)
@pecan.expose(template='json:', content_type='application/json')
@pecan.expose(template='json:', content_type='application/json-patch+json')
def patch_one(self, tld_id):
""" Update Tld """
request = pecan.request
context = request.environ['context']
body = request.body_dict
response = pecan.response
# Fetch the existing tld
tld = central_api.get_tld(context, tld_id)
# Convert to APIv2 Format
tld = self._view.show(context, request, tld)
if request.content_type == 'application/json-patch+json':
raise NotImplemented('json-patch not implemented')
else:
tld = utils.deep_dict_merge(tld, body)
# Validate the request conforms to the schema
self._resource_schema.validate(tld)
values = self._view.load(context, request, body)
tld = central_api.update_tld(context, tld_id, values)
response.status_int = 200
return self._view.show(context, request, tld)
@pecan.expose(template=None, content_type='application/json')
def delete_one(self, tld_id):
""" Delete Tld """
request = pecan.request
response = pecan.response
context = request.environ['context']
central_api.delete_tld(context, tld_id)
response.status_int = 204
# NOTE: This is a hack and a half.. But Pecan needs it.
return ''

View File

@ -0,0 +1,49 @@
# Copyright (c) 2014 Rackspace Hosting
# All Rights Reserved.
#
# Licensed 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 designate.api.v2.views import base as base_view
from designate.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class TldsView(base_view.BaseView):
""" Model a TLD API response as a python dictionary """
_resource_name = 'tld'
_collection_name = 'tlds'
def show_basic(self, context, request, tld):
""" Basic view of a tld """
return {
"id": tld['id'],
"name": tld['name'],
"description": tld['description'],
"created_at": tld['created_at'],
"updated_at": tld['updated_at'],
"links": self._get_resource_links(request, tld)
}
def load(self, context, request, body):
""" Extract a "central" compatible dict from an API call """
result = {}
item = body[self._resource_name]
# Copy keys which need no alterations
for k in ('id', 'name', 'description'):
if k in item:
result[k] = item[k]
return result

View File

@ -32,10 +32,6 @@ cfg.CONF.register_opts([
default=['\\.arpa\\.$', '\\.novalocal\\.$', '\\.localhost\\.$',
'\\.localdomain\\.$', '\\.local\\.$'],
help='DNS domain name blacklist'),
cfg.StrOpt('accepted-tlds-file', default='tlds-alpha-by-domain.txt',
help='Accepted TLDs'),
cfg.StrOpt('effective-tlds-file', default='effective_tld_names.dat',
help='Effective TLDs'),
cfg.IntOpt('max_domain_name_len', default=255,
help="Maximum domain name length"),
cfg.IntOpt('max_recordset_name_len', default=255,

View File

@ -1,179 +0,0 @@
# Copyright (c) 2013 Rackspace Hosting
# All Rights Reserved.
#
# Licensed 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 codecs
import re
from designate import utils
from designate.openstack.common import log as logging
from oslo.config import cfg
LOG = logging.getLogger(__name__)
class EffectiveTld(object):
def __init__(self, *args, **kwargs):
self._load_accepted_tld_list()
self._load_effective_tld_list()
def _load_accepted_tld_list(self):
"""
This loads the accepted TLDs from a file to a list - accepted_tld_list.
The file is expected to have one TLD per line. TLDs need to be in the
IDN format. Comments in the file are lines beginning with a #
The normal source for this file is
http://data.iana.org/TLD/tlds-alpha-by-domain.txt
"""
self.accepted_tld_list = []
accepted_tld_files = utils.find_config(
cfg.CONF['service:central'].accepted_tlds_file)
# We do not require the accepted_tld_files to be present to be
# compatible with stable/havana release.
if len(accepted_tld_files) == 0:
LOG.info('Unable to determine appropriate accepted tlds file')
return
LOG.info('Using accepted_tld_file found at: %s'
% accepted_tld_files[0])
with open(accepted_tld_files[0]) as fh:
for line in fh:
if line.startswith('#'):
continue
line = line.strip()
self.accepted_tld_list.append(line.lower())
LOG.info("Entries in Accepted TLD List: %d"
% len(self.accepted_tld_list))
# LOG.info("Accepted TLD List:\n%s" % self.accepted_tld_list)
def _load_effective_tld_list(self):
"""
This loads the effective TLDs from a file. Effective TLDs are the SLDs
that act as TLDs - e.g. co.uk. The file is in UTF-8 format.
The normal source for this file is at http://publicsuffix.org/list/
The format of the file is:
1. Lines beginning with a // or ! are ignored.
2. The domain names are 1 per line.
3. The wildcard character * (asterisk) may only be used to wildcard the
topmost level in a domain name.
The publicsuffix.org has more rules and !'s are treated differently but
this code ignores those until we find that we need to do otherwise.
The file is put into a dictionary and a list. Domain names with only 1
label are ignored as they are already present in the accepted_tld_list.
All the entries are converted to IDN format.
All the effective TLDs without a wildcard are put into a dictionary -
_effective_tld_dict.
The entries with a wildcard are converted to a regular expression and
put into a separate list - _effective_re_tld_list.
The separation to a dictionary and a regular expression list is done
to make it easier for searching.
The maximum labels in the dictionary and list are tracked to short
circuit checks later as needed.
"""
self._effective_tld_dict = {}
# _max_effective_tld_labels tracks the maximum labels in the
# dictionary self._effective_tld_dict
# This helps to determine if we need to search the dictionary while
# creating a domain
self._max_effective_tld_labels = 0
# The list _effective_re_tld_list contains domains with a *
self._effective_re_tld_list = []
# _max_effective_re_tld_labels tracks the maximum labels in the
# list self._effective_re_tld_list
self._max_effective_re_tld_labels = 0
effective_tld_files = utils.find_config(
cfg.CONF['service:central'].effective_tlds_file)
# We do not require the effective_tld_file to be present to be
# compatible with stable/havana release.
if len(effective_tld_files) == 0:
LOG.info('Unable to determine appropriate effective tlds file')
return
LOG.info('Using effective_tld_file found at: %s'
% effective_tld_files[0])
with codecs.open(effective_tld_files[0], "r", "utf-8") as fh:
for line in fh:
line = line.strip()
if line.startswith('//') or line.startswith('!') or not line:
continue
labels_len = len(line.split('.'))
# skip TLDs as they are already in the accepted_tld_list
if labels_len == 1:
continue
# Convert the public suffix list to idna format
line = line.encode('idna')
# Entries with wildcards go to a separate list.
if (line.startswith('*')):
if labels_len > self._max_effective_re_tld_labels:
self._max_effective_re_tld_labels = labels_len
# Convert the wildcard entry to a regular expression
# The ^ and $ at the beginning and end respectively are to
# match the whole term. The [^.]* is to match anything
# other than a "." This is so that only one label is
# matched. The rest of the label separators "." are
# escaped to match the "." and not any character.
self._effective_re_tld_list.append(
'^[^.]*' + '\.'.join(line.split('.'))[1:] + '$')
continue
if labels_len > self._max_effective_tld_labels:
self._max_effective_tld_labels = labels_len
# The rest of the entries go into a dictionary.
self._effective_tld_dict[line.lower()] = 1
LOG.info("Entries in Effective TLD List Dict: %d"
% len(self._effective_tld_dict))
# LOG.info("Effective TLD Dict:\n%s" % self._effective_tld_dict)
LOG.info("Entries in Effective RE TLD List: %d"
% len(self._effective_re_tld_list))
# LOG.info("Effective RE TLD List:\n%s" % self._effective_re_tld_list)
def is_effective_tld(self, domain_name):
"""
Returns True if the domain_name is the same as an effective TLD else
returns False.
"""
# Break the domain name up into its component labels
stripped_domain_name = domain_name.strip('.').lower()
domain_labels = stripped_domain_name.split('.')
if len(domain_labels) <= self._max_effective_tld_labels:
# First search the dictionary
if stripped_domain_name in self._effective_tld_dict.keys():
return True
# Now search the list of regular expressions for effective TLDs
if len(domain_labels) <= self._max_effective_re_tld_labels:
for eff_re_label in self._effective_re_tld_list:
if bool(re.search(eff_re_label, stripped_domain_name)):
return True
return False

View File

@ -34,6 +34,8 @@ class CentralAPI(rpc_proxy.RpcProxy):
2.1 - Add quota methods
3.0 - RecordSet Changes
3.1 - Add floating ip ptr methods
3.2 - TLD Api changes
"""
def __init__(self, topic=None):
topic = topic if topic else cfg.CONF.central_topic
@ -214,6 +216,37 @@ class CentralAPI(rpc_proxy.RpcProxy):
return self.call(context, msg)
# TLD Methods
def create_tld(self, context, values):
LOG.info("create_tld: Calling central's create_tld.")
msg = self.make_msg('create_tld', values=values)
return self.call(context, msg, version='3.2')
def find_tlds(self, context, criterion=None):
LOG.info("find_tlds: Calling central's find_tlds.")
msg = self.make_msg('find_tlds', criterion=criterion)
return self.call(context, msg, version='3.2')
def get_tld(self, context, tld_id):
LOG.info("get_tld: Calling central's get_tld.")
msg = self.make_msg('get_tld', tld_id=tld_id)
return self.call(context, msg, version='3.2')
def update_tld(self, context, tld_id, values):
LOG.info("update_tld: Calling central's update_tld.")
msg = self.make_msg('update_tld', tld_id=tld_id, values=values)
return self.call(context, msg, version='3.2')
def delete_tld(self, context, tld_id):
LOG.info("delete_tld: Calling central's delete_tld.")
msg = self.make_msg('delete_tld', tld_id=tld_id)
return self.call(context, msg, version='3.2')
# RecordSet Methods
def create_recordset(self, context, domain_id, values):
LOG.info("create_recordset: Calling central's create_recordset.")

View File

@ -17,7 +17,6 @@
import re
import contextlib
from oslo.config import cfg
from designate.central import effectivetld
from designate.openstack.common import log as logging
from designate.openstack.common.rpc import service as rpc_service
from designate.openstack.common.notifier import proxy as notifier
@ -46,7 +45,7 @@ def wrap_backend_call():
class Service(rpc_service.Service):
RPC_API_VERSION = '3.1'
RPC_API_VERSION = '3.2'
def __init__(self, *args, **kwargs):
backend_driver = cfg.CONF['service:central'].backend_driver
@ -68,11 +67,19 @@ class Service(rpc_service.Service):
# Get a quota manager instance
self.quota = quota.get_quota()
self.effective_tld = effectivetld.EffectiveTld()
self.network_api = network_api.get_api(cfg.CONF.network_api)
def start(self):
# Check to see if there are any TLDs in the database
tlds = self.storage_api.find_tlds({})
if tlds:
self.check_for_tlds = True
LOG.info("Checking for TLDs")
else:
self.check_for_tlds = False
LOG.info("NOT checking for TLDs")
self.backend.start()
super(Service, self).start()
@ -94,21 +101,25 @@ class Service(rpc_service.Service):
if len(domain_labels) <= 1:
raise exceptions.InvalidDomainName('More than one label is '
'required')
# Check the TLD for validity
# We cannot use the effective TLD list as the publicsuffix.org list is
# missing some top level entries. At the time of coding, the following
# entries were missing
# arpa, au, bv, gb, gn, kp, lb, lr, sj, tp, tz, xn--80ao21a, xn--l1acc
# xn--mgbx4cd0ab
if self.effective_tld.accepted_tld_list:
domain_tld = domain_labels[-1].lower()
if domain_tld not in self.effective_tld.accepted_tld_list:
raise exceptions.InvalidTLD('Unknown or invalid TLD')
# Check if the domain_name is the same as an effective TLD.
if self.effective_tld.is_effective_tld(domain_name):
raise exceptions.DomainIsSameAsAnEffectiveTLD(
'Domain name cannot be the same as an effective TLD')
# Check the TLD for validity if there are entries in the database
if self.check_for_tlds:
try:
self.storage_api.find_tld(context, {'name': domain_labels[-1]})
except exceptions.TLDNotFound:
raise exceptions.InvalidDomainName('Invalid TLD')
# Now check that the domain name is not the same as a TLD
try:
stripped_domain_name = domain_name.strip('.').lower()
self.storage_api.find_tld(
context,
{'name': stripped_domain_name})
except exceptions.TLDNotFound:
pass
else:
raise exceptions.InvalidDomainName(
'Domain name cannot be the same as a TLD')
# Check domain name blacklist
if self._is_blacklisted_domain_name(context, domain_name):
@ -335,6 +346,52 @@ class Service(rpc_service.Service):
self.notifier.info(context, 'dns.server.delete', server)
# TLD Methods
def create_tld(self, context, values):
policy.check('create_tld', context)
# The TLD is only created on central's storage and not on the backend.
with self.storage_api.create_tld(context, values) as tld:
pass
self.notifier.info(context, 'dns.tld.create', tld)
# Set check for tlds to be true
self.check_for_tlds = True
return tld
def find_tlds(self, context, criterion=None):
policy.check('find_tlds', context)
return self.storage_api.find_tlds(context, criterion)
def get_tld(self, context, tld_id):
policy.check('get_tld', context, {'tld_id': tld_id})
return self.storage_api.get_tld(context, tld_id)
def update_tld(self, context, tld_id, values):
policy.check('update_tld', context, {'tld_id': tld_id})
with self.storage_api.update_tld(context, tld_id, values) as tld:
pass
self.notifier.info(context, 'dns.tld.update', tld)
return tld
def delete_tld(self, context, tld_id):
# Known issue - self.check_for_tld is not reset here. So if the last
# TLD happens to be deleted, then we would incorrectly do the TLD
# validations.
# This decision was influenced by weighing the (ultra low) probability
# of hitting this issue vs doing the checks for every delete.
policy.check('delete_tld', context, {'tld_id': tld_id})
with self.storage_api.delete_tld(context, tld_id) as tld:
pass
self.notifier.info(context, 'dns.tld.delete', tld)
# TSIG Key Methods
def create_tsigkey(self, context, values):
policy.check('create_tsigkey', context)

View File

@ -107,16 +107,6 @@ class InvalidDomainName(Base):
error_type = 'invalid_domain_name'
class DomainIsSameAsAnEffectiveTLD(Base):
error_code = 400
error_type = 'domain_is_same_as_an_effective_tld'
class InvalidTLD(Base):
error_code = 400
error_type = 'invalid_tld'
class InvalidRecordSetName(Base):
error_code = 400
error_type = 'invalid_recordset_name'
@ -158,6 +148,10 @@ class DuplicateDomain(Duplicate):
error_type = 'duplicate_domain'
class DuplicateTLD(Duplicate):
error_type = 'duplicate_tld'
class DuplicateRecordSet(Duplicate):
error_type = 'duplicate_recordset'
@ -187,6 +181,10 @@ class DomainNotFound(NotFound):
error_type = 'domain_not_found'
class TLDNotFound(NotFound):
error_type = 'tld_not_found'
class RecordSetNotFound(NotFound):
error_type = 'recordset_not_found'

View File

@ -0,0 +1,63 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "tld",
"title": "tld",
"description": "Tld",
"additionalProperties": false,
"required": ["tld"],
"properties": {
"tld": {
"type": "object",
"additionalProperties": false,
"required": ["name"],
"properties": {
"id": {
"type": "string",
"description": "Tld identifier",
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
"readOnly": true
},
"name": {
"type": "string",
"description": "Tld name",
"format": "tldname",
"maxLength": 255,
"immutable": true
},
"description": {
"type": ["string", "null"],
"description": "Description for the tld",
"maxLength": 160
},
"created_at": {
"type": "string",
"description": "Date and time of tld creation",
"format": "date-time",
"readOnly": true
},
"updated_at": {
"type": ["string", "null"],
"description": "Date and time of last tld modification",
"format": "date-time",
"readOnly": true
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
}
}
}
}
}
}
}

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "tlds",
"title": "tlds",
"description": "Tlds",
"additionalProperties": false,
"required": ["tlds"],
"properties": {
"tlds": {
"type": "array",
"description": "Tlds",
"items": {"$ref": "tld#/properties/tld"}
},
"links": {
"type": "object",
"additionalProperties": false,
"properties": {
"self": {
"type": "string",
"format": "url"
},
"next": {
"type": ["string", "null"],
"format": "url"
},
"previous": {
"type": ["string", "null"],
"format": "url"
}
}
}
}
}

View File

@ -24,6 +24,10 @@ LOG = logging.getLogger(__name__)
RE_DOMAINNAME = r'^(?!.{255,})((?!\-)[A-Za-z0-9_\-]{1,63}(?<!\-)\.)+$'
RE_HOSTNAME = r'^(?!.{255,})((^\*|(?!\-)[A-Za-z0-9_\-]{1,63})(?<!\-)\.)+$'
# The TLD name will not end in a period.
RE_TLDNAME = r'^(?!.{255,})((?!\-)[A-Za-z0-9_\-]{1,63}(?<!\-))' \
r'(\.((?!\-)[A-Za-z0-9_\-]{1,63}(?<!\-)))*$'
draft3_format_checker = jsonschema.draft3_format_checker
draft4_format_checker = jsonschema.draft4_format_checker
@ -86,6 +90,18 @@ def is_domainname(instance):
return True
@draft3_format_checker.checks("tld-name")
@draft4_format_checker.checks("tldname")
def is_tldname(instance):
if not isinstance(instance, compat.str_types):
return True
if not re.match(RE_TLDNAME, instance):
return False
return True
@draft3_format_checker.checks("email")
@draft4_format_checker.checks("email")
def is_email(instance):

View File

@ -173,6 +173,79 @@ class StorageAPI(object):
yield self.storage.get_server(context, server_id)
self.storage.delete_server(context, server_id)
@contextlib.contextmanager
def create_tld(self, context, values):
"""
Create a TLD.
:param context: RPC Context.
:param values: Values to create the new TLD from.
"""
tld = self.storage.create_tld(context, values)
try:
yield tld
except Exception:
with excutils.save_and_reraise_exception():
self.storage.delete_tld(context, tld['id'])
def get_tld(self, context, tld_id):
"""
Get a TLD via ID.
:param context: RPC Context.
:param tld_id: TLD ID to get.
"""
return self.storage.get_tld(context, tld_id)
def find_tlds(self, context, criterion=None):
"""
Find TLDs
:param context: RPC Context.
:param criterion: Criteria to filter by.
"""
return self.storage.find_tlds(context, criterion)
def find_tld(self, context, criterion):
"""
Find a single TLD.
:param context: RPC Context.
:param criterion: Criteria to filter by.
"""
return self.storage.find_tld(context, criterion)
@contextlib.contextmanager
def update_tld(self, context, tld_id, values):
"""
Update a TLD via ID
:param context: RPC Context.
:param tld_id: TLD ID to update.
:param values: Values to update the TLD from
"""
backup = self.storage.get_tld(context, tld_id)
tld = self.storage.update_tld(context, tld_id, values)
try:
yield tld
except Exception:
with excutils.save_and_reraise_exception():
restore = self._extract_dict_subset(backup, values.keys())
self.storage.update_tld(context, tld_id, restore)
@contextlib.contextmanager
def delete_tld(self, context, tld_id):
"""
Delete a TLD via ID.
:param context: RPC Context.
:param tld_id: Delete a TLD via ID
"""
yield self.storage.get_tld(context, tld_id)
self.storage.delete_tld(context, tld_id)
@contextlib.contextmanager
def create_tsigkey(self, context, values):
"""

View File

@ -124,6 +124,61 @@ class Storage(Plugin):
:param server_id: Delete a Server via ID
"""
@abc.abstractmethod
def create_tld(self, context, values):
"""
Create a TLD.
:param context: RPC Context.
:param values: Values to create the new TLD from.
"""
@abc.abstractmethod
def get_tld(self, context, tld_id):
"""
Get a TLD via ID.
:param context: RPC Context.
:param tld_id: TLD ID to get.
"""
@abc.abstractmethod
def find_tlds(self, context, criterion=None):
"""
Find TLDs
:param context: RPC Context.
:param criterion: Criteria to filter by.
"""
@abc.abstractmethod
def find_tld(self, context, criterion):
"""
Find a single TLD.
:param context: RPC Context.
:param criterion: Criteria to filter by.
"""
@abc.abstractmethod
def update_tld(self, context, tld_id, values):
"""
Update a TLD via ID
:param context: RPC Context.
:param tld_id: TLD ID to update.
:param values: Values to update the TLD from
"""
@abc.abstractmethod
def delete_tld(self, context, tld_id):
"""
Delete a TLD via ID.
:param context: RPC Context.
:param tld_id: Delete a TLD via ID
"""
@abc.abstractmethod
def create_tsigkey(self, context, values):
"""

View File

@ -212,6 +212,51 @@ class SQLAlchemyStorage(base.Storage):
server.delete(self.session)
# TLD Methods
def _find_tlds(self, context, criterion, one=False):
try:
return self._find(models.Tld, context, criterion, one)
except exceptions.NotFound:
raise exceptions.TLDNotFound()
def create_tld(self, context, values):
tld = models.Tld()
tld.update(values)
try:
tld.save(self.session)
except exceptions.Duplicate:
raise exceptions.DuplicateTLD()
return dict(tld)
def find_tlds(self, context, criterion=None):
tlds = self._find_tlds(context, criterion)
return [dict(s) for s in tlds]
def find_tld(self, context, criterion=None):
tld = self._find_tlds(context, criterion, one=True)
return dict(tld)
def get_tld(self, context, tld_id):
tld = self._find_tlds(context, {'id': tld_id}, one=True)
return dict(tld)
def update_tld(self, context, tld_id, values):
tld = self._find_tlds(context, {'id': tld_id}, one=True)
tld.update(values)
try:
tld.save(self.session)
except exceptions.Duplicate:
raise exceptions.DuplicateTLD()
return dict(tld)
def delete_tld(self, context, tld_id):
tld = self._find_tlds(context, {'id': tld_id}, one=True)
tld.delete(self.session)
# TSIG Key Methods
def _find_tsigkeys(self, context, criterion, one=False):
try:

View File

@ -0,0 +1,49 @@
# Copyright (c) 2014 Rackspace Hosting
# All Rights Reserved.
#
# Licensed 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 sqlalchemy import Integer, String, DateTime, Unicode
from sqlalchemy.schema import Table, Column, MetaData
from designate.openstack.common import timeutils
from designate.openstack.common.uuidutils import generate_uuid
from designate.sqlalchemy.types import UUID
from designate.openstack.common import log as logging
LOG = logging.getLogger(__name__)
meta = MetaData()
tlds_table = Table(
'tlds',
meta,
Column('id', UUID(), default=generate_uuid, primary_key=True),
Column('created_at', DateTime(), default=timeutils.utcnow),
Column('updated_at', DateTime(), onupdate=timeutils.utcnow),
Column('version', Integer(), default=1, nullable=False),
Column('name', String(255), nullable=False, unique=True),
Column('description', Unicode(160), nullable=True),
mysql_engine='INNODB',
mysql_charset='utf8')
def upgrade(migrate_engine):
meta.bind = migrate_engine
tlds_table.create()
def downgrade(migrate_engine):
meta.bind = migrate_engine
tlds_table.drop()

View File

@ -72,6 +72,13 @@ class Server(Base):
name = Column(String(255), nullable=False, unique=True)
class Tld(Base):
__tablename__ = 'tlds'
name = Column(String(255), nullable=False, unique=True)
description = Column(Unicode(160), nullable=True)
class Domain(SoftDeleteMixin, Base):
__tablename__ = 'domains'
__table_args__ = (

View File

@ -144,6 +144,23 @@ class TestCase(test.BaseTestCase):
'name': 'ns2.example.org.',
}]
# The last tld is invalid
tld_fixtures = [{
'name': 'com',
}, {
'name': 'co.uk',
}, {
'name': 'com.',
}]
default_tld_fixtures = [{
'name': 'com',
}, {
'name': 'org',
}, {
'name': 'net',
}]
tsigkey_fixtures = [{
'name': 'test-key-one',
'algorithm': 'hmac-md5',
@ -312,6 +329,16 @@ class TestCase(test.BaseTestCase):
_values.update(values)
return _values
def get_tld_fixture(self, fixture=0, values={}):
_values = copy.copy(self.tld_fixtures[fixture])
_values.update(values)
return _values
def get_default_tld_fixture(self, fixture=0, values={}):
_values = copy.copy(self.default_tld_fixtures[fixture])
_values.update(values)
return _values
def get_tsigkey_fixture(self, fixture=0, values={}):
_values = copy.copy(self.tsigkey_fixtures[fixture])
_values.update(values)
@ -367,6 +394,27 @@ class TestCase(test.BaseTestCase):
values = self.get_server_fixture(fixture=fixture, values=kwargs)
return self.central_service.create_server(context, values=values)
def create_tld(self, **kwargs):
context = kwargs.pop('context', self.admin_context)
fixture = kwargs.pop('fixture', 0)
values = self.get_tld_fixture(fixture=fixture, values=kwargs)
return self.central_service.create_tld(context, values=values)
def create_default_tld(self, **kwargs):
context = kwargs.pop('context', self.admin_context)
fixture = kwargs.pop('fixture', 0)
values = self.get_default_tld_fixture(fixture=fixture, values=kwargs)
return self.central_service.create_tld(context, values=values)
def create_default_tlds(self):
for index in range(len(self.default_tld_fixtures)):
try:
self.create_default_tld(fixture=index)
except exceptions.DuplicateTLD:
pass
def create_tsigkey(self, **kwargs):
context = kwargs.pop('context', self.admin_context)
fixture = kwargs.pop('fixture', 0)

View File

@ -0,0 +1,135 @@
# Copyright (c) 2014 Rackspace Hosting
# All Rights Reserved.
#
# Licensed 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 designate.tests.test_api.test_v2 import ApiV2TestCase
class ApiV2ZTldsTest(ApiV2TestCase):
def setUp(self):
super(ApiV2ZTldsTest, self).setUp()
def test_create_tld(self):
self.policy({'create_tld': '@'})
fixture = self.get_tld_fixture(0)
response = self.client.post_json('/tlds/', {'tld': fixture})
# Check the headers are what we expect
self.assertEqual(201, response.status_int)
self.assertEqual('application/json', response.content_type)
# Check the body structure is what we expect
self.assertIn('tld', response.json)
self.assertIn('links', response.json['tld'])
self.assertIn('self', response.json['tld']['links'])
# Check the values returned are what we expect
self.assertIn('id', response.json['tld'])
self.assertIn('created_at', response.json['tld'])
self.assertIsNone(response.json['tld']['updated_at'])
self.assertEqual(fixture['name'], response.json['tld']['name'])
def test_create_tld_validation(self):
self.policy({'create_tld': '@'})
invalid_fixture = self.get_tld_fixture(-1)
# Ensure it fails with a 400
response = self.client.post_json('/tlds/', {'tld': invalid_fixture},
status=400)
self.assertEqual(400, response.status_int)
def test_get_tlds(self):
self.policy({'find_tlds': '@'})
response = self.client.get('/tlds/')
# Check the headers are what we expect
self.assertEqual(200, response.status_int)
self.assertEqual('application/json', response.content_type)
# Check the body structure is what we expect
self.assertIn('tlds', response.json)
self.assertIn('links', response.json)
self.assertIn('self', response.json['links'])
# We should start with 0 tlds
self.assertEqual(0, len(response.json['tlds']))
# Test with 1 tld
self.create_tld(fixture=0)
response = self.client.get('/tlds/')
self.assertIn('tlds', response.json)
self.assertEqual(1, len(response.json['tlds']))
# test with 2 tlds
self.create_tld(fixture=1)
response = self.client.get('/tlds/')
self.assertIn('tlds', response.json)
self.assertEqual(2, len(response.json['tlds']))
def test_get_tld(self):
tld = self.create_tld(fixture=0)
self.policy({'get_tld': '@'})
response = self.client.get('/tlds/%s' % tld['id'],
headers=[('Accept', 'application/json')])
# Check the headers are what we expect
self.assertEqual(200, response.status_int)
self.assertEqual('application/json', response.content_type)
# Check the body structure is what we expect
self.assertIn('tld', response.json)
self.assertIn('links', response.json['tld'])
self.assertIn('self', response.json['tld']['links'])
# Check the values returned are what we expect
self.assertIn('id', response.json['tld'])
self.assertIn('created_at', response.json['tld'])
self.assertIsNone(response.json['tld']['updated_at'])
self.assertEqual(self.get_tld_fixture(0)['name'],
response.json['tld']['name'])
def test_delete_tld(self):
tld = self.create_tld(fixture=0)
self.policy({'delete_tld': '@'})
self.client.delete('/tlds/%s' % tld['id'], status=204)
def test_update_tld(self):
tld = self.create_tld(fixture=0)
self.policy({'update_tld': '@'})
# Prepare an update body
body = {'tld': {'description': 'prefix-%s' % tld['description']}}
response = self.client.patch_json('/tlds/%s' % tld['id'], body,
status=200)
# Check the headers are what we expect
self.assertEqual(200, response.status_int)
self.assertEqual('application/json', response.content_type)
# Check the body structure is what we expect
self.assertIn('tld', response.json)
self.assertIn('links', response.json['tld'])
self.assertIn('self', response.json['tld']['links'])
# Check the values returned are what we expect
self.assertIn('id', response.json['tld'])
self.assertIsNotNone(response.json['tld']['updated_at'])
self.assertEqual('prefix-%s' % tld['description'],
response.json['tld']['description'])

View File

@ -28,6 +28,9 @@ class ApiV2ZonesTest(ApiV2TestCase):
# Create a server
self.create_server()
# Create the default TLDs
self.create_default_tlds()
def test_missing_accept(self):
self.client.get('/zones/123', status=400)

View File

@ -34,7 +34,6 @@ class CentralServiceTest(CentralTestCase):
def test_is_valid_domain_name(self):
self.config(max_domain_name_len=10,
accepted_tlds_file='tlds-alpha-by-domain.txt.sample',
group='service:central')
context = self.get_context()
@ -197,6 +196,86 @@ class CentralServiceTest(CentralTestCase):
self.central_service.delete_server, self.admin_context,
server2['id'])
# TLD Tests
def test_create_tld(self):
# Create a TLD with one label
tld = self.create_tld(fixture=0)
# Ensure all values have been set correctly
self.assertIsNotNone(tld['id'])
self.assertEqual(tld['name'], self.get_tld_fixture(fixture=0)['name'])
# Create a TLD with more than one label
tld = self.create_tld(fixture=1)
# Ensure all values have been set correctly
self.assertIsNotNone(tld['id'])
self.assertEqual(tld['name'], self.get_tld_fixture(fixture=1)['name'])
def test_find_tlds(self):
# Ensure we have no tlds to start with.
tlds = self.central_service.find_tlds(self.admin_context)
self.assertEqual(len(tlds), 0)
# Create a single tld
self.create_tld(fixture=0)
# Ensure we can retrieve the newly created tld
tlds = self.central_service.find_tlds(self.admin_context)
self.assertEqual(len(tlds), 1)
self.assertEqual(tlds[0]['name'],
self.get_tld_fixture(fixture=0)['name'])
# Create a second tld
self.create_tld(fixture=1)
# Ensure we can retrieve both tlds
tlds = self.central_service.find_tlds(self.admin_context)
self.assertEqual(len(tlds), 2)
self.assertEqual(tlds[0]['name'],
self.get_tld_fixture(fixture=0)['name'])
self.assertEqual(tlds[1]['name'],
self.get_tld_fixture(fixture=1)['name'])
def test_get_tld(self):
# Create a tld
tld_name = 'ns%d.co.uk' % random.randint(10, 1000)
expected_tld = self.create_tld(name=tld_name)
# Retrieve it, and ensure it's the same
tld = self.central_service.get_tld(
self.admin_context, expected_tld['id'])
self.assertEqual(tld['id'], expected_tld['id'])
self.assertEqual(tld['name'], expected_tld['name'])
def test_update_tld(self):
# Create a tld
expected_tld = self.create_tld(fixture=0)
# Update the tld
values = dict(name='prefix.%s' % expected_tld['name'])
self.central_service.update_tld(
self.admin_context, expected_tld['id'], values=values)
# Fetch the tld again
tld = self.central_service.get_tld(
self.admin_context, expected_tld['id'])
# Ensure the tld was updated correctly
self.assertEqual(tld['name'], 'prefix.%s' % expected_tld['name'])
def test_delete_tld(self):
# Create a tld
tld = self.create_tld(fixture=0)
# Delete the tld
self.central_service.delete_tld(self.admin_context, tld['id'])
# Fetch the tld again, ensuring an exception is raised
self.assertRaises(
exceptions.TLDNotFound,
self.central_service.get_tld,
self.admin_context, tld['id'])
# TsigKey Tests
def test_create_tsigkey(self):
values = self.get_tsigkey_fixture(fixture=0)
@ -344,6 +423,13 @@ class CentralServiceTest(CentralTestCase):
self._test_create_domain(values)
def test_idn_create_domain_over_tld(self):
values = dict(
name='xn--3e0b707e'
)
# Create the appropriate TLD
self.central_service.create_tld(self.admin_context, values=values)
# Test creation of a domain in 한국 (kr)
values = dict(
name='example.xn--3e0b707e.',
@ -351,28 +437,6 @@ class CentralServiceTest(CentralTestCase):
)
self._test_create_domain(values)
def test_create_domain_over_re_effective_tld(self):
values = dict(
name='example.co.uk.',
email='info@example.co.uk'
)
self._test_create_domain(values)
def test_create_domain_over_effective_tld(self):
values = dict(
name='example.com.ac.',
email='info@example.com.ac'
)
self._test_create_domain(values)
def test_idn_create_domain_over_effective_tld(self):
# Test creation of a domain in 公司.cn
values = dict(
name='example.xn--55qx5d.cn.',
email='info@example.xn--55qx5d.cn'
)
self._test_create_domain(values)
def test_create_domain_over_quota(self):
self.config(quota_domains=1)
@ -461,16 +525,6 @@ class CentralServiceTest(CentralTestCase):
self.admin_context, values=values)
def _test_create_domain_fail(self, values, exception):
self.config(accepted_tlds_file='tlds-alpha-by-domain.txt.sample',
effective_tlds_file='effective_tld_names.dat.sample',
group='service:central')
# The above configuration values are not overriden at the time when
# the initializer is called to load the accepted and effective tld
# lists. So I need to call them again explicitly to load the correct
# values
self.central_service.effective_tld._load_accepted_tld_list()
self.central_service.effective_tld._load_effective_tld_list()
with testtools.ExpectedException(exception):
# Create an invalid domain
@ -478,67 +532,31 @@ class CentralServiceTest(CentralTestCase):
self.admin_context, values=values)
def test_create_domain_invalid_tld_fail(self):
self.config(accepted_tlds_file='tlds-alpha-by-domain.txt.sample',
effective_tlds_file='effective_tld_names.dat.sample',
group='service:central')
# The above configuration values are not overriden at the time when
# the initializer is called to load the accepted and effective tld
# lists. So I need to call them again explicitly to load the correct
# values
self.central_service.effective_tld._load_accepted_tld_list()
self.central_service.effective_tld._load_effective_tld_list()
# Create a server
self.create_server()
# add a tld for com
self.create_tld(fixture=0)
values = dict(
name='invalid.cOM.',
email='info@invalid.com'
name='example.com.',
email='info@example.com'
)
# Create a valid domain
self.central_service.create_domain(self.admin_context, values=values)
values = dict(
name='invalid.NeT1.',
email='info@invalid.com'
name='example.net.',
email='info@example.net'
)
with testtools.ExpectedException(exceptions.InvalidTLD):
# There is no TLD for net so it should fail
with testtools.ExpectedException(exceptions.InvalidDomainName):
# Create an invalid domain
self.central_service.create_domain(
self.admin_context, values=values)
def test_create_domain_effective_tld_fail(self):
values = dict(
name='co.ug',
email='info@invalid.com'
)
self._test_create_domain_fail(
values, exceptions.DomainIsSameAsAnEffectiveTLD)
def test_idn_create_domain_effective_tld_fail(self):
# Test creation of the effective TLD - brønnøysund.no
values = dict(
name='xn--brnnysund-m8ac.no',
email='info@invalid.com'
)
self._test_create_domain_fail(
values, exceptions.DomainIsSameAsAnEffectiveTLD)
def test_create_domain_re_effective_tld_fail(self):
# co.uk is in the regular expression list for effective_tlds
values = dict(
name='co.uk',
email='info@invalid.com'
)
self._test_create_domain_fail(
values, exceptions.DomainIsSameAsAnEffectiveTLD)
def test_find_domains(self):
# Ensure we have no domains to start with.
domains = self.central_service.find_domains(self.admin_context)

View File

@ -35,9 +35,6 @@ backend_driver = powerdns
# List of blacklist domain name regexes
#domain_name_blacklist = \.arpa\.$, \.novalocal\.$, \.localhost\.$, \.localdomain\.$, \.local\.$
# Accepted TLD list - http://data.iana.org/TLD/tlds-alpha-by-domain.txt
#accepted_tld_list = COM, NET, ORG, IE, UK, ...
# Maximum domain name length
max_domain_name_len = 255

View File

@ -38,16 +38,6 @@ root_helper = sudo
# List of blacklist domain name regexes
#domain_name_blacklist = \.arpa\.$, \.novalocal\.$, \.localhost\.$, \.localdomain\.$, \.local\.$
# Accepted TLDs
# This is a local copy of the list at
# http://data.iana.org/TLD/tlds-alpha-by-domain.txt
#accepted_tlds_file = tlds-alpha-by-domain.txt
# Effective TLDs
# This is a local copy of the list at http://publicsuffix.org/list/
# This contains domain names that effectively act like TLDs e.g. co.uk or tx.us
#effective_tlds_file = effective_tld_names.dat
# Maximum domain name length
#max_domain_name_len = 255

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,12 @@
"update_server": "rule:admin",
"delete_server": "rule:admin",
"create_tld": "rule:admin",
"find_tlds": "rule:admin",
"get_tld": "rule:admin",
"update_tld": "rule:admin",
"delete_tld": "rule:admin",
"create_tsigkey": "rule:admin",
"find_tsigkeys": "rule:admin",
"get_tsigkey": "rule:admin",

View File

@ -1,338 +0,0 @@
# The latest version of the file can be obtained from
# http://data.iana.org/TLD/tlds-alpha-by-domain.txt
# Commented lines in this file begin with a #.
# There is one entry per line and the entries are in the IDNA format.
#
# Version 2013111800, Last Updated Mon Nov 18 07:07:01 2013 UTC
AC
AD
AE
AERO
AF
AG
AI
AL
AM
AN
AO
AQ
AR
ARPA
AS
ASIA
AT
AU
AW
AX
AZ
BA
BB
BD
BE
BF
BG
BH
BI
BIKE
BIZ
BJ
BM
BN
BO
BR
BS
BT
BV
BW
BY
BZ
CA
CAMERA
CAT
CC
CD
CF
CG
CH
CI
CK
CL
CLOTHING
CM
CN
CO
COM
CONSTRUCTION
CONTRACTORS
COOP
CR
CU
CV
CW
CX
CY
CZ
DE
DJ
DK
DM
DO
DZ
EC
EDU
EE
EG
EQUIPMENT
ER
ES
ESTATE
ET
EU
FI
FJ
FK
FM
FO
FR
GA
GALLERY
GB
GD
GE
GF
GG
GH
GI
GL
GM
GN
GOV
GP
GQ
GR
GRAPHICS
GS
GT
GU
GURU
GW
GY
HK
HM
HN
HOLDINGS
HR
HT
HU
ID
IE
IL
IM
IN
INFO
INT
IO
IQ
IR
IS
IT
JE
JM
JO
JOBS
JP
KE
KG
KH
KI
KM
KN
KP
KR
KW
KY
KZ
LA
LAND
LB
LC
LI
LIGHTING
LK
LR
LS
LT
LU
LV
LY
MA
MC
MD
ME
MG
MH
MIL
MK
ML
MM
MN
MO
MOBI
MP
MQ
MR
MS
MT
MU
MUSEUM
MV
MW
MX
MY
MZ
NA
NAME
NC
NE
NET
NF
NG
NI
NL
NO
NP
NR
NU
NZ
OM
ORG
PA
PE
PF
PG
PH
PK
PL
PLUMBING
PM
PN
POST
PR
PRO
PS
PT
PW
PY
QA
RE
RO
RS
RU
RW
SA
SB
SC
SD
SE
SEXY
SG
SH
SI
SINGLES
SJ
SK
SL
SM
SN
SO
SR
ST
SU
SV
SX
SY
SZ
TATTOO
TC
TD
TECHNOLOGY
TEL
TF
TG
TH
TJ
TK
TL
TM
TN
TO
TP
TR
TRAVEL
TT
TV
TW
TZ
UA
UG
UK
US
UY
UZ
VA
VC
VE
VENTURES
VG
VI
VN
VOYAGE
VU
WF
WS
XN--3E0B707E
XN--45BRJ9C
XN--80AO21A
XN--80ASEHDB
XN--80ASWG
XN--90A3AC
XN--CLCHC0EA0B2G2A9GCD
XN--FIQS8S
XN--FIQZ9S
XN--FPCRJ9C3D
XN--FZC2C9E2C
XN--GECRJ9C
XN--H2BRJ9C
XN--J1AMH
XN--J6W193G
XN--KPRW13D
XN--KPRY57D
XN--L1ACC
XN--LGBBAT1AD8J
XN--MGB9AWBF
XN--MGBA3A4F16A
XN--MGBAAM7A8H
XN--MGBAYH7GPA
XN--MGBBH1A71E
XN--MGBC0A9AZCG
XN--MGBERP4A5D4AR
XN--MGBX4CD0AB
XN--NGBC5AZD
XN--O3CW4H
XN--OGBPF8FL
XN--P1AI
XN--PGBS0DH
XN--S9BRJ9C
XN--UNUP4Y
XN--WGBH1C
XN--WGBL6A
XN--XKC2AL3HYE2A
XN--XKC2DL3A5EE0H
XN--YFRO4I67O
XN--YGBI2AMMX
XXX
YE
YT
ZA
ZM
ZW