# DNS management tool
#
# Copyright (C) Amitay Isaacs 2011-2012
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
import samba.getopt as options
from samba import WERRORError
from samba import werror
from struct import pack
from socket import inet_ntop, inet_pton
from socket import AF_INET
from socket import AF_INET6
import struct
import time
import ldb
from samba.ndr import ndr_unpack, ndr_pack
import re

from samba import remove_dc, dsdb_dns
from samba.samdb import SamDB
from samba.auth import system_session

from samba.netcmd import (
    Command,
    CommandError,
    Option,
    SuperCommand,
)
from samba.dcerpc import dnsp, dnsserver

from samba.dnsserver import record_from_string, DNSParseError, flag_from_string
from samba.dnsserver import dns_record_match


def dns_connect(server, lp, creds):
    if server.lower() == 'localhost':
        server = '127.0.0.1'
    binding_str = "ncacn_ip_tcp:%s[sign]" % server
    try:
        dns_conn = dnsserver.dnsserver(binding_str, lp, creds)
    except RuntimeError as e:
        raise CommandError('Connecting to DNS RPC server %s failed with %s' % (server, e))

    return dns_conn


class DnsConnWrapper:
    """A wrapper around a dnsserver.dnsserver connection that makes it
    harder not to report friendly messages.

    If, rather than

        dns_conn = dns_connect(server, lp, creds)

    you use

        dns_conn = DnsConnWrapper(server, lp, creds)

    then various common errors (for example, misspelled zones) on
    common operations will raise CommandErrors that turn into
    relatively nice messages (when compared to tracebacks).

    In addition, if you provide a messages keyword argument, it will
    override the defaults. Note that providing None will turn off the
    default, letting the original exception shine through.

        messages = {
            werror.WERR_DNS_ERROR_ZONE_DOES_NOT_EXIST: (
                f'Zone {zone} does not exist and so could not be deleted.'),
            werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST: None
        }
        res = dns_conn.DnssrvOperation2( # ...
                                        messages=messages)

    This example changes the message for ZONE_DOES_NOT_EXIST and
    avoids catching NAME_DOES_NOT_EXIST.

    Only WERRORErrors are intercepted.
    """

    default_messages = {
        werror.WERR_DNS_ERROR_DS_UNAVAILABLE: "Could not contact RPC server",
        werror.WERR_DNS_ERROR_ZONE_ALREADY_EXISTS: 'Zone already exists',
        werror.WERR_DNS_ERROR_RECORD_DOES_NOT_EXIST: 'The record does not exist',
        werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:  'The zone does not exist',
        werror.WERR_ACCESS_DENIED: 'Insufficient permissions',
    }

    def __init__(self, server, lp, creds):
        self.dns_conn = dns_connect(server, lp, creds)

    def __getattr__(self, name):
        attr = getattr(self.dns_conn, name)
        if name not in {
                "DnssrvComplexOperation2",
                "DnssrvEnumRecords2",
                "DnssrvOperation2",
                "DnssrvQuery2",
                "DnssrvUpdateRecord2"}:
            return attr

        def f(*args, messages=None):
            if messages is None:
                messages = {}

            try:
                return attr(*args)
            except WERRORError as e:
                werr, errstr = e.args
                if werr in messages:
                    if werr is None:
                        # None overrides a default message, leaving the bare exception
                        raise
                    raise CommandError(f"{messages[werr]} [{errstr}]", e)
                if werr in self.default_messages:
                    raise CommandError(f"{self.default_messages[werr]} [{errstr}]", e)
                raise

        return f


def bool_string(flag):
    if flag == 0:
        ret = 'FALSE'
    elif flag == 1:
        ret = 'TRUE'
    else:
        ret = 'UNKNOWN (0x%x)' % flag
    return ret


def enum_string(module, enum_defs, value):
    ret = None
    for e in enum_defs:
        if value == getattr(module, e):
            ret = e
            break
    if not ret:
        ret = 'UNKNOWN (0x%x)' % value
    return ret


def bitmap_string(module, bitmap_defs, value):
    ret = ''
    for b in bitmap_defs:
        if value & getattr(module, b):
            ret += '%s ' % b
    if not ret:
        ret = 'NONE'
    return ret


def boot_method_string(boot_method):
    enum_defs = ['DNS_BOOT_METHOD_UNINITIALIZED', 'DNS_BOOT_METHOD_FILE',
                 'DNS_BOOT_METHOD_REGISTRY', 'DNS_BOOT_METHOD_DIRECTORY']
    return enum_string(dnsserver, enum_defs, boot_method)


def name_check_flag_string(check_flag):
    enum_defs = ['DNS_ALLOW_RFC_NAMES_ONLY', 'DNS_ALLOW_NONRFC_NAMES',
                 'DNS_ALLOW_MULTIBYTE_NAMES', 'DNS_ALLOW_ALL_NAMES']
    return enum_string(dnsserver, enum_defs, check_flag)


def zone_type_string(zone_type):
    enum_defs = ['DNS_ZONE_TYPE_CACHE', 'DNS_ZONE_TYPE_PRIMARY',
                 'DNS_ZONE_TYPE_SECONDARY', 'DNS_ZONE_TYPE_STUB',
                 'DNS_ZONE_TYPE_FORWARDER', 'DNS_ZONE_TYPE_SECONDARY_CACHE']
    return enum_string(dnsp, enum_defs, zone_type)


def zone_update_string(zone_update):
    enum_defs = ['DNS_ZONE_UPDATE_OFF', 'DNS_ZONE_UPDATE_UNSECURE',
                 'DNS_ZONE_UPDATE_SECURE']
    return enum_string(dnsp, enum_defs, zone_update)


def zone_secondary_security_string(security):
    enum_defs = ['DNS_ZONE_SECSECURE_NO_SECURITY', 'DNS_ZONE_SECSECURE_NS_ONLY',
                 'DNS_ZONE_SECSECURE_LIST_ONLY', 'DNS_ZONE_SECSECURE_NO_XFER']
    return enum_string(dnsserver, enum_defs, security)


def zone_notify_level_string(notify_level):
    enum_defs = ['DNS_ZONE_NOTIFY_OFF', 'DNS_ZONE_NOTIFY_ALL_SECONDARIES',
                 'DNS_ZONE_NOTIFY_LIST_ONLY']
    return enum_string(dnsserver, enum_defs, notify_level)


def dp_flags_string(dp_flags):
    bitmap_defs = ['DNS_DP_AUTOCREATED', 'DNS_DP_LEGACY', 'DNS_DP_DOMAIN_DEFAULT',
                   'DNS_DP_FOREST_DEFAULT', 'DNS_DP_ENLISTED', 'DNS_DP_DELETED']
    return bitmap_string(dnsserver, bitmap_defs, dp_flags)


