Files
designate/designate/agent/handler.py
T
Erik Olof Gunnar Andersson 8050680948 Removed RPC calls from MDNS and moved them to the Worker
This patch moved the remaining RPC calls away from the
MDNS service to the Worker and re-worked them to better
match the patterns used in the Worker. This means that
the MDNS service now only handles incoming DNS queries.

In addition the metrics backend has been removed as it was
only used by the MDNS RPC implementation and the monascastatsd
implementation no longer serves a purpose.

Closes-Bug: #1978742
Closes-Bug: #1978743
Change-Id: I5ef106717546a201fd62a51adacd43495c148cd4
2022-06-23 23:31:41 +00:00

255 lines
9.1 KiB
Python

# 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.
"""
agent.handler
~~~~~~~~~~~~~
Typically runs on the resolver hosts. Listen for incoming DNS requests
on a port different than 53 and execute create_zone/delete_zone on the
backend adaptor (e.g. Bind9)
Configured in [service:agent]
"""
import dns
import dns.flags
import dns.message
import dns.opcode
import dns.rcode
from oslo_config import cfg
from oslo_log import log as logging
from designate.backend import agent_backend
import designate.backend.private_codes as pcodes
from designate import dnsutils
from designate import utils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class RequestHandler(object):
def __init__(self):
self.masters = []
for server in CONF['service:agent'].masters:
raw_server = utils.split_host_port(server)
master = {'host': raw_server[0], 'port': int(raw_server[1])}
self.masters.append(master)
LOG.info("Agent masters: %(masters)s", {'masters': self.masters})
self.allow_notify = CONF['service:agent'].allow_notify
self.transfer_source = CONF['service:agent'].transfer_source
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 == pcodes.CC:
if rdclass == pcodes.CLASSCC:
if rdtype == pcodes.CREATE:
response = self._handle_create(request)
elif rdtype == pcodes.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
yield response
return
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]
zone_name = question.name.to_text()
if isinstance(zone_name, bytes):
zone_name = zone_name.decode('utf-8')
if not self._allowed(request, requester, "CREATE", zone_name):
response.set_rcode(dns.rcode.from_text("REFUSED"))
return response
serial = self.backend.find_zone_serial(zone_name)
if serial is not None:
# Does this warrant a warning?
# There is a race condition between checking if the zone exists
# and creating it.
LOG.warning("Not creating %(name)s, zone already exists",
{'name': zone_name})
# Provide an authoritative answer
response.flags |= dns.flags.AA
return response
LOG.debug("Received %(verb)s for %(name)s from %(host)s",
{'verb': "CREATE", 'name': zone_name, 'host': requester})
try:
# Receive an AXFR from MiniDNS to populate the zone
zone = dnsutils.do_axfr(zone_name, self.masters,
source=self.transfer_source)
self.backend.create_zone(zone)
except Exception as e:
# TODO(Federico) unknown exceptions should be logged with a full
# traceback. Same in the other methods.
LOG.error("Exception while creating zone %r", e)
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]
zone_name = question.name.to_text()
if isinstance(zone_name, bytes):
zone_name = zone_name.decode('utf-8')
if not self._allowed(request, requester, "NOTIFY", zone_name):
response.set_rcode(dns.rcode.from_text("REFUSED"))
return response
serial = self.backend.find_zone_serial(zone_name)
if serial is None:
LOG.warning("Refusing NOTIFY for %(name)s, doesn't exist",
{'name': zone_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': zone_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(zone_name, 'SOA')
# Check that the serial is < serial above
try:
zone = dnsutils.do_axfr(zone_name, self.masters,
source=self.transfer_source)
self.backend.update_zone(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]
zone_name = question.name.to_text()
if isinstance(zone_name, bytes):
zone_name = zone_name.decode('utf-8')
if not self._allowed(request, requester, "DELETE", zone_name):
response.set_rcode(dns.rcode.from_text("REFUSED"))
return response
serial = self.backend.find_zone_serial(zone_name)
if serial is None:
LOG.warning("Not deleting %(name)s, zone doesn't exist",
{'name': zone_name})
# Provide an authoritative answer
response.flags |= dns.flags.AA
return response
LOG.debug("Received DELETE for %(name)s from %(host)s",
{'name': zone_name, 'host': requester})
# Provide an authoritative answer
response.flags |= dns.flags.AA
# Call into the backend to Delete
try:
self.backend.delete_zone(zone_name)
except Exception:
response.set_rcode(dns.rcode.from_text("SERVFAIL"))
return response
return response
def _allowed(self, request, requester, op, zone_name):
# If there are no explict notifiers specified, allow all
if not self.allow_notify:
return True
if requester not in self.allow_notify:
LOG.warning("%(verb)s for %(name)s from %(server)s refused",
{'verb': op, 'name': zone_name, 'server': requester})
return False
return True