The New Agent
A new Designate service to be run on a DNS master that is the 'reflection' of MiniDNS. It receives Create/NOTIFY/Delete actions, and calls into a backend that greatly resembles the style of backends in Designate before Server Pools. This is a squashed commit that formerly contained the following 'Agent' topics: Basic Service Add basic NOTIFY support Add AXFR Capability Add Support for Receiving Private CLASS/RRDATA Messages Add Backend Capabilities Cleanup (Changes here: http://paste.openstack.org/show/160022/) Implements: blueprint new-agent Change-Id: I610687878be9dee8065ebc492f662e86bbeb8ce7
This commit is contained in:
parent
8f59a264ec
commit
fbdd8e9400
39
designate/agent/__init__.py
Normal file
39
designate/agent/__init__.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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.config import cfg
|
||||
|
||||
cfg.CONF.register_group(cfg.OptGroup(
|
||||
name='service:agent', title="Configuration for the Agent Service"
|
||||
))
|
||||
|
||||
OPTS = [
|
||||
cfg.IntOpt('workers', default=None,
|
||||
help='Number of agent worker processes to spawn'),
|
||||
cfg.StrOpt('host', default='0.0.0.0',
|
||||
help='The Agent Bind Host'),
|
||||
cfg.IntOpt('port', default=5358,
|
||||
help='mDNS Port Number'),
|
||||
cfg.IntOpt('tcp-backlog', default=100,
|
||||
help='The Agent TCP Backlog'),
|
||||
cfg.ListOpt('allow-notify', default=[],
|
||||
help='List of IP addresses allowed to NOTIFY The Agent'),
|
||||
cfg.ListOpt('masters', default=[],
|
||||
help='List of masters for the Agent, format ip:port'),
|
||||
cfg.StrOpt('backend-driver', default='bind9',
|
||||
help='The backend driver to use'),
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(OPTS, group='service:agent')
|
63
designate/agent/axfr.py
Normal file
63
designate/agent/axfr.py
Normal file
@ -0,0 +1,63 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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 dns
|
||||
import dns.zone
|
||||
from oslo.config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate.i18n import _LI
|
||||
from designate.i18n import _LE
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class AXFR(object):
|
||||
|
||||
def __init__(self):
|
||||
self.masters = []
|
||||
for server in CONF['service:agent'].masters:
|
||||
raw_server = server.split(':')
|
||||
master = {'ip': raw_server[0], 'port': int(raw_server[1])}
|
||||
self.masters.append(master)
|
||||
|
||||
LOG.info(_LI("Agent masters: %(masters)s") %
|
||||
{'masters': self.masters})
|
||||
|
||||
def do_axfr(self, zone_name):
|
||||
"""
|
||||
Performs an AXFR for a given zone name
|
||||
"""
|
||||
# TODO(Tim): Try the first master, try others if they exist
|
||||
master = self.masters[0]
|
||||
|
||||
LOG.info(_LI("Doing AXFR for %(name)s from %(host)s") %
|
||||
{'name': zone_name, 'host': master})
|
||||
|
||||
xfr = dns.query.xfr(master['ip'], zone_name, relativize=False,
|
||||
port=master['port'])
|
||||
|
||||
try:
|
||||
# TODO(Tim): Add a timeout to this function
|
||||
raw_zone = dns.zone.from_xfr(xfr, relativize=False)
|
||||
except Exception:
|
||||
LOG.exception(_LE("There was a problem with the AXFR"))
|
||||
raise
|
||||
|
||||
LOG.debug("AXFR Successful for %s" % raw_zone.origin.to_text())
|
||||
|
||||
return raw_zone
|
212
designate/agent/handler.py
Normal file
212
designate/agent/handler.py
Normal file
@ -0,0 +1,212 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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 dns
|
||||
from oslo.config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate.agent import axfr
|
||||
from designate.backend import agent_backend
|
||||
from designate.i18n import _LW
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
# Command and Control OPCODE
|
||||
CC = 14
|
||||
|
||||
# Private DNS CLASS Uses
|
||||
ClassCC = 65280
|
||||
|
||||
# Private RR Code Uses
|
||||
SUCCESS = 65280
|
||||
FAILURE = 65281
|
||||
CREATE = 65282
|
||||
DELETE = 65283
|
||||
|
||||
|
||||
class RequestHandler(object):
|
||||
def __init__(self):
|
||||
self.xfr = axfr.AXFR()
|
||||
self.allow_notify = CONF['service:agent'].allow_notify
|
||||
backend_driver = cfg.CONF['service:agent'].backend_driver
|
||||
self.backend = agent_backend.get_backend(backend_driver, self)
|
||||
|
||||
def __call__(self, request):
|
||||
"""
|
||||
:param request: DNS Request Message
|
||||
:return: DNS Response Message
|
||||
"""
|
||||
# TODO(Tim): Handle multiple questions
|
||||
rdtype = request.question[0].rdtype
|
||||
rdclass = request.question[0].rdclass
|
||||
opcode = request.opcode()
|
||||
if opcode == dns.opcode.NOTIFY:
|
||||
response = self._handle_notify(request)
|
||||
elif opcode == CC:
|
||||
if rdclass == ClassCC:
|
||||
if rdtype == CREATE:
|
||||
response = self._handle_create(request)
|
||||
elif rdtype == DELETE:
|
||||
response = self._handle_delete(request)
|
||||
else:
|
||||
response = self._handle_query_error(request,
|
||||
dns.rcode.REFUSED)
|
||||
else:
|
||||
response = self._handle_query_error(request, dns.rcode.REFUSED)
|
||||
else:
|
||||
# Unhandled OpCodes include STATUS, QUERY, IQUERY, UPDATE
|
||||
response = self._handle_query_error(request, dns.rcode.REFUSED)
|
||||
|
||||
# TODO(Tim): Answer Type 65XXX queries
|
||||
return response
|
||||
|
||||
def _handle_query_error(self, request, rcode):
|
||||
"""
|
||||
Construct an error response with the rcode passed in.
|
||||
:param request: The decoded request from the wire.
|
||||
:param rcode: The response code to send back.
|
||||
:return: A dns response message with the response code set to rcode
|
||||
"""
|
||||
response = dns.message.make_response(request)
|
||||
response.set_rcode(rcode)
|
||||
|
||||
return response
|
||||
|
||||
def _handle_create(self, request):
|
||||
response = dns.message.make_response(request)
|
||||
|
||||
question = request.question[0]
|
||||
requester = request.environ['addr'][0]
|
||||
domain_name = question.name.to_text()
|
||||
|
||||
if not self._allowed(request, requester, "CREATE", domain_name):
|
||||
response.set_rcode(dns.rcode.from_text("REFUSED"))
|
||||
return response
|
||||
|
||||
serial = self.backend.find_domain_serial(domain_name)
|
||||
|
||||
if serial is not None:
|
||||
LOG.warn(_LW("Refusing CREATE for %(name)s, zone already exists") %
|
||||
{'name': domain_name})
|
||||
response.set_rcode(dns.rcode.from_text("REFUSED"))
|
||||
return response
|
||||
|
||||
LOG.debug("Received %(verb)s for %(name)s from %(host)s" %
|
||||
{'verb': "CREATE", 'name': domain_name, 'host': requester})
|
||||
|
||||
try:
|
||||
zone = self.xfr.do_axfr(domain_name)
|
||||
self.backend.create_domain(zone)
|
||||
except Exception:
|
||||
response.set_rcode(dns.rcode.from_text("SERVFAIL"))
|
||||
return response
|
||||
|
||||
# Provide an authoritative answer
|
||||
response.flags |= dns.flags.AA
|
||||
|
||||
return response
|
||||
|
||||
def _handle_notify(self, request):
|
||||
"""
|
||||
Constructs the response to a NOTIFY and acts accordingly on it.
|
||||
|
||||
* Decodes the NOTIFY
|
||||
* Checks if the master sending the NOTIFY is allowed to notify
|
||||
* Does a serial check to see if further action needs to be taken
|
||||
* Kicks off an AXFR and returns a valid response
|
||||
"""
|
||||
response = dns.message.make_response(request)
|
||||
|
||||
question = request.question[0]
|
||||
requester = request.environ['addr'][0]
|
||||
domain_name = question.name.to_text()
|
||||
|
||||
if not self._allowed(request, requester, "NOTIFY", domain_name):
|
||||
response.set_rcode(dns.rcode.from_text("REFUSED"))
|
||||
return response
|
||||
|
||||
serial = self.backend.find_domain_serial(domain_name)
|
||||
|
||||
if serial is None:
|
||||
LOG.warn(_LW("Refusing NOTIFY for %(name)s, doesn't exist") %
|
||||
{'name': domain_name})
|
||||
response.set_rcode(dns.rcode.from_text("REFUSED"))
|
||||
return response
|
||||
|
||||
LOG.debug("Received %(verb)s for %(name)s from %(host)s" %
|
||||
{'verb': "NOTIFY", 'name': domain_name, 'host': requester})
|
||||
|
||||
# According to RFC we should query the server that sent the NOTIFY
|
||||
# TODO(Tim): Reenable this when it makes more sense
|
||||
# resolver = dns.resolver.Resolver()
|
||||
# resolver.nameservers = [requester]
|
||||
# This assumes that the Master is running on port 53
|
||||
# soa_answer = resolver.query(domain_name, 'SOA')
|
||||
# Check that the serial is < serial above
|
||||
|
||||
try:
|
||||
zone = self.xfr.do_axfr(domain_name)
|
||||
self.backend.update_domain(zone)
|
||||
except Exception:
|
||||
response.set_rcode(dns.rcode.from_text("SERVFAIL"))
|
||||
return response
|
||||
|
||||
# Provide an authoritative answer
|
||||
response.flags |= dns.flags.AA
|
||||
|
||||
return response
|
||||
|
||||
def _handle_delete(self, request):
|
||||
"""
|
||||
Constructs the response to a DELETE and acts accordingly on it.
|
||||
|
||||
* Decodes the message for zone name
|
||||
* Checks if the master sending the DELETE is in the allowed notify list
|
||||
* Checks if the zone exists (maybe?)
|
||||
* Kicks a call to the backend to delete the zone in question
|
||||
"""
|
||||
response = dns.message.make_response(request)
|
||||
|
||||
question = request.question[0]
|
||||
requester = request.environ['addr'][0]
|
||||
domain_name = question.name.to_text()
|
||||
|
||||
if not self._allowed(request, requester, "DELETE", domain_name):
|
||||
response.set_rcode(dns.rcode.from_text("REFUSED"))
|
||||
return response
|
||||
|
||||
LOG.debug("Received DELETE for %(name)s from %(host)s" %
|
||||
{'name': domain_name, 'host': requester})
|
||||
|
||||
# Provide an authoritative answer
|
||||
response.flags |= dns.flags.AA
|
||||
|
||||
# Call into the backend to Delete
|
||||
try:
|
||||
self.backend.delete_domain(domain_name)
|
||||
except Exception:
|
||||
response.set_rcode(dns.rcode.from_text("SERVFAIL"))
|
||||
return response
|
||||
|
||||
return response
|
||||
|
||||
def _allowed(self, request, requester, op, domain_name):
|
||||
if requester not in self.allow_notify:
|
||||
LOG.warn(_LW("%(verb)s for %(name)s from %(server)s refused") %
|
||||
{'verb': op, 'name': domain_name, 'server': requester})
|
||||
return False
|
||||
|
||||
return True
|
42
designate/agent/middleware.py
Normal file
42
designate/agent/middleware.py
Normal file
@ -0,0 +1,42 @@
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Author: Kiall Mac Innes <kiall@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.
|
||||
|
||||
|
||||
class Middleware(object):
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
|
||||
def process_request(self, request):
|
||||
"""Called on each request.
|
||||
|
||||
If this returns None, the next application down the stack will be
|
||||
executed. If it returns a response then that response will be returned
|
||||
and execution will stop here.
|
||||
"""
|
||||
return None
|
||||
|
||||
def process_response(self, response):
|
||||
"""Do whatever you'd like to the response."""
|
||||
return response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.process_request(request)
|
||||
|
||||
if response:
|
||||
return response
|
||||
|
||||
response = self.application(request)
|
||||
return self.process_response(response)
|
180
designate/agent/service.py
Normal file
180
designate/agent/service.py
Normal file
@ -0,0 +1,180 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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 socket
|
||||
import struct
|
||||
|
||||
import dns
|
||||
from oslo.config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate import service
|
||||
from designate.agent import handler
|
||||
from designate.agent import middleware
|
||||
from designate.backend import agent_backend
|
||||
from designate.i18n import _LE
|
||||
from designate.i18n import _LI
|
||||
from designate.i18n import _LW
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class Service(service.TCPService):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Service, self).__init__(*args, **kwargs)
|
||||
|
||||
backend_driver = cfg.CONF['service:agent'].backend_driver
|
||||
self.backend = agent_backend.get_backend(backend_driver, self)
|
||||
|
||||
# Create an instance of the RequestHandler class
|
||||
self.application = handler.RequestHandler()
|
||||
|
||||
# Wrap the application in any middleware required
|
||||
# TODO(kiall): In the future, we want to allow users to pick+choose
|
||||
# the middleware to be applied, similar to how we do this
|
||||
# in the API.
|
||||
self.application = middleware.Middleware(self.application)
|
||||
|
||||
# Bind to the TCP port
|
||||
LOG.info(_LI('Opening TCP Listening Socket on %(host)s:%(port)d') %
|
||||
{'host': CONF['service:agent'].host,
|
||||
'port': CONF['service:agent'].port})
|
||||
self._sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._sock_tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self._sock_tcp.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self._sock_tcp.bind((CONF['service:agent'].host,
|
||||
CONF['service:agent'].port))
|
||||
self._sock_tcp.listen(CONF['service:agent'].tcp_backlog)
|
||||
|
||||
# Bind to the UDP port
|
||||
LOG.info(_LI('Opening UDP Listening Socket on %(host)s:%(port)d') %
|
||||
{'host': CONF['service:agent'].host,
|
||||
'port': CONF['service:agent'].port})
|
||||
self._sock_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self._sock_udp.bind((CONF['service:agent'].host,
|
||||
CONF['service:agent'].port))
|
||||
|
||||
def start(self):
|
||||
super(Service, self).start()
|
||||
self.backend.start()
|
||||
self.tg.add_thread(self._handle_tcp)
|
||||
self.tg.add_thread(self._handle_udp)
|
||||
LOG.info(_LI("Started Agent Service"))
|
||||
|
||||
def stop(self):
|
||||
super(Service, self).stop()
|
||||
LOG.info(_LI("Stopped Agent Service"))
|
||||
|
||||
def _deserialize_request(self, payload, addr):
|
||||
"""
|
||||
Deserialize a DNS Request Packet
|
||||
|
||||
:param payload: Raw DNS query payload
|
||||
:param addr: Tuple of the client's (IP, Port)
|
||||
"""
|
||||
try:
|
||||
request = dns.message.from_wire(payload)
|
||||
except dns.exception.DNSException:
|
||||
LOG.error(_LE("Failed to deserialize packet from "
|
||||
"%(host)s:%(port)d") %
|
||||
{'host': addr[0], 'port': addr[1]})
|
||||
return None
|
||||
else:
|
||||
# Create + Attach the initial "environ" dict. This is similar to
|
||||
# the environ dict used in typical WSGI middleware.
|
||||
request.environ = {'addr': addr}
|
||||
return request
|
||||
|
||||
def _serialize_response(self, response):
|
||||
"""
|
||||
Serialize a DNS Response Packet
|
||||
|
||||
:param response: DNS Response Message
|
||||
"""
|
||||
return response.to_wire()
|
||||
|
||||
def _handle_tcp(self):
|
||||
LOG.info(_LI("_handle_tcp thread started"))
|
||||
while True:
|
||||
client, addr = self._sock_tcp.accept()
|
||||
LOG.debug("Handling TCP Request from: %(host)s:%(port)d" %
|
||||
{'host': addr[0], 'port': addr[1]})
|
||||
|
||||
payload = client.recv(65535)
|
||||
(expected_length,) = struct.unpack('!H', payload[0:2])
|
||||
actual_length = len(payload[2:])
|
||||
|
||||
# For now we assume all requests are one packet
|
||||
# TODO(vinod): Handle multipacket requests
|
||||
if (expected_length != actual_length):
|
||||
LOG.warn(_LW("got a packet with unexpected length from "
|
||||
"%(host)s:%(port)d. Expected length=%(elen)d. "
|
||||
"Actual length=%(alen)d.") %
|
||||
{'host': addr[0], 'port': addr[1],
|
||||
'elen': expected_length, 'alen': actual_length})
|
||||
client.close()
|
||||
else:
|
||||
self.tg.add_thread(self._handle, addr, payload[2:], client)
|
||||
|
||||
def _handle_udp(self):
|
||||
LOG.info(_LI("_handle_udp thread started"))
|
||||
while True:
|
||||
# TODO(kiall): Determine the appropriate default value for
|
||||
# UDP recvfrom.
|
||||
payload, addr = self._sock_udp.recvfrom(8192)
|
||||
LOG.debug("Handling UDP Request from: %(host)s:%(port)d" %
|
||||
{'host': addr[0], 'port': addr[1]})
|
||||
|
||||
self.tg.add_thread(self._handle, addr, payload)
|
||||
|
||||
def _handle(self, addr, payload, client=None):
|
||||
"""
|
||||
Handle a DNS Query
|
||||
|
||||
:param addr: Tuple of the client's (IP, Port)
|
||||
:param payload: Raw DNS query payload
|
||||
:param client: Client socket (for TCP only)
|
||||
"""
|
||||
try:
|
||||
request = self._deserialize_request(payload, addr)
|
||||
|
||||
if request is None:
|
||||
# We failed to deserialize the request, generate a failure
|
||||
# response using a made up request.
|
||||
response = dns.message.make_response(
|
||||
dns.message.make_query('unknown', dns.rdatatype.A))
|
||||
response.set_rcode(dns.rcode.FORMERR)
|
||||
else:
|
||||
response = self.application(request)
|
||||
|
||||
# send back a response only if present
|
||||
if response:
|
||||
response = self._serialize_response(response)
|
||||
|
||||
if client is not None:
|
||||
# Handle TCP Responses
|
||||
msg_length = len(response)
|
||||
tcp_response = struct.pack("!H", msg_length) + response
|
||||
client.send(tcp_response)
|
||||
client.close()
|
||||
else:
|
||||
# Handle UDP Responses
|
||||
self._sock_udp.sendto(response, addr)
|
||||
except Exception:
|
||||
LOG.exception(_LE("Unhandled exception while processing request "
|
||||
"from %(host)s:%(port)d") %
|
||||
{'host': addr[0], 'port': addr[1]})
|
28
designate/backend/agent_backend/__init__.py
Normal file
28
designate/backend/agent_backend/__init__.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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_log import log as logging
|
||||
|
||||
from designate.backend.agent_backend.base import AgentBackend
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_backend(backend_driver, agent_service):
|
||||
LOG.debug("Loading backend driver: %s" % backend_driver)
|
||||
|
||||
cls = AgentBackend.get_driver(backend_driver)
|
||||
|
||||
return cls(agent_service)
|
57
designate/backend/agent_backend/base.py
Normal file
57
designate/backend/agent_backend/base.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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 abc
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate.plugin import DriverPlugin
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentBackend(DriverPlugin):
|
||||
"""Base class for backend implementations"""
|
||||
__plugin_type__ = 'backend'
|
||||
__plugin_ns__ = 'designate.backend.agent_backend'
|
||||
|
||||
def __init__(self, agent_service):
|
||||
super(AgentBackend, self).__init__()
|
||||
self.agent_service = agent_service
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def find_domain_serial(self, domain_name):
|
||||
"""Find a DNS Domain"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_domain(self, domain):
|
||||
"""Create a DNS domain"""
|
||||
"""Domain is a DNSPython Zone object"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def update_domain(self, domain):
|
||||
"""Update a DNS domain"""
|
||||
"""Domain is a DNSPython Zone object"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_domain(self, domain_name):
|
||||
"""Delete a DNS domain"""
|
149
designate/backend/agent_backend/impl_bind9.py
Normal file
149
designate/backend/agent_backend/impl_bind9.py
Normal file
@ -0,0 +1,149 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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 time
|
||||
import os
|
||||
|
||||
import dns
|
||||
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:bind9'
|
||||
|
||||
|
||||
class Bind9Backend(base.AgentBackend):
|
||||
__plugin_name__ = 'bind9'
|
||||
|
||||
@classmethod
|
||||
def get_cfg_opts(cls):
|
||||
group = cfg.OptGroup(
|
||||
name='backend:agent:bind9', title="Configuration for bind9 backend"
|
||||
)
|
||||
|
||||
opts = [
|
||||
cfg.StrOpt('rndc-host', default='127.0.0.1', help='RNDC Host'),
|
||||
cfg.IntOpt('rndc-port', default=953, help='RNDC Port'),
|
||||
cfg.StrOpt('rndc-config-file', default=None,
|
||||
help='RNDC Config File'),
|
||||
cfg.StrOpt('rndc-key-file', default=None, help='RNDC Key File'),
|
||||
cfg.StrOpt('zone-file-path', default='$state_path/zones',
|
||||
help='Path where zone files are stored')
|
||||
]
|
||||
|
||||
return [(group, opts)]
|
||||
|
||||
def start(self):
|
||||
LOG.info(_LI("Started bind9 backend"))
|
||||
|
||||
def find_domain_serial(self, domain_name):
|
||||
LOG.debug("Finding %s" % domain_name)
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = ['127.0.0.1']
|
||||
try:
|
||||
rdata = resolver.query(domain_name, 'SOA')[0]
|
||||
except Exception:
|
||||
return None
|
||||
return rdata.serial
|
||||
|
||||
def create_domain(self, domain):
|
||||
LOG.debug("Creating %s" % domain.origin.to_text())
|
||||
self._sync_domain(domain, new_domain_flag=True)
|
||||
|
||||
def update_domain(self, domain):
|
||||
LOG.debug("Updating %s" % domain.origin.to_text())
|
||||
self._sync_domain(domain)
|
||||
|
||||
def delete_domain(self, domain_name):
|
||||
LOG.debug('Delete Domain: %s' % domain_name)
|
||||
|
||||
rndc_op = 'delzone'
|
||||
# RNDC doesn't like the trailing dot on the domain name
|
||||
rndc_call = self._rndc_base() + [rndc_op, domain_name[:-1]]
|
||||
|
||||
utils.execute(*rndc_call)
|
||||
|
||||
def _rndc_base(self):
|
||||
rndc_call = [
|
||||
'rndc',
|
||||
'-s', cfg.CONF[CFG_GROUP].rndc_host,
|
||||
'-p', str(cfg.CONF[CFG_GROUP].rndc_port),
|
||||
]
|
||||
|
||||
if cfg.CONF[CFG_GROUP].rndc_config_file:
|
||||
rndc_call.extend(['-c',
|
||||
cfg.CONF[CFG_GROUP].rndc_config_file])
|
||||
|
||||
if cfg.CONF[CFG_GROUP].rndc_key_file:
|
||||
rndc_call.extend(['-k',
|
||||
cfg.CONF[CFG_GROUP].rndc_key_file])
|
||||
|
||||
return rndc_call
|
||||
|
||||
def _sync_domain(self, domain, new_domain_flag=False):
|
||||
"""Sync a single domain's zone file and reload bind config"""
|
||||
|
||||
domain_name = domain.origin.to_text()
|
||||
|
||||
# NOTE: Only one thread should be working with the Zonefile at a given
|
||||
# time. The sleep(1) below introduces a not insignificant risk
|
||||
# of more than 1 thread working with a zonefile at a given time.
|
||||
with lockutils.lock('bind9-%s' % domain_name):
|
||||
LOG.debug('Synchronising Domain: %s' % domain_name)
|
||||
|
||||
zone_path = cfg.CONF[CFG_GROUP].zone_file_path
|
||||
|
||||
output_path = os.path.join(zone_path,
|
||||
'%szone' % domain_name)
|
||||
|
||||
domain.to_file(output_path)
|
||||
|
||||
rndc_call = self._rndc_base()
|
||||
|
||||
if new_domain_flag:
|
||||
rndc_op = [
|
||||
'addzone',
|
||||
'%s { type master; file "%s"; };' % (domain_name,
|
||||
output_path),
|
||||
]
|
||||
rndc_call.extend(rndc_op)
|
||||
else:
|
||||
rndc_op = 'reload'
|
||||
rndc_call.extend([rndc_op])
|
||||
rndc_call.extend([domain_name])
|
||||
|
||||
if not new_domain_flag:
|
||||
# NOTE: Bind9 will only ever attempt to re-read a zonefile if
|
||||
# the file's timestamp has changed since the previous
|
||||
# reload. A one second sleep ensures we cross over a
|
||||
# second boundary before allowing the next change.
|
||||
time.sleep(1)
|
||||
|
||||
LOG.debug('Calling RNDC with: %s' % " ".join(rndc_call))
|
||||
self._execute_rndc(rndc_call)
|
||||
|
||||
def _execute_rndc(self, rndc_call):
|
||||
try:
|
||||
LOG.debug('Executing RNDC call: %s' % " ".join(rndc_call))
|
||||
utils.execute(*rndc_call)
|
||||
except utils.processutils.ProcessExecutionError as e:
|
||||
LOG.debug('RNDC call failure: %s' % e)
|
||||
raise exceptions.Backend(e)
|
44
designate/backend/agent_backend/impl_fake.py
Normal file
44
designate/backend/agent_backend/impl_fake.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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_log import log as logging
|
||||
|
||||
from designate.backend.agent_backend import base
|
||||
from designate.i18n import _LI
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FakeBackend(base.AgentBackend):
|
||||
__plugin_name__ = 'fake'
|
||||
|
||||
def start(self):
|
||||
LOG.info(_LI("Started fake backend, Pool Manager will not work!"))
|
||||
|
||||
def stop(self):
|
||||
LOG.info(_LI("Stopped fake backend"))
|
||||
|
||||
def find_domain_serial(self, domain_name):
|
||||
LOG.debug("Finding %s" % domain_name)
|
||||
return 0
|
||||
|
||||
def create_domain(self, domain):
|
||||
LOG.debug("Creating %s" % domain.origin.to_text())
|
||||
|
||||
def update_domain(self, domain):
|
||||
LOG.debug("Updating %s" % domain.origin.to_text())
|
||||
|
||||
def delete_domain(self, domain_name):
|
||||
LOG.debug('Delete Domain: %s' % domain_name)
|
37
designate/cmd/agent.py
Normal file
37
designate/cmd/agent.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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 sys
|
||||
|
||||
from oslo.config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate import service
|
||||
from designate import utils
|
||||
from designate.agent import service as agent_service
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.import_opt('workers', 'designate.agent', group='service:agent')
|
||||
|
||||
|
||||
def main():
|
||||
utils.read_config('designate', sys.argv)
|
||||
logging.setup(CONF, 'designate')
|
||||
|
||||
server = agent_service.Service.create(
|
||||
binary='designate-agent')
|
||||
service.serve(server, workers=CONF['service:agent'].workers)
|
||||
service.wait()
|
@ -128,6 +128,52 @@ class RPCService(Service):
|
||||
super(RPCService, self).wait()
|
||||
|
||||
|
||||
class TCPService(Service):
|
||||
"""
|
||||
Service class to be used for a service that only works in TCP
|
||||
"""
|
||||
def __init__(self, host=None, binary=None, service_name=None,
|
||||
endpoints=None, threads=1000):
|
||||
super(TCPService, self).__init__(threads)
|
||||
|
||||
self.host = host
|
||||
self.binary = binary
|
||||
self.service_name = service_name
|
||||
|
||||
self.endpoints = endpoints or [self]
|
||||
|
||||
@classmethod
|
||||
def create(cls, host=None, binary=None, service_name=None,
|
||||
endpoints=None):
|
||||
"""Instantiates class and passes back application object.
|
||||
|
||||
:param host: defaults to CONF.host
|
||||
:param binary: defaults to basename of executable
|
||||
"""
|
||||
if not host:
|
||||
host = CONF.host
|
||||
if not binary:
|
||||
binary = os.path.basename(inspect.stack()[-1][1])
|
||||
|
||||
service_obj = cls(host, binary, service_name=service_name,
|
||||
endpoints=endpoints)
|
||||
return service_obj
|
||||
|
||||
def start(self):
|
||||
for e in self.endpoints:
|
||||
if e != self and hasattr(e, 'start'):
|
||||
e.start()
|
||||
|
||||
super(TCPService, self).start()
|
||||
|
||||
def stop(self):
|
||||
for e in self.endpoints:
|
||||
if e != self and hasattr(e, 'stop'):
|
||||
e.stop()
|
||||
|
||||
super(TCPService, self).stop()
|
||||
|
||||
|
||||
class WSGIService(wsgi.Service, Service):
|
||||
"""
|
||||
Service class to be shared by all Designate WSGI Services
|
||||
|
20
designate/tests/test_agent/__init__.py
Normal file
20
designate/tests/test_agent/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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.tests import TestCase
|
||||
|
||||
|
||||
class AgentTestCase(TestCase):
|
||||
pass
|
26
designate/tests/test_agent/test_backends/__init__.py
Normal file
26
designate/tests/test_agent/test_backends/__init__.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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.config import cfg
|
||||
|
||||
from designate.backend import agent_backend
|
||||
from designate.agent import service
|
||||
|
||||
|
||||
class BackendTestMixin(object):
|
||||
def get_backend_driver(self):
|
||||
return agent_backend.get_backend(
|
||||
cfg.CONF['service:agent'].backend_driver,
|
||||
agent_service=service.Service())
|
69
designate/tests/test_agent/test_backends/test_bind9.py
Normal file
69
designate/tests/test_agent/test_backends/test_bind9.py
Normal file
@ -0,0 +1,69 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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 Bind9AgentBackendTestCase(TestCase, BackendTestMixin):
|
||||
|
||||
def setUp(self):
|
||||
super(Bind9AgentBackendTestCase, self).setUp()
|
||||
# Use a random port
|
||||
self.config(port=0, group='service:agent')
|
||||
self.backend = agent_backend.get_backend('bind9',
|
||||
agent_service=service.Service())
|
||||
self.backend.start()
|
||||
|
||||
def tearDown(self):
|
||||
super(Bind9AgentBackendTestCase, self).tearDown()
|
||||
self.backend.agent_service.stop()
|
||||
self.backend.stop()
|
||||
|
||||
def test_find_domain_serial(self):
|
||||
self.backend.find_domain_serial('example.org.')
|
||||
|
||||
@mock.patch('designate.utils.execute')
|
||||
@mock.patch(('designate.backend.agent_backend.impl_bind9.Bind9Backend'
|
||||
'._sync_domain'))
|
||||
def test_create_domain(self, execute, sync):
|
||||
domain = self._create_dnspy_zone('example.org')
|
||||
self.backend.create_domain(domain)
|
||||
|
||||
@mock.patch('designate.utils.execute')
|
||||
@mock.patch(('designate.backend.agent_backend.impl_bind9.Bind9Backend'
|
||||
'._sync_domain'))
|
||||
def test_update_domain(self, execute, sync):
|
||||
domain = self._create_dnspy_zone('example.org')
|
||||
self.backend.update_domain(domain)
|
||||
|
||||
@mock.patch('designate.utils.execute')
|
||||
@mock.patch(('designate.backend.agent_backend.impl_bind9.Bind9Backend'
|
||||
'._sync_domain'))
|
||||
def test_delete_domain(self, execute, sync):
|
||||
self.backend.delete_domain('example.org.')
|
||||
|
||||
# Helper
|
||||
def _create_dnspy_zone(self, name):
|
||||
zone_text = ('$ORIGIN %(name)s\n%(name)s 3600 IN SOA %(ns)s '
|
||||
'email.email.com. 1421777854 3600 600 86400 3600\n%(name)s 3600 IN NS '
|
||||
'%(ns)s\n') % {'name': name, 'ns': 'ns1.designate.com'}
|
||||
|
||||
return dns.zone.from_text(zone_text, check_origin=False)
|
59
designate/tests/test_agent/test_backends/test_fake.py
Normal file
59
designate/tests/test_agent/test_backends/test_fake.py
Normal file
@ -0,0 +1,59 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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 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 FakeAgentBackendTestCase(TestCase, BackendTestMixin):
|
||||
|
||||
def setUp(self):
|
||||
super(FakeAgentBackendTestCase, self).setUp()
|
||||
# Use a random port
|
||||
self.config(port=0, group='service:agent')
|
||||
self.backend = agent_backend.get_backend('fake',
|
||||
agent_service=service.Service())
|
||||
self.backend.start()
|
||||
|
||||
def tearDown(self):
|
||||
super(FakeAgentBackendTestCase, self).tearDown()
|
||||
self.backend.agent_service.stop()
|
||||
self.backend.stop()
|
||||
|
||||
def test_find_domain_serial(self):
|
||||
self.backend.find_domain_serial('example.org.')
|
||||
|
||||
def test_create_domain(self):
|
||||
domain = self._create_dnspy_zone('example.org')
|
||||
self.backend.create_domain(domain)
|
||||
|
||||
def test_update_domain(self):
|
||||
domain = self._create_dnspy_zone('example.org')
|
||||
self.backend.update_domain(domain)
|
||||
|
||||
def test_delete_domain(self):
|
||||
self.backend.delete_domain('example.org.')
|
||||
|
||||
# Helper
|
||||
def _create_dnspy_zone(self, name):
|
||||
zone_text = ('$ORIGIN %(name)s\n%(name)s 3600 IN SOA %(ns)s '
|
||||
'email.email.com. 1421777854 3600 600 86400 3600\n%(name)s 3600 IN NS '
|
||||
'%(ns)s\n') % {'name': name, 'ns': 'ns1.designate.com'}
|
||||
|
||||
return dns.zone.from_text(zone_text, check_origin=False)
|
181
designate/tests/test_agent/test_handler.py
Normal file
181
designate/tests/test_agent/test_handler.py
Normal file
@ -0,0 +1,181 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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 binascii
|
||||
|
||||
import dns
|
||||
import mock
|
||||
|
||||
import designate
|
||||
from designate.agent import handler
|
||||
from designate.tests.test_agent import AgentTestCase
|
||||
|
||||
|
||||
class AgentRequestHandlerTest(AgentTestCase):
|
||||
def setUp(self):
|
||||
super(AgentRequestHandlerTest, self).setUp()
|
||||
self.config(allow_notify=["0.0.0.0"],
|
||||
backend_driver="fake",
|
||||
group='service:agent')
|
||||
self.handler = handler.RequestHandler()
|
||||
self.addr = ["0.0.0.0", 5558]
|
||||
|
||||
@mock.patch.object(dns.resolver.Resolver, 'query')
|
||||
@mock.patch('designate.agent.axfr.AXFR.do_axfr')
|
||||
def test_receive_notify(self, func, axfrfunc):
|
||||
"""
|
||||
Get a NOTIFY and ensure the response is right,
|
||||
and an AXFR is triggered
|
||||
"""
|
||||
payload = ("1a7220000001000000000000076578616d706c6503636f6d000006"
|
||||
"0001")
|
||||
# expected response is NOERROR, other fields are
|
||||
# opcode NOTIFY
|
||||
# rcode NOERROR
|
||||
# flags QR AA
|
||||
# ;QUESTION
|
||||
# example.com. IN SOA
|
||||
# ;ANSWER
|
||||
# ;AUTHORITY
|
||||
# ;ADDITIONAL
|
||||
expected_response = ("1a72a4000001000000000000076578616d706c650363"
|
||||
"6f6d0000060001")
|
||||
request = dns.message.from_wire(binascii.a2b_hex(payload))
|
||||
request.environ = {'addr': ["0.0.0.0", 1234]}
|
||||
response = self.handler(request).to_wire()
|
||||
self.assertEqual(expected_response, binascii.b2a_hex(response))
|
||||
|
||||
def test_receive_notify_bad_notifier(self):
|
||||
"""
|
||||
Get a NOTIFY from a bad master and refuse it
|
||||
"""
|
||||
payload = "243520000001000000000000076578616d706c6503636f6d0000060001"
|
||||
# expected response is REFUSED, other fields are
|
||||
# opcode NOTIFY
|
||||
# rcode REFUSED
|
||||
# flags QR
|
||||
# ;QUESTION
|
||||
# example.com. IN SOA
|
||||
# ;ANSWER
|
||||
# ;AUTHORITY
|
||||
# ;ADDITIONAL
|
||||
expected_response = ("2435a0050001000000000000076578616d706c6503636f6d"
|
||||
"0000060001")
|
||||
request = dns.message.from_wire(binascii.a2b_hex(payload))
|
||||
# Bad 'requester'
|
||||
request.environ = {'addr': ["6.6.6.6", 1234]}
|
||||
response = self.handler(request).to_wire()
|
||||
|
||||
self.assertEqual(expected_response, binascii.b2a_hex(response))
|
||||
|
||||
@mock.patch.object(dns.resolver.Resolver, 'query')
|
||||
@mock.patch('designate.agent.axfr.AXFR.do_axfr')
|
||||
def test_receive_create(self, func, func2):
|
||||
"""
|
||||
Get a CREATE and ensure the response is right,
|
||||
and an AXFR is triggered, and the proper backend
|
||||
call is made
|
||||
"""
|
||||
payload = "735d70000001000000000000076578616d706c6503636f6d00ff02ff00"
|
||||
# Expected NOERROR other fields are
|
||||
# opcode 14
|
||||
# rcode NOERROR
|
||||
# flags QR AA
|
||||
# ;QUESTION
|
||||
# example.com. CLASS65280 TYPE65282
|
||||
# ;ANSWER
|
||||
# ;AUTHORITY
|
||||
# ;ADDITIONAL
|
||||
expected_response = ("735df4000001000000000000076578616d706c6503636f6d"
|
||||
"00ff02ff00")
|
||||
request = dns.message.from_wire(binascii.a2b_hex(payload))
|
||||
request.environ = {'addr': ["0.0.0.0", 1234]}
|
||||
with mock.patch.object(
|
||||
designate.backend.agent_backend.impl_fake.FakeBackend,
|
||||
'find_domain_serial', return_value=None):
|
||||
|
||||
response = self.handler(request).to_wire()
|
||||
self.assertEqual(expected_response, binascii.b2a_hex(response))
|
||||
|
||||
def test_receive_create_bad_notifier(self):
|
||||
"""
|
||||
Get a NOTIFY from a bad master and refuse it
|
||||
"""
|
||||
payload = "8dfd70000001000000000000076578616d706c6503636f6d00ff02ff00"
|
||||
# expected response is REFUSED, other fields are
|
||||
# opcode 14
|
||||
# rcode REFUSED
|
||||
# flags QR
|
||||
# ;QUESTION
|
||||
# example.com. CLASS65280 TYPE65282
|
||||
# ;ANSWER
|
||||
# ;AUTHORITY
|
||||
# ;ADDITIONAL
|
||||
expected_response = ("8dfdf0050001000000000000076578616d706c6503636f6d"
|
||||
"00ff02ff00")
|
||||
request = dns.message.from_wire(binascii.a2b_hex(payload))
|
||||
# Bad 'requester'
|
||||
request.environ = {'addr': ["6.6.6.6", 1234]}
|
||||
response = self.handler(request).to_wire()
|
||||
|
||||
self.assertEqual(expected_response, binascii.b2a_hex(response))
|
||||
|
||||
@mock.patch('designate.utils.execute')
|
||||
def test_receive_delete(self, func):
|
||||
"""
|
||||
Get a DELETE and ensure the response is right,
|
||||
and that the proper backend call is made
|
||||
"""
|
||||
payload = "3b9970000001000000000000076578616d706c6503636f6d00ff03ff00"
|
||||
# Expected NOERROR other fields are
|
||||
# opcode 14
|
||||
# rcode NOERROR
|
||||
# flags QR AA
|
||||
# ;QUESTION
|
||||
# example.com. CLASS65280 TYPE65283
|
||||
# ;ANSWER
|
||||
# ;AUTHORITY
|
||||
# ;ADDITIONAL
|
||||
expected_response = ("3b99f4000001000000000000076578616d706c6503636f6d"
|
||||
"00ff03ff00")
|
||||
request = dns.message.from_wire(binascii.a2b_hex(payload))
|
||||
request.environ = {'addr': ["0.0.0.0", 1234]}
|
||||
response = self.handler(request).to_wire()
|
||||
|
||||
self.assertEqual(expected_response, binascii.b2a_hex(response))
|
||||
|
||||
def test_receive_delete_bad_notifier(self):
|
||||
"""
|
||||
Get a message with an unsupported OPCODE and make
|
||||
sure that it is refused
|
||||
"""
|
||||
payload = "e6da70000001000000000000076578616d706c6503636f6d00ff03ff00"
|
||||
# expected response is REFUSED, other fields are
|
||||
# opcode 14
|
||||
# rcode REFUSED
|
||||
# flags QR
|
||||
# ;QUESTION
|
||||
# example.com. CLASS65280 TYPE65283
|
||||
# ;ANSWER
|
||||
# ;AUTHORITY
|
||||
# ;ADDITIONAL
|
||||
expected_response = ("e6daf0050001000000000000076578616d706c6503636f6d"
|
||||
"00ff03ff00")
|
||||
request = dns.message.from_wire(binascii.a2b_hex(payload))
|
||||
# Bad 'requester'
|
||||
request.environ = {'addr': ["6.6.6.6", 1234]}
|
||||
response = self.handler(request).to_wire()
|
||||
|
||||
self.assertEqual(expected_response, binascii.b2a_hex(response))
|
30
designate/tests/test_agent/test_service.py
Normal file
30
designate/tests/test_agent/test_service.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright 2014 Rackspace Inc.
|
||||
#
|
||||
# Author: Tim Simmons <tim.simmons@rackspace.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.tests.test_agent import AgentTestCase
|
||||
|
||||
|
||||
class AgentServiceTest(AgentTestCase):
|
||||
def setUp(self):
|
||||
super(AgentServiceTest, self).setUp()
|
||||
|
||||
# Use a random port
|
||||
self.config(port=0, group='service:agent')
|
||||
|
||||
self.service = self.start_service('agent')
|
||||
|
||||
def test_stop(self):
|
||||
# NOTE: Start is already done by the fixture in start_service()
|
||||
self.service.stop()
|
@ -101,6 +101,8 @@ def register_plugin_opts():
|
||||
# Register Backend Plugin Config Options
|
||||
plugin.Plugin.register_cfg_opts('designate.backend')
|
||||
plugin.Plugin.register_extra_cfg_opts('designate.backend')
|
||||
plugin.Plugin.register_cfg_opts('designate.backend.agent_backend')
|
||||
plugin.Plugin.register_extra_cfg_opts('designate.backend.agent_backend')
|
||||
|
||||
|
||||
def resource_string(*args):
|
||||
|
@ -118,6 +118,19 @@ debug = False
|
||||
#port = 5354
|
||||
#tcp_backlog =1 00
|
||||
|
||||
#-----------------------
|
||||
# Agent Service
|
||||
#-----------------------
|
||||
[service:agent]
|
||||
#workers = None
|
||||
#host = 0.0.0.0
|
||||
#port = 5358
|
||||
#tcp_backlog = 100
|
||||
#allow_notify = 127.0.0.1
|
||||
#masters = 127.0.0.1:5354
|
||||
#backend_driver = fake
|
||||
|
||||
|
||||
#-----------------------
|
||||
# Pool Manager Service
|
||||
#-----------------------
|
||||
@ -253,3 +266,13 @@ debug = False
|
||||
[backend:bind9:6a5032b6-2d96-43ee-b25b-7d784e2bf3b2]
|
||||
# host = 127.0.0.1
|
||||
# port = 53
|
||||
|
||||
#############################
|
||||
## Agent Backend Configuration
|
||||
#############################
|
||||
[backend:agent:bind9]
|
||||
#rndc_host = 127.0.0.1
|
||||
#rndc_port = 953
|
||||
#rndc_config_file = /etc/rndc.conf
|
||||
#rndc_key_file = /etc/rndc.key
|
||||
#zone_file_path = $state_path/zones
|
||||
|
@ -37,6 +37,7 @@ console_scripts =
|
||||
designate-mdns = designate.cmd.mdns:main
|
||||
designate-pool-manager = designate.cmd.pool_manager:main
|
||||
designate-sink = designate.cmd.sink:main
|
||||
designate-agent = designate.cmd.agent:main
|
||||
|
||||
designate.api.v1 =
|
||||
domains = designate.api.v1.domains:blueprint
|
||||
@ -75,6 +76,10 @@ designate.backend =
|
||||
#dynect = designate.backend.impl_dynect:DynECTBackend
|
||||
#ipa = designate.backend.impl_ipa:IPABackend
|
||||
|
||||
designate.backend.agent_backend =
|
||||
bind9 = designate.backend.agent_backend.impl_bind9:Bind9Backend
|
||||
fake = designate.backend.agent_backend.impl_fake:FakeBackend
|
||||
|
||||
designate.network_api =
|
||||
fake = designate.network_api.fake:FakeNetworkAPI
|
||||
neutron = designate.network_api.neutron:NeutronNetworkAPI
|
||||
|
Loading…
x
Reference in New Issue
Block a user