designate.backend.agent_backend.impl_gdnsd

Source code for designate.backend.agent_backend.impl_gdnsd

# Copyright 2016 Hewlett Packard Enterprise Development Company LP
#
# Author: Federico Ceratto <federico.ceratto@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.

"""
backend.agent_backend.impl_gdnsd
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
gdnsd agent backend

Create, update, delete zones locally on a gdnsd resolver using the
gdnsd utility.

Supported Knot versions: >= 2.1, < 3

`User documentation <backends/gdnsd_agent.html>`_

.. WARNING::

    Untested, do not use in production.

.. NOTE::

    If the backend is killed during a configuration transaction it might be
    required to manually abort the transaction with `sudo gdnsd conf-abort`

Configured in [service:agent:gdnsd]
"""

import os
import tempfile
import string

import dns
import dns.resolver
from oslo_concurrency.processutils import ProcessExecutionError
from oslo_config import cfg
from oslo_log import log as logging

from designate import utils
from designate import exceptions
from designate.backend.agent_backend import base
from designate.i18n import _LI
from designate.i18n import _LE


LOG = logging.getLogger(__name__)
CFG_GROUP = 'backend:agent:gdnsd'
# rootwrap requires a command name instead of full path
GDNSD_DEFAULT_PATH = 'gdnsd'
CONFDIR_PATH = '/etc/gdnsd'
SOA_QUERY_TIMEOUT = 1
ZONE_FILE_PERMISSIONS = 0o0644


"""GROUP = backend:agent:gdnsd"""
gdnsd_group = cfg.OptGroup(
            name='backend:agent:gdnsd', title="Configuration for gdnsd backend"
        )
gdnsd_opts = [
    cfg.StrOpt('gdnsd-cmd-name',
               help='gdnsd executable path or rootwrap command name',
               default='gdnsd'),
    cfg.StrOpt('confdir-path',
               help='gdnsd configuration directory path',
               default='/etc/gdnsd'),
    cfg.StrOpt('query-destination', default='127.0.0.1',
               help='Host to query when finding zones')
]

cfg.CONF.register_group(gdnsd_group)
cfg.CONF.register_opts(gdnsd_opts, group=gdnsd_group)


