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:
parent
517cb0370c
commit
8666dde1ac
@ -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()
|
||||
|
126
designate/api/v2/controllers/tlds.py
Normal file
126
designate/api/v2/controllers/tlds.py
Normal 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 ''
|
49
designate/api/v2/views/tlds.py
Normal file
49
designate/api/v2/views/tlds.py
Normal 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
|
@ -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,
|
||||
|
@ -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
|
@ -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.")
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
||||
|
63
designate/resources/schemas/v2/tld.json
Normal file
63
designate/resources/schemas/v2/tld.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
designate/resources/schemas/v2/tlds.json
Normal file
38
designate/resources/schemas/v2/tlds.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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):
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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:
|
||||
|
@ -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()
|
@ -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__ = (
|
||||
|
@ -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)
|
||||
|
135
designate/tests/test_api/test_v2/test_tlds.py
Normal file
135
designate/tests/test_api/test_v2/test_tlds.py
Normal 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'])
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
@ -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",
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user