Merge "Denominator Agent"
This commit is contained in:
commit
e9c6754576
241
designate/backend/agent_backend/impl_denominator.py
Normal file
241
designate/backend/agent_backend/impl_denominator.py
Normal file
@ -0,0 +1,241 @@
|
||||
# Copyright 2015 Dyn Inc.
|
||||
#
|
||||
# Author: Yasha Bubnov <ybubnov@dyn.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.
|
||||
import itertools
|
||||
|
||||
import dns.rdata
|
||||
import dns.rdatatype
|
||||
import dns.rdataclass
|
||||
from oslo.config import cfg
|
||||
from oslo.concurrency import lockutils
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate.backend.agent_backend import base
|
||||
from designate import exceptions
|
||||
from designate import utils
|
||||
from designate.i18n import _LI
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CFG_GROUP = 'backend:agent:denominator'
|
||||
|
||||
|
||||
class Denominator(object):
|
||||
|
||||
def __init__(self, config):
|
||||
super(Denominator, self).__init__()
|
||||
self.config = config
|
||||
|
||||
def update_record(self, zone, **kwargs):
|
||||
return self._execute(['record', '-z', zone, 'replace'], kwargs)
|
||||
|
||||
def create_record(self, zone, **kwargs):
|
||||
return self._execute(['record', '-z', zone, 'add'], kwargs)
|
||||
|
||||
def delete_record(self, zone, **kwargs):
|
||||
return self._execute(['record', '-z', zone, 'delete'], kwargs)
|
||||
|
||||
def get_record(self, zone, **kwargs):
|
||||
return self._execute(['record', '-z', zone, 'get'], kwargs)
|
||||
|
||||
def get_records(self, zone, **kwargs):
|
||||
return self._execute(['record', '-z', zone, 'list'], kwargs)
|
||||
|
||||
def create_zone(self, **kwargs):
|
||||
return self._execute(['zone', 'add'], kwargs)
|
||||
|
||||
def update_zone(self, **kwargs):
|
||||
return self._execute(['zone', 'update'], kwargs)
|
||||
|
||||
def delete_zone(self, **kwargs):
|
||||
return self._execute(['zone', 'delete'], kwargs)
|
||||
|
||||
def _params(self, **kwargs):
|
||||
params = [('--%s' % k, str(v)) for k, v in kwargs.iteritems()]
|
||||
return list(itertools.chain(*params))
|
||||
|
||||
def _base(self):
|
||||
call = ['denominator', '-q', '-n', self.config.name]
|
||||
|
||||
# NOTE: When path to denominator configuration file is ommited,
|
||||
# ~/.denominatorconfig file will be used by default.
|
||||
if self.config.config_file:
|
||||
call.extend(['-C', self.config.config_file])
|
||||
return call
|
||||
|
||||
def _execute(self, op, kwargs):
|
||||
try:
|
||||
call = self._base() + op + self._params(**kwargs)
|
||||
LOG.debug(('Executing Denominator call: %s' % ' '.join(call)))
|
||||
|
||||
stdout, _ = utils.execute(*call)
|
||||
return stdout
|
||||
except utils.processutils.ProcessExecutionError as e:
|
||||
LOG.debug('Denominator call failure: %s' % e)
|
||||
raise exceptions.Base(e)
|
||||
|
||||
|
||||
class DenominatorBackend(base.AgentBackend):
|
||||
__plugin_name__ = 'denominator'
|
||||
|
||||
def __init__(self, agent_service):
|
||||
super(DenominatorBackend, self).__init__(agent_service)
|
||||
self.denominator = Denominator(cfg.CONF[CFG_GROUP])
|
||||
|
||||
@classmethod
|
||||
def get_cfg_opts(cls):
|
||||
group = cfg.OptGroup(
|
||||
name=CFG_GROUP,
|
||||
title='Backend options for Denominator',
|
||||
)
|
||||
|
||||
opts = [
|
||||
cfg.StrOpt('name', default='fake',
|
||||
help='Name of the affected provider'),
|
||||
cfg.StrOpt('config_file', default='/etc/denominator.conf',
|
||||
help='Path to Denominator configuration file')
|
||||
]
|
||||
|
||||
return [(group, opts)]
|
||||
|
||||
def start(self):
|
||||
LOG.info(_LI("Started Denominator backend"))
|
||||
|
||||
def stop(self):
|
||||
LOG.info(_LI("Stopped Denominator backend"))
|
||||
|
||||
def find_domain_serial(self, domain_name):
|
||||
LOG.debug("Finding %s" % domain_name)
|
||||
|
||||
domain_name = domain_name.rstrip('.')
|
||||
output = self.denominator.get_record(
|
||||
zone=domain_name,
|
||||
type='SOA',
|
||||
name=domain_name)
|
||||
try:
|
||||
text = ' '.join(output.split()[3:])
|
||||
rdata = dns.rdata.from_text(dns.rdataclass.IN,
|
||||
dns.rdatatype.SOA,
|
||||
text)
|
||||
except Exception:
|
||||
return None
|
||||
return rdata.serial
|
||||
|
||||
def create_domain(self, domain):
|
||||
LOG.debug("Creating %s" % domain.origin.to_text())
|
||||
domain_name = domain.origin.to_text(omit_final_dot=True)
|
||||
|
||||
# Use SOA TTL as zone default TTL
|
||||
soa_record = domain.find_rrset(domain.origin, dns.rdatatype.SOA)
|
||||
rname = soa_record.items[0].rname.derelativize(origin=domain.origin)
|
||||
|
||||
# Lock domain to prevent concurrent changes.
|
||||
with self._sync_domain(domain.origin):
|
||||
# NOTE: If zone already exists, denominator will update it with
|
||||
# new values, in other a duplicate zone will be created if
|
||||
# provider supports such functionality.
|
||||
self.denominator.create_zone(
|
||||
name=domain_name,
|
||||
ttl=soa_record.ttl,
|
||||
email=rname)
|
||||
|
||||
# Add records one by one.
|
||||
for name, ttl, rtype, data in self._iterate_records(domain):
|
||||
# Some providers do not support creationg of SOA record.
|
||||
rdatatype = dns.rdatatype.from_text(rtype)
|
||||
if rdatatype == dns.rdatatype.SOA:
|
||||
continue
|
||||
|
||||
self.denominator.create_record(
|
||||
zone=domain_name,
|
||||
name=name,
|
||||
type=rtype,
|
||||
ttl=ttl,
|
||||
data=data)
|
||||
|
||||
def update_domain(self, domain):
|
||||
LOG.debug("Updating %s" % domain.origin)
|
||||
domain_name = domain.origin.to_text(omit_final_dot=True)
|
||||
|
||||
soa_record = domain.find_rrset(domain.origin, dns.rdatatype.SOA)
|
||||
rname = soa_record.items[0].rname.derelativize(origin=domain.origin)
|
||||
|
||||
with self._sync_domain(domain.origin):
|
||||
# Update zone with a new parameters
|
||||
self.denominator.update_zone(
|
||||
id=domain_name,
|
||||
ttl=soa_record.ttl,
|
||||
email=rname)
|
||||
|
||||
# Fetch records to create a differential update of a zone.
|
||||
output = self.denominator.get_records(domain_name)
|
||||
subdomains = dict()
|
||||
|
||||
# Subdomains dict will contain names of subdomains without
|
||||
# trailing dot.
|
||||
for raw in output.splitlines():
|
||||
data = raw.split()
|
||||
name, rtype = data[0], data[1]
|
||||
|
||||
rtypes = subdomains.get(name, set())
|
||||
rtypes.add(rtype)
|
||||
subdomains[name] = rtypes
|
||||
|
||||
for name, ttl, rtype, data in self._iterate_records(domain):
|
||||
record_action = self.denominator.create_record
|
||||
|
||||
if name in subdomains and rtype in subdomains[name]:
|
||||
# When RR set already exists, replace it with a new one.
|
||||
rdatatype = dns.rdatatype.from_text(rtype)
|
||||
record_action = self.denominator.update_record
|
||||
|
||||
# So next call will ADD a new record to record set
|
||||
# instead of replacing of the existing one.
|
||||
subdomains[name].remove(rtype)
|
||||
|
||||
# NOTE: DynECT does not support deleting of the SOA
|
||||
# record. Skip updating of the SOA record.
|
||||
if rdatatype == dns.rdatatype.SOA:
|
||||
continue
|
||||
|
||||
record_action(zone=domain_name,
|
||||
name=name,
|
||||
type=rtype,
|
||||
ttl=ttl,
|
||||
data=data)
|
||||
|
||||
# Remaining records should be deleted
|
||||
for name, types in subdomains.iteritems():
|
||||
for rtype in types:
|
||||
self.denominator.delete_record(
|
||||
zone=domain_name, id=name, type=rtype)
|
||||
|
||||
def delete_domain(self, domain_name):
|
||||
LOG.debug('Delete Domain: %s' % domain_name)
|
||||
|
||||
with self._sync_domain(domain_name):
|
||||
self.denominator.delete_zone(id=domain_name)
|
||||
|
||||
def _sync_domain(self, domain_name):
|
||||
LOG.debug('Synchronising domain: %s' % domain_name)
|
||||
return lockutils.lock('denominator-%s' % domain_name)
|
||||
|
||||
def _iterate_records(self, domain):
|
||||
for rname, ttl, rdata in domain.iterate_rdatas():
|
||||
name = rname.derelativize(origin=domain.origin)
|
||||
name = name.to_text(omit_final_dot=True)
|
||||
|
||||
data = rdata.to_text(origin=domain.origin, relativize=False)
|
||||
yield name, ttl, dns.rdatatype.to_text(rdata.rdtype), data
|
130
designate/tests/test_agent/test_backends/test_denominator.py
Normal file
130
designate/tests/test_agent/test_backends/test_denominator.py
Normal file
@ -0,0 +1,130 @@
|
||||
# Copyright 2015 Dyn Inc.
|
||||
#
|
||||
# Author: Yasha Bubnov <ybubnov@dyn.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.
|
||||
import mock
|
||||
import dns.zone
|
||||
|
||||
from designate.agent import service
|
||||
from designate.backend import agent_backend
|
||||
from designate.tests import TestCase
|
||||
from designate.tests.test_agent.test_backends import BackendTestMixin
|
||||
|
||||
|
||||
class DenominatorAgentBackendTestCase(TestCase, BackendTestMixin):
|
||||
|
||||
def setUp(self):
|
||||
super(DenominatorAgentBackendTestCase, self).setUp()
|
||||
self.config(port=0, group='service:agent')
|
||||
self.backend = agent_backend.get_backend('denominator',
|
||||
agent_service=service.Service())
|
||||
|
||||
self.backend.start()
|
||||
|
||||
def tearDown(self):
|
||||
super(DenominatorAgentBackendTestCase, self).tearDown()
|
||||
self.backend.agent_service.stop()
|
||||
self.backend.stop()
|
||||
|
||||
@mock.patch('designate.utils.execute', return_value=(
|
||||
'example.org SOA 86400 ns1.designate.com. '
|
||||
'hostmaster@example.org. 475 3600 600 604800 1800', None))
|
||||
def test_find_domain_serial(self, execute):
|
||||
serial = self.backend.find_domain_serial('example.org.')
|
||||
|
||||
# Ensure returned right serial number
|
||||
self.assertEqual(475, serial)
|
||||
|
||||
# Ensure called "denominator zone add"
|
||||
self.assertIn('record', execute.call_args[0])
|
||||
self.assertIn('get', execute.call_args[0])
|
||||
|
||||
@mock.patch('designate.utils.execute', return_value=('', None))
|
||||
def test_find_domain_serial_fail(self, execute):
|
||||
serial = self.backend.find_domain_serial('example.org.')
|
||||
self.assertIsNone(serial)
|
||||
|
||||
@mock.patch('designate.utils.execute', return_value=(None, None))
|
||||
def test_create_domain(self, execute):
|
||||
domain = self._create_dnspy_zone('example.org.')
|
||||
self.backend.create_domain(domain)
|
||||
|
||||
# Ensure denominator called for each record (except SOA)
|
||||
# plus one to update zone data
|
||||
self.assertEqual(len(list(domain.iterate_rdatas())),
|
||||
execute.call_count)
|
||||
|
||||
@mock.patch('designate.utils.execute')
|
||||
def test_update_domain(self, execute):
|
||||
# Output from 'designate record list' command
|
||||
records = ('example.org SOA 86400 ns1.designate.com. '
|
||||
'hostmaster@example.org. 475 3600 600 604800 1800\n'
|
||||
'example.org NS 86400 ns1.designator.net.\n'
|
||||
'example.org NS 86400 ns2.designator.net.\n'
|
||||
'example.org MX 86400 10 mx1.designator.net.')
|
||||
|
||||
# That should force update_domain to delete A and AAAA records
|
||||
# from the zone and create a new MX record.
|
||||
execute.return_value = (records, None)
|
||||
|
||||
domain = self._create_dnspy_zone('example.org.')
|
||||
self.backend.update_domain(domain)
|
||||
|
||||
# Ensure denominator called to:
|
||||
# *update zone info
|
||||
# *fetch list of zone records
|
||||
# *delete one MX record
|
||||
# *replace one NS record
|
||||
# *create two A and two AAAA records
|
||||
# total: 8 calls
|
||||
|
||||
print(execute.call_args_list)
|
||||
self.assertEqual(8, execute.call_count)
|
||||
|
||||
self.backend.denominator = mock.MagicMock
|
||||
methods = ['update_zone',
|
||||
'get_records',
|
||||
'create_record', 'update_record', 'delete_record']
|
||||
for method in methods:
|
||||
setattr(self.backend.denominator, method, mock.Mock(
|
||||
return_value=records))
|
||||
|
||||
self.backend.update_domain(domain)
|
||||
self.assertEqual(1, self.backend.denominator.update_zone.call_count)
|
||||
self.assertEqual(1, self.backend.denominator.get_records.call_count)
|
||||
self.assertEqual(4, self.backend.denominator.create_record.call_count)
|
||||
self.assertEqual(1, self.backend.denominator.update_record.call_count)
|
||||
self.assertEqual(1, self.backend.denominator.delete_record.call_count)
|
||||
|
||||
@mock.patch('designate.utils.execute', return_value=(None, None))
|
||||
def test_delete_domain(self, execute):
|
||||
self.backend.delete_domain('example.org.')
|
||||
|
||||
# Ensure called 'denominator zone delete'
|
||||
self.assertEqual(1, execute.call_count)
|
||||
self.assertIn('zone', execute.call_args[0])
|
||||
self.assertIn('delete', execute.call_args[0])
|
||||
|
||||
# Returns dns.zone test object
|
||||
def _create_dnspy_zone(self, name):
|
||||
zone_text = ('$ORIGIN %(name)s\n'
|
||||
'@ 3600 IN SOA %(ns)s email.%(name)s 1421777854 3600 600 86400 3600\n'
|
||||
' 3600 IN NS %(ns)s\n'
|
||||
' 1800 IN A 173.194.123.30\n'
|
||||
' 1800 IN A 173.194.123.31\n'
|
||||
's 2400 IN AAAA 2001:db8:cafe::1\n'
|
||||
's 2400 IN AAAA 2001:db8:cafe::2\n'
|
||||
% {'name': name, 'ns': 'ns1.designate.net.'})
|
||||
|
||||
return dns.zone.from_text(zone_text, check_origin=False)
|
@ -288,6 +288,10 @@ debug = False
|
||||
#rndc_key_file = /etc/rndc.key
|
||||
#zone_file_path = $state_path/zones
|
||||
#query_destination = 127.0.0.1
|
||||
#
|
||||
[backend:agent:denominator]
|
||||
#name = dynect
|
||||
#config_file = /etc/denominator.conf
|
||||
|
||||
########################
|
||||
## Library Configuration
|
||||
|
@ -88,6 +88,7 @@ designate.backend =
|
||||
|
||||
designate.backend.agent_backend =
|
||||
bind9 = designate.backend.agent_backend.impl_bind9:Bind9Backend
|
||||
denominator = designate.backend.agent_backend.impl_denominator:DenominatorBackend
|
||||
fake = designate.backend.agent_backend.impl_fake:FakeBackend
|
||||
|
||||
designate.network_api =
|
||||
|
Loading…
x
Reference in New Issue
Block a user