[docs]def filter_exceptions(fn): # Let Backend() exceptions pass through, log out every other exception # and re-raise it as Backend() def wrapper(*a, **kw): try: return fn(*a, **kw) except exceptions.Backend as e: raise e except Exception as e: LOG.error(_LE("Unhandled exception %s"), e, exc_info=True) raise exceptions.Backend(e) return wrapper
[docs]class GdnsdBackend(base.AgentBackend): __plugin_name__ = 'gdnsd' __backend_status__ = 'experimental'
[docs] @classmethod def get_cfg_opts(cls): return [(gdnsd_group, gdnsd_opts)]
[docs] def __init__(self, *a, **kw): """Configure the backend""" super(GdnsdBackend, self).__init__(*a, **kw) self._gdnsd_cmd_name = cfg.CONF[CFG_GROUP].gdnsd_cmd_name LOG.info(_LI("gdnsd command: %r"), self._gdnsd_cmd_name) self._confdir_path = cfg.CONF[CFG_GROUP].confdir_path self._zonedir_path = os.path.join(self._confdir_path, 'zones') LOG.info(_LI("gdnsd conf directory: %r"), self._confdir_path) self._resolver = dns.resolver.Resolver(configure=False) self._resolver.timeout = SOA_QUERY_TIMEOUT self._resolver.lifetime = SOA_QUERY_TIMEOUT self._resolver.nameservers = [cfg.CONF[CFG_GROUP].query_destination] LOG.info(_LI("Resolvers: %r"), self._resolver.nameservers) self._check_dirs(self._zonedir_path)
[docs] def start(self): """Start the backend, check gdnsd configuration :raises: exception.Backend on invalid configuration """ LOG.info(_LI("Started gdnsd backend")) self._check_conf()
[docs] def _check_conf(self): """Run gdnsd to check its configuration """ try: out, err = utils.execute( cfg.CONF[CFG_GROUP].gdnsd_cmd_name, '-D', '-x', 'checkconf', '-c', self._confdir_path, run_as_root=False, ) except ProcessExecutionError as e: LOG.error(_LE("Command output: %(out)r Stderr: %(err)r"), { 'out': e.stdout, 'err': e.stderr }) raise exceptions.Backend("Configuration check failed")
[docs] def _check_dirs(self, *dirnames): """Check if directories are writable """ for dn in dirnames: if not os.path.isdir(dn): raise exceptions.Backend("Missing directory %s" % dn) if not os.access(dn, os.W_OK): raise exceptions.Backend("Directory not writable: %s" % dn)
[docs] def find_zone_serial(self, zone_name): """Query the local resolver for a zone Times out after SOA_QUERY_TIMEOUT """ LOG.debug("Finding %s", zone_name) try: rdata = self._resolver.query( zone_name, rdtype=dns.rdatatype.SOA)[0] return rdata.serial except Exception: return None
[docs] def _generate_zone_filename(self, zone_name): """Generate a filename for a zone file "/" is traslated into "@" Non-valid characters are translated into \ NNN where NNN is a decimal integer in the range 0 - 255 The filename is lowercase :returns: valid filename (string) """ valid_chars = "-_.@%s%s" % (string.ascii_letters, string.digits) fname = zone_name.replace('/', '@').lower() fname = [c if c in valid_chars else "\03%d" % ord(c) for c in fname] return ''.join(fname)
[docs] def _write_zone_file(self, zone): """Create or update a zone file atomically. The zone file is written to a unique temp file and then renamed """ zone_name = zone.origin.to_text(omit_final_dot=True).decode('utf-8') zone_base_fname = self._generate_zone_filename(zone_name) zone_fname = os.path.join(self._zonedir_path, zone_base_fname) try: # gdnsd ignores hidden files tmp_zone_fname = tempfile.mkstemp( prefix=".%s" % zone_base_fname, dir=self._zonedir_path, )[1] LOG.debug("Writing zone %r to %r and renaming it to %r", zone_name, tmp_zone_fname, zone_fname) zone.to_file(tmp_zone_fname) os.chmod(tmp_zone_fname, ZONE_FILE_PERMISSIONS) os.rename(tmp_zone_fname, zone_fname) finally: try: os.remove(tmp_zone_fname) except OSError: pass
[docs] @filter_exceptions def create_zone(self, zone): """Create a new Zone Do not raise exceptions if the zone already exists. :param zone: zone to be created :type zone: raw pythondns Zone :raises: exceptions.Backend on error """ # The zone might be already in place due to a race condition between # checking if the zone is there and creating it across different # greenlets self._write_zone_file(zone)
[docs] @filter_exceptions def update_zone(self, zone): """Instruct Djbdns DNS to perform AXFR from MiniDNS :param zone: zone to be created :type zone: raw pythondns Zone :raises: exceptions.Backend on error """ self._write_zone_file(zone)
[docs] @filter_exceptions def delete_zone(self, zone_name): """Delete a new Zone Do not raise exceptions if the zone does not exist. :param zone_name: zone name :type zone_name: str :raises: exceptions.Backend on error """ zone_name = zone_name.rstrip('.') LOG.debug('Deleting Zone: %s', zone_name) zone_fn = self._generate_zone_filename(zone_name) zone_fn = os.path.join(self._zonedir_path, zone_fn) try: os.remove(zone_fn) LOG.debug('Deleted Zone: %s', zone_name) except OSError as e: if os.errno.ENOENT == e.errno: LOG.info(_LI("Zone datafile %s was already deleted"), zone_fn) return raise
Creative Commons Attribution 3.0 License

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