From 819f11285d12041f2a22a6c92ebabb8a559886c5 Mon Sep 17 00:00:00 2001 From: Dave Craft Date: Sun, 4 Dec 2011 11:08:56 -0600 Subject: [PATCH] samba_kcc NTDSConnection translation This is an advancement of samba_kcc to compute and commit the modification of a repsFrom on an NC Replica. The repsFrom is computed according to the MS tech spec for implied replicas of NTDSConnections. Proper maintenance of (DRS options, schedules, etc) from a NTDSConnection are now all present. New classes for inter-site transports, sites, and repsFrom) are now present in kcc_utils.py. Substantively this gets intra-site topology generation functional by committing the repsFrom that were computed from the DSA graph implemented in prior drops of samba_kcc Signed-off-by: Andrew Tridgell --- source4/scripting/bin/samba_kcc | 720 +++++++++++++++--- source4/scripting/python/samba/kcc_utils.py | 790 +++++++++++++++++--- 2 files changed, 1292 insertions(+), 218 deletions(-) diff --git a/source4/scripting/bin/samba_kcc b/source4/scripting/bin/samba_kcc index c024cd41ef0..c17439e6376 100755 --- a/source4/scripting/bin/samba_kcc +++ b/source4/scripting/bin/samba_kcc @@ -20,6 +20,7 @@ import os import sys import random +import copy # ensure we get messages out immediately, so they get in the samba logs, # and don't get swallowed by a timeout @@ -41,6 +42,7 @@ import logging from samba import getopt as options from samba.auth import system_session from samba.samdb import SamDB +from samba.dcerpc import drsuapi from samba.kcc_utils import * class KCC: @@ -55,12 +57,47 @@ class KCC: our local DCs partitions or all the partitions in the forest """ - self.dsa_table = {} # dsa objects - self.part_table = {} # partition objects - self.site_table = {} + self.part_table = {} # partition objects + self.site_table = {} + self.transport_table = {} + 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 = samdb + return + + def load_all_transports(self): + """Loads the inter-site transport objects for Sites + Raises an Exception on error + """ + try: + res = samdb.search("CN=Inter-Site Transports,CN=Sites,%s" % \ + 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(samdb) + + # Assign this transport to table + # and index by dn + self.transport_table[dnstr] = transport + + return def load_my_site(self): """Loads the Site class for the local DSA @@ -69,14 +106,14 @@ class KCC: self.my_site_dnstr = "CN=%s,CN=Sites,%s" % (samdb.server_site_name(), samdb.get_config_basedn()) site = Site(self.my_site_dnstr) - site.load_site(samdb) + self.site_table[self.my_site_dnstr] = site + self.my_site = site + return def load_my_dsa(self): - """Discover my nTDSDSA thru the rootDSE entry and - instantiate and load the DSA. The dsa is inserted - into the dsa_table by dn string + """Discover my nTDSDSA dn thru the rootDSE entry Raises an Exception on error. """ dn = ldb.Dn(self.samdb, "") @@ -86,49 +123,10 @@ class KCC: except ldb.LdbError, (enum, estr): raise Exception("Unable to find my nTDSDSA - (%s)" % estr) - dnstr = res[0]["dsServiceName"][0] - - # already loaded - if dnstr in self.dsa_table.keys(): - return - - self.my_dsa_dnstr = dnstr - dsa = DirectoryServiceAgent(dnstr) - - dsa.load_dsa(samdb) - - # Assign this dsa to my dsa table - # and index by dsa dn - self.dsa_table[dnstr] = dsa - - def load_all_dsa(self): - """Discover all nTDSDSA thru the sites entry and - instantiate and load the DSAs. Each dsa is inserted - into the dsa_table by dn string. - Raises an Exception on error. - """ - try: - res = self.samdb.search("CN=Sites,%s" % - self.samdb.get_config_basedn(), - scope=ldb.SCOPE_SUBTREE, - expression="(objectClass=nTDSDSA)") - except ldb.LdbError, (enum, estr): - raise Exception("Unable to find nTDSDSAs - (%s)" % estr) + self.my_dsa_dnstr = res[0]["dsServiceName"][0] + self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr) - for msg in res: - dnstr = str(msg.dn) - - # already loaded - if dnstr in self.dsa_table.keys(): - continue - - dsa = DirectoryServiceAgent(dnstr) - - dsa.load_dsa(self.samdb) - - # Assign this dsa to my dsa table - # and index by dsa dn - self.dsa_table[dnstr] = dsa + return def load_all_partitions(self): """Discover all NCs thru the Partitions dn and @@ -158,16 +156,15 @@ class KCC: self.part_table[partstr] = part def should_be_present_test(self): - """Enumerate all loaded partitions and DSAs and test - if NC should be present as replica + """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.dsa_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" % \ - (dsa.dsa_dnstr, part.nc_dnstr, needed, ro, partial)) + (dsadn, part.nc_dnstr, needed, ro, partial)) + return def refresh_failed_links_connections(self): # XXX - not implemented yet @@ -186,12 +183,500 @@ class KCC: # XXX - not implemented yet return - def remove_unneeded_ntds_connections(self): + def remove_unneeded_ntdsconn(self): # XXX - not implemented yet return - def translate_connections(self): - # XXX - not implemented yet + 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.get_dsa(s_dnstr) is 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.addr_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 "addr_attr" from the parent of the + # DSA object + try: + pdnstr = s_dsa.get_parent_dnstr() + attrs = [ x_transport.addr_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.addr_attr, pdnstr, estr)) + + msg = res[0] + nastr = str(msg[x_transport.addr_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) + return + + 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 mydsa:\n%s" % self.my_dsa) + + if self.my_dsa.should_translate_ntdsconn() == False: + 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 = {} + + # Table of replicas needed, combined with our local information + # if we already have the replica. This may be a superset list of + # replicas if we need additional NC replicas that we currently + # don't have local copies for + translate_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 + # + # Build a list of replicas that we will run translation + # against. If we have the replica and its not needed + # then we add it to the "to be deleted" list. Otherwise + # we have it and we need it so move it to the translate list + for dnstr, n_rep in current_rep_table.items(): + if dnstr not in needed_rep_table.keys(): + delete_rep_table[dnstr] = n_rep + else: + translate_rep_table[dnstr] = n_rep + + # If we need the replica yet we don't have it (not in + # translate list) then add it + for dnstr, n_rep in needed_rep_table.items(): + if dnstr not in translate_rep_table.keys(): + translate_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 translate_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(): + + # 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) + + if s_dsa == None: + 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 + + # Source dsa is gone from config (strange) + # 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: + continue + + # 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. + # + implies = (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 implies == False: + 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) + + # Commit any modified repsFrom to the NC replica + if opts.readonly is None: + n_rep.commit_repsFrom(self.samdb) + return def intersite(self): @@ -260,11 +745,13 @@ class KCC: # partition (NC x) then continue needed, ro, partial = nc_x.should_be_present(dc_local) - logger.debug("construct_intrasite_graph:\n" + \ - "nc_x: %s\ndc_local: %s\n" % \ - (nc_x, dc_local) + \ - "gc_only: %s\nneeded: %s\nro: %s\npartial: %s" % \ - (gc_only, needed, ro, partial)) + 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 needed == False: return @@ -279,6 +766,10 @@ class KCC: 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 = [] @@ -286,16 +777,16 @@ class KCC: # writeable NC replicas that match the naming # context dn for (nc_x) # - for dc_s_dn, dc_s in self.dsa_table.items(): + 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.rep_table.keys(): + 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.rep_table[nc_x.nc_dnstr] + 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() == True: @@ -320,10 +811,9 @@ class KCC: continue # DC (s) must be in the same site as the local DC - # This is the intra-site algorithm. We are not - # replicating across multiple sites - if site_local.is_same_site(dc_s) == False: - continue + # 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 @@ -361,17 +851,17 @@ class KCC: # 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.dsa_table.items(): + 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.rep_table.keys(): + 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.rep_table[nc_x.nc_dnstr] + p_of_x = dsa.current_rep_table[nc_x.nc_dnstr] # Replica (p) of NC (x) must be partial if p_of_x.is_partial() == False: @@ -396,10 +886,9 @@ class KCC: continue # DC (s) must be in the same site as the local DC - # This is the intra-site algorithm. We are not - # replicating across multiple sites - if site_local.is_same_site(dc_s) == False: - continue + # 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 @@ -476,7 +965,7 @@ class KCC: # to ri is less than n+2, the KCC adds that edge to the graph. i = 0 while i < r_len: - dsa = self.dsa_table[graph_list[i].dsa_dnstr] + dsa = self.my_site.dsa_table[graph_list[i].dsa_dnstr] graph_list[i].add_edges_from_connections(dsa) i = i + 1 @@ -533,9 +1022,9 @@ class KCC: in the samdb """ # Retrieve my DSA - mydsa = self.dsa_table[self.my_dsa_dnstr] + mydsa = self.my_dsa - logger.debug("intrasite enter:\nmydsa: %s" % mydsa) + logger.debug("intrasite(): enter mydsa:\n%s" % mydsa) # Test whether local site has topology disabled mysite = self.site_table[self.my_site_dnstr] @@ -584,52 +1073,51 @@ class KCC: False) # don't detect stale # Commit any newly created connections to the samdb - mydsa.commit_connection_table(self.samdb) - - logger.debug("intrasite exit:\nmydsa: %s" % mydsa) + if opts.readonly is None: + mydsa.commit_connection_table(self.samdb) def run(self): """Method to perform a complete run of the KCC and produce an updated topology for subsequent NC replica syncronization between domain controllers """ - # Setup try: - self.load_my_dsa() - self.load_all_dsa() - self.load_all_partitions() + # Setup self.load_my_site() + self.load_my_dsa() - except Exception, estr: - logger.error("%s" % estr) - return + self.load_all_partitions() + self.load_all_transports() - # self.should_be_present_test() + # These are the published steps (in order) for the + # MS-TECH description of the KCC algorithm - # These are the published steps (in order) for the - # MS description of the KCC algorithm + # Step 1 + self.refresh_failed_links_connections() - # Step 1 - self.refresh_failed_links_connections() + # Step 2 + self.intrasite() - # Step 2 - self.intrasite() + # Step 3 + self.intersite() - # Step 3 - self.intersite() + # Step 4 + self.remove_unneeded_ntdsconn() - # Step 4 - self.remove_unneeded_ntds_connections() + # Step 5 + self.translate_ntdsconn() - # Step 5 - self.translate_connections() + # Step 6 + self.remove_unneeded_failed_links_connections() - # Step 6 - self.remove_unneeded_failed_links_connections() + # Step 7 + self.update_rodc_connection() - # Step 7 - self.update_rodc_connection() + except Exception, estr: + logger.error("%s" % estr) + return 1 + return 0 ################################################## # Global Functions @@ -637,6 +1125,13 @@ class KCC: def sort_replica_by_dsa_guid(rep1, rep2): return cmp(rep1.rep_dsa_guid, rep2.rep_dsa_guid) +def is_smtp_replication_availalbe(): + """Currently always returns false because Samba + doesn't implement SMTP transfer for NC changes + between DCs + """ + return False + ################################################## # samba_kcc entry point ################################################## @@ -649,8 +1144,11 @@ parser.add_option_group(sambaopts) parser.add_option_group(credopts) parser.add_option_group(options.VersionOptions(parser)) -parser.add_option("--debug", help="debug output", action="store_true") -parser.add_option("--seed", help="random number seed") +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") logger = logging.getLogger("samba_kcc") logger.addHandler(logging.StreamHandler(sys.stdout)) @@ -683,4 +1181,6 @@ except ldb.LdbError, (num, msg): # Instantiate Knowledge Consistency Checker and perform run kcc = KCC(samdb) -kcc.run() +rc = kcc.run() + +sys.exit(rc) diff --git a/source4/scripting/python/samba/kcc_utils.py b/source4/scripting/python/samba/kcc_utils.py index ac7449acd02..13bc2412d63 100644 --- a/source4/scripting/python/samba/kcc_utils.py +++ b/source4/scripting/python/samba/kcc_utils.py @@ -22,7 +22,11 @@ import uuid from samba import dsdb from samba.dcerpc import misc +from samba.dcerpc import drsblobs +from samba.dcerpc import drsuapi from samba.common import dsdb_Dn +from samba.ndr import ndr_unpack +from samba.ndr import ndr_pack class NCType: (unknown, schema, domain, config, application) = range(0, 5) @@ -36,20 +40,24 @@ class NamingContext: def __init__(self, nc_dnstr, nc_guid=None, nc_sid=None): """Instantiate a NamingContext :param nc_dnstr: NC dn string - :param nc_guid: NC guid string + :param nc_guid: NC guid :param nc_sid: NC sid """ - self.nc_dnstr = nc_dnstr - self.nc_guid = nc_guid - self.nc_sid = nc_sid - self.nc_type = NCType.unknown + self.nc_dnstr = nc_dnstr + self.nc_guid = nc_guid + self.nc_sid = nc_sid + self.nc_type = NCType.unknown return def __str__(self): '''Debug dump string output of class''' - return "%s:\n\tdn=%s\n\tguid=%s\n\ttype=%s" % \ - (self.__class__.__name__, self.nc_dnstr, - self.nc_guid, self.nc_type) + text = "%s:" % self.__class__.__name__ + text = text + "\n\tnc_dnstr=%s" % self.nc_dnstr + text = text + "\n\tnc_guid=%s" % str(self.nc_guid) + text = text + "\n\tnc_sid=%s" % self.nc_sid + text = text + "\n\tnc_type=%s" % self.nc_type + + return text def is_schema(self): '''Return True if NC is schema''' @@ -79,7 +87,7 @@ class NamingContext: self.nc_type = NCType.schema elif self.nc_dnstr == str(samdb.get_config_basedn()): self.nc_type = NCType.config - elif self.nc_sid != None: + elif self.nc_sid is not None: self.nc_type = NCType.domain else: self.nc_type = NCType.application @@ -118,7 +126,6 @@ class NamingContext: return - class NCReplica(NamingContext): """Class defines a naming context replica that is relative to a specific DSA. This is a more specific form of @@ -131,15 +138,18 @@ class NCReplica(NamingContext): """Instantiate a Naming Context Replica :param dsa_guid: GUID of DSA where replica appears :param nc_dnstr: NC dn string - :param nc_guid: NC guid string + :param nc_guid: NC guid :param nc_sid: NC sid """ - self.rep_dsa_dnstr = dsa_dnstr - self.rep_dsa_guid = dsa_guid # GUID of DSA where this appears - self.rep_default = False # replica for DSA's default domain - self.rep_partial = False - self.rep_ro = False - self.rep_flags = 0 + self.rep_dsa_dnstr = dsa_dnstr + self.rep_dsa_guid = dsa_guid + self.rep_default = False # replica for DSA's default domain + self.rep_partial = False + self.rep_ro = False + self.rep_instantiated_flags = 0 + + # RepsFromTo tuples + self.rep_repsFrom = [] # The (is present) test is a combination of being # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or @@ -154,19 +164,25 @@ class NCReplica(NamingContext): def __str__(self): '''Debug dump string output of class''' - text = "default=%s" % self.rep_default + \ - ":ro=%s" % self.rep_ro + \ - ":partial=%s" % self.rep_partial + \ - ":present=%s" % self.is_present() - return "%s\n\tdsaguid=%s\n\t%s" % \ - (NamingContext.__str__(self), self.rep_dsa_guid, text) - - def set_replica_flags(self, flags=None): - '''Set or clear NC replica flags''' + text = "%s:" % self.__class__.__name__ + text = text + "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr + text = text + "\n\tdsa_guid=%s" % str(self.rep_dsa_guid) + text = text + "\n\tdefault=%s" % self.rep_default + text = text + "\n\tro=%s" % self.rep_ro + text = text + "\n\tpartial=%s" % self.rep_partial + text = text + "\n\tpresent=%s" % self.is_present() + + for rep in self.rep_repsFrom: + text = text + "\n%s" % rep + + return "%s\n%s" % (NamingContext.__str__(self), text) + + def set_instantiated_flags(self, flags=None): + '''Set or clear NC replica instantiated flags''' if (flags == None): - self.rep_flags = 0 + self.rep_instantiated_flags = 0 else: - self.rep_flags = flags + self.rep_instantiated_flags = flags return def identify_by_dsa_attr(self, samdb, attr): @@ -239,10 +255,90 @@ class NCReplica(NamingContext): set then the NC replica is not present (false) """ if self.rep_present_criteria_one and \ - self.rep_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0: + self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0: return True return False + def load_repsFrom(self, samdb): + """Given an NC replica which has been discovered thru the nTDSDSA + database object, load the repsFrom attribute for the local replica. + held by my dsa. The repsFrom attribute is not replicated so this + attribute is relative only to the local DSA that the samdb exists on + """ + try: + res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE, + attrs=[ "repsFrom" ]) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find NC for (%s) - (%s)" % \ + (self.nc_dnstr, estr)) + return + + msg = res[0] + + # Possibly no repsFrom if this is a singleton DC + if "repsFrom" in msg: + for value in msg["repsFrom"]: + rep = RepsFromTo(self.nc_dnstr, \ + ndr_unpack(drsblobs.repsFromToBlob, value)) + self.rep_repsFrom.append(rep) + return + + def commit_repsFrom(self, samdb): + """Commit repsFrom to the database""" + + # XXX - This is not truly correct according to the MS-TECH + # docs. To commit a repsFrom we should be using RPCs + # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and + # IDL_DRSReplicaDel to affect a repsFrom change. + # + # Those RPCs are missing in samba, so I'll have to + # implement them to get this to more accurately + # reflect the reference docs. As of right now this + # commit to the database will work as its what the + # older KCC also did + modify = False + newreps = [] + + for repsFrom in self.rep_repsFrom: + + # Leave out any to be deleted from + # replacement list + if repsFrom.to_be_deleted == True: + modify = True + continue + + if repsFrom.is_modified(): + modify = True + + newreps.append(ndr_pack(repsFrom.ndr_blob)) + + # Nothing to do if no reps have been modified or + # need to be deleted. Leave database record "as is" + if modify == False: + return + + m = ldb.Message() + m.dn = ldb.Dn(samdb, self.nc_dnstr) + + m["repsFrom"] = \ + ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom") + + try: + samdb.modify(m) + + except ldb.LdbError, estr: + raise Exception("Could not set repsFrom for (%s) - (%s)" % \ + (self.dsa_dnstr, estr)) + return + + def load_fsmo_roles(self, samdb): + # XXX - to be implemented + return + + def is_fsmo_role_owner(self, dsa_dnstr): + # XXX - to be implemented + return False class DirectoryServiceAgent: @@ -255,33 +351,50 @@ class DirectoryServiceAgent: self.dsa_guid = None self.dsa_ivid = None self.dsa_is_ro = False - self.dsa_is_gc = False + self.dsa_options = 0 self.dsa_behavior = 0 self.default_dnstr = None # default domain dn string for dsa - # NCReplicas for this dsa. + # NCReplicas for this dsa that are "present" # Indexed by DN string of naming context - self.rep_table = {} + self.current_rep_table = {} - # NTDSConnections for this dsa. - # Indexed by DN string of connection + # NCReplicas for this dsa that "should be present" + # Indexed by DN string of naming context + self.needed_rep_table = {} + + # NTDSConnections for this dsa. These are current + # valid connections that are committed or "to be committed" + # in the database. Indexed by DN string of connection self.connect_table = {} + return def __str__(self): '''Debug dump string output of class''' - text = "" - if self.dsa_dnstr: - text = text + "\n\tdn=%s" % self.dsa_dnstr - if self.dsa_guid: - text = text + "\n\tguid=%s" % str(self.dsa_guid) - if self.dsa_ivid: - text = text + "\n\tivid=%s" % str(self.dsa_ivid) - - text = text + "\n\tro=%s:gc=%s" % (self.dsa_is_ro, self.dsa_is_gc) - return "%s:%s\n%s\n%s" % (self.__class__.__name__, text, - self.dumpstr_replica_table(), - self.dumpstr_connect_table()) + + text = "%s:" % self.__class__.__name__ + if self.dsa_dnstr is not None: + text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr + if self.dsa_guid is not None: + text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid) + if self.dsa_ivid is not None: + text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid) + + text = text + "\n\tro=%s" % self.is_ro() + text = text + "\n\tgc=%s" % self.is_gc() + + text = text + "\ncurrent_replica_table:" + text = text + "\n%s" % self.dumpstr_current_replica_table() + text = text + "\nneeded_replica_table:" + text = text + "\n%s" % self.dumpstr_needed_replica_table() + text = text + "\nconnect_table:" + text = text + "\n%s" % self.dumpstr_connect_table() + + return text + + def get_current_replica(self, nc_dnstr): + return self.current_rep_table[nc_dnstr] def is_ro(self): '''Returns True if dsa a read only domain controller''' @@ -289,7 +402,9 @@ class DirectoryServiceAgent: def is_gc(self): '''Returns True if dsa hosts a global catalog''' - return self.dsa_is_gc + if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0: + return True + return False def is_minimum_behavior(self, version): """Is dsa at minimum windows level greater than or @@ -301,6 +416,27 @@ class DirectoryServiceAgent: return True return False + def should_translate_ntdsconn(self): + """Returns True if DSA object allows NTDSConnection + translation in its options. False otherwise. + """ + if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0: + return False + return True + + def get_rep_tables(self): + """Return DSA current and needed replica tables + """ + return self.current_rep_table, self.needed_rep_table + + def get_parent_dnstr(self): + """Drop the leading portion of the DN string + (e.g. CN=NTDS Settings,) which will give us + the parent DN string of this object + """ + head, sep, tail = self.dsa_dnstr.partition(',') + return tail + def load_dsa(self, samdb): """Method to load a DSA from the samdb. Prior initialization has given us the DN of the DSA that we are to load. This @@ -324,7 +460,7 @@ class DirectoryServiceAgent: return msg = res[0] - self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID", + self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID", \ msg["objectGUID"][0])) # RODCs don't originate changes and thus have no invocationId, @@ -333,11 +469,8 @@ class DirectoryServiceAgent: self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID", msg["invocationId"][0])) - if "options" in msg and \ - ((int(msg["options"][0]) & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0): - self.dsa_is_gc = True - else: - self.dsa_is_gc = False + if "options" in msg: + self.options = int(msg["options"][0]) if "msDS-isRODC" in msg and msg["msDS-isRODC"][0] == "TRUE": self.dsa_is_ro = True @@ -348,7 +481,7 @@ class DirectoryServiceAgent: self.dsa_behavior = int(msg['msDS-Behavior-Version'][0]) # Load the NC replicas that are enumerated on this dsa - self.load_replica_table(samdb) + self.load_current_replica_table(samdb) # Load the nTDSConnection that are enumerated on this dsa self.load_connection_table(samdb) @@ -356,7 +489,7 @@ class DirectoryServiceAgent: return - def load_replica_table(self, samdb): + def load_current_replica_table(self, samdb): """Method to load the NC replica's listed for DSA object. This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs, hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, @@ -373,7 +506,7 @@ class DirectoryServiceAgent: # not RODC - default, config, schema, app NCs "msDS-hasMasterNCs", # domain NC partial replicas - "hasPartialReplicANCs", + "hasPartialReplicaNCs", # default domain NC "msDS-HasDomainNCs", # RODC only - default, config, schema, app NCs @@ -423,17 +556,17 @@ class DirectoryServiceAgent: raise Exception("Missing GUID for (%s) - (%s: %s)" % \ (self.dsa_dnstr, k, value)) else: - guidstr = str(misc.GUID(guid)) + guid = misc.GUID(guid) if not dnstr in tmp_table: rep = NCReplica(self.dsa_dnstr, self.dsa_guid, - dnstr, guidstr, sid) + dnstr, guid, sid) tmp_table[dnstr] = rep else: rep = tmp_table[dnstr] if k == "msDS-HasInstantiatedNCs": - rep.set_replica_flags(flags) + rep.set_instantiated_flags(flags) continue rep.identify_by_dsa_attr(samdb, k) @@ -447,7 +580,16 @@ class DirectoryServiceAgent: return # Assign our newly built NC replica table to this dsa - self.rep_table = tmp_table + self.current_rep_table = tmp_table + return + + def add_needed_replica(self, rep): + """Method to add a NC replica that "should be present" to the + needed_rep_table if not already in the table + """ + if not rep.nc_dnstr in self.needed_rep_table.keys(): + self.needed_rep_table[rep.nc_dnstr] = rep + return def load_connection_table(self, samdb): @@ -480,14 +622,14 @@ class DirectoryServiceAgent: def commit_connection_table(self, samdb): """Method to commit any uncommitted nTDSConnections - that are in our table. These would be newly identified - connections that are marked as (committed = False) + that are in our table. These would be identified + connections that are marked to be added or deleted :param samdb: database to commit DSA connection list to """ for dnstr, connect in self.connect_table.items(): connect.commit_connection(samdb) - def add_connection_by_dnstr(self, dnstr, connect): + def add_connection(self, dnstr, connect): self.connect_table[dnstr] = connect return @@ -502,14 +644,24 @@ class DirectoryServiceAgent: return connect return None - def dumpstr_replica_table(self): - '''Debug dump string output of replica table''' + def dumpstr_current_replica_table(self): + '''Debug dump string output of current replica table''' + text="" + for k in self.current_rep_table.keys(): + if text: + text = text + "\n%s" % self.current_rep_table[k] + else: + text = "%s" % self.current_rep_table[k] + return text + + def dumpstr_needed_replica_table(self): + '''Debug dump string output of needed replica table''' text="" - for k in self.rep_table.keys(): + for k in self.needed_rep_table.keys(): if text: - text = text + "\n%s" % self.rep_table[k] + text = text + "\n%s" % self.needed_rep_table[k] else: - text = "%s" % self.rep_table[k] + text = "%s" % self.needed_rep_table[k] return text def dumpstr_connect_table(self): @@ -526,23 +678,50 @@ class NTDSConnection(): """Class defines a nTDSConnection found under a DSA """ def __init__(self, dnstr): - self.dnstr = dnstr - self.enabled = False - self.committed = False # appears in database - self.options = 0 - self.flags = 0 - self.from_dnstr = None - self.schedulestr = None + self.dnstr = dnstr + self.enabled = False + self.committed = False # new connection needs to be committed + self.options = 0 + self.flags = 0 + self.transport_dnstr = None + self.transport_guid = None + self.from_dnstr = None + self.from_guid = None + self.schedule = None return def __str__(self): '''Debug dump string output of NTDSConnection object''' - text = "%s: %s" % (self.__class__.__name__, self.dnstr) - text = text + "\n\tenabled: %s" % self.enabled - text = text + "\n\tcommitted: %s" % self.committed - text = text + "\n\toptions: 0x%08X" % self.options - text = text + "\n\tflags: 0x%08X" % self.flags - text = text + "\n\tfrom_dn: %s" % self.from_dnstr + + text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr) + text = text + "\n\tenabled=%s" % self.enabled + text = text + "\n\tcommitted=%s" % self.committed + text = text + "\n\toptions=0x%08X" % self.options + text = text + "\n\tflags=0x%08X" % self.flags + text = text + "\n\ttransport_dn=%s" % self.transport_dnstr + + if self.transport_guid is not None: + text = text + "\n\ttransport_guid=%s" % str(self.transport_guid) + + text = text + "\n\tfrom_dn=%s" % self.from_dnstr + text = text + "\n\tfrom_guid=%s" % str(self.from_guid) + + if self.schedule is not None: + text = text + "\n\tschedule.size=%s" % self.schedule.size + text = text + "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth + text = text + "\n\tschedule.numberOfSchedules=%s" % \ + self.schedule.numberOfSchedules + + for i, header in enumerate(self.schedule.headerArray): + text = text + "\n\tschedule.headerArray[%d].type=%d" % \ + (i, header.type) + text = text + "\n\tschedule.headerArray[%d].offset=%d" % \ + (i, header.offset) + text = text + "\n\tschedule.dataArray[%d].slots[ " % i + for slot in self.schedule.dataArray[i].slots: + text = text + "0x%X " % slot + text = text + "]" + return text def load_connection(self, samdb): @@ -551,14 +730,16 @@ class NTDSConnection(): from the samdb. Raises an Exception on error. """ + controls = ["extended_dn:1:1"] attrs = [ "options", "enabledConnection", "schedule", + "transportType", "fromServer", "systemFlags" ] try: res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE, - attrs=attrs) + attrs=attrs, controls=controls) except ldb.LdbError, (enum, estr): raise Exception("Unable to find nTDSConnection for (%s) - (%s)" % \ @@ -574,14 +755,30 @@ class NTDSConnection(): self.enabled = True if "systemFlags" in msg: self.flags = int(msg["systemFlags"][0]) + if "transportType" in msg: + dsdn = dsdb_Dn(samdb, msg["tranportType"][0]) + guid = dsdn.dn.get_extended_component('GUID') + + assert guid is not None + self.transport_guid = misc.GUID(guid) + + self.transport_dnstr = str(dsdn.dn) + assert self.transport_dnstr is not None + if "schedule" in msg: - self.schedulestr = msg["schedule"][0] + self.schedule = ndr_unpack(drsblobs.replSchedule, msg["schedule"][0]) + if "fromServer" in msg: dsdn = dsdb_Dn(samdb, msg["fromServer"][0]) + guid = dsdn.dn.get_extended_component('GUID') + + assert guid is not None + self.from_guid = misc.GUID(guid) + self.from_dnstr = str(dsdn.dn) - assert self.from_dnstr != None + assert self.from_dnstr is not None - # Appears as committed in the database + # Was loaded from database so connection is currently committed self.committed = True return @@ -589,12 +786,109 @@ class NTDSConnection(): """Given a NTDSConnection object that is not committed in the sam database, perform a commit action. """ - if self.committed: # nothing to do + # nothing to do + if self.committed == True: return - # XXX - not yet written + # First verify we don't have this entry to ensure nothing + # is programatically amiss + try: + msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE) + found = True + + except ldb.LdbError, (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + found = False + else: + raise Exception("Unable to search for (%s) - (%s)" % \ + (self.dnstr, estr)) + if found: + raise Exception("nTDSConnection for (%s) already exists!" % self.dnstr) + + if self.enabled: + enablestr = "TRUE" + else: + enablestr = "FALSE" + + # Prepare a message for adding to the samdb + m = ldb.Message() + m.dn = ldb.Dn(samdb, self.dnstr) + + m["objectClass"] = \ + ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD, \ + "objectClass") + m["showInAdvancedViewOnly"] = \ + ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD, \ + "showInAdvancedViewOnly") + m["enabledConnection"] = \ + ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD, "enabledConnection") + m["fromServer"] = \ + ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer") + m["options"] = \ + ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options") + m["systemFlags"] = \ + ldb.MessageElement(str(self.flags), ldb.FLAG_MOD_ADD, "systemFlags") + + if self.schedule is not None: + m["schedule"] = \ + ldb.MessageElement(ndr_pack(self.schedule), + ldb.FLAG_MOD_ADD, "schedule") + try: + samdb.add(m) + except ldb.LdbError, (enum, estr): + raise Exception("Could not add nTDSConnection for (%s) - (%s)" % \ + (self.dnstr, estr)) + self.committed = True return + def is_schedule_minimum_once_per_week(self): + """Returns True if our schedule includes at least one + replication interval within the week. False otherwise + """ + if self.schedule is None or self.schedule.dataArray[0] is None: + return False + + for slot in self.schedule.dataArray[0].slots: + if (slot & 0x0F) != 0x0: + return True + return False + + def convert_schedule_to_repltimes(self): + """Convert NTDS Connection schedule to replTime schedule. + NTDS Connection schedule slots are double the size of + the replTime slots but the top portion of the NTDS + Connection schedule slot (4 most significant bits in + uchar) are unused. The 4 least significant bits have + the same (15 minute interval) bit positions as replTimes. + We thus pack two elements of the NTDS Connection schedule + slots into one element of the replTimes slot + If no schedule appears in NTDS Connection then a default + of 0x11 is set in each replTimes slot as per behaviour + noted in a Windows DC. That default would cause replication + within the last 15 minutes of each hour. + """ + times = [0x11] * 84 + + for i, slot in enumerate(times): + if self.schedule is not None and \ + self.schedule.dataArray[0] is not None: + slot = (self.schedule.dataArray[0].slots[i*2] & 0xF) << 4 | \ + (self.schedule.dataArray[0].slots[i*2] & 0xF) + return times + + def is_rodc_topology(self): + """Returns True if NTDS Connection specifies RODC + topology only + """ + if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0: + return False + return True + + def is_enabled(self): + """Returns True if NTDS Connection is enabled + """ + return self.enabled + def get_from_dnstr(self): '''Return fromServer dn string attribute''' return self.from_dnstr @@ -659,11 +953,11 @@ class Partition(NamingContext): raise Exception("Missing GUID for (%s) - (%s: %s)" % \ (self.partstr, k, value)) else: - guidstr = str(misc.GUID(guid)) + guid = misc.GUID(guid) if k == "nCName": self.nc_dnstr = str(dsdn.dn) - self.nc_guid = guidstr + self.nc_guid = guid self.nc_sid = sid continue @@ -744,6 +1038,7 @@ class Site: def __init__(self, site_dnstr): self.site_dnstr = site_dnstr self.site_options = 0 + self.dsa_table = {} return def load_site(self, samdb): @@ -762,13 +1057,53 @@ class Site: msg = res[0] if "options" in msg: self.site_options = int(msg["options"][0]) + + self.load_all_dsa(samdb) return - def is_same_site(self, target_dsa): - '''Determine if target dsa is in this site''' - if self.site_dnstr in target_dsa.dsa_dnstr: - return True - return False + def load_all_dsa(self, samdb): + """Discover all nTDSDSA thru the sites entry and + instantiate and load the DSAs. Each dsa is inserted + into the dsa_table by dn string. + Raises an Exception on error. + """ + try: + res = samdb.search(self.site_dnstr, + scope=ldb.SCOPE_SUBTREE, + expression="(objectClass=nTDSDSA)") + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find nTDSDSAs - (%s)" % estr) + + for msg in res: + dnstr = str(msg.dn) + + # already loaded + if dnstr in self.dsa_table.keys(): + continue + + dsa = DirectoryServiceAgent(dnstr) + + dsa.load_dsa(samdb) + + # Assign this dsa to my dsa table + # and index by dsa dn + self.dsa_table[dnstr] = dsa + return + + def get_dsa_by_guidstr(self, guidstr): + for dsa in self.dsa_table.values(): + if str(dsa.dsa_guid) == guidstr: + return dsa + return None + + def get_dsa(self, dnstr): + """Return a previously loaded DSA object by consulting + the sites dsa_table for the provided DSA dn string + Returns None if DSA doesn't exist + """ + if dnstr in self.dsa_table.keys(): + return self.dsa_table[dnstr] + return None def is_intrasite_topology_disabled(self): '''Returns True if intrasite topology is disabled for site''' @@ -784,6 +1119,15 @@ class Site: return True return False + def __str__(self): + '''Debug dump string output of class''' + text = "%s:" % self.__class__.__name__ + text = text + "\n\tdn=%s" % self.site_dnstr + text = text + "\n\toptions=0x%X" % self.site_options + for key, dsa in self.dsa_table.items(): + text = text + "\n%s" % dsa + return text + class GraphNode: """This is a graph node describing a set of edges that should be @@ -801,16 +1145,19 @@ class GraphNode: self.edge_from = [] def __str__(self): - text = "%s: %s" % (self.__class__.__name__, self.dsa_dnstr) - for edge in self.edge_from: - text = text + "\n\tedge from: %s" % edge + text = "%s:" % self.__class__.__name__ + text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr + text = text + "\n\tmax_edges=%d" % self.max_edges + + for i, edge in enumerate(self.edge_from): + text = text + "\n\tedge_from[%d]=%s" % (i, edge) return text def add_edge_from(self, from_dsa_dnstr): """Add an edge from the dsa to our graph nodes edge from list :param from_dsa_dnstr: the dsa that the edge emanates from """ - assert from_dsa_dnstr != None + assert from_dsa_dnstr is not None # No edges from myself to myself if from_dsa_dnstr == self.dsa_dnstr: @@ -855,8 +1202,7 @@ class GraphNode: # the DC on which ri "is present". # # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY - if connect and \ - connect.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0: + if connect and connect.is_rodc_topology() == False: exists = True else: exists = False @@ -870,16 +1216,38 @@ class GraphNode: dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr connect = NTDSConnection(dnstr) - connect.enabled = True - connect.committed = False - connect.from_dnstr = edge_dnstr - connect.options = dsdb.NTDSCONN_OPT_IS_GENERATED - connect.flags = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \ - dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE + connect.committed = False + connect.enabled = True + connect.from_dnstr = edge_dnstr + connect.options = dsdb.NTDSCONN_OPT_IS_GENERATED + connect.flags = dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME + \ + dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE + + # Create schedule. Attribute valuse set according to MS-TECH + # intrasite connection creation document + connect.schedule = drsblobs.schedule() + + connect.schedule.size = 188 + connect.schedule.bandwidth = 0 + connect.schedule.numberOfSchedules = 1 - # XXX I need to write the schedule blob + header = drsblobs.scheduleHeader() + header.type = 0 + header.offset = 20 - dsa.add_connection_by_dnstr(dnstr, connect); + connect.schedule.headerArray = [ header ] + + # 168 byte instances of the 0x01 value. The low order 4 bits + # of the byte equate to 15 minute intervals within a single hour. + # There are 168 bytes because there are 168 hours in a full week + # Effectively we are saying to perform replication at the end of + # each hour of the week + data = drsblobs.scheduleSlots() + data.slots = [ 0x01 ] * 168 + + connect.schedule.dataArray = [ data ] + + dsa.add_connection(dnstr, connect); return @@ -888,3 +1256,209 @@ class GraphNode: if len(self.edge_from) >= self.max_edges: return True return False + +class Transport(): + """Class defines a Inter-site transport found under Sites + """ + def __init__(self, dnstr): + self.dnstr = dnstr + self.options = 0 + self.guid = None + self.address_attr = None + return + + def __str__(self): + '''Debug dump string output of Transport object''' + + text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr) + text = text + "\n\tguid=%s" % str(self.guid) + text = text + "\n\toptions=%d" % self.options + text = text + "\n\taddress_attr=%s" % self.address_attr + + return text + + def load_transport(self, samdb): + """Given a Transport object with an prior initialization + for the object's DN, search for the DN and load attributes + from the samdb. + Raises an Exception on error. + """ + attrs = [ "objectGUID", + "options", + "transportAddressAttribute" ] + try: + res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE, + attrs=attrs) + + except ldb.LdbError, (enum, estr): + raise Exception("Unable to find Transport for (%s) - (%s)" % \ + (self.dnstr, estr)) + return + + msg = res[0] + self.guid = misc.GUID(samdb.schema_format_value("objectGUID", + msg["objectGUID"][0])) + + if "options" in msg: + self.options = int(msg["options"][0]) + if "transportAddressAttribute" in msg: + self.address_attr = str(msg["transportAddressAttribute"][0]) + + return + +class RepsFromTo: + """Class encapsulation of the NDR repsFromToBlob. + Removes the necessity of external code having to + understand about other_info or manipulation of + update flags. + """ + def __init__(self, nc_dnstr=None, ndr_blob=None): + + self.__dict__['to_be_deleted'] = False + self.__dict__['nc_dnstr'] = nc_dnstr + self.__dict__['update_flags'] = 0x0 + + # WARNING: + # + # There is a very subtle bug here with python + # and our NDR code. If you assign directly to + # a NDR produced struct (e.g. t_repsFrom.ctr.other_info) + # then a proper python GC reference count is not + # maintained. + # + # To work around this we maintain an internal + # reference to "dns_name(x)" and "other_info" elements + # of repsFromToBlob. This internal reference + # is hidden within this class but it is why you + # see statements like this below: + # + # self.__dict__['ndr_blob'].ctr.other_info = \ + # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo() + # + # That would appear to be a redundant assignment but + # it is necessary to hold a proper python GC reference + # count. + if ndr_blob is None: + self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob() + self.__dict__['ndr_blob'].version = 0x1 + self.__dict__['dns_name1'] = None + self.__dict__['dns_name2'] = None + + self.__dict__['ndr_blob'].ctr.other_info = \ + self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo() + + else: + self.__dict__['ndr_blob'] = ndr_blob + self.__dict__['other_info'] = ndr_blob.ctr.other_info + + if ndr_blob.version == 0x1: + self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name + self.__dict__['dns_name2'] = None + else: + self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1 + self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2 + return + + def __str__(self): + '''Debug dump string output of class''' + + text = "%s:" % self.__class__.__name__ + text = text + "\n\tdnstr=%s" % self.nc_dnstr + text = text + "\n\tupdate_flags=0x%X" % self.update_flags + + text = text + "\n\tversion=%d" % self.version + text = text + "\n\tsource_dsa_obj_guid=%s" % \ + str(self.source_dsa_obj_guid) + text = text + "\n\tsource_dsa_invocation_id=%s" % \ + str(self.source_dsa_invocation_id) + text = text + "\n\ttransport_guid=%s" % \ + str(self.transport_guid) + text = text + "\n\treplica_flags=0x%X" % \ + self.replica_flags + text = text + "\n\tconsecutive_sync_failures=%d" % \ + self.consecutive_sync_failures + text = text + "\n\tlast_success=%s" % \ + self.last_success + text = text + "\n\tlast_attempt=%s" % \ + self.last_attempt + text = text + "\n\tdns_name1=%s" % \ + str(self.dns_name1) + text = text + "\n\tdns_name2=%s" % \ + str(self.dns_name2) + text = text + "\n\tschedule[ " + for slot in self.schedule: + text = text + "0x%X " % slot + text = text + "]" + + return text + + def __setattr__(self, item, value): + + if item in [ 'schedule', 'replica_flags', 'transport_guid', \ + 'source_dsa_obj_guid', 'source_dsa_invocation_id', \ + 'consecutive_sync_failures', 'last_success', \ + 'last_attempt' ]: + setattr(self.__dict__['ndr_blob'].ctr, item, value) + + elif item in ['dns_name1']: + self.__dict__['dns_name1'] = value + + if self.__dict__['ndr_blob'].version == 0x1: + self.__dict__['ndr_blob'].ctr.other_info.dns_name = \ + self.__dict__['dns_name1'] + else: + self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \ + self.__dict__['dns_name1'] + + elif item in ['dns_name2']: + self.__dict__['dns_name2'] = value + + if self.__dict__['ndr_blob'].version == 0x1: + raise AttributeError(item) + else: + self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \ + self.__dict__['dns_name2'] + + elif item in ['version']: + raise AttributeError, "Attempt to set readonly attribute %s" % item + else: + raise AttributeError, "Unknown attribute %s" % item + + if item in ['replica_flags']: + self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS + elif item in ['schedule']: + self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE + else: + self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS + + return + + def __getattr__(self, item): + """Overload of RepsFromTo attribute retrieval. Allows + external code to ignore substructures within the blob + """ + if item in [ 'schedule', 'replica_flags', 'transport_guid', \ + 'source_dsa_obj_guid', 'source_dsa_invocation_id', \ + 'consecutive_sync_failures', 'last_success', \ + 'last_attempt' ]: + return getattr(self.__dict__['ndr_blob'].ctr, item) + + elif item in ['version']: + return self.__dict__['ndr_blob'].version + + elif item in ['dns_name1']: + if self.__dict__['ndr_blob'].version == 0x1: + return self.__dict__['ndr_blob'].ctr.other_info.dns_name + else: + return self.__dict__['ndr_blob'].ctr.other_info.dns_name1 + + elif item in ['dns_name2']: + if self.__dict__['ndr_blob'].version == 0x1: + raise AttributeError(item) + else: + return self.__dict__['ndr_blob'].ctr.other_info.dns_name2 + + raise AttributeError, "Unknwown attribute %s" % item + + def is_modified(self): + return (self.update_flags != 0x0) -- 2.34.1