summaryrefslogtreecommitdiff
path: root/designate/mdns/notify.py
blob: 0d66970de6a02e965c057ed2bafcc7695c9915b2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# Copyright (c) 2014 Rackspace Hosting
# All Rights Reserved.
#
#    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 socket

import eventlet
import dns
import dns.rdataclass
import dns.rdatatype
import dns.exception
import dns.flags
import dns.rcode
import dns.message
import dns.opcode
from oslo_config import cfg
from oslo_log import log as logging

from designate.mdns import base
from designate.metrics import metrics

dns_query = eventlet.import_patched('dns.query')

LOG = logging.getLogger(__name__)
CONF = cfg.CONF


class NotifyEndpoint(base.BaseEndpoint):
    RPC_API_VERSION = '2.0'
    RPC_API_NAMESPACE = 'notify'

    def notify_zone_changed(self, context, zone, host, port, timeout,
                            retry_interval, max_retries, delay):
        """
        :param context: The user context.
        :param zone: The designate zone object.  This contains the zone
            name.
        :param host: A notify is sent to this host.
        :param port: A notify is sent to this port.
        :param timeout: The time (in seconds) to wait for a NOTIFY response
            from server.
        :param retry_interval: The time (in seconds) between retries.
        :param max_retries: The maximum number of retries mindns would do for
            sending a NOTIFY message. After this many retries, mindns gives up.
        :param delay: The time to wait before sending the first NOTIFY request.
        :return: a tuple of (response, current_retry) where
            response is the response on success or None on failure.
            current_retry is the current retry number.
            The return value is just used for testing and not by pool manager.
        """
        start_time = time.time()
        try:
            time.sleep(delay)
            return self._make_and_send_dns_message(
                zone, host, port, timeout, retry_interval, max_retries,
                notify=True)
        finally:
            metrics.timing('mdns.notify_zone_changed',
                           time.time() - start_time)

    def poll_for_serial_number(self, context, zone, nameserver, timeout,
                               retry_interval, max_retries, delay):
        return

    def get_serial_number(self, context, zone, host, port, timeout,
                          retry_interval, max_retries, delay):
        """
        Get zone serial number from a resolver using retries.

        :param context: The user context.
        :param zone: The designate zone object.  This contains the zone
            name. zone.serial = expected_serial
        :param host: A notify is sent to this host.
        :param port: A notify is sent to this port.
        :param timeout: The time (in seconds) to wait for a SOA response from
            nameserver.
        :param retry_interval: The time (in seconds) between retries.
        :param max_retries: The maximum number of retries mindns would do for
            an expected serial number. After this many retries, mindns returns
            an ERROR.
        :param delay: The time to wait before sending the first request.
        :return: a tuple of (status, actual_serial, retries)
            status is either "SUCCESS" or "ERROR".
            actual_serial is either the serial number returned in the SOA
            message from the nameserver or None.
            retries is the number of retries left.
            The return value is just used for testing and not by pool manager.
            The pool manager is informed of the status with update_status.
        """
        actual_serial = None
        status = 'ERROR'
        retries_left = max_retries
        time.sleep(delay)
        while True:
            response, retry_cnt = self._make_and_send_dns_message(
                zone, host, port, timeout, retry_interval, retries_left)

            if response and (response.rcode() in (dns.rcode.NXDOMAIN,
                                                  dns.rcode.REFUSED,
                                                  dns.rcode.SERVFAIL) or
                             not bool(response.answer)):
                status = 'NO_ZONE'
                if zone.serial == 0 and zone.action in ('DELETE', 'NONE'):
                    actual_serial = 0
                    break  # Zone not expected to exist

            elif response and len(response.answer) == 1 \
                    and str(response.answer[0].name) == str(zone.name) \
                    and response.answer[0].rdclass == dns.rdataclass.IN \
                    and response.answer[0].rdtype == dns.rdatatype.SOA:
                # parse the SOA response and get the serial number
                rrset = response.answer[0]
                actual_serial = rrset.to_rdataset().items[0].serial

            # TODO(vinod): Account for serial number wrap around. Unix
            # timestamps are used where Designate is primary, but secondary
            # zones use different values.
            if actual_serial is not None and actual_serial >= zone.serial:
                # Everything looks good at this point. Return SUCCESS.
                status = 'SUCCESS'
                break

            retries_left -= retry_cnt
            msg = ("Got lower serial for '%(zone)s' to '%(host)s:"
                   "%(port)s'. Expected:'%(es)d'. Got:'%(as)s'."
                   "Retries left='%(retries)d'") % {
                      'zone': zone.name, 'host': host, 'port': port,
                      'es': zone.serial, 'as': actual_serial,
                      'retries': retries_left}

            if not retries_left:
                # return with error
                LOG.warning(msg)
                break

            LOG.debug(msg)
            # retry again
            time.sleep(retry_interval)

        # Return retries_left for testing purposes.
        return status, actual_serial, retries_left

    def _make_and_send_dns_message(self, zone, host, port, timeout,
                                   retry_interval, max_retries, notify=False):
        """
        Generate and send a DNS message over TCP or UDP using retries
        and return response.

        :param zone: The designate zone object.  This contains the zone
            name.
        :param host: The destination host for the dns message.
        :param port: The destination port for the dns message.
        :param timeout: The time (in seconds) to wait for a response from
            destination.
        :param retry_interval: The time (in seconds) between retries.
        :param max_retries: The maximum number of retries mindns would do for
            a response. After this many retries, the function returns.
        :param notify: If true, a notify message is constructed else a SOA
            message is constructed.
        :return: a tuple of (response, current_retry) where
            response is the response on success or None on failure.
            current_retry is the current retry number
        """
        dns_message = self._make_dns_message(zone.name, notify=notify)

        retry = 0
        response = None

        while retry < max_retries:
            retry += 1
            LOG.info("Sending '%(msg)s' for '%(zone)s' to '%(server)s:"
                     "%(port)d'.",
                     {'msg': 'NOTIFY' if notify else 'SOA',
                      'zone': zone.name, 'server': host,
                      'port': port})
            try:
                response = self._send_dns_message(dns_message, host, port,
                                                  timeout)

            except socket.error as e:
                if e.errno != socket.errno.EAGAIN:
                    raise  # unknown error, let it traceback

                # Initial workaround for bug #1558096
                LOG.info("Got EAGAIN while trying to send '%(msg)s' for "
                         "'%(zone)s' to '%(server)s:%(port)d'. "
                         "Timeout='%(timeout)d' seconds. Retry='%(retry)d'",
                         {'msg': 'NOTIFY' if notify else 'SOA',
                          'zone': zone.name, 'server': host,
                          'port': port, 'timeout': timeout,
                          'retry': retry})
                # retry sending the message
                time.sleep(retry_interval)
                continue

            except dns.exception.Timeout:
                LOG.warning(
                    "Got Timeout while trying to send '%(msg)s' for "
                    "'%(zone)s' to '%(server)s:%(port)d'. "
                    "Timeout='%(timeout)d' seconds. Retry='%(retry)d'",
                    {'msg': 'NOTIFY' if notify else 'SOA',
                     'zone': zone.name, 'server': host,
                     'port': port, 'timeout': timeout,
                     'retry': retry})
                # retry sending the message if we get a Timeout.
                time.sleep(retry_interval)
                continue

            except dns_query.BadResponse:
                LOG.warning("Got BadResponse while trying to send '%(msg)s' "
                            "for '%(zone)s' to '%(server)s:%(port)d'. "
                            "Timeout='%(timeout)d' seconds. Retry='%(retry)d'",
                            {'msg': 'NOTIFY' if notify else 'SOA',
                             'zone': zone.name, 'server': host,
                             'port': port, 'timeout': timeout,
                             'retry': retry})
                break  # no retries after BadResponse

            # either we have a good response or an error that we don't want to
            # recover by retrying
            break

        if not response:
            return None, retry

        # Check that we actually got a NOERROR in the rcode and and an
        # authoritative answer
        refused_statuses = (
            dns.rcode.NXDOMAIN, dns.rcode.REFUSED, dns.rcode.SERVFAIL
        )
        if (response.rcode() in refused_statuses or
                (response.rcode() == dns.rcode.NOERROR and
                 not bool(response.answer))):
            if notify:
                LOG.info(
                    '%(zone)s not found on %(server)s:%(port)d',
                    {
                        'zone': zone.name,
                        'server': host,
                        'port': port
                    }
                )
        elif (not (response.flags & dns.flags.AA) or
                dns.rcode.from_flags(response.flags,
                                     response.ednsflags) != dns.rcode.NOERROR):
            LOG.warning("Failed to get expected response while trying to "
                        "send '%(msg)s' for '%(zone)s' to '%(server)s:"
                        "%(port)d'.\nResponse message:\n%(resp)s\n",
                        {'msg': 'NOTIFY' if notify else 'SOA',
                         'zone': zone.name, 'server': host,
                         'port': port, 'resp': str(response)})
            response = None

        return response, retry

    def _make_dns_message(self, zone_name, notify=False):
        """
        This constructs a SOA query or a dns NOTIFY message.
        :param zone_name: The zone name for which a SOA/NOTIFY needs to be
            sent.
        :param notify: If true, a notify message is constructed else a SOA
            message is constructed.
        :return: The constructed message.
        """
        dns_message = dns.message.make_query(zone_name, dns.rdatatype.SOA)
        dns_message.flags = 0
        if notify:
            dns_message.set_opcode(dns.opcode.NOTIFY)
            dns_message.flags |= dns.flags.AA
        else:
            # Setting the flags to RD causes BIND9 to respond with a NXDOMAIN.
            dns_message.set_opcode(dns.opcode.QUERY)
            dns_message.flags |= dns.flags.RD

        return dns_message

    def _send_dns_message(self, dns_message, host, port, timeout):
        """
        Send DNS Message over TCP or UDP, return response.

        :param dns_message: The dns message that needs to be sent.
        :param host: The destination ip of dns_message.
        :param port: The destination port of dns_message.
        :param timeout: The timeout in seconds to wait for a response.
        :return: response
        """
        send = dns_query.tcp if CONF['service:mdns'].all_tcp else dns_query.udp
        return send(
            dns_message,
            socket.gethostbyname(host),
            port=port,
            timeout=timeout
        )