mirror of
https://github.com/samba-team/samba.git
synced 2025-01-04 05:18:06 +03:00
2436 lines
95 KiB
Python
Executable File
2436 lines
95 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Compute our KCC topology
|
|
#
|
|
# Copyright (C) Dave Craft 2011
|
|
#
|
|
# 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 os
|
|
import sys
|
|
import random
|
|
|
|
# ensure we get messages out immediately, so they get in the samba logs,
|
|
# and don't get swallowed by a timeout
|
|
os.environ['PYTHONUNBUFFERED'] = '1'
|
|
|
|
# forcing GMT avoids a problem in some timezones with kerberos. Both MIT
|
|
# heimdal can get mutual authentication errors due to the 24 second difference
|
|
# between UTC and GMT when using some zone files (eg. the PDT zone from
|
|
# the US)
|
|
os.environ["TZ"] = "GMT"
|
|
|
|
# Find right directory when running from source tree
|
|
sys.path.insert(0, "bin/python")
|
|
|
|
import optparse
|
|
import logging
|
|
|
|
from samba import (
|
|
getopt as options,
|
|
Ldb,
|
|
ldb,
|
|
dsdb,
|
|
read_and_sub_file)
|
|
from samba.auth import system_session
|
|
from samba.samdb import SamDB
|
|
from samba.dcerpc import drsuapi
|
|
from samba.kcc_utils import *
|
|
|
|
class KCC(object):
|
|
"""The Knowledge Consistency Checker class.
|
|
|
|
A container for objects and methods allowing a run of the KCC. Produces a
|
|
set of connections in the samdb for which the Distributed Replication
|
|
Service can then utilize to replicate naming contexts
|
|
"""
|
|
def __init__(self):
|
|
"""Initializes the partitions class which can hold
|
|
our local DCs partitions or all the partitions in
|
|
the forest
|
|
"""
|
|
self.part_table = {} # partition objects
|
|
self.site_table = {}
|
|
self.transport_table = {}
|
|
self.sitelink_table = {}
|
|
|
|
# Used in inter-site topology computation. A list
|
|
# of connections (by NTDSConnection object) that are
|
|
# to be kept when pruning un-needed NTDS Connections
|
|
self.keep_connection_list = []
|
|
|
|
self.my_dsa_dnstr = None # My dsa DN
|
|
self.my_dsa = None # My dsa object
|
|
|
|
self.my_site_dnstr = None
|
|
self.my_site = None
|
|
|
|
self.samdb = None
|
|
|
|
def load_all_transports(self):
|
|
"""Loads the inter-site transport objects for Sites
|
|
|
|
::returns: Raises an Exception on error
|
|
"""
|
|
try:
|
|
res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
|
|
self.samdb.get_config_basedn(),
|
|
scope=ldb.SCOPE_SUBTREE,
|
|
expression="(objectClass=interSiteTransport)")
|
|
except ldb.LdbError, (enum, estr):
|
|
raise Exception("Unable to find inter-site transports - (%s)" %
|
|
estr)
|
|
|
|
for msg in res:
|
|
dnstr = str(msg.dn)
|
|
|
|
# already loaded
|
|
if dnstr in self.transport_table.keys():
|
|
continue
|
|
|
|
transport = Transport(dnstr)
|
|
|
|
transport.load_transport(self.samdb)
|
|
|
|
# Assign this transport to table
|
|
# and index by dn
|
|
self.transport_table[dnstr] = transport
|
|
|
|
def load_all_sitelinks(self):
|
|
"""Loads the inter-site siteLink objects
|
|
|
|
::returns: Raises an Exception on error
|
|
"""
|
|
try:
|
|
res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
|
|
self.samdb.get_config_basedn(),
|
|
scope=ldb.SCOPE_SUBTREE,
|
|
expression="(objectClass=siteLink)")
|
|
except ldb.LdbError, (enum, estr):
|
|
raise Exception("Unable to find inter-site siteLinks - (%s)" % estr)
|
|
|
|
for msg in res:
|
|
dnstr = str(msg.dn)
|
|
|
|
# already loaded
|
|
if dnstr in self.sitelink_table.keys():
|
|
continue
|
|
|
|
sitelink = SiteLink(dnstr)
|
|
|
|
sitelink.load_sitelink(self.samdb)
|
|
|
|
# Assign this siteLink to table
|
|
# and index by dn
|
|
self.sitelink_table[dnstr] = sitelink
|
|
|
|
def get_sitelink(self, site1_dnstr, site2_dnstr):
|
|
"""Return the siteLink (if it exists) that connects the
|
|
two input site DNs
|
|
"""
|
|
for sitelink in self.sitelink_table.values():
|
|
if sitelink.is_sitelink(site1_dnstr, site2_dnstr):
|
|
return sitelink
|
|
return None
|
|
|
|
def load_my_site(self):
|
|
"""Loads the Site class for the local DSA
|
|
|
|
::returns: Raises an Exception on error
|
|
"""
|
|
self.my_site_dnstr = "CN=%s,CN=Sites,%s" % (
|
|
self.samdb.server_site_name(),
|
|
self.samdb.get_config_basedn())
|
|
site = Site(self.my_site_dnstr)
|
|
site.load_site(self.samdb)
|
|
|
|
self.site_table[self.my_site_dnstr] = site
|
|
self.my_site = site
|
|
|
|
def load_all_sites(self):
|
|
"""Discover all sites and instantiate and load each
|
|
NTDS Site settings.
|
|
|
|
::returns: Raises an Exception on error
|
|
"""
|
|
try:
|
|
res = self.samdb.search("CN=Sites,%s" %
|
|
self.samdb.get_config_basedn(),
|
|
scope=ldb.SCOPE_SUBTREE,
|
|
expression="(objectClass=site)")
|
|
except ldb.LdbError, (enum, estr):
|
|
raise Exception("Unable to find sites - (%s)" % estr)
|
|
|
|
for msg in res:
|
|
sitestr = str(msg.dn)
|
|
|
|
# already loaded
|
|
if sitestr in self.site_table.keys():
|
|
continue
|
|
|
|
site = Site(sitestr)
|
|
site.load_site(self.samdb)
|
|
|
|
self.site_table[sitestr] = site
|
|
|
|
def load_my_dsa(self):
|
|
"""Discover my nTDSDSA dn thru the rootDSE entry
|
|
|
|
::returns: Raises an Exception on error.
|
|
"""
|
|
dn = ldb.Dn(self.samdb, "")
|
|
try:
|
|
res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
|
|
attrs=["dsServiceName"])
|
|
except ldb.LdbError, (enum, estr):
|
|
raise Exception("Unable to find my nTDSDSA - (%s)" % estr)
|
|
|
|
self.my_dsa_dnstr = res[0]["dsServiceName"][0]
|
|
self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
|
|
|
|
def load_all_partitions(self):
|
|
"""Discover all NCs thru the Partitions dn and
|
|
instantiate and load the NCs.
|
|
|
|
Each NC is inserted into the part_table by partition
|
|
dn string (not the nCName dn string)
|
|
|
|
::returns: Raises an Exception on error
|
|
"""
|
|
try:
|
|
res = self.samdb.search("CN=Partitions,%s" %
|
|
self.samdb.get_config_basedn(),
|
|
scope=ldb.SCOPE_SUBTREE,
|
|
expression="(objectClass=crossRef)")
|
|
except ldb.LdbError, (enum, estr):
|
|
raise Exception("Unable to find partitions - (%s)" % estr)
|
|
|
|
for msg in res:
|
|
partstr = str(msg.dn)
|
|
|
|
# already loaded
|
|
if partstr in self.part_table.keys():
|
|
continue
|
|
|
|
part = Partition(partstr)
|
|
|
|
part.load_partition(self.samdb)
|
|
self.part_table[partstr] = part
|
|
|
|
def should_be_present_test(self):
|
|
"""Enumerate all loaded partitions and DSAs in local
|
|
site and test if NC should be present as replica
|
|
"""
|
|
for partdn, part in self.part_table.items():
|
|
for dsadn, dsa in self.my_site.dsa_table.items():
|
|
needed, ro, partial = part.should_be_present(dsa)
|
|
logger.info("dsadn:%s\nncdn:%s\nneeded=%s:ro=%s:partial=%s\n" %
|
|
(dsadn, part.nc_dnstr, needed, ro, partial))
|
|
|
|
def refresh_failed_links_connections(self):
|
|
# XXX - not implemented yet
|
|
pass
|
|
|
|
def is_stale_link_connection(self, target_dsa):
|
|
"""Returns False if no tuple z exists in the kCCFailedLinks or
|
|
kCCFailedConnections variables such that z.UUIDDsa is the
|
|
objectGUID of the target dsa, z.FailureCount > 0, and
|
|
the current time - z.TimeFirstFailure > 2 hours.
|
|
"""
|
|
# XXX - not implemented yet
|
|
return False
|
|
|
|
def remove_unneeded_failed_links_connections(self):
|
|
# XXX - not implemented yet
|
|
pass
|
|
|
|
def remove_unneeded_ntdsconn(self, all_connected):
|
|
"""Removes unneeded NTDS Connections after computation
|
|
of KCC intra and inter-site topology has finished.
|
|
"""
|
|
mydsa = self.my_dsa
|
|
|
|
# Loop thru connections
|
|
for cn_dnstr, cn_conn in mydsa.connect_table.items():
|
|
|
|
s_dnstr = cn_conn.get_from_dnstr()
|
|
if s_dnstr is None:
|
|
cn_conn.to_be_deleted = True
|
|
continue
|
|
|
|
# Get the source DSA no matter what site
|
|
s_dsa = self.get_dsa(s_dnstr)
|
|
|
|
# Check if the DSA is in our site
|
|
if self.my_site.same_site(s_dsa):
|
|
same_site = True
|
|
else:
|
|
same_site = False
|
|
|
|
# Given an nTDSConnection object cn, if the DC with the
|
|
# nTDSDSA object dc that is the parent object of cn and
|
|
# the DC with the nTDSDA object referenced by cn!fromServer
|
|
# are in the same site, the KCC on dc deletes cn if all of
|
|
# the following are true:
|
|
#
|
|
# Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
|
|
#
|
|
# No site settings object s exists for the local DC's site, or
|
|
# bit NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED is clear in
|
|
# s!options.
|
|
#
|
|
# Another nTDSConnection object cn2 exists such that cn and
|
|
# cn2 have the same parent object, cn!fromServer = cn2!fromServer,
|
|
# and either
|
|
#
|
|
# cn!whenCreated < cn2!whenCreated
|
|
#
|
|
# cn!whenCreated = cn2!whenCreated and
|
|
# cn!objectGUID < cn2!objectGUID
|
|
#
|
|
# Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
|
|
if same_site:
|
|
if cn_conn.is_generated() == False:
|
|
continue
|
|
|
|
if self.my_site.is_cleanup_ntdsconn_disabled() == True:
|
|
continue
|
|
|
|
# Loop thru connections looking for a duplicate that
|
|
# fulfills the previous criteria
|
|
lesser = False
|
|
|
|
for cn2_dnstr, cn2_conn in mydsa.connect_table.items():
|
|
if cn2_conn is cn_conn:
|
|
continue
|
|
|
|
s2_dnstr = cn2_conn.get_from_dnstr()
|
|
if s2_dnstr is None:
|
|
continue
|
|
|
|
# If the NTDS Connections has a different
|
|
# fromServer field then no match
|
|
if s2_dnstr != s_dnstr:
|
|
continue
|
|
|
|
lesser = (cn_conn.whenCreated < cn2_conn.whenCreated or
|
|
(cn_conn.whenCreated == cn2_conn.whenCreated and
|
|
cmp(cn_conn.guid, cn2_conn.guid) < 0))
|
|
|
|
if lesser == True:
|
|
break
|
|
|
|
if lesser and cn_conn.is_rodc_topology() == False:
|
|
cn_conn.to_be_deleted = True
|
|
|
|
# Given an nTDSConnection object cn, if the DC with the nTDSDSA
|
|
# object dc that is the parent object of cn and the DC with
|
|
# the nTDSDSA object referenced by cn!fromServer are in
|
|
# different sites, a KCC acting as an ISTG in dc's site
|
|
# deletes cn if all of the following are true:
|
|
#
|
|
# Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
|
|
#
|
|
# cn!fromServer references an nTDSDSA object for a DC
|
|
# in a site other than the local DC's site.
|
|
#
|
|
# The keepConnections sequence returned by
|
|
# CreateIntersiteConnections() does not contain
|
|
# cn!objectGUID, or cn is "superseded by" (see below)
|
|
# another nTDSConnection cn2 and keepConnections
|
|
# contains cn2!objectGUID.
|
|
#
|
|
# The return value of CreateIntersiteConnections()
|
|
# was true.
|
|
#
|
|
# Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in
|
|
# cn!options
|
|
#
|
|
else: # different site
|
|
|
|
if mydsa.is_istg() == False:
|
|
continue
|
|
|
|
if cn_conn.is_generated() == False:
|
|
continue
|
|
|
|
if self.keep_connection(cn_conn) == True:
|
|
continue
|
|
|
|
# XXX - To be implemented
|
|
|
|
if all_connected == False:
|
|
continue
|
|
|
|
if cn_conn.is_rodc_topology() == False:
|
|
cn_conn.to_be_deleted = True
|
|
|
|
|
|
if opts.readonly:
|
|
for dnstr, connect in mydsa.connect_table.items():
|
|
if connect.to_be_deleted == True:
|
|
logger.info("TO BE DELETED:\n%s" % connect)
|
|
if connect.to_be_added == True:
|
|
logger.info("TO BE ADDED:\n%s" % connect)
|
|
|
|
# Peform deletion from our tables but perform
|
|
# no database modification
|
|
mydsa.commit_connections(self.samdb, ro=True)
|
|
else:
|
|
# Commit any modified connections
|
|
mydsa.commit_connections(self.samdb)
|
|
|
|
def get_dsa_by_guidstr(self, guidstr):
|
|
"""Given a DSA guid string, consule all sites looking
|
|
for the corresponding DSA and return it.
|
|
"""
|
|
for site in self.site_table.values():
|
|
dsa = site.get_dsa_by_guidstr(guidstr)
|
|
if dsa is not None:
|
|
return dsa
|
|
return None
|
|
|
|
def get_dsa(self, dnstr):
|
|
"""Given a DSA dn string, consule all sites looking
|
|
for the corresponding DSA and return it.
|
|
"""
|
|
for site in self.site_table.values():
|
|
dsa = site.get_dsa(dnstr)
|
|
if dsa is not None:
|
|
return dsa
|
|
return None
|
|
|
|
def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
|
|
"""Update t_repsFrom if necessary to satisfy requirements. Such
|
|
updates are typically required when the IDL_DRSGetNCChanges
|
|
server has moved from one site to another--for example, to
|
|
enable compression when the server is moved from the
|
|
client's site to another site.
|
|
|
|
:param n_rep: NC replica we need
|
|
:param t_repsFrom: repsFrom tuple to modify
|
|
:param s_rep: NC replica at source DSA
|
|
:param s_dsa: source DSA
|
|
:param cn_conn: Local DSA NTDSConnection child
|
|
|
|
::returns: (update) bit field containing which portion of the
|
|
repsFrom was modified. This bit field is suitable as input
|
|
to IDL_DRSReplicaModify ulModifyFields element, as it consists
|
|
of these bits:
|
|
drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
|
|
drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
|
|
drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
|
|
"""
|
|
s_dnstr = s_dsa.dsa_dnstr
|
|
update = 0x0
|
|
|
|
if self.my_site.same_site(s_dsa):
|
|
same_site = True
|
|
else:
|
|
same_site = False
|
|
|
|
times = cn_conn.convert_schedule_to_repltimes()
|
|
|
|
# if schedule doesn't match then update and modify
|
|
if times != t_repsFrom.schedule:
|
|
t_repsFrom.schedule = times
|
|
|
|
# Bit DRS_PER_SYNC is set in replicaFlags if and only
|
|
# if nTDSConnection schedule has a value v that specifies
|
|
# scheduled replication is to be performed at least once
|
|
# per week.
|
|
if cn_conn.is_schedule_minimum_once_per_week():
|
|
|
|
if (t_repsFrom.replica_flags &
|
|
drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0:
|
|
t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
|
|
|
|
# Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
|
|
# if the source DSA and the local DC's nTDSDSA object are
|
|
# in the same site or source dsa is the FSMO role owner
|
|
# of one or more FSMO roles in the NC replica.
|
|
if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
|
|
|
|
if (t_repsFrom.replica_flags &
|
|
drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0:
|
|
t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
|
|
|
|
# If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
|
|
# cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
|
|
# if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
|
|
# cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
|
|
# t.replicaFlags if and only if s and the local DC's
|
|
# nTDSDSA object are in different sites.
|
|
if (cn_conn.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0:
|
|
|
|
if (cn_conn.option & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
|
|
|
|
if (t_repsFrom.replica_flags &
|
|
drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
|
|
t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
|
|
|
|
elif same_site == False:
|
|
|
|
if (t_repsFrom.replica_flags &
|
|
drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
|
|
t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
|
|
|
|
# Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
|
|
# and only if s and the local DC's nTDSDSA object are
|
|
# not in the same site and the
|
|
# NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
|
|
# clear in cn!options
|
|
if (same_site == False and
|
|
(cn_conn.options &
|
|
dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
|
|
|
|
if (t_repsFrom.replica_flags &
|
|
drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0:
|
|
t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
|
|
|
|
# Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
|
|
# if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
|
|
if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
|
|
|
|
if (t_repsFrom.replica_flags &
|
|
drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0:
|
|
t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
|
|
|
|
# Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
|
|
# set in t.replicaFlags if and only if cn!enabledConnection = false.
|
|
if cn_conn.is_enabled() == False:
|
|
|
|
if (t_repsFrom.replica_flags &
|
|
drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0:
|
|
t_repsFrom.replica_flags |= \
|
|
drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
|
|
|
|
if (t_repsFrom.replica_flags &
|
|
drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0:
|
|
t_repsFrom.replica_flags |= \
|
|
drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
|
|
|
|
# If s and the local DC's nTDSDSA object are in the same site,
|
|
# cn!transportType has no value, or the RDN of cn!transportType
|
|
# is CN=IP:
|
|
#
|
|
# Bit DRS_MAIL_REP in t.replicaFlags is clear.
|
|
#
|
|
# t.uuidTransport = NULL GUID.
|
|
#
|
|
# t.uuidDsa = The GUID-based DNS name of s.
|
|
#
|
|
# Otherwise:
|
|
#
|
|
# Bit DRS_MAIL_REP in t.replicaFlags is set.
|
|
#
|
|
# If x is the object with dsname cn!transportType,
|
|
# t.uuidTransport = x!objectGUID.
|
|
#
|
|
# Let a be the attribute identified by
|
|
# x!transportAddressAttribute. If a is
|
|
# the dNSHostName attribute, t.uuidDsa = the GUID-based
|
|
# DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
|
|
#
|
|
# It appears that the first statement i.e.
|
|
#
|
|
# "If s and the local DC's nTDSDSA object are in the same
|
|
# site, cn!transportType has no value, or the RDN of
|
|
# cn!transportType is CN=IP:"
|
|
#
|
|
# could be a slightly tighter statement if it had an "or"
|
|
# between each condition. I believe this should
|
|
# be interpreted as:
|
|
#
|
|
# IF (same-site) OR (no-value) OR (type-ip)
|
|
#
|
|
# because IP should be the primary transport mechanism
|
|
# (even in inter-site) and the absense of the transportType
|
|
# attribute should always imply IP no matter if its multi-site
|
|
#
|
|
# NOTE MS-TECH INCORRECT:
|
|
#
|
|
# All indications point to these statements above being
|
|
# incorrectly stated:
|
|
#
|
|
# t.uuidDsa = The GUID-based DNS name of s.
|
|
#
|
|
# Let a be the attribute identified by
|
|
# x!transportAddressAttribute. If a is
|
|
# the dNSHostName attribute, t.uuidDsa = the GUID-based
|
|
# DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
|
|
#
|
|
# because the uuidDSA is a GUID and not a GUID-base DNS
|
|
# name. Nor can uuidDsa hold (s!parent)!a if not
|
|
# dNSHostName. What should have been said is:
|
|
#
|
|
# t.naDsa = The GUID-based DNS name of s
|
|
#
|
|
# That would also be correct if transportAddressAttribute
|
|
# were "mailAddress" because (naDsa) can also correctly
|
|
# hold the SMTP ISM service address.
|
|
#
|
|
nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
|
|
|
|
# We're not currently supporting SMTP replication
|
|
# so is_smtp_replication_available() is currently
|
|
# always returning False
|
|
if (same_site == True or
|
|
cn_conn.transport_dnstr == None or
|
|
cn_conn.transport_dnstr.find("CN=IP") == 0 or
|
|
is_smtp_replication_available() == False):
|
|
|
|
if (t_repsFrom.replica_flags &
|
|
drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0:
|
|
t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
|
|
|
|
null_guid = misc.GUID()
|
|
if (t_repsFrom.transport_guid is None or
|
|
t_repsFrom.transport_guid != null_guid):
|
|
t_repsFrom.transport_guid = null_guid
|
|
|
|
# See (NOTE MS-TECH INCORRECT) above
|
|
if t_repsFrom.version == 0x1:
|
|
if t_repsFrom.dns_name1 is None or \
|
|
t_repsFrom.dns_name1 != nastr:
|
|
t_repsFrom.dns_name1 = nastr
|
|
else:
|
|
if t_repsFrom.dns_name1 is None or \
|
|
t_repsFrom.dns_name2 is None or \
|
|
t_repsFrom.dns_name1 != nastr or \
|
|
t_repsFrom.dns_name2 != nastr:
|
|
t_repsFrom.dns_name1 = nastr
|
|
t_repsFrom.dns_name2 = nastr
|
|
|
|
else:
|
|
if (t_repsFrom.replica_flags & \
|
|
drsuapi.DRSUAPI_DRS_MAIL_REP) == 0x0:
|
|
t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_MAIL_REP
|
|
|
|
# We have a transport type but its not an
|
|
# object in the database
|
|
if cn_conn.transport_dnstr not in self.transport_table.keys():
|
|
raise Exception("Missing inter-site transport - (%s)" % \
|
|
cn_conn.transport_dnstr)
|
|
|
|
x_transport = self.transport_table[cn_conn.transport_dnstr]
|
|
|
|
if t_repsFrom.transport_guid != x_transport.guid:
|
|
t_repsFrom.transport_guid = x_transport.guid
|
|
|
|
# See (NOTE MS-TECH INCORRECT) above
|
|
if x_transport.address_attr == "dNSHostName":
|
|
|
|
if t_repsFrom.version == 0x1:
|
|
if t_repsFrom.dns_name1 is None or \
|
|
t_repsFrom.dns_name1 != nastr:
|
|
t_repsFrom.dns_name1 = nastr
|
|
else:
|
|
if t_repsFrom.dns_name1 is None or \
|
|
t_repsFrom.dns_name2 is None or \
|
|
t_repsFrom.dns_name1 != nastr or \
|
|
t_repsFrom.dns_name2 != nastr:
|
|
t_repsFrom.dns_name1 = nastr
|
|
t_repsFrom.dns_name2 = nastr
|
|
|
|
else:
|
|
# MS tech specification says we retrieve the named
|
|
# attribute in "transportAddressAttribute" from the parent of
|
|
# the DSA object
|
|
try:
|
|
pdnstr = s_dsa.get_parent_dnstr()
|
|
attrs = [ x_transport.address_attr ]
|
|
|
|
res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
|
|
attrs=attrs)
|
|
except ldb.ldbError, (enum, estr):
|
|
raise Exception \
|
|
("Unable to find attr (%s) for (%s) - (%s)" % \
|
|
(x_transport.address_attr, pdnstr, estr))
|
|
|
|
msg = res[0]
|
|
nastr = str(msg[x_transport.address_attr][0])
|
|
|
|
# See (NOTE MS-TECH INCORRECT) above
|
|
if t_repsFrom.version == 0x1:
|
|
if t_repsFrom.dns_name1 is None or \
|
|
t_repsFrom.dns_name1 != nastr:
|
|
t_repsFrom.dns_name1 = nastr
|
|
else:
|
|
if t_repsFrom.dns_name1 is None or \
|
|
t_repsFrom.dns_name2 is None or \
|
|
t_repsFrom.dns_name1 != nastr or \
|
|
t_repsFrom.dns_name2 != nastr:
|
|
|
|
t_repsFrom.dns_name1 = nastr
|
|
t_repsFrom.dns_name2 = nastr
|
|
|
|
if t_repsFrom.is_modified():
|
|
logger.debug("modify_repsFrom(): %s" % t_repsFrom)
|
|
|
|
def is_repsFrom_implied(self, n_rep, cn_conn):
|
|
"""Given a NC replica and NTDS Connection, determine if the connection
|
|
implies a repsFrom tuple should be present from the source DSA listed
|
|
in the connection to the naming context
|
|
|
|
:param n_rep: NC replica
|
|
:param conn: NTDS Connection
|
|
::returns (True || False), source DSA:
|
|
"""
|
|
# NTDS Connection must satisfy all the following criteria
|
|
# to imply a repsFrom tuple is needed:
|
|
#
|
|
# cn!enabledConnection = true.
|
|
# cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
|
|
# cn!fromServer references an nTDSDSA object.
|
|
s_dsa = None
|
|
|
|
if cn_conn.is_enabled() == True and \
|
|
cn_conn.is_rodc_topology() == False:
|
|
|
|
s_dnstr = cn_conn.get_from_dnstr()
|
|
if s_dnstr is not None:
|
|
s_dsa = self.get_dsa(s_dnstr)
|
|
|
|
# No DSA matching this source DN string?
|
|
if s_dsa == None:
|
|
return False, None
|
|
|
|
# To imply a repsFrom tuple is needed, each of these
|
|
# must be True:
|
|
#
|
|
# An NC replica of the NC "is present" on the DC to
|
|
# which the nTDSDSA object referenced by cn!fromServer
|
|
# corresponds.
|
|
#
|
|
# An NC replica of the NC "should be present" on
|
|
# the local DC
|
|
s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
|
|
|
|
if s_rep is None or s_rep.is_present() == False:
|
|
return False, None
|
|
|
|
# To imply a repsFrom tuple is needed, each of these
|
|
# must be True:
|
|
#
|
|
# The NC replica on the DC referenced by cn!fromServer is
|
|
# a writable replica or the NC replica that "should be
|
|
# present" on the local DC is a partial replica.
|
|
#
|
|
# The NC is not a domain NC, the NC replica that
|
|
# "should be present" on the local DC is a partial
|
|
# replica, cn!transportType has no value, or
|
|
# cn!transportType has an RDN of CN=IP.
|
|
#
|
|
implied = (s_rep.is_ro() == False or n_rep.is_partial() == True) and \
|
|
(n_rep.is_domain() == False or
|
|
n_rep.is_partial() == True or
|
|
cn_conn.transport_dnstr == None or
|
|
cn_conn.transport_dnstr.find("CN=IP") == 0)
|
|
|
|
if implied:
|
|
return True, s_dsa
|
|
else:
|
|
return False, None
|
|
|
|
def translate_ntdsconn(self):
|
|
"""This function adjusts values of repsFrom abstract attributes of NC
|
|
replicas on the local DC to match those implied by
|
|
nTDSConnection objects.
|
|
"""
|
|
logger.debug("translate_ntdsconn(): enter")
|
|
|
|
if self.my_dsa.is_translate_ntdsconn_disabled():
|
|
return
|
|
|
|
current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
|
|
|
|
# Filled in with replicas we currently have that need deleting
|
|
delete_rep_table = {}
|
|
|
|
# We're using the MS notation names here to allow
|
|
# correlation back to the published algorithm.
|
|
#
|
|
# n_rep - NC replica (n)
|
|
# t_repsFrom - tuple (t) in n!repsFrom
|
|
# s_dsa - Source DSA of the replica. Defined as nTDSDSA
|
|
# object (s) such that (s!objectGUID = t.uuidDsa)
|
|
# In our IDL representation of repsFrom the (uuidDsa)
|
|
# attribute is called (source_dsa_obj_guid)
|
|
# cn_conn - (cn) is nTDSConnection object and child of the local DC's
|
|
# nTDSDSA object and (cn!fromServer = s)
|
|
# s_rep - source DSA replica of n
|
|
#
|
|
# If we have the replica and its not needed
|
|
# then we add it to the "to be deleted" list.
|
|
for dnstr, n_rep in current_rep_table.items():
|
|
if dnstr not in needed_rep_table.keys():
|
|
delete_rep_table[dnstr] = n_rep
|
|
|
|
# Now perform the scan of replicas we'll need
|
|
# and compare any current repsFrom against the
|
|
# connections
|
|
for dnstr, n_rep in needed_rep_table.items():
|
|
|
|
# load any repsFrom and fsmo roles as we'll
|
|
# need them during connection translation
|
|
n_rep.load_repsFrom(self.samdb)
|
|
n_rep.load_fsmo_roles(self.samdb)
|
|
|
|
# Loop thru the existing repsFrom tupples (if any)
|
|
for i, t_repsFrom in enumerate(n_rep.rep_repsFrom):
|
|
|
|
# for each tuple t in n!repsFrom, let s be the nTDSDSA
|
|
# object such that s!objectGUID = t.uuidDsa
|
|
guidstr = str(t_repsFrom.source_dsa_obj_guid)
|
|
s_dsa = self.get_dsa_by_guidstr(guidstr)
|
|
|
|
# Source dsa is gone from config (strange)
|
|
# so cleanup stale repsFrom for unlisted DSA
|
|
if s_dsa is None:
|
|
logger.debug("repsFrom source DSA guid (%s) not found" %
|
|
guidstr)
|
|
t_repsFrom.to_be_deleted = True
|
|
continue
|
|
|
|
s_dnstr = s_dsa.dsa_dnstr
|
|
|
|
# Retrieve my DSAs connection object (if it exists)
|
|
# that specifies the fromServer equivalent to
|
|
# the DSA that is specified in the repsFrom source
|
|
cn_conn = self.my_dsa.get_connection_by_from_dnstr(s_dnstr)
|
|
|
|
# Let (cn) be the nTDSConnection object such that (cn)
|
|
# is a child of the local DC's nTDSDSA object and
|
|
# (cn!fromServer = s) and (cn!options) does not contain
|
|
# NTDSCONN_OPT_RODC_TOPOLOGY or NULL if no such (cn) exists.
|
|
if cn_conn and cn_conn.is_rodc_topology() == True:
|
|
cn_conn = None
|
|
|
|
# KCC removes this repsFrom tuple if any of the following
|
|
# is true:
|
|
# cn = NULL.
|
|
#
|
|
# No NC replica of the NC "is present" on DSA that
|
|
# would be source of replica
|
|
#
|
|
# A writable replica of the NC "should be present" on
|
|
# the local DC, but a partial replica "is present" on
|
|
# the source DSA
|
|
s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
|
|
|
|
if cn_conn is None or \
|
|
s_rep is None or s_rep.is_present() == False or \
|
|
(n_rep.is_ro() == False and s_rep.is_partial() == True):
|
|
|
|
t_repsFrom.to_be_deleted = True
|
|
continue
|
|
|
|
# If the KCC did not remove t from n!repsFrom, it updates t
|
|
self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
|
|
|
|
# Loop thru connections and add implied repsFrom tuples
|
|
# for each NTDSConnection under our local DSA if the
|
|
# repsFrom is not already present
|
|
for cn_dnstr, cn_conn in self.my_dsa.connect_table.items():
|
|
|
|
implied, s_dsa = self.is_repsFrom_implied(n_rep, cn_conn)
|
|
if implied == False:
|
|
continue
|
|
|
|
# Loop thru the existing repsFrom tupples (if any) and
|
|
# if we already have a tuple for this connection then
|
|
# no need to proceed to add. It will have been changed
|
|
# to have the correct attributes above
|
|
for i, t_repsFrom in enumerate(n_rep.rep_repsFrom):
|
|
|
|
guidstr = str(t_repsFrom.source_dsa_obj_guid)
|
|
if s_dsa is self.get_dsa_by_guidstr(guidstr):
|
|
s_dsa = None
|
|
break
|
|
|
|
if s_dsa == None:
|
|
continue
|
|
|
|
# Create a new RepsFromTo and proceed to modify
|
|
# it according to specification
|
|
t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
|
|
|
|
t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
|
|
|
|
self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
|
|
|
|
# Add to our NC repsFrom as this is newly computed
|
|
if t_repsFrom.is_modified():
|
|
n_rep.rep_repsFrom.append(t_repsFrom)
|
|
|
|
if opts.readonly:
|
|
# Display any to be deleted or modified repsFrom
|
|
text = n_rep.dumpstr_to_be_deleted()
|
|
if text:
|
|
logger.info("TO BE DELETED:\n%s" % text)
|
|
text = n_rep.dumpstr_to_be_modified()
|
|
if text:
|
|
logger.info("TO BE MODIFIED:\n%s" % text)
|
|
|
|
# Peform deletion from our tables but perform
|
|
# no database modification
|
|
n_rep.commit_repsFrom(self.samdb, ro=True)
|
|
else:
|
|
# Commit any modified repsFrom to the NC replica
|
|
n_rep.commit_repsFrom(self.samdb)
|
|
|
|
def keep_connection(self, cn_conn):
|
|
"""Determines if the connection is meant to be kept during the
|
|
pruning of unneeded connections operation.
|
|
|
|
Consults the keep_connection_list[] which was built during
|
|
intersite NC replica graph computation.
|
|
|
|
::returns (True or False): if (True) connection should not be pruned
|
|
"""
|
|
if cn_conn in self.keep_connection_list:
|
|
return True
|
|
return False
|
|
|
|
def merge_failed_links(self):
|
|
"""Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
|
|
The KCC on a writable DC attempts to merge the link and connection
|
|
failure information from bridgehead DCs in its own site to help it
|
|
identify failed bridgehead DCs.
|
|
"""
|
|
# MS-TECH Ref 6.2.2.3.2 Merge of kCCFailedLinks and kCCFailedLinks
|
|
# from Bridgeheads
|
|
|
|
# XXX - not implemented yet
|
|
|
|
def setup_graph(self):
|
|
"""Set up a GRAPH, populated with a VERTEX for each site
|
|
object, a MULTIEDGE for each siteLink object, and a
|
|
MUTLIEDGESET for each siteLinkBridge object (or implied
|
|
siteLinkBridge).
|
|
|
|
::returns: a new graph
|
|
"""
|
|
# XXX - not implemented yet
|
|
return None
|
|
|
|
def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
|
|
"""Get a bridghead DC.
|
|
|
|
:param site: site object representing for which a bridgehead
|
|
DC is desired.
|
|
:param part: crossRef for NC to replicate.
|
|
:param transport: interSiteTransport object for replication
|
|
traffic.
|
|
:param partial_ok: True if a DC containing a partial
|
|
replica or a full replica will suffice, False if only
|
|
a full replica will suffice.
|
|
:param detect_failed: True to detect failed DCs and route
|
|
replication traffic around them, False to assume no DC
|
|
has failed.
|
|
::returns: dsa object for the bridgehead DC or None
|
|
"""
|
|
|
|
bhs = self.get_all_bridgeheads(site, part, transport,
|
|
partial_ok, detect_failed)
|
|
if len(bhs) == 0:
|
|
logger.debug("get_bridgehead: exit\n\tsitedn=%s\n\tbhdn=None" %
|
|
site.site_dnstr)
|
|
return None
|
|
else:
|
|
logger.debug("get_bridgehead: exit\n\tsitedn=%s\n\tbhdn=%s" %
|
|
(site.site_dnstr, bhs[0].dsa_dnstr))
|
|
return bhs[0]
|
|
|
|
def get_all_bridgeheads(self, site, part, transport,
|
|
partial_ok, detect_failed):
|
|
"""Get all bridghead DCs satisfying the given criteria
|
|
|
|
:param site: site object representing the site for which
|
|
bridgehead DCs are desired.
|
|
:param part: partition for NC to replicate.
|
|
:param transport: interSiteTransport object for
|
|
replication traffic.
|
|
:param partial_ok: True if a DC containing a partial
|
|
replica or a full replica will suffice, False if
|
|
only a full replica will suffice.
|
|
:param detect_ok: True to detect failed DCs and route
|
|
replication traffic around them, FALSE to assume
|
|
no DC has failed.
|
|
::returns: list of dsa object for available bridgehead
|
|
DCs or None
|
|
"""
|
|
|
|
bhs = []
|
|
|
|
logger.debug("get_all_bridgeheads: %s" % transport)
|
|
|
|
for key, dsa in site.dsa_table.items():
|
|
|
|
pdnstr = dsa.get_parent_dnstr()
|
|
|
|
# IF t!bridgeheadServerListBL has one or more values and
|
|
# t!bridgeheadServerListBL does not contain a reference
|
|
# to the parent object of dc then skip dc
|
|
if (len(transport.bridgehead_list) != 0 and
|
|
pdnstr not in transport.bridgehead_list):
|
|
continue
|
|
|
|
# IF dc is in the same site as the local DC
|
|
# IF a replica of cr!nCName is not in the set of NC replicas
|
|
# that "should be present" on dc or a partial replica of the
|
|
# NC "should be present" but partialReplicasOkay = FALSE
|
|
# Skip dc
|
|
if self.my_site.same_site(dsa):
|
|
needed, ro, partial = part.should_be_present(dsa)
|
|
if needed == False or (partial == True and partial_ok == False):
|
|
continue
|
|
|
|
# ELSE
|
|
# IF an NC replica of cr!nCName is not in the set of NC
|
|
# replicas that "are present" on dc or a partial replica of
|
|
# the NC "is present" but partialReplicasOkay = FALSE
|
|
# Skip dc
|
|
else:
|
|
rep = dsa.get_current_replica(part.nc_dnstr)
|
|
if rep is None or (rep.is_partial() and partial_ok == False):
|
|
continue
|
|
|
|
# IF AmIRODC() and cr!nCName corresponds to default NC then
|
|
# Let dsaobj be the nTDSDSA object of the dc
|
|
# IF dsaobj.msDS-Behavior-Version < DS_BEHAVIOR_WIN2008
|
|
# Skip dc
|
|
if self.my_dsa.is_ro() and part.is_default():
|
|
if dsa.is_minimum_behavior(DS_BEHAVIOR_WIN2008) == False:
|
|
continue
|
|
|
|
# IF t!name != "IP" and the parent object of dc has no value for
|
|
# the attribute specified by t!transportAddressAttribute
|
|
# Skip dc
|
|
if transport.name != "IP":
|
|
# MS tech specification says we retrieve the named
|
|
# attribute in "transportAddressAttribute" from the parent
|
|
# of the DSA object
|
|
try:
|
|
attrs = [ transport.address_attr ]
|
|
|
|
res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
|
|
attrs=attrs)
|
|
except ldb.ldbError, (enum, estr):
|
|
continue
|
|
|
|
msg = res[0]
|
|
nastr = str(msg[transport.address_attr][0])
|
|
|
|
# IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
|
|
# Skip dc
|
|
if self.is_bridgehead_failed(dsa, detect_failed) == True:
|
|
continue
|
|
|
|
logger.debug("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
|
|
bhs.append(dsa)
|
|
|
|
# IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
|
|
# s!options
|
|
# SORT bhs such that all GC servers precede DCs that are not GC
|
|
# servers, and otherwise by ascending objectGUID
|
|
# ELSE
|
|
# SORT bhs in a random order
|
|
if site.is_random_bridgehead_disabled() == True:
|
|
bhs.sort(sort_dsa_by_gc_and_guid)
|
|
else:
|
|
random.shuffle(bhs)
|
|
|
|
return bhs
|
|
|
|
|
|
def is_bridgehead_failed(self, dsa, detect_failed):
|
|
"""Determine whether a given DC is known to be in a failed state
|
|
::returns: True if and only if the DC should be considered failed
|
|
"""
|
|
# XXX - not implemented yet
|
|
return False
|
|
|
|
def create_connection(self, part, rbh, rsite, transport,
|
|
lbh, lsite, link_opt, link_sched,
|
|
partial_ok, detect_failed):
|
|
"""Create an nTDSConnection object with the given parameters
|
|
if one does not already exist.
|
|
|
|
:param part: crossRef object for the NC to replicate.
|
|
:param rbh: nTDSDSA object for DC to act as the
|
|
IDL_DRSGetNCChanges server (which is in a site other
|
|
than the local DC's site).
|
|
:param rsite: site of the rbh
|
|
:param transport: interSiteTransport object for the transport
|
|
to use for replication traffic.
|
|
:param lbh: nTDSDSA object for DC to act as the
|
|
IDL_DRSGetNCChanges client (which is in the local DC's site).
|
|
:param lsite: site of the lbh
|
|
:param link_opt: Replication parameters (aggregated siteLink options, etc.)
|
|
:param link_sched: Schedule specifying the times at which
|
|
to begin replicating.
|
|
:partial_ok: True if bridgehead DCs containing partial
|
|
replicas of the NC are acceptable.
|
|
:param detect_failed: True to detect failed DCs and route
|
|
replication traffic around them, FALSE to assume no DC
|
|
has failed.
|
|
"""
|
|
rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
|
|
partial_ok, False)
|
|
|
|
# MS-TECH says to compute rbhs_avail but then doesn't use it
|
|
# rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
|
|
# partial_ok, detect_failed)
|
|
|
|
lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
|
|
partial_ok, False)
|
|
|
|
# MS-TECH says to compute lbhs_avail but then doesn't use it
|
|
# lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
|
|
# partial_ok, detect_failed)
|
|
|
|
# FOR each nTDSConnection object cn such that the parent of cn is
|
|
# a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
|
|
for ldsa in lbhs_all:
|
|
for cn in ldsa.connect_table.values():
|
|
|
|
rdsa = None
|
|
for rdsa in rbhs_all:
|
|
if cn.from_dnstr == rdsa.dsa_dnstr:
|
|
break
|
|
|
|
if rdsa is None:
|
|
continue
|
|
|
|
# IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
|
|
# NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
|
|
# cn!transportType references t
|
|
if (cn.is_generated() and not cn.is_rodc_topology() and
|
|
cn.transport_dnstr == transport.dnstr):
|
|
|
|
# IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
|
|
# cn!options and cn!schedule != sch
|
|
# Perform an originating update to set cn!schedule to
|
|
# sched
|
|
if (not cn.is_user_owned_schedule() and
|
|
not cn.is_equivalent_schedule(link_sched)):
|
|
cn.schedule = link_sched
|
|
cn.set_modified(True)
|
|
|
|
# IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
|
|
# NTDSCONN_OPT_USE_NOTIFY are set in cn
|
|
if cn.is_override_notify_default() == True and \
|
|
cn.is_use_notify() == True:
|
|
|
|
# IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
|
|
# ri.Options
|
|
# Perform an originating update to clear bits
|
|
# NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
|
|
# NTDSCONN_OPT_USE_NOTIFY in cn!options
|
|
if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
|
|
cn.options &= \
|
|
~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT | \
|
|
dsdb.NTDSCONN_OPT_USE_NOTIFY)
|
|
cn.set_modified(True)
|
|
|
|
# ELSE
|
|
else:
|
|
|
|
# IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
|
|
# ri.Options
|
|
# Perform an originating update to set bits
|
|
# NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
|
|
# NTDSCONN_OPT_USE_NOTIFY in cn!options
|
|
if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
|
|
cn.options |= \
|
|
(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT | \
|
|
dsdb.NTDSCONN_OPT_USE_NOTIFY)
|
|
cn.set_modified(True)
|
|
|
|
|
|
# IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
|
|
if cn.is_twoway_sync() == True:
|
|
|
|
# IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
|
|
# ri.Options
|
|
# Perform an originating update to clear bit
|
|
# NTDSCONN_OPT_TWOWAY_SYNC in cn!options
|
|
if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
|
|
cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
|
|
cn.set_modified(True)
|
|
|
|
# ELSE
|
|
else:
|
|
|
|
# IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
|
|
# ri.Options
|
|
# Perform an originating update to set bit
|
|
# NTDSCONN_OPT_TWOWAY_SYNC in cn!options
|
|
if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
|
|
cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
|
|
cn.set_modified(True)
|
|
|
|
|
|
# IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
|
|
# in cn!options
|
|
if cn.is_intersite_compression_disabled() == True:
|
|
|
|
# IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
|
|
# in ri.Options
|
|
# Perform an originating update to clear bit
|
|
# NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
|
|
# cn!options
|
|
if (link_opt & \
|
|
dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0:
|
|
cn.options &= \
|
|
~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
|
|
cn.set_modified(True)
|
|
|
|
# ELSE
|
|
else:
|
|
# IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
|
|
# ri.Options
|
|
# Perform an originating update to set bit
|
|
# NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
|
|
# cn!options
|
|
if (link_opt & \
|
|
dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
|
|
cn.options |= \
|
|
dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
|
|
cn.set_modified(True)
|
|
|
|
# Display any modified connection
|
|
if opts.readonly:
|
|
if cn.to_be_modified == True:
|
|
logger.info("TO BE MODIFIED:\n%s" % cn)
|
|
|
|
ldsa.commit_connections(self.samdb, ro=True)
|
|
else:
|
|
ldsa.commit_connections(self.samdb)
|
|
# ENDFOR
|
|
|
|
valid_connections = 0
|
|
|
|
# FOR each nTDSConnection object cn such that cn!parent is
|
|
# a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
|
|
for ldsa in lbhs_all:
|
|
for cn in ldsa.connect_table.values():
|
|
|
|
rdsa = None
|
|
for rdsa in rbhs_all:
|
|
if cn.from_dnstr == rdsa.dsa_dnstr:
|
|
break
|
|
|
|
if rdsa is None:
|
|
continue
|
|
|
|
# IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
|
|
# cn!transportType references t) and
|
|
# NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
|
|
if ((not cn.is_generated() or
|
|
cn.transport_dnstr == transport.dnstr) and
|
|
not cn.is_rodc_topology()):
|
|
|
|
# LET rguid be the objectGUID of the nTDSDSA object
|
|
# referenced by cn!fromServer
|
|
# LET lguid be (cn!parent)!objectGUID
|
|
|
|
# IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
|
|
# BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
|
|
# Increment cValidConnections by 1
|
|
if (not self.is_bridgehead_failed(rdsa, detect_failed) and
|
|
not self.is_bridgehead_failed(ldsa, detect_failed)):
|
|
valid_connections += 1
|
|
|
|
# IF keepConnections does not contain cn!objectGUID
|
|
# APPEND cn!objectGUID to keepConnections
|
|
if not self.keep_connection(cn):
|
|
self.keep_connection_list.append(cn)
|
|
|
|
# ENDFOR
|
|
|
|
# IF cValidConnections = 0
|
|
if valid_connections == 0:
|
|
|
|
# LET opt be NTDSCONN_OPT_IS_GENERATED
|
|
opt = dsdb.NTDSCONN_OPT_IS_GENERATED
|
|
|
|
# IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
|
|
# SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
|
|
# NTDSCONN_OPT_USE_NOTIFY in opt
|
|
if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
|
|
opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
|
|
dsdb.NTDSCONN_USE_NOTIFY)
|
|
|
|
# IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
|
|
# SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
|
|
if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
|
|
opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
|
|
|
|
# IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
|
|
# ri.Options
|
|
# SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
|
|
if (link_opt &
|
|
dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
|
|
opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
|
|
|
|
# Perform an originating update to create a new nTDSConnection
|
|
# object cn that is a child of lbh, cn!enabledConnection = TRUE,
|
|
# cn!options = opt, cn!transportType is a reference to t,
|
|
# cn!fromServer is a reference to rbh, and cn!schedule = sch
|
|
cn = lbh.new_connection(opt, 0, transport, lbh.dsa_dnstr, link_sched)
|
|
|
|
# Display any added connection
|
|
if opts.readonly:
|
|
if cn.to_be_added == True:
|
|
logger.info("TO BE ADDED:\n%s" % cn)
|
|
|
|
lbh.commit_connections(self.samdb, ro=True)
|
|
else:
|
|
lbh.commit_connections(self.samdb)
|
|
|
|
# APPEND cn!objectGUID to keepConnections
|
|
if not self.keep_connection(cn):
|
|
self.keep_connection_list.append(cn)
|
|
|
|
def create_connections(self, graph, part, detect_failed):
|
|
"""Construct an NC replica graph for the NC identified by
|
|
the given crossRef, then create any additional nTDSConnection
|
|
objects required.
|
|
|
|
:param graph: site graph.
|
|
:param part: crossRef object for NC.
|
|
:param detect_failed: True to detect failed DCs and route
|
|
replication traffic around them, False to assume no DC
|
|
has failed.
|
|
|
|
Modifies self.keep_connection_list by adding any connections
|
|
deemed to be "in use".
|
|
|
|
::returns: (all_connected, found_failed_dc)
|
|
(all_connected) True if the resulting NC replica graph
|
|
connects all sites that need to be connected.
|
|
(found_failed_dc) True if one or more failed DCs were
|
|
detected.
|
|
"""
|
|
all_connected = True
|
|
found_failed = False
|
|
|
|
logger.debug("create_connections(): enter\n\tpartdn=%s\n\tdetect_failed=%s" %
|
|
(part.nc_dnstr, detect_failed))
|
|
|
|
# XXX - This is a highly abbreviated function from the MS-TECH
|
|
# ref. It creates connections between bridgeheads to all
|
|
# sites that have appropriate replicas. Thus we are not
|
|
# creating a minimum cost spanning tree but instead
|
|
# producing a fully connected tree. This should produce
|
|
# a full (albeit not optimal cost) replication topology.
|
|
my_vertex = Vertex(self.my_site, part)
|
|
my_vertex.color_vertex()
|
|
|
|
# No NC replicas for this NC in the site of the local DC,
|
|
# so no nTDSConnection objects need be created
|
|
if my_vertex.is_white():
|
|
return all_connected, found_failed
|
|
|
|
# LET partialReplicaOkay be TRUE if and only if
|
|
# localSiteVertex.Color = COLOR.BLACK
|
|
if my_vertex.is_black():
|
|
partial_ok = True
|
|
else:
|
|
partial_ok = False
|
|
|
|
# Utilize the IP transport only for now
|
|
transport = None
|
|
for transport in self.transport_table.values():
|
|
if transport.name == "IP":
|
|
break
|
|
|
|
if transport is None:
|
|
raise Exception("Unable to find inter-site transport for IP")
|
|
|
|
for rsite in self.site_table.values():
|
|
|
|
# We don't make connections to our own site as that
|
|
# is intrasite topology generator's job
|
|
if rsite is self.my_site:
|
|
continue
|
|
|
|
# Determine bridgehead server in remote site
|
|
rbh = self.get_bridgehead(rsite, part, transport,
|
|
partial_ok, detect_failed)
|
|
|
|
# RODC acts as an BH for itself
|
|
# IF AmIRODC() then
|
|
# LET lbh be the nTDSDSA object of the local DC
|
|
# ELSE
|
|
# LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
|
|
# cr, t, partialReplicaOkay, detectFailedDCs)
|
|
if self.my_dsa.is_ro():
|
|
lsite = self.my_site
|
|
lbh = self.my_dsa
|
|
else:
|
|
lsite = self.my_site
|
|
lbh = self.get_bridgehead(lsite, part, transport,
|
|
partial_ok, detect_failed)
|
|
|
|
# Find the siteLink object that enumerates the connection
|
|
# between the two sites if it is present
|
|
sitelink = self.get_sitelink(lsite.site_dnstr, rsite.site_dnstr)
|
|
if sitelink is None:
|
|
link_opt = 0x0
|
|
link_sched = None
|
|
else:
|
|
link_opt = sitelink.options
|
|
link_sched = sitelink.schedule
|
|
|
|
self.create_connection(part, rbh, rsite, transport,
|
|
lbh, lsite, link_opt, link_sched,
|
|
partial_ok, detect_failed)
|
|
|
|
return all_connected, found_failed
|
|
|
|
def create_intersite_connections(self):
|
|
"""Computes an NC replica graph for each NC replica that "should be
|
|
present" on the local DC or "is present" on any DC in the same site
|
|
as the local DC. For each edge directed to an NC replica on such a
|
|
DC from an NC replica on a DC in another site, the KCC creates an
|
|
nTDSConnection object to imply that edge if one does not already
|
|
exist.
|
|
|
|
Modifies self.keep_connection_list - A list of nTDSConnection
|
|
objects for edges that are directed
|
|
to the local DC's site in one or more NC replica graphs.
|
|
|
|
returns: True if spanning trees were created for all NC replica
|
|
graphs, otherwise False.
|
|
"""
|
|
all_connected = True
|
|
self.keep_connection_list = []
|
|
|
|
# LET crossRefList be the set containing each object o of class
|
|
# crossRef such that o is a child of the CN=Partitions child of the
|
|
# config NC
|
|
|
|
# FOR each crossRef object cr in crossRefList
|
|
# IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
|
|
# is clear in cr!systemFlags, skip cr.
|
|
# LET g be the GRAPH return of SetupGraph()
|
|
|
|
for part in self.part_table.values():
|
|
|
|
if not part.is_enabled():
|
|
continue
|
|
|
|
if part.is_foreign():
|
|
continue
|
|
|
|
graph = self.setup_graph()
|
|
|
|
# Create nTDSConnection objects, routing replication traffic
|
|
# around "failed" DCs.
|
|
found_failed = False
|
|
|
|
connected, found_failed = self.create_connections(graph, part, True)
|
|
|
|
if not connected:
|
|
all_connected = False
|
|
|
|
if found_failed:
|
|
# One or more failed DCs preclude use of the ideal NC
|
|
# replica graph. Add connections for the ideal graph.
|
|
self.create_connections(graph, part, False)
|
|
|
|
return all_connected
|
|
|
|
def intersite(self):
|
|
"""The head method for generating the inter-site KCC replica
|
|
connection graph and attendant nTDSConnection objects
|
|
in the samdb.
|
|
|
|
Produces self.keep_connection_list[] of NTDS Connections
|
|
that should be kept during subsequent pruning process.
|
|
|
|
::return (True or False): (True) if the produced NC replica
|
|
graph connects all sites that need to be connected
|
|
"""
|
|
|
|
# Retrieve my DSA
|
|
mydsa = self.my_dsa
|
|
mysite = self.my_site
|
|
all_connected = True
|
|
|
|
logger.debug("intersite(): enter")
|
|
|
|
# Determine who is the ISTG
|
|
if opts.readonly:
|
|
mysite.select_istg(self.samdb, mydsa, ro=True)
|
|
else:
|
|
mysite.select_istg(self.samdb, mydsa, ro=False)
|
|
|
|
# Test whether local site has topology disabled
|
|
if mysite.is_intersite_topology_disabled():
|
|
logger.debug("intersite(): exit disabled all_connected=%d" %
|
|
all_connected)
|
|
return all_connected
|
|
|
|
if not mydsa.is_istg():
|
|
logger.debug("intersite(): exit not istg all_connected=%d" %
|
|
all_connected)
|
|
return all_connected
|
|
|
|
self.merge_failed_links()
|
|
|
|
# For each NC with an NC replica that "should be present" on the
|
|
# local DC or "is present" on any DC in the same site as the
|
|
# local DC, the KCC constructs a site graph--a precursor to an NC
|
|
# replica graph. The site connectivity for a site graph is defined
|
|
# by objects of class interSiteTransport, siteLink, and
|
|
# siteLinkBridge in the config NC.
|
|
|
|
all_connected = self.create_intersite_connections()
|
|
|
|
logger.debug("intersite(): exit all_connected=%d" % all_connected)
|
|
return all_connected
|
|
|
|
def update_rodc_connection(self):
|
|
"""Runs when the local DC is an RODC and updates the RODC NTFRS
|
|
connection object.
|
|
"""
|
|
# Given an nTDSConnection object cn1, such that cn1.options contains
|
|
# NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
|
|
# does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
|
|
# that the following is true:
|
|
#
|
|
# cn1.fromServer = cn2.fromServer
|
|
# cn1.schedule = cn2.schedule
|
|
#
|
|
# If no such cn2 can be found, cn1 is not modified.
|
|
# If no such cn1 can be found, nothing is modified by this task.
|
|
|
|
# XXX - not implemented yet
|
|
|
|
def intrasite_max_node_edges(self, node_count):
|
|
"""Returns the maximum number of edges directed to a node in
|
|
the intrasite replica graph.
|
|
|
|
The KCC does not create more
|
|
than 50 edges directed to a single DC. To optimize replication,
|
|
we compute that each node should have n+2 total edges directed
|
|
to it such that (n) is the smallest non-negative integer
|
|
satisfying (node_count <= 2*(n*n) + 6*n + 7)
|
|
|
|
:param node_count: total number of nodes in the replica graph
|
|
"""
|
|
n = 0
|
|
while True:
|
|
if node_count <= (2 * (n * n) + (6 * n) + 7):
|
|
break
|
|
n = n + 1
|
|
n = n + 2
|
|
if n < 50:
|
|
return n
|
|
return 50
|
|
|
|
def construct_intrasite_graph(self, site_local, dc_local,
|
|
nc_x, gc_only, detect_stale):
|
|
|
|
# We're using the MS notation names here to allow
|
|
# correlation back to the published algorithm.
|
|
#
|
|
# nc_x - naming context (x) that we are testing if it
|
|
# "should be present" on the local DC
|
|
# f_of_x - replica (f) found on a DC (s) for NC (x)
|
|
# dc_s - DC where f_of_x replica was found
|
|
# dc_local - local DC that potentially needs a replica
|
|
# (f_of_x)
|
|
# r_list - replica list R
|
|
# p_of_x - replica (p) is partial and found on a DC (s)
|
|
# for NC (x)
|
|
# l_of_x - replica (l) is the local replica for NC (x)
|
|
# that should appear on the local DC
|
|
# r_len = is length of replica list |R|
|
|
#
|
|
# If the DSA doesn't need a replica for this
|
|
# partition (NC x) then continue
|
|
needed, ro, partial = nc_x.should_be_present(dc_local)
|
|
|
|
logger.debug("construct_intrasite_graph(): enter" +
|
|
"\n\tgc_only=%d" % gc_only +
|
|
"\n\tdetect_stale=%d" % detect_stale +
|
|
"\n\tneeded=%s" % needed +
|
|
"\n\tro=%s" % ro +
|
|
"\n\tpartial=%s" % partial +
|
|
"\n%s" % nc_x)
|
|
|
|
if not needed:
|
|
return
|
|
|
|
# Create a NCReplica that matches what the local replica
|
|
# should say. We'll use this below in our r_list
|
|
l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
|
|
nc_x.nc_dnstr)
|
|
|
|
l_of_x.identify_by_basedn(self.samdb)
|
|
|
|
l_of_x.rep_partial = partial
|
|
l_of_x.rep_ro = ro
|
|
|
|
# Add this replica that "should be present" to the
|
|
# needed replica table for this DSA
|
|
dc_local.add_needed_replica(l_of_x)
|
|
|
|
# Empty replica sequence list
|
|
r_list = []
|
|
|
|
# We'll loop thru all the DSAs looking for
|
|
# writeable NC replicas that match the naming
|
|
# context dn for (nc_x)
|
|
#
|
|
for dc_s_dn, dc_s in self.my_site.dsa_table.items():
|
|
|
|
# If this partition (nc_x) doesn't appear as a
|
|
# replica (f_of_x) on (dc_s) then continue
|
|
if not nc_x.nc_dnstr in dc_s.current_rep_table.keys():
|
|
continue
|
|
|
|
# Pull out the NCReplica (f) of (x) with the dn
|
|
# that matches NC (x) we are examining.
|
|
f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
|
|
|
|
# Replica (f) of NC (x) must be writable
|
|
if f_of_x.is_ro():
|
|
continue
|
|
|
|
# Replica (f) of NC (x) must satisfy the
|
|
# "is present" criteria for DC (s) that
|
|
# it was found on
|
|
if not f_of_x.is_present():
|
|
continue
|
|
|
|
# DC (s) must be a writable DSA other than
|
|
# my local DC. In other words we'd only replicate
|
|
# from other writable DC
|
|
if dc_s.is_ro() or dc_s is dc_local:
|
|
continue
|
|
|
|
# Certain replica graphs are produced only
|
|
# for global catalogs, so test against
|
|
# method input parameter
|
|
if gc_only and not dc_s.is_gc():
|
|
continue
|
|
|
|
# DC (s) must be in the same site as the local DC
|
|
# as this is the intra-site algorithm. This is
|
|
# handled by virtue of placing DSAs in per
|
|
# site objects (see enclosing for() loop)
|
|
|
|
# If NC (x) is intended to be read-only full replica
|
|
# for a domain NC on the target DC then the source
|
|
# DC should have functional level at minimum WIN2008
|
|
#
|
|
# Effectively we're saying that in order to replicate
|
|
# to a targeted RODC (which was introduced in Windows 2008)
|
|
# then we have to replicate from a DC that is also minimally
|
|
# at that level.
|
|
#
|
|
# You can also see this requirement in the MS special
|
|
# considerations for RODC which state that to deploy
|
|
# an RODC, at least one writable domain controller in
|
|
# the domain must be running Windows Server 2008
|
|
if ro and not partial and nc_x.nc_type == NCType.domain:
|
|
if not dc_s.is_minimum_behavior(DS_BEHAVIOR_WIN2008):
|
|
continue
|
|
|
|
# If we haven't been told to turn off stale connection
|
|
# detection and this dsa has a stale connection then
|
|
# continue
|
|
if detect_stale and self.is_stale_link_connection(dc_s) == True:
|
|
continue
|
|
|
|
# Replica meets criteria. Add it to table indexed
|
|
# by the GUID of the DC that it appears on
|
|
r_list.append(f_of_x)
|
|
|
|
# If a partial (not full) replica of NC (x) "should be present"
|
|
# on the local DC, append to R each partial replica (p of x)
|
|
# such that p "is present" on a DC satisfying the same
|
|
# criteria defined above for full replica DCs.
|
|
if partial == True:
|
|
|
|
# Now we loop thru all the DSAs looking for
|
|
# partial NC replicas that match the naming
|
|
# context dn for (NC x)
|
|
for dc_s_dn, dc_s in self.my_site.dsa_table.items():
|
|
|
|
# If this partition NC (x) doesn't appear as a
|
|
# replica (p) of NC (x) on the dsa DC (s) then
|
|
# continue
|
|
if not nc_x.nc_dnstr in dc_s.current_rep_table.keys():
|
|
continue
|
|
|
|
# Pull out the NCReplica with the dn that
|
|
# matches NC (x) we are examining.
|
|
p_of_x = dsa.current_rep_table[nc_x.nc_dnstr]
|
|
|
|
# Replica (p) of NC (x) must be partial
|
|
if not p_of_x.is_partial():
|
|
continue
|
|
|
|
# Replica (p) of NC (x) must satisfy the
|
|
# "is present" criteria for DC (s) that
|
|
# it was found on
|
|
if not p_of_x.is_present():
|
|
continue
|
|
|
|
# DC (s) must be a writable DSA other than
|
|
# my DSA. In other words we'd only replicate
|
|
# from other writable DSA
|
|
if dc_s.is_ro() or dc_s is dc_local:
|
|
continue
|
|
|
|
# Certain replica graphs are produced only
|
|
# for global catalogs, so test against
|
|
# method input parameter
|
|
if gc_only and not dc_s.is_gc():
|
|
continue
|
|
|
|
# DC (s) must be in the same site as the local DC
|
|
# as this is the intra-site algorithm. This is
|
|
# handled by virtue of placing DSAs in per
|
|
# site objects (see enclosing for() loop)
|
|
|
|
# This criteria is moot (a no-op) for this case
|
|
# because we are scanning for (partial = True). The
|
|
# MS algorithm statement says partial replica scans
|
|
# should adhere to the "same" criteria as full replica
|
|
# scans so the criteria doesn't change here...its just
|
|
# rendered pointless.
|
|
#
|
|
# The case that is occurring would be a partial domain
|
|
# replica is needed on a local DC global catalog. There
|
|
# is no minimum windows behavior for those since GCs
|
|
# have always been present.
|
|
if ro and not partial and nc_x.nc_type == NCType.domain:
|
|
if not dc_s.is_minimum_behavior(DS_BEHAVIOR_WIN2008):
|
|
continue
|
|
|
|
# If we haven't been told to turn off stale connection
|
|
# detection and this dsa has a stale connection then
|
|
# continue
|
|
if detect_stale and self.is_stale_link_connection(dc_s) == True:
|
|
continue
|
|
|
|
# Replica meets criteria. Add it to table indexed
|
|
# by the GUID of the DSA that it appears on
|
|
r_list.append(p_of_x)
|
|
|
|
# Append to R the NC replica that "should be present"
|
|
# on the local DC
|
|
r_list.append(l_of_x)
|
|
|
|
r_list.sort(sort_replica_by_dsa_guid)
|
|
|
|
r_len = len(r_list)
|
|
|
|
max_node_edges = self.intrasite_max_node_edges(r_len)
|
|
|
|
# Add a node for each r_list element to the replica graph
|
|
graph_list = []
|
|
for rep in r_list:
|
|
node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
|
|
graph_list.append(node)
|
|
|
|
# For each r(i) from (0 <= i < |R|-1)
|
|
i = 0
|
|
while i < (r_len-1):
|
|
# Add an edge from r(i) to r(i+1) if r(i) is a full
|
|
# replica or r(i+1) is a partial replica
|
|
if not r_list[i].is_partial() or r_list[i+1].is_partial():
|
|
graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
|
|
|
|
# Add an edge from r(i+1) to r(i) if r(i+1) is a full
|
|
# replica or ri is a partial replica.
|
|
if not r_list[i+1].is_partial() or r_list[i].is_partial():
|
|
graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
|
|
i = i + 1
|
|
|
|
# Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
|
|
# or r0 is a partial replica.
|
|
if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
|
|
graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
|
|
|
|
# Add an edge from r0 to r|R|-1 if r0 is a full replica or
|
|
# r|R|-1 is a partial replica.
|
|
if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
|
|
graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
|
|
|
|
# For each existing nTDSConnection object implying an edge
|
|
# from rj of R to ri such that j != i, an edge from rj to ri
|
|
# is not already in the graph, and the total edges directed
|
|
# to ri is less than n+2, the KCC adds that edge to the graph.
|
|
i = 0
|
|
while i < r_len:
|
|
dsa = self.my_site.dsa_table[graph_list[i].dsa_dnstr]
|
|
graph_list[i].add_edges_from_connections(dsa)
|
|
i = i + 1
|
|
|
|
i = 0
|
|
while i < r_len:
|
|
tnode = graph_list[i]
|
|
|
|
# To optimize replication latency in sites with many NC replicas, the
|
|
# KCC adds new edges directed to ri to bring the total edges to n+2,
|
|
# where the NC replica rk of R from which the edge is directed
|
|
# is chosen at random such that k != i and an edge from rk to ri
|
|
# is not already in the graph.
|
|
#
|
|
# Note that the KCC tech ref does not give a number for the definition
|
|
# of "sites with many NC replicas". At a bare minimum to satisfy
|
|
# n+2 edges directed at a node we have to have at least three replicas
|
|
# in |R| (i.e. if n is zero then at least replicas from two other graph
|
|
# nodes may direct edges to us).
|
|
if r_len >= 3:
|
|
# pick a random index
|
|
findex = rindex = random.randint(0, r_len-1)
|
|
|
|
# while this node doesn't have sufficient edges
|
|
while tnode.has_sufficient_edges() == False:
|
|
# If this edge can be successfully added (i.e. not
|
|
# the same node and edge doesn't already exist) then
|
|
# select a new random index for the next round
|
|
if tnode.add_edge_from(graph_list[rindex].dsa_dnstr) == True:
|
|
findex = rindex = random.randint(0, r_len-1)
|
|
else:
|
|
# Otherwise continue looking against each node
|
|
# after the random selection
|
|
rindex = rindex + 1
|
|
if rindex >= r_len:
|
|
rindex = 0
|
|
|
|
if rindex == findex:
|
|
logger.error("Unable to satisfy max edge criteria!")
|
|
break
|
|
|
|
# Print the graph node in debug mode
|
|
logger.debug("%s" % tnode)
|
|
|
|
# For each edge directed to the local DC, ensure a nTDSConnection
|
|
# points to us that satisfies the KCC criteria
|
|
if graph_list[i].dsa_dnstr == dc_local.dsa_dnstr:
|
|
graph_list[i].add_connections_from_edges(dc_local)
|
|
|
|
i = i + 1
|
|
|
|
def intrasite(self):
|
|
"""The head method for generating the intra-site KCC replica
|
|
connection graph and attendant nTDSConnection objects
|
|
in the samdb
|
|
"""
|
|
# Retrieve my DSA
|
|
mydsa = self.my_dsa
|
|
|
|
logger.debug("intrasite(): enter")
|
|
|
|
# Test whether local site has topology disabled
|
|
mysite = self.site_table[self.my_site_dnstr]
|
|
if mysite.is_intrasite_topology_disabled():
|
|
return
|
|
|
|
detect_stale = (mysite.is_detect_stale_disabled() == False)
|
|
|
|
# Loop thru all the partitions.
|
|
for partdn, part in self.part_table.items():
|
|
self.construct_intrasite_graph(mysite, mydsa, part, False,
|
|
detect_stale)
|
|
|
|
# If the DC is a GC server, the KCC constructs an additional NC
|
|
# replica graph (and creates nTDSConnection objects) for the
|
|
# config NC as above, except that only NC replicas that "are present"
|
|
# on GC servers are added to R.
|
|
for partdn, part in self.part_table.items():
|
|
if part.is_config():
|
|
self.construct_intrasite_graph(mysite, mydsa, part, True,
|
|
detect_stale)
|
|
|
|
# The DC repeats the NC replica graph computation and nTDSConnection
|
|
# creation for each of the NC replica graphs, this time assuming
|
|
# that no DC has failed. It does so by re-executing the steps as
|
|
# if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
|
|
# set in the options attribute of the site settings object for
|
|
# the local DC's site. (ie. we set "detec_stale" flag to False)
|
|
|
|
# Loop thru all the partitions.
|
|
for partdn, part in self.part_table.items():
|
|
self.construct_intrasite_graph(mysite, mydsa, part, False,
|
|
False) # don't detect stale
|
|
|
|
# If the DC is a GC server, the KCC constructs an additional NC
|
|
# replica graph (and creates nTDSConnection objects) for the
|
|
# config NC as above, except that only NC replicas that "are present"
|
|
# on GC servers are added to R.
|
|
for partdn, part in self.part_table.items():
|
|
if part.is_config():
|
|
self.construct_intrasite_graph(mysite, mydsa, part, True,
|
|
False) # don't detect stale
|
|
|
|
if opts.readonly:
|
|
# Display any to be added or modified repsFrom
|
|
for dnstr, connect in mydsa.connect_table.items():
|
|
if connect.to_be_deleted == True:
|
|
logger.info("TO BE DELETED:\n%s" % connect)
|
|
if connect.to_be_modified == True:
|
|
logger.info("TO BE MODIFIED:\n%s" % connect)
|
|
if connect.to_be_added == True:
|
|
logger.info("TO BE ADDED:\n%s" % connect)
|
|
|
|
mydsa.commit_connections(self.samdb, ro=True)
|
|
else:
|
|
# Commit any newly created connections to the samdb
|
|
mydsa.commit_connections(self.samdb)
|
|
|
|
def run(self, dburl, lp, creds):
|
|
"""Method to perform a complete run of the KCC and
|
|
produce an updated topology for subsequent NC replica
|
|
syncronization between domain controllers
|
|
"""
|
|
# We may already have a samdb setup if we are
|
|
# currently importing an ldif for a test run
|
|
if self.samdb is None:
|
|
try:
|
|
self.samdb = SamDB(url=lp.samdb_url(),
|
|
session_info=system_session(),
|
|
credentials=creds, lp=lp)
|
|
|
|
except ldb.LdbError, (num, msg):
|
|
logger.error("Unable to open sam database %s : %s" %
|
|
(lp.samdb_url(), msg))
|
|
return 1
|
|
|
|
try:
|
|
# Setup
|
|
self.load_my_site()
|
|
self.load_my_dsa()
|
|
|
|
self.load_all_sites()
|
|
self.load_all_partitions()
|
|
self.load_all_transports()
|
|
self.load_all_sitelinks()
|
|
|
|
# These are the published steps (in order) for the
|
|
# MS-TECH description of the KCC algorithm
|
|
|
|
# Step 1
|
|
self.refresh_failed_links_connections()
|
|
|
|
# Step 2
|
|
self.intrasite()
|
|
|
|
# Step 3
|
|
all_connected = self.intersite()
|
|
|
|
# Step 4
|
|
self.remove_unneeded_ntdsconn(all_connected)
|
|
|
|
# Step 5
|
|
self.translate_ntdsconn()
|
|
|
|
# Step 6
|
|
self.remove_unneeded_failed_links_connections()
|
|
|
|
# Step 7
|
|
self.update_rodc_connection()
|
|
|
|
except Exception, estr:
|
|
logger.error("%s" % estr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
def import_ldif(self, dburl, lp, creds, ldif_file):
|
|
"""Routine to import all objects and attributes that are relevent
|
|
to the KCC algorithms from a previously exported LDIF file.
|
|
|
|
The point of this function is to allow a programmer/debugger to
|
|
import an LDIF file with non-security relevent information that
|
|
was previously extracted from a DC database. The LDIF file is used
|
|
to create a temporary abbreviated database. The KCC algorithm can
|
|
then run against this abbreviated database for debug or test
|
|
verification that the topology generated is computationally the
|
|
same between different OSes and algorithms.
|
|
|
|
:param dburl: path to the temporary abbreviated db to create
|
|
:param ldif_file: path to the ldif file to import
|
|
"""
|
|
if os.path.exists(dburl):
|
|
logger.error("Specify a database (%s) that doesn't already exist." %
|
|
dburl)
|
|
return 1
|
|
|
|
# Use ["modules:"] as we are attempting to build a sam
|
|
# database as opposed to start it here.
|
|
self.samdb = Ldb(url=dburl, session_info=system_session(),
|
|
lp=lp, options=["modules:"])
|
|
|
|
self.samdb.transaction_start()
|
|
try:
|
|
data = read_and_sub_file(ldif_file, None)
|
|
self.samdb.add_ldif(data, None)
|
|
|
|
except Exception, estr:
|
|
logger.error("%s" % estr)
|
|
self.samdb.transaction_cancel()
|
|
return 1
|
|
else:
|
|
self.samdb.transaction_commit()
|
|
|
|
self.samdb = None
|
|
|
|
# We have an abbreviated list of options here because we have built
|
|
# an abbreviated database. We use the rootdse and extended-dn
|
|
# modules only during this re-open
|
|
self.samdb = SamDB(url=dburl, session_info=system_session(),
|
|
credentials=creds, lp=lp,
|
|
options=["modules:rootdse,extended_dn_out_ldb"])
|
|
return 0
|
|
|
|
def export_ldif(self, dburl, lp, creds, ldif_file):
|
|
"""Routine to extract all objects and attributes that are relevent
|
|
to the KCC algorithms from a DC database.
|
|
|
|
The point of this function is to allow a programmer/debugger to
|
|
extract an LDIF file with non-security relevent information from
|
|
a DC database. The LDIF file can then be used to "import" via
|
|
the import_ldif() function this file into a temporary abbreviated
|
|
database. The KCC algorithm can then run against this abbreviated
|
|
database for debug or test verification that the topology generated
|
|
is computationally the same between different OSes and algorithms.
|
|
|
|
:param dburl: LDAP database URL to extract info from
|
|
:param ldif_file: output LDIF file name to create
|
|
"""
|
|
try:
|
|
self.samdb = SamDB(url=dburl,
|
|
session_info=system_session(),
|
|
credentials=creds, lp=lp)
|
|
except ldb.LdbError, (enum, estr):
|
|
logger.error("Unable to open sam database (%s) : %s" %
|
|
(lp.samdb_url(), estr))
|
|
return 1
|
|
|
|
if os.path.exists(ldif_file):
|
|
logger.error("Specify a file (%s) that doesn't already exist." %
|
|
ldif_file)
|
|
return 1
|
|
|
|
try:
|
|
f = open(ldif_file, "w")
|
|
except (enum, estr):
|
|
logger.error("Unable to open (%s) : %s" % (ldif_file, estr))
|
|
return 1
|
|
|
|
try:
|
|
# Query Partitions
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"objectSid",
|
|
"Enabled",
|
|
"systemFlags",
|
|
"dnsRoot",
|
|
"nCName",
|
|
"msDS-NC-Replica-Locations",
|
|
"msDS-NC-RO-Replica-Locations" ]
|
|
|
|
sstr = "CN=Partitions,%s" % self.samdb.get_config_basedn()
|
|
res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
|
|
attrs=attrs,
|
|
expression="(objectClass=crossRef)")
|
|
|
|
# Write partitions output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
# Query cross reference container
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"fSMORoleOwner",
|
|
"systemFlags",
|
|
"msDS-Behavior-Version",
|
|
"msDS-EnabledFeature" ]
|
|
|
|
sstr = "CN=Partitions,%s" % self.samdb.get_config_basedn()
|
|
res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
|
|
attrs=attrs,
|
|
expression="(objectClass=crossRefContainer)")
|
|
|
|
# Write cross reference container output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
# Query Sites
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"systemFlags" ]
|
|
|
|
sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
|
|
sites = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
|
|
attrs=attrs,
|
|
expression="(objectClass=site)")
|
|
|
|
# Write sites output
|
|
write_search_result(self.samdb, f, sites)
|
|
|
|
# Query NTDS Site Settings
|
|
for msg in sites:
|
|
sitestr = str(msg.dn)
|
|
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"interSiteTopologyGenerator",
|
|
"interSiteTopologyFailover",
|
|
"schedule",
|
|
"options" ]
|
|
|
|
sstr = "CN=NTDS Site Settings,%s" % sitestr
|
|
res = self.samdb.search(base=sstr, scope=ldb.SCOPE_BASE,
|
|
attrs=attrs)
|
|
|
|
# Write Site Settings output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
# Naming context list
|
|
nclist = []
|
|
|
|
# Query Directory Service Agents
|
|
for msg in sites:
|
|
sstr = str(msg.dn)
|
|
|
|
ncattrs = [ "hasMasterNCs",
|
|
"msDS-hasMasterNCs",
|
|
"hasPartialReplicaNCs",
|
|
"msDS-HasDomainNCs",
|
|
"msDS-hasFullReplicaNCs",
|
|
"msDS-HasInstantiatedNCs" ]
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"invocationID",
|
|
"options",
|
|
"msDS-isRODC",
|
|
"msDS-Behavior-Version" ]
|
|
|
|
res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
|
|
attrs=attrs + ncattrs,
|
|
expression="(objectClass=nTDSDSA)")
|
|
|
|
# Spin thru all the DSAs looking for NC replicas
|
|
# and build a list of all possible Naming Contexts
|
|
# for subsequent retrieval below
|
|
for msg in res:
|
|
for k in msg.keys():
|
|
if k in ncattrs:
|
|
for value in msg[k]:
|
|
# Some of these have binary DNs so
|
|
# use dsdb_Dn to split out relevent parts
|
|
dsdn = dsdb_Dn(self.samdb, value)
|
|
dnstr = str(dsdn.dn)
|
|
if dnstr not in nclist:
|
|
nclist.append(dnstr)
|
|
|
|
# Write DSA output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
# Query NTDS Connections
|
|
for msg in sites:
|
|
sstr = str(msg.dn)
|
|
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"options",
|
|
"whenCreated",
|
|
"enabledConnection",
|
|
"schedule",
|
|
"transportType",
|
|
"fromServer",
|
|
"systemFlags" ]
|
|
|
|
res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
|
|
attrs=attrs,
|
|
expression="(objectClass=nTDSConnection)")
|
|
# Write NTDS Connection output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
|
|
# Query Intersite transports
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"options",
|
|
"name",
|
|
"bridgeheadServerListBL",
|
|
"transportAddressAttribute" ]
|
|
|
|
sstr = "CN=Inter-Site Transports,CN=Sites,%s" % \
|
|
self.samdb.get_config_basedn()
|
|
res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
|
|
attrs=attrs,
|
|
expression="(objectClass=interSiteTransport)")
|
|
|
|
# Write inter-site transport output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
# Query siteLink
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"systemFlags",
|
|
"options",
|
|
"schedule",
|
|
"replInterval",
|
|
"siteList",
|
|
"cost" ]
|
|
|
|
sstr = "CN=Sites,%s" % \
|
|
self.samdb.get_config_basedn()
|
|
res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
|
|
attrs=attrs,
|
|
expression="(objectClass=siteLink)")
|
|
|
|
# Write siteLink output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
# Query siteLinkBridge
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"siteLinkList" ]
|
|
|
|
sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
|
|
res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
|
|
attrs=attrs,
|
|
expression="(objectClass=siteLinkBridge)")
|
|
|
|
# Write siteLinkBridge output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
# Query servers containers
|
|
# Needed for samdb.server_site_name()
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"systemFlags" ]
|
|
|
|
sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
|
|
res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
|
|
attrs=attrs,
|
|
expression="(objectClass=serversContainer)")
|
|
|
|
# Write servers container output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
# Query servers
|
|
# Needed because some transport interfaces refer back to
|
|
# attributes found in the server object. Also needed
|
|
# so extended-dn will be happy with dsServiceName in rootDSE
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"systemFlags",
|
|
"dNSHostName",
|
|
"mailAddress" ]
|
|
|
|
sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
|
|
res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
|
|
attrs=attrs,
|
|
expression="(objectClass=server)")
|
|
|
|
# Write server output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
# Query Naming Context replicas
|
|
attrs = [ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"objectSid",
|
|
"fSMORoleOwner",
|
|
"msDS-Behavior-Version",
|
|
"repsFrom",
|
|
"repsTo" ]
|
|
|
|
for sstr in nclist:
|
|
res = self.samdb.search(sstr, scope=ldb.SCOPE_BASE,
|
|
attrs=attrs)
|
|
|
|
# Write naming context output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
# Query rootDSE replicas
|
|
attrs=[ "objectClass",
|
|
"objectGUID",
|
|
"cn",
|
|
"whenChanged",
|
|
"rootDomainNamingContext",
|
|
"configurationNamingContext",
|
|
"schemaNamingContext",
|
|
"defaultNamingContext",
|
|
"dsServiceName" ]
|
|
|
|
sstr = ""
|
|
res = self.samdb.search(sstr, scope=ldb.SCOPE_BASE,
|
|
attrs=attrs)
|
|
|
|
# Record the rootDSE object as a dn as it
|
|
# would appear in the base ldb file. We have
|
|
# to save it this way because we are going to
|
|
# be importing as an abbreviated database.
|
|
res[0].dn = ldb.Dn(self.samdb, "@ROOTDSE")
|
|
|
|
# Write rootdse output
|
|
write_search_result(self.samdb, f, res)
|
|
|
|
except ldb.LdbError, (enum, estr):
|
|
logger.error("Error processing (%s) : %s" % (sstr, estr))
|
|
return 1
|
|
|
|
f.close()
|
|
return 0
|
|
|
|
##################################################
|
|
# Global Functions
|
|
##################################################
|
|
def sort_replica_by_dsa_guid(rep1, rep2):
|
|
return cmp(rep1.rep_dsa_guid, rep2.rep_dsa_guid)
|
|
|
|
def sort_dsa_by_gc_and_guid(dsa1, dsa2):
|
|
if dsa1.is_gc() == True and dsa2.is_gc() == False:
|
|
return -1
|
|
if dsa1.is_gc() == False and dsa2.is_gc() == True:
|
|
return +1
|
|
return cmp(dsa1.dsa_guid, dsa2.dsa_guid)
|
|
|
|
def is_smtp_replication_available():
|
|
"""Currently always returns false because Samba
|
|
doesn't implement SMTP transfer for NC changes
|
|
between DCs
|
|
"""
|
|
return False
|
|
|
|
def write_search_result(samdb, f, res):
|
|
for msg in res:
|
|
lstr = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
|
|
f.write("%s" % lstr)
|
|
|
|
##################################################
|
|
# samba_kcc entry point
|
|
##################################################
|
|
|
|
parser = optparse.OptionParser("samba_kcc [options]")
|
|
sambaopts = options.SambaOptions(parser)
|
|
credopts = options.CredentialsOptions(parser)
|
|
|
|
parser.add_option_group(sambaopts)
|
|
parser.add_option_group(credopts)
|
|
parser.add_option_group(options.VersionOptions(parser))
|
|
|
|
parser.add_option("--readonly",
|
|
help="compute topology but do not update database",
|
|
action="store_true")
|
|
|
|
parser.add_option("--debug",
|
|
help="debug output",
|
|
action="store_true")
|
|
|
|
parser.add_option("--seed",
|
|
help="random number seed",
|
|
type=str, metavar="<number>")
|
|
|
|
parser.add_option("--importldif",
|
|
help="import topology ldif file",
|
|
type=str, metavar="<file>")
|
|
|
|
parser.add_option("--exportldif",
|
|
help="export topology ldif file",
|
|
type=str, metavar="<file>")
|
|
|
|
parser.add_option("-H", "--URL" ,
|
|
help="LDB URL for database or target server",
|
|
type=str, metavar="<URL>", dest="dburl")
|
|
|
|
parser.add_option("--tmpdb",
|
|
help="schemaless database file to create for ldif import",
|
|
type=str, metavar="<file>")
|
|
|
|
logger = logging.getLogger("samba_kcc")
|
|
logger.addHandler(logging.StreamHandler(sys.stdout))
|
|
|
|
lp = sambaopts.get_loadparm()
|
|
creds = credopts.get_credentials(lp, fallback_machine=True)
|
|
|
|
opts, args = parser.parse_args()
|
|
|
|
if opts.readonly is None:
|
|
opts.readonly = False
|
|
|
|
if opts.debug:
|
|
logger.setLevel(logging.DEBUG)
|
|
elif opts.readonly:
|
|
logger.setLevel(logging.INFO)
|
|
else:
|
|
logger.setLevel(logging.WARNING)
|
|
|
|
# initialize seed from optional input parameter
|
|
if opts.seed:
|
|
random.seed(int(opts.seed))
|
|
else:
|
|
random.seed(0xACE5CA11)
|
|
|
|
if opts.dburl is None:
|
|
opts.dburl = lp.samdb_url()
|
|
|
|
# Instantiate Knowledge Consistency Checker and perform run
|
|
kcc = KCC()
|
|
|
|
if opts.exportldif:
|
|
rc = kcc.export_ldif(opts.dburl, lp, creds, opts.exportldif)
|
|
sys.exit(rc)
|
|
|
|
if opts.importldif:
|
|
if opts.tmpdb is None or opts.tmpdb.startswith('ldap'):
|
|
logger.error("Specify a target temp database file with --tmpdb option.")
|
|
sys.exit(1)
|
|
|
|
rc = kcc.import_ldif(opts.tmpdb, lp, creds, opts.importldif)
|
|
if rc != 0:
|
|
sys.exit(rc)
|
|
|
|
rc = kcc.run(opts.dburl, lp, creds)
|
|
sys.exit(rc)
|