def zone_flags_string(flags):
    bitmap_defs = ['DNS_RPC_ZONE_PAUSED', 'DNS_RPC_ZONE_SHUTDOWN',
                   'DNS_RPC_ZONE_REVERSE', 'DNS_RPC_ZONE_AUTOCREATED',
                   'DNS_RPC_ZONE_DSINTEGRATED', 'DNS_RPC_ZONE_AGING',
                   'DNS_RPC_ZONE_UPDATE_UNSECURE', 'DNS_RPC_ZONE_UPDATE_SECURE',
                   'DNS_RPC_ZONE_READONLY']
    return bitmap_string(dnsserver, bitmap_defs, flags)


def ip4_array_string(array):
    ret = []
    if not array:
        return ret
    for i in range(array.AddrCount):
        addr = inet_ntop(AF_INET, pack('I', array.AddrArray[i]))
        ret.append(addr)
    return ret


def dns_addr_array_string(array):
    ret = []
    if not array:
        return ret
    for i in range(array.AddrCount):
        if array.AddrArray[i].MaxSa[0] == 0x02:
            x = struct.pack('4B', *array.AddrArray[i].MaxSa[4:8])
            addr = inet_ntop(AF_INET, x)
        elif array.AddrArray[i].MaxSa[0] == 0x17:
            x = struct.pack('16B', *array.AddrArray[i].MaxSa[8:24])
            addr = inet_ntop(AF_INET6, x)
        else:
            addr = 'UNKNOWN'
        ret.append(addr)
    return ret


def dns_type_flag(rec_type):
    try:
        return flag_from_string(rec_type)
    except DNSParseError as e:
        raise CommandError(*e.args)


def dns_client_version(cli_version):
    version = cli_version.upper()
    if version == 'W2K':
        client_version = dnsserver.DNS_CLIENT_VERSION_W2K
    elif version == 'DOTNET':
        client_version = dnsserver.DNS_CLIENT_VERSION_DOTNET
    elif version == 'LONGHORN':
        client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN
    else:
        raise CommandError('Unknown client version %s' % cli_version)
    return client_version


def print_serverinfo(outf, typeid, serverinfo):
    outf.write('  dwVersion                   : 0x%x\n' % serverinfo.dwVersion)
    outf.write('  fBootMethod                 : %s\n' % boot_method_string(serverinfo.fBootMethod))
    outf.write('  fAdminConfigured            : %s\n' % bool_string(serverinfo.fAdminConfigured))
    outf.write('  fAllowUpdate                : %s\n' % bool_string(serverinfo.fAllowUpdate))
    outf.write('  fDsAvailable                : %s\n' % bool_string(serverinfo.fDsAvailable))
    outf.write('  pszServerName               : %s\n' % serverinfo.pszServerName)
    outf.write('  pszDsContainer              : %s\n' % serverinfo.pszDsContainer)

    if typeid != dnsserver.DNSSRV_TYPEID_SERVER_INFO:
        outf.write('  aipServerAddrs              : %s\n' %
                   ip4_array_string(serverinfo.aipServerAddrs))
        outf.write('  aipListenAddrs              : %s\n' %
                   ip4_array_string(serverinfo.aipListenAddrs))
        outf.write('  aipForwarders               : %s\n' %
                   ip4_array_string(serverinfo.aipForwarders))
    else:
        outf.write('  aipServerAddrs              : %s\n' %
                   dns_addr_array_string(serverinfo.aipServerAddrs))
        outf.write('  aipListenAddrs              : %s\n' %
                   dns_addr_array_string(serverinfo.aipListenAddrs))
        outf.write('  aipForwarders               : %s\n' %
                   dns_addr_array_string(serverinfo.aipForwarders))

    outf.write('  dwLogLevel                  : %d\n' % serverinfo.dwLogLevel)
    outf.write('  dwDebugLevel                : %d\n' % serverinfo.dwDebugLevel)
    outf.write('  dwForwardTimeout            : %d\n' % serverinfo.dwForwardTimeout)
    outf.write('  dwRpcPrototol               : 0x%x\n' % serverinfo.dwRpcProtocol)
    outf.write('  dwNameCheckFlag             : %s\n' % name_check_flag_string(serverinfo.dwNameCheckFlag))
    outf.write('  cAddressAnswerLimit         : %d\n' % serverinfo.cAddressAnswerLimit)
    outf.write('  dwRecursionRetry            : %d\n' % serverinfo.dwRecursionRetry)
    outf.write('  dwRecursionTimeout          : %d\n' % serverinfo.dwRecursionTimeout)
    outf.write('  dwMaxCacheTtl               : %d\n' % serverinfo.dwMaxCacheTtl)
    outf.write('  dwDsPollingInterval         : %d\n' % serverinfo.dwDsPollingInterval)
    outf.write('  dwScavengingInterval        : %d\n' % serverinfo.dwScavengingInterval)
    outf.write('  dwDefaultRefreshInterval    : %d\n' % serverinfo.dwDefaultRefreshInterval)
    outf.write('  dwDefaultNoRefreshInterval  : %d\n' % serverinfo.dwDefaultNoRefreshInterval)
    outf.write('  fAutoReverseZones           : %s\n' % bool_string(serverinfo.fAutoReverseZones))
    outf.write('  fAutoCacheUpdate            : %s\n' % bool_string(serverinfo.fAutoCacheUpdate))
    outf.write('  fRecurseAfterForwarding     : %s\n' % bool_string(serverinfo.fRecurseAfterForwarding))
    outf.write('  fForwardDelegations         : %s\n' % bool_string(serverinfo.fForwardDelegations))
    outf.write('  fNoRecursion                : %s\n' % bool_string(serverinfo.fNoRecursion))
    outf.write('  fSecureResponses            : %s\n' % bool_string(serverinfo.fSecureResponses))
    outf.write('  fRoundRobin                 : %s\n' % bool_string(serverinfo.fRoundRobin))
    outf.write('  fLocalNetPriority           : %s\n' % bool_string(serverinfo.fLocalNetPriority))
    outf.write('  fBindSecondaries            : %s\n' % bool_string(serverinfo.fBindSecondaries))
    outf.write('  fWriteAuthorityNs           : %s\n' % bool_string(serverinfo.fWriteAuthorityNs))
    outf.write('  fStrictFileParsing          : %s\n' % bool_string(serverinfo.fStrictFileParsing))
    outf.write('  fLooseWildcarding           : %s\n' % bool_string(serverinfo.fLooseWildcarding))
    outf.write('  fDefaultAgingState          : %s\n' % bool_string(serverinfo.fDefaultAgingState))

    if typeid != dnsserver.DNSSRV_TYPEID_SERVER_INFO_W2K:
        outf.write('  dwRpcStructureVersion       : 0x%x\n' % serverinfo.dwRpcStructureVersion)
        outf.write('  aipLogFilter                : %s\n' % dns_addr_array_string(serverinfo.aipLogFilter))
        outf.write('  pwszLogFilePath             : %s\n' % serverinfo.pwszLogFilePath)
        outf.write('  pszDomainName               : %s\n' % serverinfo.pszDomainName)
        outf.write('  pszForestName               : %s\n' % serverinfo.pszForestName)
        outf.write('  pszDomainDirectoryPartition : %s\n' % serverinfo.pszDomainDirectoryPartition)
        outf.write('  pszForestDirectoryPartition : %s\n' % serverinfo.pszForestDirectoryPartition)

        outf.write('  dwLocalNetPriorityNetMask   : 0x%x\n' % serverinfo.dwLocalNetPriorityNetMask)
        outf.write('  dwLastScavengeTime          : %d\n' % serverinfo.dwLastScavengeTime)
        outf.write('  dwEventLogLevel             : %d\n' % serverinfo.dwEventLogLevel)
        outf.write('  dwLogFileMaxSize            : %d\n' % serverinfo.dwLogFileMaxSize)
        outf.write('  dwDsForestVersion           : %d\n' % serverinfo.dwDsForestVersion)
        outf.write('  dwDsDomainVersion           : %d\n' % serverinfo.dwDsDomainVersion)
        outf.write('  dwDsDsaVersion              : %d\n' % serverinfo.dwDsDsaVersion)

    if typeid == dnsserver.DNSSRV_TYPEID_SERVER_INFO:
        outf.write('  fReadOnlyDC                 : %s\n' % bool_string(serverinfo.fReadOnlyDC))


