# designate.backend.impl_dynect

# Source code for designate.backend.impl_dynect

    # Copyright 2014 Hewlett-Packard Development Company, L.P.
    #
    # Author: Endre Karlson <endre.karlson@hpe.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

    from oslo_log import log as logging
    from oslo_serialization import jsonutils
    import requests
    from requests.adapters import HTTPAdapter

    from designate.backend import base
    import designate.conf
    from designate import exceptions
    from designate import utils

    CONF = designate.conf.CONF
    LOG = logging.getLogger(__name__)
    CFG_GROUP_NAME = 'backend:dynect'

    class DynClientError(exceptions.Backend):
        """The base exception class for all HTTP exceptions.
        """

        def __init__(self, data=None, job_id=None, msgs=None,
                     http_status=None, url=None, method=None, details=None):
            self.data = data
            self.job_id = job_id
            self.msgs = msgs

            self.http_status = http_status
            self.url = url
            self.method = method
            self.details = details
            formatted_string = (
                f'{self.msgs} (HTTP {self.method} to {self.url} - '
                f'{self.http_status}) - {self.details}'
            )
            if job_id:
                formatted_string += f' (Job-ID: {job_id})'
            super().__init__(formatted_string)

        @staticmethod
        def from_response(response, details=None):
            data = response.json()

            exc_kwargs = dict(
                data=data['data'],
                job_id=data['job_id'],
                msgs=data['msgs'],
                http_status=response.status_code,
                url=response.url,
                method=response.request.method,
                details=details)

            for msg in data.get('msgs', []):
                if msg['INFO'].startswith('login:'):
                    raise DynClientAuthError(**exc_kwargs)
                elif 'Operation blocked' in msg['INFO']:
                    raise DynClientOperationBlocked(**exc_kwargs)
            return DynClientError(**exc_kwargs)

    class DynClientAuthError(DynClientError):
        pass

    class DynTimeoutError(exceptions.Backend):
        """
        A job timedout.
        """
        error_code = 408
        error_type = 'dyn_timeout'

    class DynClientOperationBlocked(exceptions.BadRequest, DynClientError):
        error_type = 'operation_blocked'

    class DynClient:
        """
        DynECT service client.

        https://help.dynect.net/rest/
        """

        def __init__(self, customer_name, user_name, password, timeout, timings,
                     verify=True, retries=1, pool_maxsize=10, pool_connections=10):
            self.endpoint = 'https://api.dynect.net:443'
            self.api_version = '3.5.6'

            self.customer_name = customer_name
            self.user_name = user_name
            self.password = password

            self.times = []  # [("item", starttime, endtime), ...]
            self.timings = timings
            self.timeout = timeout

            self.authing = False
            self.token = None

            session = requests.Session()
            session.verify = verify

            session.headers = {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                'API-Version': self.api_version,
                'User-Agent': 'DynECTClient'
            }

            adapter = HTTPAdapter(
                max_retries=int(retries),
                pool_maxsize=int(pool_maxsize),
                pool_connections=int(pool_connections),
                pool_block=True
            )
            session.mount(self.endpoint, adapter)
            self.http = session

        def _http_log_req(self, method, url, kwargs):
            string_parts = [
                "curl -i",
                "-X '%s'" % method,
                "'%s'" % url,
            ]

            for element in kwargs['headers']:
                header = "-H '{}: {}'".format(element, kwargs['headers'][element])
                string_parts.append(header)

            LOG.debug('REQ: %s', ' '.join(string_parts))
            if 'data' in kwargs:
                LOG.debug('REQ BODY: %s\n', kwargs['data'])

        def _http_log_resp(self, resp):
            LOG.debug('RESP: [%s] %s\n', resp.status_code, resp.headers)

        def _request(self, method, url, **kwargs):
            """
            Low level request helper that actually executes the request towards a
            wanted URL.

            This does NOT do any authentication.
            """
            # NOTE: Allow passing the url as just the path or a full url
            if not url.startswith('http'):
                if not url.startswith('/REST'):
                    url = '/REST' + url
                url = self.endpoint + url

            kwargs.setdefault("headers", kwargs.get("headers", {}))
            kwargs['proxies'] = utils.get_proxies()

            if self.token is not None:
                kwargs['headers']['Auth-Token'] = self.token
            if self.timeout is not None:
                kwargs.setdefault("timeout", self.timeout)

            data = kwargs.get('data')
            if data is not None:
                kwargs['data'] = data.copy()

                # NOTE: We don't want to log the credentials (password) that are
                # used in a auth request.
                if 'password' in kwargs['data']:
                    kwargs['data']['password'] = '**SECRET**'

                self._http_log_req(method, url, kwargs)

                # NOTE: Set it back to the original data and serialize it.
                kwargs['data'] = jsonutils.dump_as_bytes(data)
            else:
                self._http_log_req(method, url, kwargs)

            start_time = time.monotonic()
            resp = self.http.request(method, url, **kwargs)
            if self.timings:
                self.times.append((f"{method} {url}",
                                   start_time, time.monotonic()))
            self._http_log_resp(resp)

            if resp.status_code >= 400:
                LOG.debug('Request returned failure status: %s', resp.status_code)
                raise DynClientError.from_response(resp)
            return resp

        def request(self, method, url, retries=2, **kwargs):
            if self.token is None and not self.authing:
                self.login()

            try:
                response = self._request(method, url, **kwargs)
            except DynClientAuthError:
                if retries > 0:
                    self.token = None
                    retries = retries - 1
                    return self.request(method, url, retries, **kwargs)
                else:
                    raise

            return response.json()

        def login(self):
            self.authing = True
            data = {
                'customer_name': self.customer_name,
                'user_name': self.user_name,
                'password': self.password
            }
            response = self.post('/Session', data=data)
            self.token = response['data']['token']
            self.authing = False

        def logout(self):
            self.delete('/Session')
            self.token = None

        def post(self, *args, **kwargs):
            response = self.request('POST', *args, **kwargs)
            return response

        def put(self, *args, **kwargs):
            response = self.request('PUT', *args, **kwargs)
            return response

        def delete(self, *args, **kwargs):
            response = self.request('DELETE', *args, **kwargs)
            return response

    class DynECTBackend(base.Backend):
        """
        Support for DynECT as a secondary DNS.
        """
        __plugin_name__ = 'dynect'

        __backend_status__ = 'untested'

        def __init__(self, target):
            super().__init__(target)

            self.customer_name = self.options.get('customer_name')
            self.username = self.options.get('username')
            self.password = self.options.get('password')
            self.contact_nickname = self.options.get('contact_nickname', None)
            self.tsig_key_name = self.options.get('tsig_key_name', None)

            for m in self.masters:
                if m.port != 53:
                    raise exceptions.ConfigurationError(
                        "DynECT only supports mDNS instances on port 53")

        def get_client(self):
            return DynClient(
                customer_name=self.customer_name,
                user_name=self.username,
                password=self.password,
                timeout=CONF[CFG_GROUP_NAME].timeout,
                timings=CONF[CFG_GROUP_NAME].timings)

        def create_zone(self, context, zone):
            LOG.info(
                'Creating zone %(d_id)s / %(d_name)s',
                {
                    'd_id': zone['id'],
                    'd_name': zone['name']
                }
            )

            url = '/Secondary/%s' % zone['name'].rstrip('.')
            data = {
                'masters': [m.host for m in self.masters]
            }

            if self.contact_nickname is not None:
                data['contact_nickname'] = self.contact_nickname

            if self.tsig_key_name is not None:
                data['tsig_key_name'] = self.tsig_key_name

            client = self.get_client()

            try:
                client.post(url, data=data)
            except DynClientError as e:
                for emsg in e.msgs:
                    if emsg['ERR_CD'] == 'TARGET_EXISTS':
                        LOG.info(
                            'Zone already exists, updating existing zone '
                            'instead %s', zone['name']
                        )
                        client.put(url, data=data)
                        break
                else:
                    raise

            client.put(url, data={'activate': True})
            client.logout()

        def delete_zone(self, context, zone, zone_params=None):
            LOG.info(
                'Deleting zone %(d_id)s / %(d_name)s', {
                    'd_id': zone['id'],
                    'd_name': zone['name']
                }
            )
            url = '/Zone/%s' % zone['name'].rstrip('.')
            client = self.get_client()
            try:
                client.delete(url)
            except DynClientError as e:
                if e.http_status == 404:
                    LOG.warning(
                        'Attempt to delete %(d_id)s / %(d_name)s caused 404, '
                        'ignoring.', {
                            'd_id': zone['id'],
                            'd_name': zone['name']
                        }
                    )
                    pass
                else:
                    raise
            client.logout()

this page last updated: 2026-08-19 02:25:39

Except where otherwise noted, this document is licensed under Creative Commons Attribution 3.0 License. See all OpenStack Legal Documents.

found an error? report a bug