Merge "Agent: Optional middleware to rate limit NOTIFYs"

This commit is contained in:
Jenkins 2015-08-13 12:31:01 +00:00 committed by Gerrit Code Review
commit 725a717818
5 changed files with 149 additions and 0 deletions

View File

@ -40,6 +40,9 @@ OPTS = [
help='The backend driver to use'), help='The backend driver to use'),
cfg.StrOpt('transfer-source', default=None, cfg.StrOpt('transfer-source', default=None,
help='An IP address to be used to fetch zones transferred in'), help='An IP address to be used to fetch zones transferred in'),
cfg.FloatOpt('notify-delay', default=0.0,
help='Delay after a NOTIFY arrives for a zone that the Agent '
'will pause and drop subsequent NOTIFYs for that zone'),
] ]
cfg.CONF.register_opts(OPTS, group='service:agent') cfg.CONF.register_opts(OPTS, group='service:agent')

View File

@ -43,6 +43,8 @@ class Service(service.DNSService, service.Service):
def _dns_application(self): def _dns_application(self):
# Create an instance of the RequestHandler class # Create an instance of the RequestHandler class
application = handler.RequestHandler() application = handler.RequestHandler()
if cfg.CONF['service:agent'].notify_delay > 0.0:
application = dnsutils.LimitNotifyMiddleware(application)
application = dnsutils.SerializationMiddleware(application) application = dnsutils.SerializationMiddleware(application)
return application return application

View File

@ -16,6 +16,8 @@
import random import random
import socket import socket
import base64 import base64
import time
from threading import Lock
import six import six
import dns import dns
@ -191,6 +193,78 @@ class TsigKeyring(object):
return default return default
class ZoneLock(object):
"""A Lock across all zones that enforces a rate limit on NOTIFYs"""
def __init__(self, delay):
self.lock = Lock()
self.data = {}
self.delay = delay
def acquire(self, zone):
with self.lock:
# If no one holds the lock for the zone, grant it
if zone not in self.data:
self.data[zone] = time.time()
return True
# Otherwise, get the time that it was locked
locktime = self.data[zone]
now = time.time()
period = now - locktime
# If it has been locked for longer than the allowed period
# give the lock to the new requester
if period > self.delay:
self.data[zone] = now
return True
LOG.debug('Lock for %(zone)s can\'t be releaesed for %(period)s'
'seconds' % {'zone': zone,
'period': str(self.delay - period)})
# Don't grant the lock for the zone
return False
def release(self, zone):
# Release the lock
with self.lock:
try:
self.data.pop(zone)
except KeyError:
pass
class LimitNotifyMiddleware(DNSMiddleware):
"""Middleware that rate limits NOTIFYs to the Agent"""
def __init__(self, application):
super(LimitNotifyMiddleware, self).__init__(application)
self.delay = cfg.CONF['service:agent'].notify_delay
self.locker = ZoneLock(self.delay)
def process_request(self, request):
opcode = request.opcode()
if opcode != dns.opcode.NOTIFY:
return None
zone_name = request.question[0].name.to_text()
if self.locker.acquire(zone_name):
time.sleep(self.delay)
self.locker.release(zone_name)
return None
else:
LOG.debug('Threw away NOTIFY for %(zone)s, already '
'working on an update.' % {'zone': zone_name})
response = dns.message.make_response(request)
# Provide an authoritative answer
response.flags |= dns.flags.AA
return (response,)
def from_dnspython_zone(dnspython_zone): def from_dnspython_zone(dnspython_zone):
# dnspython never builds a zone with more than one SOA, even if we give # dnspython never builds a zone with more than one SOA, even if we give
# it a zonefile that contains more than one # it a zonefile that contains more than one

View File

@ -13,7 +13,11 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mock
from dns import zone as dnszone from dns import zone as dnszone
import dns.message
import dns.rdatatype
import dns.rcode
from designate import dnsutils from designate import dnsutils
from designate.tests import TestCase from designate.tests import TestCase
@ -95,3 +99,68 @@ class TestUtils(TestCase):
self.assertEqual(len(SAMPLES), len(zone.recordsets)) self.assertEqual(len(SAMPLES), len(zone.recordsets))
self.assertEqual('example.com.', zone.name) self.assertEqual('example.com.', zone.name)
def test_zone_lock(self):
# Initialize a ZoneLock
lock = dnsutils.ZoneLock(0.1)
# Ensure there's no lock for different zones
for zone_name in ['foo.com.', 'bar.com.', 'example.com.']:
self.assertEqual(True, lock.acquire(zone_name))
# Ensure a lock for successive calls for the same zone
self.assertEqual(True, lock.acquire('example2.com.'))
self.assertEqual(False, lock.acquire('example2.com.'))
# Acquire, release, and reacquire
self.assertEqual(True, lock.acquire('example3.com.'))
lock.release('example3.com.')
self.assertEqual(True, lock.acquire('example3.com.'))
def test_limit_notify_middleware(self):
# Set the delay
self.config(notify_delay=.1,
group='service:agent')
# Initialize the middlware
placeholder_app = None
middleware = dnsutils.LimitNotifyMiddleware(placeholder_app)
# Prepare a NOTIFY
zone_name = 'example.com.'
notify = dns.message.make_query(zone_name, dns.rdatatype.SOA)
notify.flags = 0
notify.set_opcode(dns.opcode.NOTIFY)
notify.flags |= dns.flags.AA
# Send the NOTIFY through the middleware
# No problem, middleware should return None to pass it on
self.assertEqual(middleware.process_request(notify), None)
@mock.patch('designate.dnsutils.ZoneLock.acquire', return_value=False)
def test_limit_notify_middleware_no_acquire(self, acquire):
# Set the delay
self.config(notify_delay=.1,
group='service:agent')
# Initialize the middlware
placeholder_app = None
middleware = dnsutils.LimitNotifyMiddleware(placeholder_app)
# Prepare a NOTIFY
zone_name = 'example.com.'
notify = dns.message.make_query(zone_name, dns.rdatatype.SOA)
notify.flags = 0
notify.set_opcode(dns.opcode.NOTIFY)
notify.flags |= dns.flags.AA
# Make a response object to match the middleware's return
response = dns.message.make_response(notify)
# Provide an authoritative answer
response.flags |= dns.flags.AA
# Send the NOTIFY through the middleware
# Lock can't be acquired, a NOTIFY is already being worked on
# so just return what would have come back for a successful NOTIFY
# This needs to be a one item tuple for the serialization middleware
self.assertEqual(middleware.process_request(notify), (response,))

View File

@ -200,6 +200,7 @@ debug = False
#masters = 127.0.0.1:5354 #masters = 127.0.0.1:5354
#backend_driver = fake #backend_driver = fake
#transfer_source = None #transfer_source = None
#notify_delay = 0
#----------------------- #-----------------------
# Zone Manager Service # Zone Manager Service