def print_zoneinfo(outf, typeid, zoneinfo):
    outf.write('  pszZoneName                 : %s\n' % zoneinfo.pszZoneName)
    outf.write('  dwZoneType                  : %s\n' % zone_type_string(zoneinfo.dwZoneType))
    outf.write('  fReverse                    : %s\n' % bool_string(zoneinfo.fReverse))
    outf.write('  fAllowUpdate                : %s\n' % zone_update_string(zoneinfo.fAllowUpdate))
    outf.write('  fPaused                     : %s\n' % bool_string(zoneinfo.fPaused))
    outf.write('  fShutdown                   : %s\n' % bool_string(zoneinfo.fShutdown))
    outf.write('  fAutoCreated                : %s\n' % bool_string(zoneinfo.fAutoCreated))
    outf.write('  fUseDatabase                : %s\n' % bool_string(zoneinfo.fUseDatabase))
    outf.write('  pszDataFile                 : %s\n' % zoneinfo.pszDataFile)
    if typeid != dnsserver.DNSSRV_TYPEID_ZONE_INFO:
        outf.write('  aipMasters                  : %s\n' %
                   ip4_array_string(zoneinfo.aipMasters))
    else:
        outf.write('  aipMasters                  : %s\n' %
                   dns_addr_array_string(zoneinfo.aipMasters))
    outf.write('  fSecureSecondaries          : %s\n' % zone_secondary_security_string(zoneinfo.fSecureSecondaries))
    outf.write('  fNotifyLevel                : %s\n' % zone_notify_level_string(zoneinfo.fNotifyLevel))
    if typeid != dnsserver.DNSSRV_TYPEID_ZONE_INFO:
        outf.write('  aipSecondaries              : %s\n' %
                   ip4_array_string(zoneinfo.aipSecondaries))
        outf.write('  aipNotify                   : %s\n' %
                   ip4_array_string(zoneinfo.aipNotify))
    else:
        outf.write('  aipSecondaries              : %s\n' %
                   dns_addr_array_string(zoneinfo.aipSecondaries))
        outf.write('  aipNotify                   : %s\n' %
                   dns_addr_array_string(zoneinfo.aipNotify))
    outf.write('  fUseWins                    : %s\n' % bool_string(zoneinfo.fUseWins))
    outf.write('  fUseNbstat                  : %s\n' % bool_string(zoneinfo.fUseNbstat))
    outf.write('  fAging                      : %s\n' % bool_string(zoneinfo.fAging))
    outf.write('  dwNoRefreshInterval         : %d\n' % zoneinfo.dwNoRefreshInterval)
    outf.write('  dwRefreshInterval           : %d\n' % zoneinfo.dwRefreshInterval)
    outf.write('  dwAvailForScavengeTime      : %d\n' % zoneinfo.dwAvailForScavengeTime)
    if typeid != dnsserver.DNSSRV_TYPEID_ZONE_INFO:
        outf.write('  aipScavengeServers          : %s\n' %
                   ip4_array_string(zoneinfo.aipScavengeServers))
    else:
        outf.write('  aipScavengeServers          : %s\n' %
                   dns_addr_array_string(zoneinfo.aipScavengeServers))

    if typeid != dnsserver.DNSSRV_TYPEID_ZONE_INFO_W2K:
        outf.write('  dwRpcStructureVersion       : 0x%x\n' % zoneinfo.dwRpcStructureVersion)
        outf.write('  dwForwarderTimeout          : %d\n' % zoneinfo.dwForwarderTimeout)
        outf.write('  fForwarderSlave             : %d\n' % zoneinfo.fForwarderSlave)
        if typeid != dnsserver.DNSSRV_TYPEID_ZONE_INFO:
            outf.write('  aipLocalMasters             : %s\n' %
                       ip4_array_string(zoneinfo.aipLocalMasters))
        else:
            outf.write('  aipLocalMasters             : %s\n' %
                       dns_addr_array_string(zoneinfo.aipLocalMasters))
        outf.write('  dwDpFlags                   : %s\n' % dp_flags_string(zoneinfo.dwDpFlags))
        outf.write('  pszDpFqdn                   : %s\n' % zoneinfo.pszDpFqdn)
        outf.write('  pwszZoneDn                  : %s\n' % zoneinfo.pwszZoneDn)
        outf.write('  dwLastSuccessfulSoaCheck    : %d\n' % zoneinfo.dwLastSuccessfulSoaCheck)
        outf.write('  dwLastSuccessfulXfr         : %d\n' % zoneinfo.dwLastSuccessfulXfr)

    if typeid == dnsserver.DNSSRV_TYPEID_ZONE_INFO:
        outf.write('  fQueuedForBackgroundLoad    : %s\n' % bool_string(zoneinfo.fQueuedForBackgroundLoad))
        outf.write('  fBackgroundLoadInProgress   : %s\n' % bool_string(zoneinfo.fBackgroundLoadInProgress))
        outf.write('  fReadOnlyZone               : %s\n' % bool_string(zoneinfo.fReadOnlyZone))
        outf.write('  dwLastXfrAttempt            : %d\n' % zoneinfo.dwLastXfrAttempt)
        outf.write('  dwLastXfrResult             : %d\n' % zoneinfo.dwLastXfrResult)


