Support secondary zones
Change-Id: If9fd6351087fcfd1873cb9adb2bcf753ce42700d
This commit is contained in:
parent
963e4d0151
commit
d801098b0f
@ -62,6 +62,10 @@ def create_domain():
|
||||
domain_schema.validate(values)
|
||||
|
||||
central_api = central_rpcapi.CentralAPI.get_instance()
|
||||
|
||||
# A V1 zone only supports being a primary (No notion of a type)
|
||||
values['type'] = 'PRIMARY'
|
||||
|
||||
domain = central_api.create_domain(context, objects.Domain(**values))
|
||||
|
||||
response = flask.jsonify(domain_schema.filter(domain))
|
||||
@ -76,7 +80,8 @@ def get_domains():
|
||||
context = flask.request.environ.get('context')
|
||||
|
||||
central_api = central_rpcapi.CentralAPI.get_instance()
|
||||
domains = central_api.find_domains(context)
|
||||
|
||||
domains = central_api.find_domains(context, criterion={"type": "PRIMARY"})
|
||||
|
||||
return flask.jsonify(domains_schema.filter({'domains': domains}))
|
||||
|
||||
@ -86,7 +91,9 @@ def get_domain(domain_id):
|
||||
context = flask.request.environ.get('context')
|
||||
|
||||
central_api = central_rpcapi.CentralAPI.get_instance()
|
||||
domain = central_api.get_domain(context, domain_id)
|
||||
|
||||
criterion = {"id": domain_id, "type": "PRIMARY"}
|
||||
domain = central_api.find_domain(context, criterion=criterion)
|
||||
|
||||
return flask.jsonify(domain_schema.filter(domain))
|
||||
|
||||
@ -99,7 +106,8 @@ def update_domain(domain_id):
|
||||
central_api = central_rpcapi.CentralAPI.get_instance()
|
||||
|
||||
# Fetch the existing resource
|
||||
domain = central_api.get_domain(context, domain_id)
|
||||
criterion = {"id": domain_id, "type": "PRIMARY"}
|
||||
domain = central_api.find_domain(context, criterion=criterion)
|
||||
|
||||
# Prepare a dict of fields for validation
|
||||
domain_data = domain_schema.filter(domain)
|
||||
@ -120,6 +128,11 @@ def delete_domain(domain_id):
|
||||
context = flask.request.environ.get('context')
|
||||
|
||||
central_api = central_rpcapi.CentralAPI.get_instance()
|
||||
|
||||
# TODO(ekarlso): Fix this to something better.
|
||||
criterion = {"id": domain_id, "type": "PRIMARY"}
|
||||
central_api.find_domain(context, criterion=criterion)
|
||||
|
||||
central_api.delete_domain(context, domain_id)
|
||||
|
||||
return flask.Response(status=200)
|
||||
@ -131,6 +144,10 @@ def get_domain_servers(domain_id):
|
||||
|
||||
central_api = central_rpcapi.CentralAPI.get_instance()
|
||||
|
||||
# TODO(ekarlso): Fix this to something better.
|
||||
criterion = {"id": domain_id, "type": "PRIMARY"}
|
||||
central_api.find_domain(context, criterion=criterion)
|
||||
|
||||
nameservers = central_api.get_domain_servers(context, domain_id)
|
||||
|
||||
servers = objects.ServerList()
|
||||
|
@ -40,6 +40,11 @@ def _find_recordset(context, domain_id, name, type):
|
||||
|
||||
|
||||
def _find_or_create_recordset(context, domain_id, name, type, ttl):
|
||||
central_api = central_rpcapi.CentralAPI.get_instance()
|
||||
|
||||
criterion = {"id": domain_id, "type": "PRIMARY"}
|
||||
central_api.find_domain(context, criterion=criterion)
|
||||
|
||||
try:
|
||||
recordset = _find_recordset(context, domain_id, name, type)
|
||||
except exceptions.RecordSetNotFound:
|
||||
@ -49,7 +54,6 @@ def _find_or_create_recordset(context, domain_id, name, type, ttl):
|
||||
'type': type,
|
||||
'ttl': ttl,
|
||||
}
|
||||
central_api = central_rpcapi.CentralAPI.get_instance()
|
||||
|
||||
recordset = central_api.create_recordset(
|
||||
context, domain_id, objects.RecordSet(**values))
|
||||
@ -193,7 +197,8 @@ def update_record(domain_id, record_id):
|
||||
|
||||
# NOTE: We need to ensure the domain actually exists, otherwise we may
|
||||
# return a record not found instead of a domain not found
|
||||
central_api.get_domain(context, domain_id)
|
||||
criterion = {"id": domain_id, "type": "PRIMARY"}
|
||||
central_api.find_domain(context, criterion)
|
||||
|
||||
# Fetch the existing resource
|
||||
# NOTE(kiall): We use "find_record" rather than "get_record" as we do not
|
||||
@ -251,7 +256,8 @@ def delete_record(domain_id, record_id):
|
||||
|
||||
# NOTE: We need to ensure the domain actually exists, otherwise we may
|
||||
# return a record not found instead of a domain not found
|
||||
central_api.get_domain(context, domain_id)
|
||||
criterion = {"id": domain_id, "type": "PRIMARY"}
|
||||
central_api.find_domain(context, criterion=criterion)
|
||||
|
||||
# Find the record
|
||||
criterion = {'domain_id': domain_id, 'id': record_id}
|
||||
|
@ -16,6 +16,7 @@
|
||||
import pecan
|
||||
from dns import zone as dnszone
|
||||
from dns import exception as dnsexception
|
||||
from oslo.config import cfg
|
||||
|
||||
from designate import exceptions
|
||||
from designate import utils
|
||||
@ -26,7 +27,10 @@ from designate.api.v2.controllers import nameservers
|
||||
from designate.api.v2.controllers import recordsets
|
||||
from designate.api.v2.controllers.zones import tasks
|
||||
from designate.api.v2.views import zones as zones_view
|
||||
from designate.objects import Domain
|
||||
from designate import objects
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class ZonesController(rest.RestController):
|
||||
@ -134,14 +138,30 @@ class ZonesController(rest.RestController):
|
||||
"""'Normal' zone creation"""
|
||||
body = request.body_dict
|
||||
|
||||
# We need to check the zone type before validating the schema since if
|
||||
# it's the type is SECONDARY we need to set the email to the mgmt email
|
||||
zone = body.get('zone')
|
||||
if isinstance(zone, dict):
|
||||
if 'type' not in zone:
|
||||
zone['type'] = 'PRIMARY'
|
||||
|
||||
if zone['type'] == 'SECONDARY':
|
||||
mgmt_email = CONF['service:central'].managed_resource_email
|
||||
body['zone']['email'] = mgmt_email
|
||||
|
||||
# Validate the request conforms to the schema
|
||||
self._resource_schema.validate(body)
|
||||
|
||||
# Convert from APIv2 -> Central format
|
||||
values = self._view.load(context, request, body)
|
||||
|
||||
# TODO(ekarlso): Fix this once setter or so works.
|
||||
masters = values.pop('masters', [])
|
||||
zone = objects.Domain.from_dict(values)
|
||||
zone.set_masters(masters)
|
||||
|
||||
# Create the zone
|
||||
zone = self.central_api.create_domain(context, Domain(**values))
|
||||
zone = self.central_api.create_domain(context, zone)
|
||||
|
||||
# Prepare the response headers
|
||||
# If the zone has been created asynchronously
|
||||
@ -168,7 +188,9 @@ class ZonesController(rest.RestController):
|
||||
# records are taken care of in _create_zone).
|
||||
check_origin=False)
|
||||
domain = dnsutils.from_dnspython_zone(dnspython_zone)
|
||||
for rrset in domain.recordsets:
|
||||
domain.type = 'PRIMARY'
|
||||
|
||||
for rrset in list(domain.recordsets):
|
||||
if rrset.type in ('NS', 'SOA'):
|
||||
domain.recordsets.remove(rrset)
|
||||
|
||||
@ -229,9 +251,26 @@ class ZonesController(rest.RestController):
|
||||
# Validate the new set of data
|
||||
self._resource_schema.validate(zone_data)
|
||||
|
||||
# Unpack the values
|
||||
values = self._view.load(context, request, body)
|
||||
|
||||
zone.set_masters(values.pop('masters', []))
|
||||
|
||||
# If masters are specified then we set zone.transferred_at to None
|
||||
# which will cause a new transfer
|
||||
if 'attributes' in zone.obj_what_changed():
|
||||
zone.transferred_at = None
|
||||
|
||||
# Update and persist the resource
|
||||
zone.update(self._view.load(context, request, body))
|
||||
zone = self.central_api.update_domain(context, zone)
|
||||
zone.update(values)
|
||||
|
||||
if zone.type == 'SECONDARY' and 'email' in zone.obj_what_changed():
|
||||
msg = "Changed email is not allowed."
|
||||
raise exceptions.InvalidObject(msg)
|
||||
|
||||
increment_serial = zone.type == 'PRIMARY'
|
||||
zone = self.central_api.update_domain(
|
||||
context, zone, increment_serial=increment_serial)
|
||||
|
||||
if zone.status == 'PENDING':
|
||||
response.status_int = 202
|
||||
|
@ -29,11 +29,12 @@ class ZonesView(base_view.BaseView):
|
||||
|
||||
def show_basic(self, context, request, zone):
|
||||
"""Basic view of a zone"""
|
||||
return {
|
||||
values = {
|
||||
"id": zone['id'],
|
||||
"pool_id": zone['pool_id'],
|
||||
"project_id": zone['tenant_id'],
|
||||
"name": zone['name'],
|
||||
"type": zone['type'],
|
||||
"email": zone['email'],
|
||||
"description": zone['description'],
|
||||
"ttl": zone['ttl'],
|
||||
@ -43,10 +44,14 @@ class ZonesView(base_view.BaseView):
|
||||
"version": zone['version'],
|
||||
"created_at": zone['created_at'],
|
||||
"updated_at": zone['updated_at'],
|
||||
"transferred_at": zone['transferred_at'],
|
||||
"masters": zone.masters,
|
||||
"links": self._get_resource_links(request, zone)
|
||||
}
|
||||
|
||||
return values
|
||||
|
||||
def load(self, context, request, body):
|
||||
"""Extract a "central" compatible dict from an API call"""
|
||||
valid_keys = ('name', 'email', 'description', 'ttl')
|
||||
return self._load(context, request, body, valid_keys)
|
||||
zone_keys = ('name', 'description', 'type', 'email', 'masters', 'ttl')
|
||||
return self._load(context, request, body, zone_keys)
|
||||
|
@ -482,6 +482,7 @@ class Service(service.RPCService, service.Service):
|
||||
zone['minimum'])
|
||||
|
||||
def _create_soa(self, context, zone):
|
||||
# Need elevated context to get the servers
|
||||
elevated_context = context.elevated()
|
||||
elevated_context.all_tenants = True
|
||||
|
||||
@ -505,6 +506,9 @@ class Service(service.RPCService, service.Service):
|
||||
return soa
|
||||
|
||||
def _update_soa(self, context, zone):
|
||||
# NOTE: We should not be updating SOA records when a zone is SECONDARY.
|
||||
if zone.type != 'PRIMARY':
|
||||
return
|
||||
|
||||
nameservers = self.get_domain_servers(context, zone['id'])
|
||||
|
||||
@ -519,6 +523,10 @@ class Service(service.RPCService, service.Service):
|
||||
|
||||
# NS Recordset Methods
|
||||
def _create_ns(self, context, zone, nameservers):
|
||||
# NOTE: We should not be creating NS records when a zone is SECONDARY.
|
||||
if zone.type != 'PRIMARY':
|
||||
return
|
||||
|
||||
# Create an NS record for each server
|
||||
ns_values = []
|
||||
for s in nameservers:
|
||||
@ -537,6 +545,10 @@ class Service(service.RPCService, service.Service):
|
||||
return ns
|
||||
|
||||
def _update_ns(self, context, zone, orig_name, new_name):
|
||||
# NOTE: We should not be updating NS records when a zone is SECONDARY.
|
||||
if zone.type != 'PRIMARY':
|
||||
return
|
||||
|
||||
# Get the zone's NS recordset
|
||||
ns = self.find_recordset(context,
|
||||
criterion={'domain_id': zone['id'],
|
||||
@ -826,6 +838,9 @@ class Service(service.RPCService, service.Service):
|
||||
'Please create at least one nameserver'))
|
||||
raise exceptions.NoServersConfigured()
|
||||
|
||||
if domain.type == 'SECONDARY' and domain.serial is None:
|
||||
domain.serial = 1
|
||||
|
||||
domain = self._create_domain_in_storage(context, domain)
|
||||
|
||||
self.pool_manager_api.create_domain(context, domain)
|
||||
@ -847,9 +862,6 @@ class Service(service.RPCService, service.Service):
|
||||
domain.action = 'CREATE'
|
||||
domain.status = 'PENDING'
|
||||
|
||||
# Set the serial number
|
||||
domain.serial = utils.increment_serial()
|
||||
|
||||
domain = self.storage.create_domain(context, domain)
|
||||
nameservers = self.get_domain_servers(context, domain['id'])
|
||||
|
||||
@ -1079,6 +1091,7 @@ class Service(service.RPCService, service.Service):
|
||||
target = {
|
||||
'domain_id': domain_id,
|
||||
'domain_name': domain.name,
|
||||
'domain_type': domain.type,
|
||||
'recordset_name': recordset.name,
|
||||
'tenant_id': domain.tenant_id,
|
||||
}
|
||||
@ -1190,6 +1203,7 @@ class Service(service.RPCService, service.Service):
|
||||
|
||||
target = {
|
||||
'domain_id': recordset.obj_get_original_value('domain_id'),
|
||||
'domain_type': domain.type,
|
||||
'recordset_id': recordset.obj_get_original_value('id'),
|
||||
'domain_name': domain.name,
|
||||
'tenant_id': domain.tenant_id
|
||||
@ -1253,6 +1267,7 @@ class Service(service.RPCService, service.Service):
|
||||
target = {
|
||||
'domain_id': domain_id,
|
||||
'domain_name': domain.name,
|
||||
'domain_type': domain.type,
|
||||
'recordset_id': recordset.id,
|
||||
'tenant_id': domain.tenant_id
|
||||
}
|
||||
@ -1305,11 +1320,13 @@ class Service(service.RPCService, service.Service):
|
||||
def create_record(self, context, domain_id, recordset_id, record,
|
||||
increment_serial=True):
|
||||
domain = self.storage.get_domain(context, domain_id)
|
||||
|
||||
recordset = self.storage.get_recordset(context, recordset_id)
|
||||
|
||||
target = {
|
||||
'domain_id': domain_id,
|
||||
'domain_name': domain.name,
|
||||
'domain_type': domain.type,
|
||||
'recordset_id': recordset_id,
|
||||
'recordset_name': recordset.name,
|
||||
'tenant_id': domain.tenant_id
|
||||
@ -1413,6 +1430,7 @@ class Service(service.RPCService, service.Service):
|
||||
target = {
|
||||
'domain_id': record.obj_get_original_value('domain_id'),
|
||||
'domain_name': domain.name,
|
||||
'domain_type': domain.type,
|
||||
'recordset_id': record.obj_get_original_value('recordset_id'),
|
||||
'recordset_name': recordset.name,
|
||||
'record_id': record.obj_get_original_value('id'),
|
||||
@ -1451,6 +1469,7 @@ class Service(service.RPCService, service.Service):
|
||||
def delete_record(self, context, domain_id, recordset_id, record_id,
|
||||
increment_serial=True):
|
||||
domain = self.storage.get_domain(context, domain_id)
|
||||
|
||||
recordset = self.storage.get_recordset(context, recordset_id)
|
||||
record = self.storage.get_record(context, record_id)
|
||||
|
||||
@ -1465,6 +1484,7 @@ class Service(service.RPCService, service.Service):
|
||||
target = {
|
||||
'domain_id': domain_id,
|
||||
'domain_name': domain.name,
|
||||
'domain_type': domain.type,
|
||||
'recordset_id': recordset_id,
|
||||
'recordset_name': recordset.name,
|
||||
'record_id': record.id,
|
||||
@ -1778,6 +1798,7 @@ class Service(service.RPCService, service.Service):
|
||||
tenant_id = cfg.CONF['service:central'].managed_resource_tenant_id
|
||||
|
||||
zone_values = {
|
||||
'type': 'PRIMARY',
|
||||
'name': zone_name,
|
||||
'email': email,
|
||||
'tenant_id': tenant_id
|
||||
|
@ -244,6 +244,10 @@ class DuplicatePoolAttribute(Duplicate):
|
||||
error_type = 'duplicate_pool_attribute'
|
||||
|
||||
|
||||
class DuplicateDomainAttribute(Duplicate):
|
||||
error_type = 'duplicate_domain_attribute'
|
||||
|
||||
|
||||
class MethodNotAllowed(Base):
|
||||
expected = True
|
||||
error_code = 405
|
||||
@ -284,6 +288,10 @@ class DomainNotFound(NotFound):
|
||||
error_type = 'domain_not_found'
|
||||
|
||||
|
||||
class DomainAttributeNotFound(NotFound):
|
||||
error_type = 'domain_attribute_not_found'
|
||||
|
||||
|
||||
class TldNotFound(NotFound):
|
||||
error_type = 'tld_not_found'
|
||||
|
||||
|
@ -21,6 +21,7 @@ from designate.objects.base import PagedListObjectMixin # noqa
|
||||
from designate.objects.backend_option import BackendOption, BackendOptionList # noqa
|
||||
from designate.objects.blacklist import Blacklist, BlacklistList # noqa
|
||||
from designate.objects.domain import Domain, DomainList # noqa
|
||||
from designate.objects.domain_attribute import DomainAttribute, DomainAttributeList # noqa
|
||||
from designate.objects.pool_manager_status import PoolManagerStatus, PoolManagerStatusList # noqa
|
||||
from designate.objects.pool_server import PoolServer, PoolServerList # noqa
|
||||
from designate.objects.pool import Pool, PoolList # noqa
|
||||
|
@ -13,6 +13,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from designate.objects import base
|
||||
from designate.objects.domain_attribute import DomainAttribute
|
||||
from designate.objects.domain_attribute import DomainAttributeList
|
||||
|
||||
|
||||
class Domain(base.DictObjectMixin, base.SoftDeleteObjectMixin,
|
||||
@ -36,8 +38,30 @@ class Domain(base.DictObjectMixin, base.SoftDeleteObjectMixin,
|
||||
'relation': True,
|
||||
'relation_cls': 'RecordSetList'
|
||||
},
|
||||
'attributes': {
|
||||
'relation': True,
|
||||
'relation_cls': 'DomainAttributeList'
|
||||
},
|
||||
'type': {},
|
||||
'transferred_at': {},
|
||||
}
|
||||
|
||||
@property
|
||||
def masters(self):
|
||||
if self.obj_attr_is_set('attributes'):
|
||||
return [i.value for i in self.attributes if i.key == 'master']
|
||||
else:
|
||||
return None
|
||||
|
||||
# TODO(ekarlso): Make this a property sette rpr Kiall's comments later.
|
||||
def set_masters(self, masters):
|
||||
attributes = DomainAttributeList()
|
||||
|
||||
for m in masters:
|
||||
obj = DomainAttribute(key='master', value=m)
|
||||
attributes.append(obj)
|
||||
self.attributes = attributes
|
||||
|
||||
|
||||
class DomainList(base.ListObjectMixin, base.DesignateObject,
|
||||
base.PagedListObjectMixin):
|
||||
|
29
designate/objects/domain_attribute.py
Normal file
29
designate/objects/domain_attribute.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@hp.com>
|
||||
#
|
||||
# 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.objects import base
|
||||
|
||||
|
||||
class DomainAttribute(base.DictObjectMixin, base.PersistentObjectMixin,
|
||||
base.DesignateObject):
|
||||
FIELDS = {
|
||||
'domain_id': {},
|
||||
'key': {},
|
||||
'value': {}
|
||||
}
|
||||
|
||||
|
||||
class DomainAttributeList(base.ListObjectMixin, base.DesignateObject):
|
||||
LIST_ITEM_TYPE = DomainAttribute
|
@ -1,3 +1,4 @@
|
||||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/hyper-schema",
|
||||
|
||||
@ -13,7 +14,7 @@
|
||||
"zone": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "email"],
|
||||
"required": ["name"],
|
||||
|
||||
"properties": {
|
||||
"id": {
|
||||
@ -41,6 +42,19 @@
|
||||
"maxLength": 255,
|
||||
"immutable": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Zone Type",
|
||||
"enum": ["PRIMARY", "SECONDARY"]
|
||||
},
|
||||
"masters": {
|
||||
"type": ["array", "null"],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Masters for this Zone",
|
||||
"uniqueItems": true
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "Hostmaster email address",
|
||||
@ -73,7 +87,7 @@
|
||||
"serial": {
|
||||
"type": "integer",
|
||||
"description": "Zone serial number",
|
||||
"minimum": 1,
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295,
|
||||
"readOnly": true
|
||||
},
|
||||
@ -94,6 +108,12 @@
|
||||
"format": "date-time",
|
||||
"readOnly": true
|
||||
},
|
||||
"transferred_at": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Date and time of last successful transfer",
|
||||
"format": "date-time",
|
||||
"readOnly": true
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@ -105,7 +125,43 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Primary zone",
|
||||
"required": ["email"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["PRIMARY"]
|
||||
},
|
||||
"masters": {
|
||||
"type": ["null", "array"],
|
||||
"maxItems": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Secondary zone",
|
||||
"required": ["type", "masters"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["SECONDARY"]
|
||||
},
|
||||
"masters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "ipandport"
|
||||
},
|
||||
"description": "Masters for this Zone",
|
||||
"uniqueItems": true,
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -33,6 +33,12 @@ RE_TLDNAME = r'^(?!.{255,})(?:(?!\-)[A-Za-z0-9_\-]{1,63}(?<!\-))' \
|
||||
RE_UUID = r'^(?:[0-9a-fA-F]){8}-(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){4}-' \
|
||||
r'(?:[0-9a-fA-F]){4}-(?:[0-9a-fA-F]){12}$'
|
||||
|
||||
RE_IP_AND_PORT = r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}' \
|
||||
r'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' \
|
||||
r'(?::(6553[0-5]|655[0-2]\d|65[0-4]\d\d|6[0-4]\d{3}' \
|
||||
r'|[1-5]\d{4}|[1-9]\d{0,3}|0))?$'
|
||||
|
||||
|
||||
draft3_format_checker = jsonschema.draft3_format_checker
|
||||
draft4_format_checker = jsonschema.draft4_format_checker
|
||||
|
||||
@ -135,3 +141,15 @@ def is_uuid(instance):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@draft3_format_checker.checks("ip-and-port")
|
||||
@draft4_format_checker.checks("ipandport")
|
||||
def is_ip_and_port(instance):
|
||||
if not isinstance(instance, compat.str_types):
|
||||
return True
|
||||
|
||||
if not re.match(RE_IP_AND_PORT, instance):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -230,8 +230,18 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
|
||||
exceptions.DomainNotFound, criterion, one, marker, limit,
|
||||
sort_key, sort_dir)
|
||||
|
||||
if not one:
|
||||
def _load_relations(domain):
|
||||
if domain.type == 'SECONDARY':
|
||||
domain.attributes = self._find_domain_attributes(
|
||||
context, {'domain_id': domain.id})
|
||||
domain.obj_reset_changes(['attributes'])
|
||||
|
||||
if one:
|
||||
_load_relations(domains)
|
||||
else:
|
||||
domains.total_count = self.count_domains(context, criterion)
|
||||
for d in domains:
|
||||
_load_relations(d)
|
||||
|
||||
return domains
|
||||
|
||||
@ -240,32 +250,85 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
|
||||
extra_values = {"reverse_name": domain.name[::-1]}
|
||||
|
||||
# Don't handle recordsets for now
|
||||
return self._create(
|
||||
tables.domains, domain, exceptions.DuplicateDomain, ['recordsets'],
|
||||
domain = self._create(
|
||||
tables.domains, domain, exceptions.DuplicateDomain,
|
||||
['attributes', 'recordsets'],
|
||||
extra_values=extra_values)
|
||||
|
||||
if domain.obj_attr_is_set('attributes'):
|
||||
for attrib in domain.attributes:
|
||||
self.create_domain_attribute(context, domain.id, attrib)
|
||||
else:
|
||||
domain.attributes = objects.DomainAttributeList()
|
||||
domain.obj_reset_changes('attributes')
|
||||
|
||||
return domain
|
||||
|
||||
def get_domain(self, context, domain_id):
|
||||
return self._find_domains(context, {'id': domain_id}, one=True)
|
||||
domain = self._find_domains(context, {'id': domain_id}, one=True)
|
||||
return domain
|
||||
|
||||
def find_domains(self, context, criterion=None, marker=None, limit=None,
|
||||
sort_key=None, sort_dir=None):
|
||||
return self._find_domains(context, criterion, marker=marker,
|
||||
limit=limit, sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
domains = self._find_domains(context, criterion, marker=marker,
|
||||
limit=limit, sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
return domains
|
||||
|
||||
def find_domain(self, context, criterion):
|
||||
return self._find_domains(context, criterion, one=True)
|
||||
domain = self._find_domains(context, criterion, one=True)
|
||||
return domain
|
||||
|
||||
def update_domain(self, context, domain):
|
||||
# Don't handle recordsets for now
|
||||
|
||||
tenant_id_changed = False
|
||||
if 'tenant_id' in domain.obj_what_changed():
|
||||
tenant_id_changed = True
|
||||
|
||||
# Don't handle recordsets for now
|
||||
updated_domain = self._update(
|
||||
context, tables.domains, domain, exceptions.DuplicateDomain,
|
||||
exceptions.DomainNotFound, ['recordsets'])
|
||||
exceptions.DomainNotFound,
|
||||
['attributes', 'recordsets'])
|
||||
|
||||
if domain.obj_attr_is_set('attributes'):
|
||||
# Gather the Attribute ID's we have
|
||||
have = set([r.id for r in self._find_domain_attributes(
|
||||
context, {'domain_id': domain.id})])
|
||||
|
||||
# Prep some lists of changes
|
||||
keep = set([])
|
||||
create = []
|
||||
update = []
|
||||
|
||||
# Determine what to change
|
||||
for i in domain.attributes:
|
||||
keep.add(i.id)
|
||||
try:
|
||||
i.obj_get_original_value('id')
|
||||
except KeyError:
|
||||
create.append(i)
|
||||
else:
|
||||
update.append(i)
|
||||
|
||||
# NOTE: Since we're dealing with mutable objects, the return value
|
||||
# of create/update/delete attribute is not needed.
|
||||
# The original item will be mutated in place on the input
|
||||
# "domain.attributes" list.
|
||||
|
||||
# Delete Attributes
|
||||
for i_id in have - keep:
|
||||
attr = self._find_domain_attributes(
|
||||
context, {'id': i_id}, one=True)
|
||||
self.delete_domain_attribute(context, attr.id)
|
||||
|
||||
# Update Attributes
|
||||
for i in update:
|
||||
self.update_domain_attribute(context, i)
|
||||
|
||||
# Create Attributes
|
||||
for attr in create:
|
||||
attr.domain_id = domain.id
|
||||
self.create_domain_attribute(context, domain.id, attr)
|
||||
|
||||
if tenant_id_changed:
|
||||
recordsets_query = tables.recordsets.update().\
|
||||
@ -301,6 +364,48 @@ class SQLAlchemyStorage(sqlalchemy_base.SQLAlchemy, storage_base.Storage):
|
||||
|
||||
return result[0]
|
||||
|
||||
# Domain attribute methods
|
||||
def _find_domain_attributes(self, context, criterion, one=False,
|
||||
marker=None, limit=None, sort_key=None,
|
||||
sort_dir=None):
|
||||
return self._find(context, tables.domain_attributes,
|
||||
objects.DomainAttribute, objects.DomainAttributeList,
|
||||
exceptions.DomainAttributeNotFound, criterion, one,
|
||||
marker, limit, sort_key, sort_dir)
|
||||
|
||||
def create_domain_attribute(self, context, domain_id, domain_attribute):
|
||||
domain_attribute.domain_id = domain_id
|
||||
return self._create(tables.domain_attributes, domain_attribute,
|
||||
exceptions.DuplicateDomainAttribute)
|
||||
|
||||
def get_domain_attributes(self, context, domain_attribute_id):
|
||||
return self._find_domain_attributes(
|
||||
context, {'id': domain_attribute_id}, one=True)
|
||||
|
||||
def find_domain_attributes(self, context, criterion=None, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
return self._find_domain_attributes(context, criterion, marker=marker,
|
||||
limit=limit, sort_key=sort_key,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
def find_domain_attribute(self, context, criterion):
|
||||
return self._find_domain_attributes(context, criterion, one=True)
|
||||
|
||||
def update_domain_attribute(self, context, domain_attribute):
|
||||
return self._update(context, tables.domain_attributes,
|
||||
domain_attribute,
|
||||
exceptions.DuplicateDomainAttribute,
|
||||
exceptions.DomainAttributeNotFound)
|
||||
|
||||
def delete_domain_attribute(self, context, domain_attribute_id):
|
||||
domain_attribute = self._find_domain_attributes(
|
||||
context, {'id': domain_attribute_id}, one=True)
|
||||
deleted_domain_attribute = self._delete(
|
||||
context, tables.domain_attributes, domain_attribute,
|
||||
exceptions.DomainAttributeNotFound)
|
||||
|
||||
return deleted_domain_attribute
|
||||
|
||||
# RecordSet Methods
|
||||
def _find_recordsets(self, context, criterion, one=False, marker=None,
|
||||
limit=None, sort_key=None, sort_dir=None):
|
||||
|
@ -0,0 +1,105 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Author: Endre Karlson <endre.karlson@hp.com>
|
||||
#
|
||||
# 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 oslo.utils import timeutils
|
||||
|
||||
from sqlalchemy import DateTime, Enum, Integer, String, ForeignKeyConstraint
|
||||
from sqlalchemy.schema import Column, MetaData, Table
|
||||
from sqlalchemy.sql import select
|
||||
from migrate.changeset.constraint import UniqueConstraint
|
||||
|
||||
from designate import utils
|
||||
from designate.sqlalchemy.types import UUID
|
||||
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
ZONE_ATTRIBUTE_KEYS = ('master',)
|
||||
|
||||
ZONE_TYPES = ('PRIMARY', 'SECONDARY')
|
||||
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
|
||||
domain_attributes = Table(
|
||||
'domain_attributes', meta,
|
||||
Column('id', UUID(), default=utils.generate_uuid, primary_key=True),
|
||||
Column('version', Integer(), default=1, nullable=False),
|
||||
Column('created_at', DateTime, default=lambda: timeutils.utcnow()),
|
||||
Column('updated_at', DateTime, onupdate=lambda: timeutils.utcnow()),
|
||||
|
||||
Column('key', Enum(name='key', *ZONE_ATTRIBUTE_KEYS)),
|
||||
Column('value', String(255), nullable=False),
|
||||
Column('domain_id', UUID(), nullable=False),
|
||||
|
||||
UniqueConstraint('key', 'value', 'domain_id', name='unique_attributes'),
|
||||
ForeignKeyConstraint(['domain_id'], ['domains.id'], ondelete='CASCADE'),
|
||||
|
||||
mysql_engine='INNODB',
|
||||
mysql_charset='utf8'
|
||||
)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
|
||||
domains_table = Table('domains', meta, autoload=True)
|
||||
|
||||
# Add type and transferred_at to domains
|
||||
type_ = Column('type', Enum(name='type', *ZONE_TYPES), default='PRIMARY',
|
||||
server_default='PRIMARY')
|
||||
transferred_at = Column('transferred_at', DateTime, default=None)
|
||||
|
||||
type_.create(domains_table, populate_default=True)
|
||||
transferred_at.create(domains_table, populate_default=True)
|
||||
|
||||
domain_attributes.create()
|
||||
|
||||
dialect = migrate_engine.url.get_dialect().name
|
||||
if dialect.startswith('sqlite'):
|
||||
constraint = UniqueConstraint(
|
||||
'name', 'deleted', name='unique_domain_name', table=domains_table)
|
||||
|
||||
# Add missing unique index
|
||||
constraint.create()
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
|
||||
domains_table = Table('domains', meta, autoload=True)
|
||||
|
||||
domains = select(columns=[domains_table.c.id, domains_table.c.type])\
|
||||
.where(domains_table.c.type == 'SECONDARY')\
|
||||
.execute().fetchall()
|
||||
|
||||
for dom in domains:
|
||||
delete = domains_table.delete()\
|
||||
.where(domains_table.id == dom.id)
|
||||
delete.execute()
|
||||
|
||||
domains_table.c.type.drop()
|
||||
domains_table.c.transferred_at.drop()
|
||||
|
||||
domain_attributes.drop()
|
||||
|
||||
dialect = migrate_engine.url.get_dialect().name
|
||||
if dialect.startswith('sqlite'):
|
||||
constraint = UniqueConstraint(
|
||||
'name', 'deleted', name='unique_domain_name', table=domains_table)
|
||||
|
||||
# Add missing unique index
|
||||
constraint.create()
|
@ -36,6 +36,11 @@ TSIG_SCOPES = ['POOL', 'ZONE']
|
||||
POOL_PROVISIONERS = ['UNMANAGED']
|
||||
ACTIONS = ['CREATE', 'DELETE', 'UPDATE', 'NONE']
|
||||
|
||||
ZONE_ATTRIBUTE_KEYS = ('master',)
|
||||
|
||||
ZONE_TYPES = ('PRIMARY', 'SECONDARY',)
|
||||
|
||||
|
||||
metadata = MetaData()
|
||||
|
||||
quotas = Table('quotas', metadata,
|
||||
@ -79,6 +84,8 @@ domains = Table('domains', metadata,
|
||||
Column('name', String(255), nullable=False),
|
||||
Column('email', String(255), nullable=False),
|
||||
Column('description', Unicode(160), nullable=True),
|
||||
Column("type", Enum(name='type', *ZONE_TYPES), nullable=False),
|
||||
Column('transferred_at', DateTime, default=None),
|
||||
Column('ttl', Integer, default=CONF.default_ttl, nullable=False),
|
||||
Column('serial', Integer, default=timeutils.utcnow_ts, nullable=False),
|
||||
Column('refresh', Integer, default=CONF.default_soa_refresh,
|
||||
@ -104,6 +111,23 @@ domains = Table('domains', metadata,
|
||||
mysql_charset='utf8',
|
||||
)
|
||||
|
||||
domain_attributes = Table('domain_attributes', metadata,
|
||||
Column('id', UUID(), default=utils.generate_uuid, primary_key=True),
|
||||
Column('version', Integer(), default=1, nullable=False),
|
||||
Column('created_at', DateTime, default=lambda: timeutils.utcnow()),
|
||||
Column('updated_at', DateTime, onupdate=lambda: timeutils.utcnow()),
|
||||
|
||||
Column('key', Enum(name='key', *ZONE_ATTRIBUTE_KEYS)),
|
||||
Column('value', String(255), nullable=False),
|
||||
Column('domain_id', UUID(), nullable=False),
|
||||
|
||||
UniqueConstraint('key', 'value', 'domain_id', name='unique_attributes'),
|
||||
ForeignKeyConstraint(['domain_id'], ['domains.id'], ondelete='CASCADE'),
|
||||
|
||||
mysql_engine='INNODB',
|
||||
mysql_charset='utf8'
|
||||
)
|
||||
|
||||
recordsets = Table('recordsets', metadata,
|
||||
Column('id', UUID, default=utils.generate_uuid, primary_key=True),
|
||||
Column('version', Integer(), default=1, nullable=False),
|
||||
|
@ -101,19 +101,43 @@ class TestCase(base.BaseTestCase):
|
||||
}]
|
||||
|
||||
# The last domain is invalid
|
||||
domain_fixtures = [{
|
||||
'name': 'example.com.',
|
||||
'email': 'example@example.com',
|
||||
}, {
|
||||
'name': 'example.net.',
|
||||
'email': 'example@example.net',
|
||||
}, {
|
||||
'name': 'example.org.',
|
||||
'email': 'example@example.org',
|
||||
}, {
|
||||
'name': 'invalid.com.....',
|
||||
'email': 'example@invalid.com',
|
||||
}]
|
||||
domain_fixtures = {
|
||||
'PRIMARY': [
|
||||
{
|
||||
'name': 'example.com.',
|
||||
'type': 'PRIMARY',
|
||||
'email': 'example@example.com',
|
||||
}, {
|
||||
'name': 'example.net.',
|
||||
'type': 'PRIMARY',
|
||||
'email': 'example@example.net',
|
||||
}, {
|
||||
'name': 'example.org.',
|
||||
'type': 'PRIMARY',
|
||||
'email': 'example@example.org',
|
||||
}, {
|
||||
'name': 'invalid.com.....',
|
||||
'type': 'PRIMARY',
|
||||
'email': 'example@invalid.com',
|
||||
}
|
||||
],
|
||||
'SECONDARY': [
|
||||
{
|
||||
'name': 'example.com.',
|
||||
'type': 'SECONDARY',
|
||||
}, {
|
||||
'name': 'example.net.',
|
||||
'type': 'SECONDARY',
|
||||
}, {
|
||||
'name': 'example.org.',
|
||||
'type': 'SECONDARY',
|
||||
}, {
|
||||
'name': 'invalid.com.....',
|
||||
'type': 'SECONDARY',
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
recordset_fixtures = {
|
||||
'A': [
|
||||
@ -381,10 +405,12 @@ class TestCase(base.BaseTestCase):
|
||||
_values.update(values)
|
||||
return _values
|
||||
|
||||
def get_domain_fixture(self, fixture=0, values=None):
|
||||
def get_domain_fixture(self, domain_type=None, fixture=0, values=None):
|
||||
domain_type = domain_type or 'PRIMARY'
|
||||
values = values or {}
|
||||
|
||||
_values = copy.copy(self.domain_fixtures[fixture])
|
||||
_values = copy.copy(self.domain_fixtures[domain_type][fixture])
|
||||
|
||||
_values.update(values)
|
||||
return _values
|
||||
|
||||
@ -552,6 +578,7 @@ class TestCase(base.BaseTestCase):
|
||||
def create_domain(self, **kwargs):
|
||||
context = kwargs.pop('context', self.admin_context)
|
||||
fixture = kwargs.pop('fixture', 0)
|
||||
domain_type = kwargs.pop('type', None)
|
||||
|
||||
try:
|
||||
# We always need a server to create a server
|
||||
@ -565,7 +592,8 @@ class TestCase(base.BaseTestCase):
|
||||
except exceptions.DuplicatePoolAttribute:
|
||||
pass
|
||||
|
||||
values = self.get_domain_fixture(fixture=fixture, values=kwargs)
|
||||
values = self.get_domain_fixture(domain_type=domain_type,
|
||||
fixture=fixture, values=kwargs)
|
||||
|
||||
if 'tenant_id' not in values:
|
||||
values['tenant_id'] = context.tenant
|
||||
|
@ -18,6 +18,7 @@ import datetime
|
||||
|
||||
from mock import patch
|
||||
from oslo import messaging
|
||||
from oslo.config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate import exceptions
|
||||
@ -36,6 +37,9 @@ class ApiV1DomainsTest(ApiV1Test):
|
||||
# Create a domain
|
||||
fixture = self.get_domain_fixture(0)
|
||||
|
||||
# V1 doesn't have these
|
||||
del fixture['type']
|
||||
|
||||
response = self.post('domains', data=fixture)
|
||||
|
||||
self.assertIn('id', response.json)
|
||||
@ -59,6 +63,9 @@ class ApiV1DomainsTest(ApiV1Test):
|
||||
# Create a domain
|
||||
fixture = self.get_domain_fixture(0)
|
||||
|
||||
# V1 doesn't have these
|
||||
del fixture['type']
|
||||
|
||||
self.post('domains', data=fixture, status_code=500)
|
||||
|
||||
@patch.object(central_service.Service, 'create_domain',
|
||||
@ -67,6 +74,9 @@ class ApiV1DomainsTest(ApiV1Test):
|
||||
# Create a domain
|
||||
fixture = self.get_domain_fixture(0)
|
||||
|
||||
# V1 doesn't have these
|
||||
del fixture['type']
|
||||
|
||||
self.post('domains', data=fixture, status_code=504)
|
||||
|
||||
@patch.object(central_service.Service, 'create_domain',
|
||||
@ -74,6 +84,10 @@ class ApiV1DomainsTest(ApiV1Test):
|
||||
def test_create_domain_duplicate(self, _):
|
||||
# Create a domain
|
||||
fixture = self.get_domain_fixture(0)
|
||||
|
||||
# V1 doesn't have these
|
||||
del fixture['type']
|
||||
|
||||
self.post('domains', data=fixture, status_code=409)
|
||||
|
||||
def test_create_domain_null_ttl(self):
|
||||
@ -101,6 +115,9 @@ class ApiV1DomainsTest(ApiV1Test):
|
||||
# Create a domain
|
||||
fixture = self.get_domain_fixture(0)
|
||||
|
||||
# V1 doesn't have type
|
||||
del fixture['type']
|
||||
|
||||
# Give it a UTF-8 filled description
|
||||
fixture['description'] = "utf-8:2H₂+O₂⇌2H₂O,R=4.7kΩ,⌀200mm∮E⋅da=Q,n" \
|
||||
",∑f(i)=∏g(i),∀x∈ℝ:⌈x⌉"
|
||||
@ -218,7 +235,7 @@ class ApiV1DomainsTest(ApiV1Test):
|
||||
self.assertIn('id', response.json)
|
||||
self.assertEqual(response.json['id'], domain['id'])
|
||||
|
||||
@patch.object(central_service.Service, 'get_domain',
|
||||
@patch.object(central_service.Service, 'find_domain',
|
||||
side_effect=messaging.MessagingTimeout())
|
||||
def test_get_domain_timeout(self, _):
|
||||
# Create a domain
|
||||
@ -353,3 +370,33 @@ class ApiV1DomainsTest(ApiV1Test):
|
||||
|
||||
self.delete('domains/2fdadfb1cf964259ac6bbb7b6d2ff980',
|
||||
status_code=404)
|
||||
|
||||
def test_get_secondary_missing(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 0)
|
||||
fixture['email'] = cfg.CONF['service:central'].managed_resource_email
|
||||
|
||||
domain = self.create_domain(**fixture)
|
||||
|
||||
self.get('domains/%s' % domain.id, status_code=404)
|
||||
|
||||
def test_update_secondary_missing(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 0)
|
||||
fixture['email'] = cfg.CONF['service:central'].managed_resource_email
|
||||
|
||||
domain = self.create_domain(**fixture)
|
||||
|
||||
self.put('domains/%s' % domain.id, {}, status_code=404)
|
||||
|
||||
def test_delete_secondary_missing(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 0)
|
||||
fixture['email'] = cfg.CONF['service:central'].managed_resource_email
|
||||
|
||||
domain = self.create_domain(**fixture)
|
||||
self.delete('domains/%s' % domain.id, status_code=404)
|
||||
|
||||
def test_get_domain_servers_from_secondary(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 0)
|
||||
fixture['email'] = cfg.CONF['service:central'].managed_resource_email
|
||||
|
||||
domain = self.create_domain(**fixture)
|
||||
self.get('domains/%s/servers' % domain.id, status_code=404)
|
||||
|
@ -456,7 +456,7 @@ class ApiV1RecordsTest(ApiV1Test):
|
||||
self.put('domains/%s/records/%s' % (self.domain['id'], record['id']),
|
||||
data=data, status_code=400)
|
||||
|
||||
@patch.object(central_service.Service, 'get_domain',
|
||||
@patch.object(central_service.Service, 'find_domain',
|
||||
side_effect=messaging.MessagingTimeout())
|
||||
def test_update_record_timeout(self, _):
|
||||
# Create a record
|
||||
@ -517,7 +517,7 @@ class ApiV1RecordsTest(ApiV1Test):
|
||||
record['id']),
|
||||
status_code=404)
|
||||
|
||||
@patch.object(central_service.Service, 'get_domain',
|
||||
@patch.object(central_service.Service, 'find_domain',
|
||||
side_effect=messaging.MessagingTimeout())
|
||||
def test_delete_record_timeout(self, _):
|
||||
# Create a record
|
||||
@ -546,3 +546,51 @@ class ApiV1RecordsTest(ApiV1Test):
|
||||
self.delete('domains/%s/records/2fdadfb1-cf96-4259-ac6b-'
|
||||
'bb7b6d2ff980GH' % self.domain['id'],
|
||||
status_code=404)
|
||||
|
||||
def test_get_record_in_secondary(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 1)
|
||||
fixture['email'] = "root@example.com"
|
||||
|
||||
domain = self.create_domain(**fixture)
|
||||
|
||||
record = self.create_record(domain, self.recordset)
|
||||
|
||||
url = 'domains/%s/records/%s' % (domain.id, record.id)
|
||||
self.get(url, status_code=404)
|
||||
|
||||
def test_create_record_in_secondary(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 1)
|
||||
fixture['email'] = "root@example.com"
|
||||
|
||||
domain = self.create_domain(**fixture)
|
||||
|
||||
record = {
|
||||
"name": "foo.%s" % domain.name,
|
||||
"type": "A",
|
||||
"data": "10.0.0.1"
|
||||
}
|
||||
|
||||
url = 'domains/%s/records' % domain.id
|
||||
self.post(url, record, status_code=404)
|
||||
|
||||
def test_update_record_in_secondary(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 1)
|
||||
fixture['email'] = "root@example.com"
|
||||
|
||||
domain = self.create_domain(**fixture)
|
||||
|
||||
record = self.create_record(domain, self.recordset)
|
||||
|
||||
url = 'domains/%s/records/%s' % (domain.id, record.id)
|
||||
self.put(url, {"data": "10.0.0.1"}, status_code=404)
|
||||
|
||||
def test_delete_record_in_secondary(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 1)
|
||||
fixture['email'] = "root@example.com"
|
||||
|
||||
domain = self.create_domain(**fixture)
|
||||
|
||||
record = self.create_record(domain, self.recordset)
|
||||
|
||||
url = 'domains/%s/records/%s' % (domain.id, record.id)
|
||||
self.delete(url, status_code=404)
|
||||
|
@ -645,3 +645,102 @@ class ApiV2RecordSetsTest(ApiV2TestCase):
|
||||
|
||||
# But there should be four in total (NS/SOA + the created)
|
||||
self.assertEqual(4, response.json['metadata']['total_count'])
|
||||
|
||||
# Secondary Zones specific tests
|
||||
def test_get_secondary_zone_recordset(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 1)
|
||||
fixture['email'] = 'root@example.com'
|
||||
secondary = self.create_domain(**fixture)
|
||||
|
||||
# Create a recordset
|
||||
recordset = self.create_recordset(secondary)
|
||||
|
||||
url = '/zones/%s/recordsets/%s' % (secondary['id'], recordset['id'])
|
||||
response = self.client.get(url)
|
||||
|
||||
# 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('recordset', response.json)
|
||||
self.assertIn('links', response.json['recordset'])
|
||||
self.assertIn('self', response.json['recordset']['links'])
|
||||
|
||||
# Check the values returned are what we expect
|
||||
self.assertIn('id', response.json['recordset'])
|
||||
self.assertIn('created_at', response.json['recordset'])
|
||||
self.assertIsNone(response.json['recordset']['updated_at'])
|
||||
self.assertEqual(recordset['name'], response.json['recordset']['name'])
|
||||
self.assertEqual(recordset['type'], response.json['recordset']['type'])
|
||||
|
||||
def test_get_secondary_zone_recordsets(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 1)
|
||||
fixture['email'] = 'foo@bar.io'
|
||||
secondary = self.create_domain(**fixture)
|
||||
|
||||
url = '/zones/%s/recordsets' % secondary['id']
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
# 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('recordsets', response.json)
|
||||
self.assertIn('links', response.json)
|
||||
self.assertIn('self', response.json['links'])
|
||||
|
||||
# We should start with 2 recordsets for SOA & NS
|
||||
self.assertEqual(1, len(response.json['recordsets']))
|
||||
|
||||
soa = self.central_service.find_recordset(
|
||||
self.admin_context, criterion={'domain_id': secondary['id'],
|
||||
'type': 'SOA'})
|
||||
data = [self.create_recordset(secondary,
|
||||
name='x-%s.%s' % (i, secondary['name']))
|
||||
for i in xrange(0, 10)]
|
||||
data.insert(0, soa)
|
||||
|
||||
self._assert_paging(data, url, key='recordsets')
|
||||
|
||||
self._assert_invalid_paging(data, url, key='recordsets')
|
||||
|
||||
def test_create_secondary_zone_recordset(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 1)
|
||||
fixture['email'] = 'foo@bar.io'
|
||||
secondary = self.create_domain(**fixture)
|
||||
|
||||
fixture = self.get_recordset_fixture(secondary['name'], fixture=0)
|
||||
|
||||
url = '/zones/%s/recordsets' % secondary['id']
|
||||
self._assert_exception('forbidden', 403, self.client.post_json, url,
|
||||
{'recordset': fixture})
|
||||
|
||||
def test_update_secondary_zone_recordset(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 1)
|
||||
fixture['email'] = 'foo@bar.io'
|
||||
secondary = self.create_domain(**fixture)
|
||||
|
||||
# Set the context so that we can create a RRSet
|
||||
recordset = self.create_recordset(secondary)
|
||||
|
||||
url = '/zones/%s/recordsets/%s' % (recordset['domain_id'],
|
||||
recordset['id'])
|
||||
|
||||
self._assert_exception('forbidden', 403, self.client.put_json, url,
|
||||
{'recordset': {'ttl': 100}})
|
||||
|
||||
def test_delete_secondary_zone_recordset(self):
|
||||
fixture = self.get_domain_fixture('SECONDARY', 1)
|
||||
fixture['email'] = 'foo@bar.io'
|
||||
secondary = self.create_domain(**fixture)
|
||||
|
||||
# Set the context so that we can create a RRSet
|
||||
recordset = self.create_recordset(secondary)
|
||||
|
||||
url = '/zones/%s/recordsets/%s' % (recordset['domain_id'],
|
||||
recordset['id'])
|
||||
|
||||
self._assert_exception('forbidden', 403, self.client.delete, url)
|
||||
|
@ -15,6 +15,7 @@
|
||||
# under the License.
|
||||
from dns import zone as dnszone
|
||||
from mock import patch
|
||||
from oslo.config import cfg
|
||||
from oslo import messaging
|
||||
from oslo_log import log as logging
|
||||
|
||||
@ -38,7 +39,8 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
|
||||
def test_create_zone(self):
|
||||
# Create a zone
|
||||
fixture = self.get_domain_fixture(0)
|
||||
fixture = self.get_domain_fixture(fixture=0)
|
||||
|
||||
response = self.client.post_json('/zones/', {'zone': fixture})
|
||||
|
||||
# Check the headers are what we expect
|
||||
@ -54,6 +56,35 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
self.assertIn('id', response.json['zone'])
|
||||
self.assertIn('created_at', response.json['zone'])
|
||||
self.assertEqual('PENDING', response.json['zone']['status'])
|
||||
self.assertEqual('PRIMARY', response.json['zone']['type'])
|
||||
self.assertEqual([], response.json['zone']['masters'])
|
||||
self.assertIsNone(response.json['zone']['updated_at'])
|
||||
|
||||
for k in fixture:
|
||||
self.assertEqual(fixture[k], response.json['zone'][k])
|
||||
|
||||
def test_create_zone_no_type(self):
|
||||
# Create a zone
|
||||
fixture = self.get_domain_fixture(fixture=0)
|
||||
del fixture['type']
|
||||
|
||||
response = self.client.post_json('/zones/', {'zone': fixture})
|
||||
|
||||
# Check the headers are what we expect
|
||||
self.assertEqual(202, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
|
||||
# Check the body structure is what we expect
|
||||
self.assertIn('zone', response.json)
|
||||
self.assertIn('links', response.json['zone'])
|
||||
self.assertIn('self', response.json['zone']['links'])
|
||||
|
||||
# Check the values returned are what we expect
|
||||
self.assertIn('id', response.json['zone'])
|
||||
self.assertIn('created_at', response.json['zone'])
|
||||
self.assertEqual('PENDING', response.json['zone']['status'])
|
||||
self.assertEqual('PRIMARY', response.json['zone']['type'])
|
||||
self.assertEqual([], response.json['zone']['masters'])
|
||||
self.assertIsNone(response.json['zone']['updated_at'])
|
||||
|
||||
for k in fixture:
|
||||
@ -63,7 +94,7 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
# NOTE: The schemas should be tested separately to the API. So we
|
||||
# don't need to test every variation via the API itself.
|
||||
# Fetch a fixture
|
||||
fixture = self.get_domain_fixture(0)
|
||||
fixture = self.get_domain_fixture(fixture=0)
|
||||
|
||||
# Add a junk field to the wrapper
|
||||
body = {'zone': fixture, 'junk': 'Junk Field'}
|
||||
@ -82,7 +113,7 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
'/zones', body)
|
||||
|
||||
def test_create_zone_body_validation(self):
|
||||
fixture = self.get_domain_fixture(0)
|
||||
fixture = self.get_domain_fixture(fixture=0)
|
||||
# Add id to the body
|
||||
fixture['id'] = '2fdadfb1-cf96-4259-ac6b-bb7b6d2ff980'
|
||||
# Ensure it fails with a 400
|
||||
@ -90,7 +121,7 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
self._assert_exception('invalid_object', 400, self.client.post_json,
|
||||
'/zones', body)
|
||||
|
||||
fixture = self.get_domain_fixture(0)
|
||||
fixture = self.get_domain_fixture(fixture=0)
|
||||
# Add created_at to the body
|
||||
fixture['created_at'] = '2014-03-12T19:07:53.000000'
|
||||
# Ensure it fails with a 400
|
||||
@ -100,7 +131,7 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
|
||||
def test_create_zone_invalid_name(self):
|
||||
# Try to create a zone with an invalid name
|
||||
fixture = self.get_domain_fixture(-1)
|
||||
fixture = self.get_domain_fixture(fixture=-1)
|
||||
|
||||
# Ensure it fails with a 400
|
||||
self._assert_exception('invalid_object', 400, self.client.post_json,
|
||||
@ -109,7 +140,7 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
@patch.object(central_service.Service, 'create_domain',
|
||||
side_effect=messaging.MessagingTimeout())
|
||||
def test_create_zone_timeout(self, _):
|
||||
fixture = self.get_domain_fixture(0)
|
||||
fixture = self.get_domain_fixture(fixture=0)
|
||||
|
||||
body = {'zone': fixture}
|
||||
|
||||
@ -119,7 +150,7 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
@patch.object(central_service.Service, 'create_domain',
|
||||
side_effect=exceptions.DuplicateDomain())
|
||||
def test_create_zone_duplicate(self, _):
|
||||
fixture = self.get_domain_fixture(0)
|
||||
fixture = self.get_domain_fixture(fixture=0)
|
||||
|
||||
body = {'zone': fixture}
|
||||
|
||||
@ -389,6 +420,91 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
url = '/zones/%s/tasks' % zone.id
|
||||
self._assert_exception('not_found', 404, self.client.get, url)
|
||||
|
||||
def test_create_secondary(self):
|
||||
# Create a zone
|
||||
fixture = self.get_domain_fixture('SECONDARY', 0)
|
||||
fixture['masters'] = ["10.0.0.1"]
|
||||
|
||||
response = self.client.post_json('/zones/', {'zone': fixture})
|
||||
|
||||
# Check the headers are what we expect
|
||||
self.assertEqual(202, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
|
||||
# Check the body structure is what we expect
|
||||
self.assertIn('zone', response.json)
|
||||
self.assertIn('links', response.json['zone'])
|
||||
self.assertIn('self', response.json['zone']['links'])
|
||||
|
||||
# Check the values returned are what we expect
|
||||
self.assertIn('id', response.json['zone'])
|
||||
self.assertIn('created_at', response.json['zone'])
|
||||
self.assertEqual('PENDING', response.json['zone']['status'])
|
||||
self.assertEqual(cfg.CONF['service:central'].managed_resource_email,
|
||||
response.json['zone']['email'])
|
||||
|
||||
self.assertIsNone(response.json['zone']['updated_at'])
|
||||
# Zone is not transferred yet
|
||||
self.assertIsNone(response.json['zone']['transferred_at'])
|
||||
# Serial defaults to 1
|
||||
self.assertEqual(response.json['zone']['serial'], 1)
|
||||
|
||||
for k in fixture:
|
||||
self.assertEqual(fixture[k], response.json['zone'][k])
|
||||
|
||||
def test_create_secondary_no_masters(self):
|
||||
# Create a zone
|
||||
fixture = self.get_domain_fixture('SECONDARY', 0)
|
||||
|
||||
self._assert_exception('invalid_object', 400, self.client.post_json,
|
||||
'/zones/', {'zone': fixture})
|
||||
|
||||
def test_update_secondary(self):
|
||||
# Create a zone
|
||||
fixture = self.get_domain_fixture('SECONDARY', 0)
|
||||
fixture['email'] = cfg.CONF['service:central'].managed_resource_email
|
||||
fixture['attributes'] = [{"key": "master", "value": "10.0.0.10"}]
|
||||
|
||||
# Create a zone
|
||||
zone = self.create_domain(**fixture)
|
||||
|
||||
masters = ['10.0.0.1', '10.0.0.2']
|
||||
|
||||
# Prepare an update body
|
||||
body = {'zone': {'masters': masters}}
|
||||
|
||||
response = self.client.patch_json('/zones/%s' % zone['id'], body,
|
||||
status=202)
|
||||
|
||||
# Check the headers are what we expect
|
||||
self.assertEqual(202, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
|
||||
# Check the body structure is what we expect
|
||||
self.assertIn('zone', response.json)
|
||||
self.assertIn('links', response.json['zone'])
|
||||
self.assertIn('self', response.json['zone']['links'])
|
||||
self.assertIn('status', response.json['zone'])
|
||||
|
||||
# Check the values returned are what we expect
|
||||
self.assertIn('id', response.json['zone'])
|
||||
self.assertIsNotNone(response.json['zone']['updated_at'])
|
||||
self.assertEqual(masters, response.json['zone']['masters'])
|
||||
self.assertEqual(1, response.json['zone']['serial'])
|
||||
|
||||
def test_update_secondary_email_invalid_object(self):
|
||||
# Create a zone
|
||||
fixture = self.get_domain_fixture('SECONDARY', 0)
|
||||
fixture['email'] = cfg.CONF['service:central'].managed_resource_email
|
||||
|
||||
# Create a zone
|
||||
zone = self.create_domain(**fixture)
|
||||
|
||||
body = {'zone': {'email': 'foo@bar.io'}}
|
||||
|
||||
self._assert_exception('invalid_object', 400, self.client.patch_json,
|
||||
'/zones/%s' % zone['id'], body)
|
||||
|
||||
# Zone import/export
|
||||
def test_missing_origin(self):
|
||||
fixture = self.get_zonefile_fixture(variant='noorigin')
|
||||
@ -417,6 +533,7 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
get_response = self.client.get('/zones/%s' %
|
||||
post_response.json['zone']['id'],
|
||||
headers={'Accept': 'text/dns'})
|
||||
|
||||
exported_zonefile = get_response.body
|
||||
imported = dnszone.from_text(self.get_zonefile_fixture())
|
||||
exported = dnszone.from_text(exported_zonefile)
|
||||
@ -450,7 +567,7 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
self.assertEqual(0, response.json['metadata']['total_count'])
|
||||
|
||||
# Create a zone
|
||||
fixture = self.get_domain_fixture(0)
|
||||
fixture = self.get_domain_fixture(fixture=0)
|
||||
response = self.client.post_json('/zones/', {'zone': fixture})
|
||||
|
||||
response = self.client.get('/zones/')
|
||||
@ -460,10 +577,10 @@ class ApiV2ZonesTest(ApiV2TestCase):
|
||||
|
||||
def test_total_count_pagination(self):
|
||||
# Create two zones
|
||||
fixture = self.get_domain_fixture(0)
|
||||
fixture = self.get_domain_fixture(fixture=0)
|
||||
response = self.client.post_json('/zones/', {'zone': fixture})
|
||||
|
||||
fixture = self.get_domain_fixture(1)
|
||||
fixture = self.get_domain_fixture(fixture=1)
|
||||
response = self.client.post_json('/zones/', {'zone': fixture})
|
||||
|
||||
# Paginate so that there is only one zone returned
|
||||
|
@ -214,7 +214,7 @@ class CentralServiceTest(CentralTestCase):
|
||||
group='service:central')
|
||||
context = self.get_context()
|
||||
|
||||
values = self.get_domain_fixture(1)
|
||||
values = self.get_domain_fixture(fixture=1)
|
||||
values['ttl'] = 0
|
||||
|
||||
with testtools.ExpectedException(exceptions.InvalidTTL):
|
||||
@ -443,7 +443,8 @@ class CentralServiceTest(CentralTestCase):
|
||||
def test_create_domain_over_tld(self):
|
||||
values = dict(
|
||||
name='example.com.',
|
||||
email='info@example.com'
|
||||
email='info@example.com',
|
||||
type='PRIMARY'
|
||||
)
|
||||
self._test_create_domain(values)
|
||||
|
||||
@ -459,7 +460,8 @@ class CentralServiceTest(CentralTestCase):
|
||||
# Test creation of a domain in 한국 (kr)
|
||||
values = dict(
|
||||
name='example.xn--3e0b707e.',
|
||||
email='info@example.xn--3e0b707e'
|
||||
email='info@example.xn--3e0b707e',
|
||||
type='PRIMARY'
|
||||
)
|
||||
self._test_create_domain(values)
|
||||
|
||||
@ -476,7 +478,7 @@ class CentralServiceTest(CentralTestCase):
|
||||
parent_domain = self.create_domain(fixture=0)
|
||||
|
||||
# Prepare values for the subdomain using fixture 1 as a base
|
||||
values = self.get_domain_fixture(1)
|
||||
values = self.get_domain_fixture(fixture=1)
|
||||
values['name'] = 'www.%s' % parent_domain['name']
|
||||
|
||||
# Create the subdomain
|
||||
@ -490,7 +492,7 @@ class CentralServiceTest(CentralTestCase):
|
||||
def test_create_superdomain(self):
|
||||
# Prepare values for the domain and subdomain
|
||||
# using fixture 1 as a base
|
||||
domain_values = self.get_domain_fixture(1)
|
||||
domain_values = self.get_domain_fixture(fixture=1)
|
||||
|
||||
subdomain_values = copy.deepcopy(domain_values)
|
||||
subdomain_values['name'] = 'www.%s' % domain_values['name']
|
||||
@ -529,7 +531,7 @@ class CentralServiceTest(CentralTestCase):
|
||||
context.tenant = '2'
|
||||
|
||||
# Prepare values for the subdomain using fixture 1 as a base
|
||||
values = self.get_domain_fixture(1)
|
||||
values = self.get_domain_fixture(fixture=1)
|
||||
values['name'] = 'www.%s' % parent_domain['name']
|
||||
|
||||
# Attempt to create the subdomain
|
||||
@ -544,7 +546,7 @@ class CentralServiceTest(CentralTestCase):
|
||||
context.tenant = '1'
|
||||
|
||||
# Set up domain and subdomain values
|
||||
domain_values = self.get_domain_fixture(1)
|
||||
domain_values = self.get_domain_fixture(fixture=1)
|
||||
domain_name = domain_values['name']
|
||||
|
||||
subdomain_values = copy.deepcopy(domain_values)
|
||||
@ -644,7 +646,7 @@ class CentralServiceTest(CentralTestCase):
|
||||
group='service:central')
|
||||
context = self.get_context()
|
||||
|
||||
values = self.get_domain_fixture(1)
|
||||
values = self.get_domain_fixture(fixture=1)
|
||||
values['ttl'] = 0
|
||||
|
||||
# Create a server
|
||||
@ -658,7 +660,7 @@ class CentralServiceTest(CentralTestCase):
|
||||
self.policy({'use_low_ttl': '!'})
|
||||
self.config(min_ttl="None",
|
||||
group='service:central')
|
||||
values = self.get_domain_fixture(1)
|
||||
values = self.get_domain_fixture(fixture=1)
|
||||
values['ttl'] = -100
|
||||
|
||||
# Create a server
|
||||
|
462
doc/source/howtos/secondary_zones.rst
Normal file
462
doc/source/howtos/secondary_zones.rst
Normal file
@ -0,0 +1,462 @@
|
||||
..
|
||||
Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
|
||||
Author: Endre Karlson <endre.karlson@hp.com>
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Secondary Zones
|
||||
===============
|
||||
|
||||
The Designate v2 API introduced functionality that allows Designate to act as a
|
||||
DNS slave, rather than a master for a zone. This is accomplished by completing
|
||||
a zone transfer (AXFR) from a DNS server managed outside of Designate.
|
||||
|
||||
RecordSets / Records
|
||||
--------------------
|
||||
|
||||
Changes to secondary zones are managed outside of Designate. Users must make
|
||||
the changes they wish, and prompt a fresh zone transfer (AXFR) into Designate
|
||||
to make those changes live on any DNS servers Designate manages.
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
To add a secondary zone to Designate, there must be a DNS master for the zone,
|
||||
to which Designate can act as a slave. For this guide, we assume that you have
|
||||
already set this up.
|
||||
|
||||
The remaining Designate set up will be similar to a non-secondary zone setup.
|
||||
You'll need a primary DNS server for Designate to manage and transfer secondary
|
||||
zones to.
|
||||
|
||||
In our examples we'll use the following values:
|
||||
|
||||
*Name* - example.com.
|
||||
|
||||
*Masters* - 192.168.27.100
|
||||
|
||||
|
||||
Setup - example NSD4
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Skip this section if you have a master already to use.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
For this it is assumed that you are running on Ubuntu.
|
||||
|
||||
Install
|
||||
^^^^^^^
|
||||
|
||||
For some reason there's a bug with the nsd package so it doesn't create the user that it needs for the installation. So we'll create that before installing the package.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo apt-get install nsd
|
||||
|
||||
|
||||
Configure
|
||||
^^^^^^^^^
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo zcat /usr/share/doc/nsd/examples/nsd.conf.sample.gz >/tmp/nsd.conf
|
||||
$ sudo mv /tmp/nsd.conf /etc/nsd/nsd.conf
|
||||
|
||||
Add the following to /etc/nsd/nsd.conf
|
||||
|
||||
.. note::
|
||||
|
||||
If you're wondering why we set notify to `192.168.27.100`:`5354` it's because MDNS runs on 5354 by default.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo vi /etc/nsd/nsd.conf
|
||||
|
||||
Add the contents:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
pattern:
|
||||
name: "mdns"
|
||||
zonefile: "%s.zone"
|
||||
notify: 192.168.27.100@5354 NOKEY
|
||||
provide-xfr: 192.168.27.100 NOKEY
|
||||
allow-axfr-fallback: yes
|
||||
|
||||
Add a zone file
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Create a new *Zone* in NSD called *example.com.*
|
||||
|
||||
**/etc/nsd/example.com.zone**
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo vi /etc/nsd/example.com.zone
|
||||
|
||||
And add the contents:
|
||||
|
||||
::
|
||||
|
||||
$TTL 1800 ;minimum ttl
|
||||
example.com. IN SOA ns1.example.com. admin.example.net. (
|
||||
2014111301 ;serial
|
||||
3600 ;refresh
|
||||
600 ;retry
|
||||
180000 ;expire
|
||||
600 ;negative ttl
|
||||
)
|
||||
|
||||
TXT "v=spf1 +a +mx ~all"
|
||||
SPF "v=spf1 +a +mx ~all"
|
||||
|
||||
NS ns1.example.com.
|
||||
NS ns2.example.com.
|
||||
NS ns3.example.com.
|
||||
|
||||
MX 0 mail1.example.com.
|
||||
MX 5 mail2.example.com.
|
||||
MX 10 mail3.example.com.
|
||||
|
||||
A 10.0.0.1
|
||||
A 10.0.0.2
|
||||
A 10.0.0.3
|
||||
|
||||
|
||||
ns1 A 172.16.28.100
|
||||
ns2 A 172.16.28.101
|
||||
ns3 A 172.16.28.103
|
||||
|
||||
mail1 A 10.0.10.1
|
||||
mail2 A 10.0.10.2
|
||||
mail3 A 10.0.10.3
|
||||
|
||||
google CNAME google.com.
|
||||
|
||||
|
||||
Restart NSD
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo service nsd restart
|
||||
|
||||
Check that it's working
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo nsd-control status
|
||||
|
||||
Activate the zone in NSD
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo nsd-control addzone example.com mdns
|
||||
|
||||
Creating the Zone
|
||||
-----------------
|
||||
|
||||
When you create a domain in Designate there are two possible initial actions:
|
||||
|
||||
- Domain is created but transfer fails if it's not available yet in master,
|
||||
then typically the initial transfer will be done once the master sends first
|
||||
NOTIFY.
|
||||
|
||||
- Domain is created and transfers straight away.
|
||||
|
||||
In both cases the interaction between your master and Designate is handled by
|
||||
the MDNS instance at the Designate side.
|
||||
|
||||
|
||||
Defintion of values:
|
||||
|
||||
- *email* set to the value of the *managed_resource_email* option in the
|
||||
*central* section of the Designate configuration.
|
||||
|
||||
- *transferred_at* is **null** and *version* is **1** since the zone has not
|
||||
transferred yet.
|
||||
|
||||
- *serial* gets set automatically by the system initially and to the value of the master's serial post initial AXFR.
|
||||
|
||||
|
||||
.. http:post:: /zones
|
||||
|
||||
Creates a new zone.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /v2/zones HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"zone": {
|
||||
"name": "example.com.",
|
||||
"type": "SECONDARY",
|
||||
"masters": ["192.168.27.100"],
|
||||
"description": "This is a slave for example.com."
|
||||
}
|
||||
}
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"zone": {
|
||||
"id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3",
|
||||
"pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2",
|
||||
"project_id": "4335d1f0-f793-11e2-b778-0800200c9a66",
|
||||
"name": "example.com.",
|
||||
"email": "email@example.io",
|
||||
"ttl": 3600,
|
||||
"serial": 1404757531,
|
||||
"status": "ACTIVE",
|
||||
"description": "This is a slave for example.com."
|
||||
"masters": ["192.168.27.100"],
|
||||
"type": "SECONDARY",
|
||||
"transferred_at": null,
|
||||
"version": 1,
|
||||
"created_at": "2014-07-07T18:25:31.275934",
|
||||
"updated_at": null,
|
||||
"links": {
|
||||
"self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Get Zone
|
||||
--------
|
||||
|
||||
.. http:get:: /zones/(uuid:id)
|
||||
|
||||
Retrieves a secondary zone with the specified zone ID.
|
||||
|
||||
**Example request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3 HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
**Example response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"zone": {
|
||||
"id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3",
|
||||
"pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2",
|
||||
"project_id": "4335d1f0-f793-11e2-b778-0800200c9a66",
|
||||
"name": "example.com.",
|
||||
"email": "email@example.io",
|
||||
"ttl": 3600,
|
||||
"serial": 1404757531,
|
||||
"status": "ACTIVE",
|
||||
"description": "This is a slave for example.com."
|
||||
"masters": ["192.168.27.100"],
|
||||
"type": "SECONDARY",
|
||||
"transferred_at": null,
|
||||
"version": 1,
|
||||
"created_at": "2014-07-07T18:25:31.275934",
|
||||
"updated_at": null,
|
||||
"links": {
|
||||
"self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:statuscode 200: Success
|
||||
:statuscode 401: Access Denied
|
||||
|
||||
|
||||
List Secondary Zones
|
||||
--------------------
|
||||
|
||||
.. http:get:: /zones
|
||||
|
||||
Lists all zones.
|
||||
|
||||
**Example Request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v2/zones?type=SECONDARY HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
**Example Response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"zones": [{
|
||||
"id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3",
|
||||
"pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2",
|
||||
"project_id": "4335d1f0-f793-11e2-b778-0800200c9a66",
|
||||
"name": "example.com.",
|
||||
"email": "email@example.io",
|
||||
"ttl": 3600,
|
||||
"serial": 1404757531,
|
||||
"status": "ACTIVE",
|
||||
"description": "This is a slave for example.com."
|
||||
"masters": ["192.168.27.100"],
|
||||
"type": "SECONDARY",
|
||||
"transferred_at": null,
|
||||
"version": 1,
|
||||
"created_at": "2014-07-07T18:25:31.275934",
|
||||
"updated_at": null,
|
||||
"links": {
|
||||
"self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca4",
|
||||
"pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2",
|
||||
"project_id": "4335d1f0-f793-11e2-b778-0800200c9a66",
|
||||
"name": "bar.io.",
|
||||
"email": "email@example.io",
|
||||
"ttl": 3600,
|
||||
"serial": 10,
|
||||
"status": "ACTIVE",
|
||||
"description": "This is a slave for bar.io."
|
||||
"masters": ["192.168.27.100"],
|
||||
"type": "SECONDARY",
|
||||
"transferred_at": 2014-07-07T18:25:35.275934,
|
||||
"version": 2,
|
||||
"created_at": "2014-07-07T18:25:31.275934",
|
||||
"updated_at": null,
|
||||
"links": {
|
||||
"self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3"
|
||||
}
|
||||
}],
|
||||
"links": {
|
||||
"self": "https://127.0.0.1:9001/v2/zones"
|
||||
}
|
||||
}
|
||||
|
||||
:statuscode 200: Success
|
||||
:statuscode 401: Access Denied
|
||||
|
||||
Update Zone
|
||||
-----------
|
||||
|
||||
.. http:patch:: /zones/(uuid:id)
|
||||
|
||||
Changes the specified attribute(s) for an existing zone.
|
||||
|
||||
In the example below, we update the TTL to 3600.
|
||||
|
||||
**Request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3 HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"zone": {
|
||||
"masters": ["192.168.27.101"]
|
||||
}
|
||||
}
|
||||
|
||||
**Response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"zone": {
|
||||
"id": "a86dba58-0043-4cc6-a1bb-69d5e86f3ca3",
|
||||
"pool_id": "572ba08c-d929-4c70-8e42-03824bb24ca2",
|
||||
"project_id": "4335d1f0-f793-11e2-b778-0800200c9a66",
|
||||
"name": "example.com.",
|
||||
"email": "email@example.io",
|
||||
"ttl": 3600,
|
||||
"serial": 1404757531,
|
||||
"status": "ACTIVE",
|
||||
"description": "This is a slave for example.com."
|
||||
"masters": ["192.168.27.101"],
|
||||
"type": "SECONDARY",
|
||||
"transferred_at": null,
|
||||
"version": 2,
|
||||
"created_at": "2014-07-07T18:25:31.275934",
|
||||
"updated_at": 2014-07-07T18:25:34.275934,
|
||||
"links": {
|
||||
"self": "https://127.0.0.1:9001/v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:form description: UTF-8 text field.
|
||||
:form name: Valid zone name (Immutable).
|
||||
:form type: Enum PRIMARY/SECONDARY, default PRIMARY (Immutable).
|
||||
:form email: email address, required for type PRIMARY, NULL for SECONDARY.
|
||||
:form ttl: time-to-live numeric value in seconds, NULL for SECONDARY
|
||||
:form masters: Array of master nameservers. (NULL for type PRIMARY, required for SECONDARY otherwise zone will not be transferred before set.)
|
||||
|
||||
:statuscode 200: Success
|
||||
:statuscode 202: Accepted
|
||||
:statuscode 401: Access Denied
|
||||
|
||||
Delete Zone
|
||||
-----------
|
||||
|
||||
.. http:delete:: zones/(uuid:id)
|
||||
|
||||
Deletes a zone with the specified zone ID.
|
||||
|
||||
**Example Request:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /v2/zones/a86dba58-0043-4cc6-a1bb-69d5e86f3ca3 HTTP/1.1
|
||||
Host: 127.0.0.1:9001
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
**Example Response:**
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204 No Content
|
||||
|
||||
:statuscode 204: No content
|
@ -63,6 +63,9 @@ Create Zone
|
||||
"serial": 1404757531,
|
||||
"status": "ACTIVE",
|
||||
"description": "This is an example zone.",
|
||||
"masters": [],
|
||||
"type": "PRIMARY",
|
||||
"transferred_at": null,
|
||||
"version": 1,
|
||||
"created_at": "2014-07-07T18:25:31.275934",
|
||||
"updated_at": null,
|
||||
@ -72,10 +75,12 @@ Create Zone
|
||||
}
|
||||
}
|
||||
|
||||
:form description: UTF-8 text field
|
||||
:form email: email address
|
||||
:form name: zone name
|
||||
:form ttl: time-to-live numeric value in seconds
|
||||
:form description: UTF-8 text field.
|
||||
:form name: Valid zone name (Immutable).
|
||||
:form type: Enum PRIMARY/SECONDARY, default PRIMARY (Immutable).
|
||||
:form email: email address, required for type PRIMARY, NULL for SECONDARY.
|
||||
:form ttl: time-to-live numeric value in seconds, NULL for SECONDARY.
|
||||
:form masters: Array of master nameservers. (NULL for type PRIMARY, required for SECONDARY otherwise zone will not be transferred before set).
|
||||
|
||||
:statuscode 201: Created
|
||||
:statuscode 202: Accepted
|
||||
@ -117,6 +122,9 @@ Get Zone
|
||||
"serial": 1404757531,
|
||||
"status": "ACTIVE",
|
||||
"description": "This is an example zone.",
|
||||
"masters": [],
|
||||
"type": "PRIMARY",
|
||||
"transferred_at": null,
|
||||
"version": 1,
|
||||
"created_at": "2014-07-07T18:25:31.275934",
|
||||
"updated_at": null,
|
||||
@ -165,6 +173,9 @@ List Zones
|
||||
"serial": 1404757531,
|
||||
"status": "ACTIVE",
|
||||
"description": "This is an example zone.",
|
||||
"masters": [],
|
||||
"type": "PRIMARY",
|
||||
"transferred_at": null,
|
||||
"version": 1,
|
||||
"created_at": "2014-07-07T18:25:31.275934",
|
||||
"updated_at": null,
|
||||
@ -181,6 +192,9 @@ List Zones
|
||||
"serial": 1404756682,
|
||||
"status": "ACTIVE",
|
||||
"description": "This is another example zone.",
|
||||
"masters": [],
|
||||
"type": "PRIMARY",
|
||||
"transferred_at": null,
|
||||
"version": 1,
|
||||
"created_at": "2014-07-07T18:22:08.287743",
|
||||
"updated_at": null,
|
||||
@ -238,6 +252,9 @@ Update Zone
|
||||
"serial": 1404760160,
|
||||
"status": "ACTIVE",
|
||||
"description": "This is an example zone.",
|
||||
"masters": [],
|
||||
"type": "PRIMARY",
|
||||
"transferred_at": null,
|
||||
"version": 1,
|
||||
"created_at": "2014-07-07T18:25:31.275934",
|
||||
"updated_at": "2014-07-07T19:09:20.876366",
|
||||
@ -247,10 +264,12 @@ Update Zone
|
||||
}
|
||||
}
|
||||
|
||||
:form description: UTF-8 text field
|
||||
:form email: email address
|
||||
:form name: zone name
|
||||
:form ttl: time-to-live numeric value in seconds
|
||||
:form description: UTF-8 text field.
|
||||
:form name: Valid zone name (Immutable).
|
||||
:form type: Enum PRIMARY/SECONDARY, default PRIMARY (Immutable).
|
||||
:form email: email address, required for type PRIMARY, NULL for SECONDARY.
|
||||
:form ttl: time-to-live numeric value in seconds, NULL for SECONDARY
|
||||
:form masters: Array of master nameservers. (NULL for type PRIMARY, required for SECONDARY otherwise zone will not be transferred before set.)
|
||||
|
||||
:statuscode 200: Success
|
||||
:statuscode 202: Accepted
|
||||
@ -328,7 +347,10 @@ Import Zone
|
||||
"ttl": "42",
|
||||
"created_at": "2014-07-07T18:25:31.275934",
|
||||
"updated_at": null,
|
||||
"version": 1
|
||||
"version": 1,
|
||||
"masters": [],
|
||||
"type": "PRIMARY",
|
||||
"transferred_at": null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
{
|
||||
"admin": "role:admin or is_admin:True",
|
||||
"primary_zone": "target.domain_type:SECONDARY",
|
||||
|
||||
"owner": "tenant:%(tenant_id)s",
|
||||
"admin_or_owner": "rule:admin or rule:owner",
|
||||
"target": "tenant:%(target_tenant_id)s",
|
||||
@ -46,6 +48,15 @@
|
||||
"count_domains": "rule:admin_or_owner",
|
||||
"touch_domain": "rule:admin_or_owner",
|
||||
|
||||
"create_recordset": "('PRIMARY':%(domain_type)s and rule:admin_or_owner) OR ('SECONDARY':%(domain_type)s AND is_admin:True)",
|
||||
"get_recordsets": "rule:admin_or_owner",
|
||||
"get_recordset": "rule:admin_or_owner",
|
||||
"find_recordsets": "rule:admin_or_owner",
|
||||
"find_recordset": "rule:admin_or_owner",
|
||||
"update_recordset": "('PRIMARY':%(domain_type)s and rule:admin_or_owner) OR ('SECONDARY':%(domain_type)s AND is_admin:True)",
|
||||
"delete_recordset": "('PRIMARY':%(domain_type)s and rule:admin_or_owner) OR ('SECONDARY':%(domain_type)s AND is_admin:True)",
|
||||
"count_recordset": "rule:admin_or_owner",
|
||||
|
||||
"create_record": "rule:admin_or_owner",
|
||||
"get_records": "rule:admin_or_owner",
|
||||
"get_record": "rule:admin_or_owner",
|
||||
|
Loading…
Reference in New Issue
Block a user