mirror of
https://github.com/samba-team/samba.git
synced 2024-12-25 23:21:54 +03:00
e046986d04
Signed-off-by: Andreas Schneider <asn@samba.org> Reviewed-by: Andrew Bartlett <abartlet@samba.org>
1395 lines
55 KiB
Python
1395 lines
55 KiB
Python
# 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,
|
|
}
|
|
|
|
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('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}]')
|
|
}
|
|
dns_conn.DnssrvUpdateRecord2(dnsserver.DNS_CLIENT_VERSION_LONGHORN,
|
|
0, server, zone, name, add_rec_buf, None,
|
|
messages=messages)
|
|
|
|
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()
|