def print_zone(outf, typeid, zone):
    outf.write('  pszZoneName                 : %s\n' % zone.pszZoneName)
    outf.write('  Flags                       : %s\n' % zone_flags_string(zone.Flags))
    outf.write('  ZoneType                    : %s\n' % zone_type_string(zone.ZoneType))
    outf.write('  Version                     : %s\n' % zone.Version)

    if typeid != dnsserver.DNSSRV_TYPEID_ZONE_W2K:
        outf.write('  dwDpFlags                   : %s\n' % dp_flags_string(zone.dwDpFlags))
        outf.write('  pszDpFqdn                   : %s\n' % zone.pszDpFqdn)


def print_enumzones(outf, typeid, zones):
    outf.write('  %d zone(s) found\n' % zones.dwZoneCount)
    for zone in zones.ZoneArray:
        outf.write('\n')
        print_zone(outf, typeid, zone)


def print_dns_record(outf, rec):
    if rec.wType == dnsp.DNS_TYPE_A:
        mesg = 'A: %s' % (rec.data)
    elif rec.wType == dnsp.DNS_TYPE_AAAA:
        mesg = 'AAAA: %s' % (rec.data)
    elif rec.wType == dnsp.DNS_TYPE_PTR:
        mesg = 'PTR: %s' % (rec.data.str)
    elif rec.wType == dnsp.DNS_TYPE_NS:
        mesg = 'NS: %s' % (rec.data.str)
    elif rec.wType == dnsp.DNS_TYPE_CNAME:
        mesg = 'CNAME: %s' % (rec.data.str)
    elif rec.wType == dnsp.DNS_TYPE_SOA:
        mesg = 'SOA: serial=%d, refresh=%d, retry=%d, expire=%d, minttl=%d, ns=%s, email=%s' % (
                    rec.data.dwSerialNo,
                    rec.data.dwRefresh,
                    rec.data.dwRetry,
                    rec.data.dwExpire,
                    rec.data.dwMinimumTtl,
                    rec.data.NamePrimaryServer.str,
                    rec.data.ZoneAdministratorEmail.str)
    elif rec.wType == dnsp.DNS_TYPE_MX:
        mesg = 'MX: %s (%d)' % (rec.data.nameExchange.str, rec.data.wPreference)
    elif rec.wType == dnsp.DNS_TYPE_SRV:
        mesg = 'SRV: %s (%d, %d, %d)' % (rec.data.nameTarget.str, rec.data.wPort,
                                         rec.data.wPriority, rec.data.wWeight)
    elif rec.wType == dnsp.DNS_TYPE_TXT:
        slist = ['"%s"' % name.str for name in rec.data.str]
        mesg = 'TXT: %s' % ','.join(slist)
    else:
        mesg = 'Unknown: '
    outf.write('    %s (flags=%x, serial=%d, ttl=%d)\n' % (
                mesg, rec.dwFlags, rec.dwSerial, rec.dwTtlSeconds))


def print_dnsrecords(outf, records):
    for rec in records.rec:
        outf.write('  Name=%s, Records=%d, Children=%d\n' % (
                    rec.dnsNodeName.str,
                    rec.wRecordCount,
                    rec.dwChildCount))
        for dns_rec in rec.records:
                print_dns_record(outf, dns_rec)


# Convert data into a dns record
def data_to_dns_record(record_type, data):
    try:
        rec = record_from_string(record_type, data)
    except DNSParseError as e:
        raise CommandError(*e.args) from None

    return rec


class cmd_serverinfo(Command):
    """Query for Server information."""

    synopsis = '%prog <server> [options]'

    takes_args = ['server']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    takes_options = [
        Option('--client-version', help='Client Version',
               default='longhorn', metavar='w2k|dotnet|longhorn',
               choices=['w2k', 'dotnet', 'longhorn'], dest='cli_ver'),
    ]

    def run(self, server, cli_ver, sambaopts=None, credopts=None,
            versionopts=None):
        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp)
        dns_conn = DnsConnWrapper(server, self.lp, self.creds)

        client_version = dns_client_version(cli_ver)

        typeid, res = dns_conn.DnssrvQuery2(client_version, 0, server,
                                            None, 'ServerInfo')
        print_serverinfo(self.outf, typeid, res)


def _add_integer_options(table, takes_options, integer_properties):
    """Generate options for cmd_zoneoptions"""
    for k, doc, _min, _max in table:
        o = '--' + k.lower()
        opt =  Option(o,
                      help=f"{doc} [{_min}-{_max}]",
                      type="int",
                      dest=k)
        takes_options.append(opt)
        integer_properties.append((k, _min, _max, o))


class cmd_zoneoptions(Command):
    """Change zone aging options."""

    synopsis = '%prog <server> <zone> [options]'

    takes_args = ['server', 'zone']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    takes_options = [
        Option('--client-version', help='Client Version',
               default='longhorn', metavar='w2k|dotnet|longhorn',
               choices=['w2k', 'dotnet', 'longhorn'], dest='cli_ver'),
        Option('--mark-old-records-static', metavar="YYYY-MM-DD",
               help="Make records older than this (YYYY-MM-DD) static"),
        Option('--mark-records-static-regex', metavar="REGEXP",
               help="Make records matching this regular expression static"),
        Option('--mark-records-dynamic-regex', metavar="REGEXP",
               help="Make records matching this regular expression dynamic"),
        Option('-n', '--dry-run', action='store_true',
               help="Don't change anything, say what would happen"),
    ]

    integer_properties = []
    # Any zone parameter that is stored as an integer (which is most of
    # them) can be added to this table. The name should be the dnsp
    # mixed case name, which will get munged into a lowercase name for
    # the option. (e.g. "Aging" becomes "--aging").
    #
    # Note: just because we add a name here doesn't mean we will use
    # it.
    _add_integer_options([
    #       ( name,   help-string,         min, max )
            ('Aging', 'Enable record aging', 0, 1),
            ('NoRefreshInterval',
             'Aging no refresh interval in hours (0: use default)',
             0, 10 * 365 * 24),
            ('RefreshInterval',
             'Aging refresh interval in hours (0: use default)',
             0, 10 * 365 * 24),
            ],
                         takes_options,
                         integer_properties)

    def run(self, server, zone, cli_ver, sambaopts=None, credopts=None,
            versionopts=None, dry_run=False,
            mark_old_records_static=None,
            mark_records_static_regex=None,
            mark_records_dynamic_regex=None,
            **kwargs):
        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp)
        dns_conn = DnsConnWrapper(server, self.lp, self.creds)

        client_version = dns_client_version(cli_ver)
        nap_type = dnsserver.DNSSRV_TYPEID_NAME_AND_PARAM

        for k, _min, _max, o in self.integer_properties:
            if kwargs.get(k) is None:
                continue
            v = kwargs[k]
            if _min is not None and v < _min:
                raise CommandError(f"{o} must be at least {_min}")
            if _max is not None and v > _max:
                raise CommandError(f"{o} can't exceed {_max}")

            name_param = dnsserver.DNS_RPC_NAME_AND_PARAM()
            name_param.dwParam = v
            name_param.pszNodeName = k
            if dry_run:
                print(f"would set {k} to {v} for {zone}", file=self.outf)
                continue
            try:
                dns_conn.DnssrvOperation2(client_version,
                                          0,
                                          server,
                                          zone,
                                          0,
                                          'ResetDwordProperty',
                                          nap_type,
                                          name_param)
            except WERRORError as e:
                raise CommandError(f"Could not set {k} to {v}") from None

            print(f"Set {k} to {v}", file=self.outf)

        # We don't want to allow more than one of these --mark-*
        # options at a time, as they are sensitive to ordering and
        # the order is not documented.
        n_mark_options = 0
        for x in (mark_old_records_static,
                  mark_records_static_regex,
                  mark_records_dynamic_regex):
            if x is not None:
                n_mark_options += 1

        if n_mark_options > 1:
            raise CommandError("Multiple --mark-* options will not work\n")

        if mark_old_records_static is not None:
            self.mark_old_records_static(server, zone,
                                         mark_old_records_static,
                                         dry_run)

        if mark_records_static_regex is not None:
            self.mark_records_static_regex(server,
                                           zone,
                                           mark_records_static_regex,
                                           dry_run)

        if mark_records_dynamic_regex is not None:
            self.mark_records_dynamic_regex(server,
                                            zone,
                                            mark_records_dynamic_regex,
                                            dry_run)


    def _get_dns_nodes(self, server, zone_name):
        samdb = SamDB(url="ldap://%s" % server,
                      session_info=system_session(),
                      credentials=self.creds, lp=self.lp)

        zone_dn = (f"DC={zone_name},CN=MicrosoftDNS,DC=DomainDNSZones,"
                   f"{samdb.get_default_basedn()}")

        nodes = samdb.search(base=zone_dn,
                             scope=ldb.SCOPE_SUBTREE,
                             expression=("(&(objectClass=dnsNode)"
                                         "(!(dNSTombstoned=TRUE)))"),
                             attrs=["dnsRecord", "name"])
        return samdb, nodes

    def mark_old_records_static(self, server, zone_name, date_string, dry_run):
        try:
            ts = time.strptime(date_string, "%Y-%m-%d")
            t = time.mktime(ts)
        except ValueError as e:
            raise CommandError(f"Invalid date {date_string}: should be YYY-MM-DD")
        threshold = dsdb_dns.unix_to_dns_timestamp(int(t))

        samdb, nodes = self._get_dns_nodes(server, zone_name)

        for node in nodes:
            if "dnsRecord" not in node:
                continue

            values = list(node["dnsRecord"])
            changes = 0
            for i, v in enumerate(values):
                rec = ndr_unpack(dnsp.DnssrvRpcRecord, v)
                if rec.dwTimeStamp < threshold and rec.dwTimeStamp != 0:
                    rec.dwTimeStamp = 0
                    values[i] = ndr_pack(rec)
                    changes += 1

            if changes == 0:
                continue

            name = node["name"][0].decode()

            if dry_run:
                print(f"would make {changes}/{len(values)} records static "
                      f"on {name}.{zone_name}.", file=self.outf)
                continue

            msg = ldb.Message.from_dict(samdb,
                                        {'dn': node.dn,
                                         'dnsRecord': values
                                        },
                                        ldb.FLAG_MOD_REPLACE)
            samdb.modify(msg)
            print(f"made {changes}/{len(values)} records static on "
                  f"{name}.{zone_name}.", file=self.outf)

    def mark_records_static_regex(self, server, zone_name, regex, dry_run):
        """Make the records of nodes with matching names static.
        """
        r = re.compile(regex)
        samdb, nodes = self._get_dns_nodes(server, zone_name)

        for node in nodes:
            name = node["name"][0].decode()
            if not r.search(name):
                continue
            if "dnsRecord" not in node:
                continue

            values = list(node["dnsRecord"])
            if len(values) == 0:
                continue

            changes = 0
            for i, v in enumerate(values):
                rec = ndr_unpack(dnsp.DnssrvRpcRecord, v)
                if rec.dwTimeStamp != 0:
                    rec.dwTimeStamp = 0
                    values[i] = ndr_pack(rec)
                    changes += 1

            if changes == 0:
                continue

            if dry_run:
                print(f"would make {changes}/{len(values)} records static "
                      f"on {name}.{zone_name}.", file=self.outf)
                continue

            msg = ldb.Message.from_dict(samdb,
                                        {'dn': node.dn,
                                         'dnsRecord': values
                                        },
                                        ldb.FLAG_MOD_REPLACE)
            samdb.modify(msg)
            print(f"made {changes}/{len(values)} records static on "
                  f"{name}.{zone_name}.", file=self.outf)

    def mark_records_dynamic_regex(self, server, zone_name, regex, dry_run):
        """Make the records of nodes with matching names dynamic, with a
        current timestamp. In this case we only adjust the A, AAAA,
        and TXT records.
        """
        r = re.compile(regex)
        samdb, nodes = self._get_dns_nodes(server, zone_name)
        now = time.time()
        dns_timestamp = dsdb_dns.unix_to_dns_timestamp(int(now))
        safe_wtypes = {
            dnsp.DNS_TYPE_A,
            dnsp.DNS_TYPE_AAAA,
            dnsp.DNS_TYPE_TXT
        }

        for node in nodes:
            name = node["name"][0].decode()
            if not r.search(name):
                continue
            if "dnsRecord" not in node:
                continue

            values = list(node["dnsRecord"])
            if len(values) == 0:
                continue

            changes = 0
            for i, v in enumerate(values):
                rec = ndr_unpack(dnsp.DnssrvRpcRecord, v)
                if rec.wType in safe_wtypes and rec.dwTimeStamp == 0:
                    rec.dwTimeStamp = dns_timestamp
                    values[i] = ndr_pack(rec)
                    changes += 1

            if changes == 0:
                continue

            if dry_run:
                print(f"would make {changes}/{len(values)} records dynamic "
                      f"on {name}.{zone_name}.", file=self.outf)
                continue

            msg = ldb.Message.from_dict(samdb,
                                        {'dn': node.dn,
                                         'dnsRecord': values
                                        },
                                        ldb.FLAG_MOD_REPLACE)
            samdb.modify(msg)
            print(f"made {changes}/{len(values)} records dynamic on "
                  f"{name}.{zone_name}.", file=self.outf)


class cmd_zoneinfo(Command):
    """Query for zone information."""

    synopsis = '%prog <server> <zone> [options]'

    takes_args = ['server', 'zone']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    takes_options = [
        Option('--client-version', help='Client Version',
               default='longhorn', metavar='w2k|dotnet|longhorn',
               choices=['w2k', 'dotnet', 'longhorn'], dest='cli_ver'),
    ]

    def run(self, server, zone, cli_ver, sambaopts=None, credopts=None,
            versionopts=None):
        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp)
        dns_conn = DnsConnWrapper(server, self.lp, self.creds)

        client_version = dns_client_version(cli_ver)

        typeid, res = dns_conn.DnssrvQuery2(client_version, 0, server, zone,
                                            'ZoneInfo')
        print_zoneinfo(self.outf, typeid, res)


class cmd_zonelist(Command):
    """Query for zones."""

    synopsis = '%prog <server> [options]'

    takes_args = ['server']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    takes_options = [
        Option('--client-version', help='Client Version',
               default='longhorn', metavar='w2k|dotnet|longhorn',
               choices=['w2k', 'dotnet', 'longhorn'], dest='cli_ver'),
        Option('--primary', help='List primary zones (default)',
               action='store_true', dest='primary'),
        Option('--secondary', help='List secondary zones',
               action='store_true', dest='secondary'),
        Option('--cache', help='List cached zones',
               action='store_true', dest='cache'),
        Option('--auto', help='List automatically created zones',
               action='store_true', dest='auto'),
        Option('--forward', help='List forward zones',
               action='store_true', dest='forward'),
        Option('--reverse', help='List reverse zones',
               action='store_true', dest='reverse'),
        Option('--ds', help='List directory integrated zones',
               action='store_true', dest='ds'),
        Option('--non-ds', help='List non-directory zones',
               action='store_true', dest='nonds')
    ]

    def run(self, server, cli_ver, primary=False, secondary=False, cache=False,
            auto=False, forward=False, reverse=False, ds=False, nonds=False,
            sambaopts=None, credopts=None, versionopts=None):
        request_filter = 0

        if primary:
            request_filter |= dnsserver.DNS_ZONE_REQUEST_PRIMARY
        if secondary:
            request_filter |= dnsserver.DNS_ZONE_REQUEST_SECONDARY
        if cache:
            request_filter |= dnsserver.DNS_ZONE_REQUEST_CACHE
        if auto:
            request_filter |= dnsserver.DNS_ZONE_REQUEST_AUTO
        if forward:
            request_filter |= dnsserver.DNS_ZONE_REQUEST_FORWARD
        if reverse:
            request_filter |= dnsserver.DNS_ZONE_REQUEST_REVERSE
        if ds:
            request_filter |= dnsserver.DNS_ZONE_REQUEST_DS
        if nonds:
            request_filter |= dnsserver.DNS_ZONE_REQUEST_NON_DS

        if request_filter == 0:
            request_filter = dnsserver.DNS_ZONE_REQUEST_PRIMARY

        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp)
        dns_conn = DnsConnWrapper(server, self.lp, self.creds)

        client_version = dns_client_version(cli_ver)

        typeid, res = dns_conn.DnssrvComplexOperation2(client_version,
                                                       0, server, None,
                                                       'EnumZones',
                                                       dnsserver.DNSSRV_TYPEID_DWORD,
                                                       request_filter)

        if client_version == dnsserver.DNS_CLIENT_VERSION_W2K:
            typeid = dnsserver.DNSSRV_TYPEID_ZONE_W2K
        else:
            typeid = dnsserver.DNSSRV_TYPEID_ZONE
        print_enumzones(self.outf, typeid, res)


class cmd_zonecreate(Command):
    """Create a zone."""

    synopsis = '%prog <server> <zone> [options]'

    takes_args = ['server', 'zone']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    takes_options = [
        Option('--client-version', help='Client Version',
               default='longhorn', metavar='w2k|dotnet|longhorn',
               choices=['w2k', 'dotnet', 'longhorn'], dest='cli_ver'),
        Option('--dns-directory-partition',
               help='Specify the naming context for the new zone, which '
                    'affects the replication scope (domain or forest wide '
                    'replication, default: domain).',
               default='domain',
               metavar='domain|forest',
               choices=['domain', 'forest'],
               dest='dns_dp'),
    ]

    def run(self,
            server,
            zone,
            cli_ver,
            dns_dp,
            sambaopts=None,
            credopts=None,
            versionopts=None):
        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp)
        dns_conn = DnsConnWrapper(server, self.lp, self.creds)

        zone = zone.lower()

        dns_directorypartition = dnsserver.DNS_DP_DOMAIN_DEFAULT
        if dns_dp == 'forest':
            dns_directorypartition = dnsserver.DNS_DP_FOREST_DEFAULT

        client_version = dns_client_version(cli_ver)
        if client_version == dnsserver.DNS_CLIENT_VERSION_W2K:
            typeid = dnsserver.DNSSRV_TYPEID_ZONE_CREATE_W2K
            zone_create_info = dnsserver.DNS_RPC_ZONE_CREATE_INFO_W2K()
            zone_create_info.pszZoneName = zone
            zone_create_info.dwZoneType = dnsp.DNS_ZONE_TYPE_PRIMARY
            zone_create_info.fAging = 0
            zone_create_info.fDsIntegrated = 1
            zone_create_info.fLoadExisting = 1
        elif client_version == dnsserver.DNS_CLIENT_VERSION_DOTNET:
            typeid = dnsserver.DNSSRV_TYPEID_ZONE_CREATE_DOTNET
            zone_create_info = dnsserver.DNS_RPC_ZONE_CREATE_INFO_DOTNET()
            zone_create_info.pszZoneName = zone
            zone_create_info.dwZoneType = dnsp.DNS_ZONE_TYPE_PRIMARY
            zone_create_info.fAging = 0
            zone_create_info.fDsIntegrated = 1
            zone_create_info.fLoadExisting = 1
            zone_create_info.dwDpFlags = dns_directorypartition
        else:
            typeid = dnsserver.DNSSRV_TYPEID_ZONE_CREATE
            zone_create_info = dnsserver.DNS_RPC_ZONE_CREATE_INFO_LONGHORN()
            zone_create_info.pszZoneName = zone
            zone_create_info.dwZoneType = dnsp.DNS_ZONE_TYPE_PRIMARY
            zone_create_info.fAging = 0
            zone_create_info.fDsIntegrated = 1
            zone_create_info.fLoadExisting = 1
            zone_create_info.dwDpFlags = dns_directorypartition

        dns_conn.DnssrvOperation2(client_version, 0, server, None,
                                  0, 'ZoneCreate', typeid,
                                  zone_create_info)

        typeid = dnsserver.DNSSRV_TYPEID_NAME_AND_PARAM
        name_and_param = dnsserver.DNS_RPC_NAME_AND_PARAM()
        name_and_param.pszNodeName = 'AllowUpdate'
        name_and_param.dwParam = dnsp.DNS_ZONE_UPDATE_SECURE

        messages = {
            werror.WERR_DNS_ERROR_ZONE_ALREADY_EXISTS: (
                f'Zone "{zone}" already exists.')
        }

        dns_conn.DnssrvOperation2(client_version, 0, server, zone,
                                  0, 'ResetDwordProperty', typeid,
                                  name_and_param, messages=messages)

        self.outf.write('Zone %s created successfully\n' % zone)


class cmd_zonedelete(Command):
    """Delete a zone."""

    synopsis = '%prog <server> <zone> [options]'

    takes_args = ['server', 'zone']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    def run(self, server, zone, sambaopts=None, credopts=None,
            versionopts=None):

        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp)
        dns_conn = DnsConnWrapper(server, self.lp, self.creds)

        zone = zone.lower()

        messages = {
            werror.WERR_DNS_ERROR_ZONE_DOES_NOT_EXIST: (
                f'Zone {zone} does not exist and so could not be deleted.'),
        }
        res = dns_conn.DnssrvOperation2(dnsserver.DNS_CLIENT_VERSION_LONGHORN,
                                        0, server, zone, 0, 'DeleteZoneFromDs',
                                        dnsserver.DNSSRV_TYPEID_NULL,
                                        None, messages=messages)

        self.outf.write('Zone %s deleted successfully\n' % zone)


class cmd_query(Command):
    """Query a name."""

    synopsis = ('%prog <server> <zone> <name> '
                '<A|AAAA|PTR|CNAME|MX|NS|SOA|SRV|TXT|ALL> [options]')

    takes_args = ['server', 'zone', 'name', 'rtype']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    takes_options = [
        Option('--authority', help='Search authoritative records (default)',
               action='store_true', dest='authority'),
        Option('--cache', help='Search cached records',
               action='store_true', dest='cache'),
        Option('--glue', help='Search glue records',
               action='store_true', dest='glue'),
        Option('--root', help='Search root hints',
               action='store_true', dest='root'),
        Option('--additional', help='List additional records',
               action='store_true', dest='additional'),
        Option('--no-children', help='Do not list children',
               action='store_true', dest='no_children'),
        Option('--only-children', help='List only children',
               action='store_true', dest='only_children')
    ]

    def run(self, server, zone, name, rtype, authority=False, cache=False,
            glue=False, root=False, additional=False, no_children=False,
            only_children=False, sambaopts=None, credopts=None,
            versionopts=None):
        record_type = dns_type_flag(rtype)

        if name.find('*') != -1:
            self.outf.write('use "@" to dump entire domain, looking up %s\n' %
                            name)

        select_flags = 0
        if authority:
            select_flags |= dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA
        if cache:
            select_flags |= dnsserver.DNS_RPC_VIEW_CACHE_DATA
        if glue:
            select_flags |= dnsserver.DNS_RPC_VIEW_GLUE_DATA
        if root:
            select_flags |= dnsserver.DNS_RPC_VIEW_ROOT_HINT_DATA
        if additional:
            select_flags |= dnsserver.DNS_RPC_VIEW_ADDITIONAL_DATA
        if no_children:
            select_flags |= dnsserver.DNS_RPC_VIEW_NO_CHILDREN
        if only_children:
            select_flags |= dnsserver.DNS_RPC_VIEW_ONLY_CHILDREN

        if select_flags == 0:
            select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA

        if select_flags == dnsserver.DNS_RPC_VIEW_ADDITIONAL_DATA:
            self.outf.write('Specify either --authority or --root along with --additional.\n')
            self.outf.write('Assuming --authority.\n')
            select_flags |= dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA

        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp)
        dns_conn = DnsConnWrapper(server, self.lp, self.creds)

        messages = {
            werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST: (
                'Record or zone does not exist.')
        }
        buflen, res = dns_conn.DnssrvEnumRecords2(
            dnsserver.DNS_CLIENT_VERSION_LONGHORN, 0, server, zone, name,
            None, record_type, select_flags, None, None,
            messages=messages)

        print_dnsrecords(self.outf, res)


class cmd_roothints(Command):
    """Query root hints."""

    synopsis = '%prog <server> [<name>] [options]'

    takes_args = ['server', 'name?']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    def run(self, server, name='.', sambaopts=None, credopts=None,
            versionopts=None):
        record_type = dnsp.DNS_TYPE_NS
        select_flags = (dnsserver.DNS_RPC_VIEW_ROOT_HINT_DATA |
                        dnsserver.DNS_RPC_VIEW_ADDITIONAL_DATA)

        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp)
        dns_conn = DnsConnWrapper(server, self.lp, self.creds)

        buflen, res = dns_conn.DnssrvEnumRecords2(
            dnsserver.DNS_CLIENT_VERSION_LONGHORN, 0, server, '..RootHints',
            name, None, record_type, select_flags, None, None)
        print_dnsrecords(self.outf, res)


class cmd_add_record(Command):
    """Add a DNS record

       For each type data contents are as follows:
         A      ipv4_address_string
         AAAA   ipv6_address_string
         PTR    fqdn_string
         CNAME  fqdn_string
         NS     fqdn_string
         MX     "fqdn_string preference"
         SRV    "fqdn_string port priority weight"
         TXT    "'string1' 'string2' ..."
    """

    synopsis = '%prog <server> <zone> <name> <A|AAAA|PTR|CNAME|NS|MX|SRV|TXT> <data>'

    takes_args = ['server', 'zone', 'name', 'rtype', 'data']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    takes_options = [
        Option("--allow-existing", help="no error if record already exists",
               action="store_true"),
    ]

    def run(self, server, zone, name, rtype, data, allow_existing=False,
            sambaopts=None, credopts=None, versionopts=None):

        if rtype.upper() not in ('A', 'AAAA', 'PTR', 'CNAME', 'NS', 'MX', 'SRV', 'TXT'):
            raise CommandError('Adding record of type %s is not supported' % rtype)

        record_type = dns_type_flag(rtype)
        rec = data_to_dns_record(record_type, data)

        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp)
        dns_conn = DnsConnWrapper(server, self.lp, self.creds)

        add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
        add_rec_buf.rec = rec

        messages = {
            werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST: (
                'Zone does not exist; record could not be added. '
                f'zone[{zone}] name[{name}'),
            werror.WERR_DNS_ERROR_RECORD_ALREADY_EXISTS: (
                'Record already exists; record could not be added. '
                f'zone[{zone}] name[{name}]')
        }
        try:
            dns_conn.DnssrvUpdateRecord2(dnsserver.DNS_CLIENT_VERSION_LONGHORN,
                                         0, server, zone, name, add_rec_buf, None,
                                         messages=messages)
        except CommandError as e:
            if e.inner_exception.args[0] == werror.WERR_DNS_ERROR_RECORD_ALREADY_EXISTS:
                if allow_existing:
                    self.outf.write('Record already exists, not adding\n')
                    return
            raise

        self.outf.write('Record added successfully\n')


class cmd_update_record(Command):
    """Update a DNS record

       For each type data contents are as follows:
         A      ipv4_address_string
         AAAA   ipv6_address_string
         PTR    fqdn_string
         CNAME  fqdn_string
         NS     fqdn_string
         MX     "fqdn_string preference"
         SOA    "fqdn_dns fqdn_email serial refresh retry expire minimumttl"
         SRV    "fqdn_string port priority weight"
         TXT    "'string1' 'string2' ..."
    """

    synopsis = '%prog <server> <zone> <name> <A|AAAA|PTR|CNAME|NS|MX|SOA|SRV|TXT> <olddata> <newdata>'

    takes_args = ['server', 'zone', 'name', 'rtype', 'olddata', 'newdata']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    def run(self, server, zone, name, rtype, olddata, newdata,
            sambaopts=None, credopts=None, versionopts=None):

        rtype = rtype.upper()
        if rtype not in ('A', 'AAAA', 'PTR', 'CNAME', 'NS', 'MX', 'SOA', 'SRV', 'TXT'):
            raise CommandError('Updating record of type %s is not supported' % rtype)

        try:
            if rtype == 'A':
                inet_pton(AF_INET, newdata)
            elif rtype == 'AAAA':
                inet_pton(AF_INET6, newdata)
        except OSError as e:
            raise CommandError(f"bad data for {rtype}: {e!r}")

        record_type = dns_type_flag(rtype)
        rec = data_to_dns_record(record_type, newdata)

        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp)
        dns_conn = DnsConnWrapper(server, self.lp, self.creds)

        try:
            rec_match = dns_record_match(dns_conn.dns_conn, server, zone,
                                         name, record_type, olddata)
        except DNSParseError as e:
            raise CommandError(*e.args) from None

        if not rec_match:
            raise CommandError('Record or zone does not exist.')

        # Copy properties from existing record to new record
        rec.dwFlags = rec_match.dwFlags
        rec.dwSerial = rec_match.dwSerial
        rec.dwTtlSeconds = rec_match.dwTtlSeconds
        rec.dwTimeStamp = rec_match.dwTimeStamp

        add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
        add_rec_buf.rec = rec

        del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
        del_rec_buf.rec = rec_match

        messages = {
            werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST: (
                f'Zone {zone} does not exist; record could not be updated.'),
        }

        dns_conn.DnssrvUpdateRecord2(dnsserver.DNS_CLIENT_VERSION_LONGHORN,
                                     0,
                                     server,
                                     zone,
                                     name,
                                     add_rec_buf,
                                     del_rec_buf,
                                     messages=messages)

        self.outf.write('Record updated successfully\n')


class cmd_delete_record(Command):
    """Delete a DNS record

       For each type data contents are as follows:
         A      ipv4_address_string
         AAAA   ipv6_address_string
         PTR    fqdn_string
         CNAME  fqdn_string
         NS     fqdn_string
         MX     "fqdn_string preference"
         SRV    "fqdn_string port priority weight"
         TXT    "'string1' 'string2' ..."
    """

    synopsis = '%prog <server> <zone> <name> <A|AAAA|PTR|CNAME|NS|MX|SRV|TXT> <data>'

    takes_args = ['server', 'zone', 'name', 'rtype', 'data']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    def run(self, server, zone, name, rtype, data, sambaopts=None, credopts=None, versionopts=None):

        if rtype.upper() not in ('A', 'AAAA', 'PTR', 'CNAME', 'NS', 'MX', 'SRV', 'TXT'):
            raise CommandError('Deleting record of type %s is not supported' % rtype)

        record_type = dns_type_flag(rtype)
        rec = data_to_dns_record(record_type, data)

        self.lp = sambaopts.get_loadparm()
        self.creds = credopts.get_credentials(self.lp)
        dns_conn = DnsConnWrapper(server, self.lp, self.creds)

        del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
        del_rec_buf.rec = rec

        messages = {
            werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST: (
                'Zone does not exist; record could not be deleted. '
                f'zone[{zone}] name[{name}'),
            werror.WERR_DNS_ERROR_RECORD_ALREADY_EXISTS: (
                'Record already exists; record could not be deleted. '
                f'zone[{zone}] name[{name}]')
        }
        dns_conn.DnssrvUpdateRecord2(dnsserver.DNS_CLIENT_VERSION_LONGHORN,
                                     0,
                                     server,
                                     zone,
                                     name,
                                     None,
                                     del_rec_buf,
                                     messages=messages)

        self.outf.write('Record deleted successfully\n')


class cmd_cleanup_record(Command):
    """Cleanup DNS records for a DNS host.

    example:

        samba-tool dns cleanup dc1 dc1.samdom.test.site -U USER%PASSWORD

    NOTE: This command in many cases will only mark the `dNSTombstoned` attr
    as `TRUE` on the DNS records. Querying will no longer return results but
    there may still be some placeholder entries in the database.
    """

    synopsis = '%prog <server> <dnshostname>'

    takes_args = ['server', 'dnshostname']

    takes_optiongroups = {
        "sambaopts": options.SambaOptions,
        "versionopts": options.VersionOptions,
        "credopts": options.CredentialsOptions,
    }

    takes_options = [
        Option("-v", "--verbose", help="Be verbose", action="store_true"),
        Option("-q", "--quiet", help="Be quiet", action="store_true"),
    ]

    def run(self, server, dnshostname, sambaopts=None, credopts=None,
            versionopts=None, verbose=False, quiet=False):
        lp = sambaopts.get_loadparm()
        creds = credopts.get_credentials(lp)

        logger = self.get_logger(verbose=verbose, quiet=quiet)

        samdb = SamDB(url="ldap://%s" % server,
                      session_info=system_session(),
                      credentials=creds, lp=lp)

        remove_dc.remove_dns_references(samdb, logger, dnshostname,
                                        ignore_no_name=True)


class cmd_dns(SuperCommand):
    """Domain Name Service (DNS) management."""

    subcommands = {}
    subcommands['serverinfo'] = cmd_serverinfo()
    subcommands['zoneoptions'] = cmd_zoneoptions()
    subcommands['zoneinfo'] = cmd_zoneinfo()
    subcommands['zonelist'] = cmd_zonelist()
    subcommands['zonecreate'] = cmd_zonecreate()
    subcommands['zonedelete'] = cmd_zonedelete()
    subcommands['query'] = cmd_query()
    subcommands['roothints'] = cmd_roothints()
    subcommands['add'] = cmd_add_record()
    subcommands['update'] = cmd_update_record()
    subcommands['delete'] = cmd_delete_record()
    subcommands['cleanup'] = cmd_cleanup_record()
