smbd: Simplify an if-condition
[Samba.git] / python / samba / kcc / kcc_utils.py
blob326889d8488e921726b67a2c7561e2e8ed37f96e
1 # KCC topology utilities
3 # Copyright (C) Dave Craft 2011
4 # Copyright (C) Jelmer Vernooij 2011
5 # Copyright (C) Andrew Bartlett 2015
7 # Andrew Bartlett's alleged work performed by his underlings Douglas
8 # Bagnall and Garming Sam.
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 import sys
23 import ldb
24 import uuid
26 from samba import dsdb
27 from samba.dcerpc import (
28 drsblobs,
29 drsuapi,
30 misc,
32 from samba.samdb import dsdb_Dn
33 from samba.ndr import ndr_unpack, ndr_pack
34 from collections import Counter
37 class KCCError(Exception):
38 pass
41 class NCType(object):
42 (unknown, schema, domain, config, application) = range(0, 5)
45 # map the NCType enum to strings for debugging
46 nctype_lut = dict((v, k) for k, v in NCType.__dict__.items() if k[:2] != '__')
49 class NamingContext(object):
50 """Base class for a naming context.
52 Holds the DN, GUID, SID (if available) and type of the DN.
53 Subclasses may inherit from this and specialize
54 """
56 def __init__(self, nc_dnstr):
57 """Instantiate a NamingContext
59 :param nc_dnstr: NC dn string
60 """
61 self.nc_dnstr = nc_dnstr
62 self.nc_guid = None
63 self.nc_sid = None
64 self.nc_type = NCType.unknown
66 def __str__(self):
67 """Debug dump string output of class"""
68 text = "%s:" % (self.__class__.__name__,) +\
69 "\n\tnc_dnstr=%s" % self.nc_dnstr +\
70 "\n\tnc_guid=%s" % str(self.nc_guid)
72 if self.nc_sid is None:
73 text = text + "\n\tnc_sid=<absent>"
74 else:
75 text = text + "\n\tnc_sid=<present>"
77 text = text + "\n\tnc_type=%s (%s)" % (nctype_lut[self.nc_type],
78 self.nc_type)
79 return text
81 def load_nc(self, samdb):
82 attrs = ["objectGUID",
83 "objectSid"]
84 try:
85 res = samdb.search(base=self.nc_dnstr,
86 scope=ldb.SCOPE_BASE, attrs=attrs)
88 except ldb.LdbError as e:
89 (enum, estr) = e.args
90 raise KCCError("Unable to find naming context (%s) - (%s)" %
91 (self.nc_dnstr, estr))
92 msg = res[0]
93 if "objectGUID" in msg:
94 self.nc_guid = misc.GUID(samdb.schema_format_value("objectGUID",
95 msg["objectGUID"][0]))
96 if "objectSid" in msg:
97 self.nc_sid = msg["objectSid"][0]
99 assert self.nc_guid is not None
101 def is_config(self):
102 """Return True if NC is config"""
103 assert self.nc_type != NCType.unknown
104 return self.nc_type == NCType.config
106 def identify_by_basedn(self, samdb):
107 """Given an NC object, identify what type it is thru
108 the samdb basedn strings and NC sid value
110 # Invoke loader to initialize guid and more
111 # importantly sid value (sid is used to identify
112 # domain NCs)
113 if self.nc_guid is None:
114 self.load_nc(samdb)
116 # We check against schema and config because they
117 # will be the same for all nTDSDSAs in the forest.
118 # That leaves the domain NCs which can be identified
119 # by sid and application NCs as the last identified
120 if self.nc_dnstr == str(samdb.get_schema_basedn()):
121 self.nc_type = NCType.schema
122 elif self.nc_dnstr == str(samdb.get_config_basedn()):
123 self.nc_type = NCType.config
124 elif self.nc_sid is not None:
125 self.nc_type = NCType.domain
126 else:
127 self.nc_type = NCType.application
129 def identify_by_dsa_attr(self, samdb, attr):
130 """Given an NC which has been discovered thru the
131 nTDSDSA database object, determine what type of NC
132 it is (i.e. schema, config, domain, application) via
133 the use of the schema attribute under which the NC
134 was found.
136 :param attr: attr of nTDSDSA object where NC DN appears
138 # If the NC is listed under msDS-HasDomainNCs then
139 # this can only be a domain NC and it is our default
140 # domain for this dsa
141 if attr == "msDS-HasDomainNCs":
142 self.nc_type = NCType.domain
144 # If the NC is listed under hasPartialReplicaNCs
145 # this is only a domain NC
146 elif attr == "hasPartialReplicaNCs":
147 self.nc_type = NCType.domain
149 # NCs listed under hasMasterNCs are either
150 # default domain, schema, or config. We
151 # utilize the identify_by_basedn() to
152 # identify those
153 elif attr == "hasMasterNCs":
154 self.identify_by_basedn(samdb)
156 # Still unknown (unlikely) but for completeness
157 # and for finally identifying application NCs
158 if self.nc_type == NCType.unknown:
159 self.identify_by_basedn(samdb)
162 class NCReplica(NamingContext):
163 """Naming context replica that is relative to a specific DSA.
165 This is a more specific form of NamingContext class (inheriting from that
166 class) and it identifies unique attributes of the DSA's replica for a NC.
169 def __init__(self, dsa, nc_dnstr):
170 """Instantiate a Naming Context Replica
172 :param dsa_guid: GUID of DSA where replica appears
173 :param nc_dnstr: NC dn string
175 self.rep_dsa_dnstr = dsa.dsa_dnstr
176 self.rep_dsa_guid = dsa.dsa_guid
177 self.rep_default = False # replica for DSA's default domain
178 self.rep_partial = False
179 self.rep_ro = False
180 self.rep_instantiated_flags = 0
182 self.rep_fsmo_role_owner = None
184 # RepsFromTo tuples
185 self.rep_repsFrom = []
187 # RepsFromTo tuples
188 self.rep_repsTo = []
190 # The (is present) test is a combination of being
191 # enumerated in (hasMasterNCs or msDS-hasFullReplicaNCs or
192 # hasPartialReplicaNCs) as well as its replica flags found
193 # thru the msDS-HasInstantiatedNCs. If the NC replica meets
194 # the first enumeration test then this flag is set true
195 self.rep_present_criteria_one = False
197 # Call my super class we inherited from
198 NamingContext.__init__(self, nc_dnstr)
200 def __str__(self):
201 """Debug dump string output of class"""
202 text = "%s:" % self.__class__.__name__ +\
203 "\n\tdsa_dnstr=%s" % self.rep_dsa_dnstr +\
204 "\n\tdsa_guid=%s" % self.rep_dsa_guid +\
205 "\n\tdefault=%s" % self.rep_default +\
206 "\n\tro=%s" % self.rep_ro +\
207 "\n\tpartial=%s" % self.rep_partial +\
208 "\n\tpresent=%s" % self.is_present() +\
209 "\n\tfsmo_role_owner=%s" % self.rep_fsmo_role_owner +\
210 "".join("\n%s" % rep for rep in self.rep_repsFrom) +\
211 "".join("\n%s" % rep for rep in self.rep_repsTo)
213 return "%s\n%s" % (NamingContext.__str__(self), text)
215 def set_instantiated_flags(self, flags=0):
216 """Set or clear NC replica instantiated flags"""
217 self.rep_instantiated_flags = flags
219 def identify_by_dsa_attr(self, samdb, attr):
220 """Given an NC which has been discovered thru the
221 nTDSDSA database object, determine what type of NC
222 replica it is (i.e. partial, read only, default)
224 :param attr: attr of nTDSDSA object where NC DN appears
226 # If the NC was found under hasPartialReplicaNCs
227 # then a partial replica at this dsa
228 if attr == "hasPartialReplicaNCs":
229 self.rep_partial = True
230 self.rep_present_criteria_one = True
232 # If the NC is listed under msDS-HasDomainNCs then
233 # this can only be a domain NC and it is the DSA's
234 # default domain NC
235 elif attr == "msDS-HasDomainNCs":
236 self.rep_default = True
238 # NCs listed under hasMasterNCs are either
239 # default domain, schema, or config. We check
240 # against schema and config because they will be
241 # the same for all nTDSDSAs in the forest. That
242 # leaves the default domain NC remaining which
243 # may be different for each nTDSDSAs (and thus
244 # we don't compare against this samdb's default
245 # basedn
246 elif attr == "hasMasterNCs":
247 self.rep_present_criteria_one = True
249 if self.nc_dnstr != str(samdb.get_schema_basedn()) and \
250 self.nc_dnstr != str(samdb.get_config_basedn()):
251 self.rep_default = True
253 # RODC only
254 elif attr == "msDS-hasFullReplicaNCs":
255 self.rep_present_criteria_one = True
256 self.rep_ro = True
258 # Not RODC
259 elif attr == "msDS-hasMasterNCs":
260 self.rep_present_criteria_one = True
261 self.rep_ro = False
263 # Now use this DSA attribute to identify the naming
264 # context type by calling the super class method
265 # of the same name
266 NamingContext.identify_by_dsa_attr(self, samdb, attr)
268 def is_default(self):
269 """Whether this is a default domain for the dsa that this NC appears on
271 return self.rep_default
273 def is_ro(self):
274 """Return True if NC replica is read only"""
275 return self.rep_ro
277 def is_partial(self):
278 """Return True if NC replica is partial"""
279 return self.rep_partial
281 def is_present(self):
282 """Given an NC replica which has been discovered thru the
283 nTDSDSA database object and populated with replica flags
284 from the msDS-HasInstantiatedNCs; return whether the NC
285 replica is present (true) or if the IT_NC_GOING flag is
286 set then the NC replica is not present (false)
288 if self.rep_present_criteria_one and \
289 self.rep_instantiated_flags & dsdb.INSTANCE_TYPE_NC_GOING == 0:
290 return True
291 return False
293 def load_repsFrom(self, samdb):
294 """Given an NC replica which has been discovered thru the nTDSDSA
295 database object, load the repsFrom attribute for the local replica.
296 held by my dsa. The repsFrom attribute is not replicated so this
297 attribute is relative only to the local DSA that the samdb exists on
299 try:
300 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
301 attrs=["repsFrom"])
303 except ldb.LdbError as e1:
304 (enum, estr) = e1.args
305 raise KCCError("Unable to find NC for (%s) - (%s)" %
306 (self.nc_dnstr, estr))
308 msg = res[0]
310 # Possibly no repsFrom if this is a singleton DC
311 if "repsFrom" in msg:
312 for value in msg["repsFrom"]:
313 try:
314 unpacked = ndr_unpack(drsblobs.repsFromToBlob, value)
315 except RuntimeError as e:
316 print("bad repsFrom NDR: %r" % (value),
317 file=sys.stderr)
318 continue
319 rep = RepsFromTo(self.nc_dnstr, unpacked)
320 self.rep_repsFrom.append(rep)
322 def commit_repsFrom(self, samdb, ro=False):
323 """Commit repsFrom to the database"""
325 # XXX - This is not truly correct according to the MS-TECH
326 # docs. To commit a repsFrom we should be using RPCs
327 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
328 # IDL_DRSReplicaDel to affect a repsFrom change.
330 # Those RPCs are missing in samba, so I'll have to
331 # implement them to get this to more accurately
332 # reflect the reference docs. As of right now this
333 # commit to the database will work as its what the
334 # older KCC also did
335 modify = False
336 newreps = []
337 delreps = []
339 for repsFrom in self.rep_repsFrom:
341 # Leave out any to be deleted from
342 # replacement list. Build a list
343 # of to be deleted reps which we will
344 # remove from rep_repsFrom list below
345 if repsFrom.to_be_deleted:
346 delreps.append(repsFrom)
347 modify = True
348 continue
350 if repsFrom.is_modified():
351 repsFrom.set_unmodified()
352 modify = True
354 # current (unmodified) elements also get
355 # appended here but no changes will occur
356 # unless something is "to be modified" or
357 # "to be deleted"
358 newreps.append(ndr_pack(repsFrom.ndr_blob))
360 # Now delete these from our list of rep_repsFrom
361 for repsFrom in delreps:
362 self.rep_repsFrom.remove(repsFrom)
363 delreps = []
365 # Nothing to do if no reps have been modified or
366 # need to be deleted or input option has informed
367 # us to be "readonly" (ro). Leave database
368 # record "as is"
369 if not modify or ro:
370 return
372 m = ldb.Message()
373 m.dn = ldb.Dn(samdb, self.nc_dnstr)
375 m["repsFrom"] = \
376 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsFrom")
378 try:
379 samdb.modify(m)
381 except ldb.LdbError as estr:
382 raise KCCError("Could not set repsFrom for (%s) - (%s)" %
383 (self.nc_dnstr, estr))
385 def load_replUpToDateVector(self, samdb):
386 """Given an NC replica which has been discovered thru the nTDSDSA
387 database object, load the replUpToDateVector attribute for the
388 local replica. held by my dsa. The replUpToDateVector
389 attribute is not replicated so this attribute is relative only
390 to the local DSA that the samdb exists on
393 try:
394 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
395 attrs=["replUpToDateVector"])
397 except ldb.LdbError as e2:
398 (enum, estr) = e2.args
399 raise KCCError("Unable to find NC for (%s) - (%s)" %
400 (self.nc_dnstr, estr))
402 msg = res[0]
404 # Possibly no replUpToDateVector if this is a singleton DC
405 if "replUpToDateVector" in msg:
406 value = msg["replUpToDateVector"][0]
407 blob = ndr_unpack(drsblobs.replUpToDateVectorBlob,
408 value)
409 if blob.version != 2:
410 # Samba only generates version 2, and this runs locally
411 raise AttributeError("Unexpected replUpToDateVector version %d"
412 % blob.version)
414 self.rep_replUpToDateVector_cursors = blob.ctr.cursors
415 else:
416 self.rep_replUpToDateVector_cursors = []
418 def dumpstr_to_be_deleted(self):
419 return '\n'.join(str(x) for x in self.rep_repsFrom if x.to_be_deleted)
421 def dumpstr_to_be_modified(self):
422 return '\n'.join(str(x) for x in self.rep_repsFrom if x.is_modified())
424 def load_fsmo_roles(self, samdb):
425 """Given an NC replica which has been discovered thru the nTDSDSA
426 database object, load the fSMORoleOwner attribute.
428 try:
429 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
430 attrs=["fSMORoleOwner"])
432 except ldb.LdbError as e3:
433 (enum, estr) = e3.args
434 raise KCCError("Unable to find NC for (%s) - (%s)" %
435 (self.nc_dnstr, estr))
437 msg = res[0]
439 # Possibly no fSMORoleOwner
440 if "fSMORoleOwner" in msg:
441 self.rep_fsmo_role_owner = msg["fSMORoleOwner"]
443 def is_fsmo_role_owner(self, dsa_dnstr):
444 if self.rep_fsmo_role_owner is not None and \
445 self.rep_fsmo_role_owner == dsa_dnstr:
446 return True
447 return False
449 def load_repsTo(self, samdb):
450 """Given an NC replica which has been discovered thru the nTDSDSA
451 database object, load the repsTo attribute for the local replica.
452 held by my dsa. The repsTo attribute is not replicated so this
453 attribute is relative only to the local DSA that the samdb exists on
455 This is responsible for push replication, not scheduled pull
456 replication. Not to be confused for repsFrom.
458 try:
459 res = samdb.search(base=self.nc_dnstr, scope=ldb.SCOPE_BASE,
460 attrs=["repsTo"])
462 except ldb.LdbError as e4:
463 (enum, estr) = e4.args
464 raise KCCError("Unable to find NC for (%s) - (%s)" %
465 (self.nc_dnstr, estr))
467 msg = res[0]
469 # Possibly no repsTo if this is a singleton DC
470 if "repsTo" in msg:
471 for value in msg["repsTo"]:
472 try:
473 unpacked = ndr_unpack(drsblobs.repsFromToBlob, value)
474 except RuntimeError as e:
475 print("bad repsTo NDR: %r" % (value),
476 file=sys.stderr)
477 continue
478 rep = RepsFromTo(self.nc_dnstr, unpacked)
479 self.rep_repsTo.append(rep)
481 def commit_repsTo(self, samdb, ro=False):
482 """Commit repsTo to the database"""
484 # XXX - This is not truly correct according to the MS-TECH
485 # docs. To commit a repsTo we should be using RPCs
486 # IDL_DRSReplicaAdd, IDL_DRSReplicaModify, and
487 # IDL_DRSReplicaDel to affect a repsTo change.
489 # Those RPCs are missing in samba, so I'll have to
490 # implement them to get this to more accurately
491 # reflect the reference docs. As of right now this
492 # commit to the database will work as its what the
493 # older KCC also did
494 modify = False
495 newreps = []
496 delreps = []
498 for repsTo in self.rep_repsTo:
500 # Leave out any to be deleted from
501 # replacement list. Build a list
502 # of to be deleted reps which we will
503 # remove from rep_repsTo list below
504 if repsTo.to_be_deleted:
505 delreps.append(repsTo)
506 modify = True
507 continue
509 if repsTo.is_modified():
510 repsTo.set_unmodified()
511 modify = True
513 # current (unmodified) elements also get
514 # appended here but no changes will occur
515 # unless something is "to be modified" or
516 # "to be deleted"
517 newreps.append(ndr_pack(repsTo.ndr_blob))
519 # Now delete these from our list of rep_repsTo
520 for repsTo in delreps:
521 self.rep_repsTo.remove(repsTo)
522 delreps = []
524 # Nothing to do if no reps have been modified or
525 # need to be deleted or input option has informed
526 # us to be "readonly" (ro). Leave database
527 # record "as is"
528 if not modify or ro:
529 return
531 m = ldb.Message()
532 m.dn = ldb.Dn(samdb, self.nc_dnstr)
534 m["repsTo"] = \
535 ldb.MessageElement(newreps, ldb.FLAG_MOD_REPLACE, "repsTo")
537 try:
538 samdb.modify(m)
540 except ldb.LdbError as estr:
541 raise KCCError("Could not set repsTo for (%s) - (%s)" %
542 (self.nc_dnstr, estr))
545 class DirectoryServiceAgent(object):
547 def __init__(self, dsa_dnstr):
548 """Initialize DSA class.
550 Class is subsequently fully populated by calling the load_dsa() method
552 :param dsa_dnstr: DN of the nTDSDSA
554 self.dsa_dnstr = dsa_dnstr
555 self.dsa_guid = None
556 self.dsa_ivid = None
557 self.dsa_is_ro = False
558 self.dsa_is_istg = False
559 self.options = 0
560 self.dsa_behavior = 0
561 self.default_dnstr = None # default domain dn string for dsa
563 # NCReplicas for this dsa that are "present"
564 # Indexed by DN string of naming context
565 self.current_rep_table = {}
567 # NCReplicas for this dsa that "should be present"
568 # Indexed by DN string of naming context
569 self.needed_rep_table = {}
571 # NTDSConnections for this dsa. These are current
572 # valid connections that are committed or pending a commit
573 # in the database. Indexed by DN string of connection
574 self.connect_table = {}
576 def __str__(self):
577 """Debug dump string output of class"""
579 text = "%s:" % self.__class__.__name__
580 if self.dsa_dnstr is not None:
581 text = text + "\n\tdsa_dnstr=%s" % self.dsa_dnstr
582 if self.dsa_guid is not None:
583 text = text + "\n\tdsa_guid=%s" % str(self.dsa_guid)
584 if self.dsa_ivid is not None:
585 text = text + "\n\tdsa_ivid=%s" % str(self.dsa_ivid)
587 text += "\n\tro=%s" % self.is_ro() +\
588 "\n\tgc=%s" % self.is_gc() +\
589 "\n\tistg=%s" % self.is_istg() +\
590 "\ncurrent_replica_table:" +\
591 "\n%s" % self.dumpstr_current_replica_table() +\
592 "\nneeded_replica_table:" +\
593 "\n%s" % self.dumpstr_needed_replica_table() +\
594 "\nconnect_table:" +\
595 "\n%s" % self.dumpstr_connect_table()
597 return text
599 def get_current_replica(self, nc_dnstr):
600 return self.current_rep_table.get(nc_dnstr)
602 def is_istg(self):
603 """Returns True if dsa is intersite topology generator for it's site"""
604 # The KCC on an RODC always acts as an ISTG for itself
605 return self.dsa_is_istg or self.dsa_is_ro
607 def is_ro(self):
608 """Returns True if dsa a read only domain controller"""
609 return self.dsa_is_ro
611 def is_gc(self):
612 """Returns True if dsa hosts a global catalog"""
613 if (self.options & dsdb.DS_NTDSDSA_OPT_IS_GC) != 0:
614 return True
615 return False
617 def is_minimum_behavior(self, version):
618 """Is dsa at minimum windows level greater than or equal to (version)
620 :param version: Windows version to test against
621 (e.g. DS_DOMAIN_FUNCTION_2008)
623 if self.dsa_behavior >= version:
624 return True
625 return False
627 def is_translate_ntdsconn_disabled(self):
628 """Whether this allows NTDSConnection translation in its options."""
629 if (self.options & dsdb.DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE) != 0:
630 return True
631 return False
633 def get_rep_tables(self):
634 """Return DSA current and needed replica tables
636 return self.current_rep_table, self.needed_rep_table
638 def get_parent_dnstr(self):
639 """Get the parent DN string of this object."""
640 head, sep, tail = self.dsa_dnstr.partition(',')
641 return tail
643 def load_dsa(self, samdb):
644 """Load a DSA from the samdb.
646 Prior initialization has given us the DN of the DSA that we are to
647 load. This method initializes all other attributes, including loading
648 the NC replica table for this DSA.
650 attrs = ["objectGUID",
651 "invocationID",
652 "options",
653 "msDS-isRODC",
654 "msDS-Behavior-Version"]
655 try:
656 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
657 attrs=attrs)
659 except ldb.LdbError as e5:
660 (enum, estr) = e5.args
661 raise KCCError("Unable to find nTDSDSA for (%s) - (%s)" %
662 (self.dsa_dnstr, estr))
664 msg = res[0]
665 self.dsa_guid = misc.GUID(samdb.schema_format_value("objectGUID",
666 msg["objectGUID"][0]))
668 # RODCs don't originate changes and thus have no invocationId,
669 # therefore we must check for existence first
670 if "invocationId" in msg:
671 self.dsa_ivid = misc.GUID(samdb.schema_format_value("objectGUID",
672 msg["invocationId"][0]))
674 if "options" in msg:
675 self.options = int(msg["options"][0])
677 if "msDS-isRODC" in msg and str(msg["msDS-isRODC"][0]) == "TRUE":
678 self.dsa_is_ro = True
679 else:
680 self.dsa_is_ro = False
682 if "msDS-Behavior-Version" in msg:
683 self.dsa_behavior = int(msg['msDS-Behavior-Version'][0])
685 # Load the NC replicas that are enumerated on this dsa
686 self.load_current_replica_table(samdb)
688 # Load the nTDSConnection that are enumerated on this dsa
689 self.load_connection_table(samdb)
691 def load_current_replica_table(self, samdb):
692 """Method to load the NC replica's listed for DSA object.
694 This method queries the samdb for (hasMasterNCs, msDS-hasMasterNCs,
695 hasPartialReplicaNCs, msDS-HasDomainNCs, msDS-hasFullReplicaNCs, and
696 msDS-HasInstantiatedNCs) to determine complete list of NC replicas that
697 are enumerated for the DSA. Once a NC replica is loaded it is
698 identified (schema, config, etc) and the other replica attributes
699 (partial, ro, etc) are determined.
701 :param samdb: database to query for DSA replica list
703 ncattrs = [
704 # not RODC - default, config, schema (old style)
705 "hasMasterNCs",
706 # not RODC - default, config, schema, app NCs
707 "msDS-hasMasterNCs",
708 # domain NC partial replicas
709 "hasPartialReplicaNCs",
710 # default domain NC
711 "msDS-HasDomainNCs",
712 # RODC only - default, config, schema, app NCs
713 "msDS-hasFullReplicaNCs",
714 # Identifies if replica is coming, going, or stable
715 "msDS-HasInstantiatedNCs"
717 try:
718 res = samdb.search(base=self.dsa_dnstr, scope=ldb.SCOPE_BASE,
719 attrs=ncattrs)
721 except ldb.LdbError as e6:
722 (enum, estr) = e6.args
723 raise KCCError("Unable to find nTDSDSA NCs for (%s) - (%s)" %
724 (self.dsa_dnstr, estr))
726 # The table of NCs for the dsa we are searching
727 tmp_table = {}
729 # We should get one response to our query here for
730 # the ntds that we requested
731 if len(res[0]) > 0:
733 # Our response will contain a number of elements including
734 # the dn of the dsa as well as elements for each
735 # attribute (e.g. hasMasterNCs). Each of these elements
736 # is a dictionary list which we retrieve the keys for and
737 # then iterate over them
738 for k in res[0].keys():
739 if k == "dn":
740 continue
742 # For each attribute type there will be one or more DNs
743 # listed. For instance DCs normally have 3 hasMasterNCs
744 # listed.
745 for value in res[0][k]:
746 # Turn dn into a dsdb_Dn so we can use
747 # its methods to parse a binary DN
748 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
749 flags = dsdn.get_binary_integer()
750 dnstr = str(dsdn.dn)
752 if dnstr not in tmp_table:
753 rep = NCReplica(self, dnstr)
754 tmp_table[dnstr] = rep
755 else:
756 rep = tmp_table[dnstr]
758 if k == "msDS-HasInstantiatedNCs":
759 rep.set_instantiated_flags(flags)
760 continue
762 rep.identify_by_dsa_attr(samdb, k)
764 # if we've identified the default domain NC
765 # then save its DN string
766 if rep.is_default():
767 self.default_dnstr = dnstr
768 else:
769 raise KCCError("No nTDSDSA NCs for (%s)" % self.dsa_dnstr)
771 # Assign our newly built NC replica table to this dsa
772 self.current_rep_table = tmp_table
774 def add_needed_replica(self, rep):
775 """Method to add a NC replica that "should be present" to the
776 needed_rep_table.
778 self.needed_rep_table[rep.nc_dnstr] = rep
780 def load_connection_table(self, samdb):
781 """Method to load the nTDSConnections listed for DSA object.
783 :param samdb: database to query for DSA connection list
785 try:
786 res = samdb.search(base=self.dsa_dnstr,
787 scope=ldb.SCOPE_SUBTREE,
788 expression="(objectClass=nTDSConnection)")
790 except ldb.LdbError as e7:
791 (enum, estr) = e7.args
792 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
793 (self.dsa_dnstr, estr))
795 for msg in res:
796 dnstr = str(msg.dn)
798 # already loaded
799 if dnstr in self.connect_table:
800 continue
802 connect = NTDSConnection(dnstr)
804 connect.load_connection(samdb)
805 self.connect_table[dnstr] = connect
807 def commit_connections(self, samdb, ro=False):
808 """Method to commit any uncommitted nTDSConnections
809 modifications that are in our table. These would be
810 identified connections that are marked to be added or
811 deleted
813 :param samdb: database to commit DSA connection list to
814 :param ro: if (true) then perform internal operations but
815 do not write to the database (readonly)
817 delconn = []
819 for dnstr, connect in self.connect_table.items():
820 if connect.to_be_added:
821 connect.commit_added(samdb, ro)
823 if connect.to_be_modified:
824 connect.commit_modified(samdb, ro)
826 if connect.to_be_deleted:
827 connect.commit_deleted(samdb, ro)
828 delconn.append(dnstr)
830 # Now delete the connection from the table
831 for dnstr in delconn:
832 del self.connect_table[dnstr]
834 def add_connection(self, dnstr, connect):
835 assert dnstr not in self.connect_table
836 self.connect_table[dnstr] = connect
838 def get_connection_by_from_dnstr(self, from_dnstr):
839 """Scan DSA nTDSConnection table and return connection
840 with a "fromServer" dn string equivalent to method
841 input parameter.
843 :param from_dnstr: search for this from server entry
845 answer = []
846 for connect in self.connect_table.values():
847 if connect.get_from_dnstr() == from_dnstr:
848 answer.append(connect)
850 return answer
852 def dumpstr_current_replica_table(self):
853 """Debug dump string output of current replica table"""
854 return '\n'.join(str(x) for x in self.current_rep_table)
856 def dumpstr_needed_replica_table(self):
857 """Debug dump string output of needed replica table"""
858 return '\n'.join(str(x) for x in self.needed_rep_table)
860 def dumpstr_connect_table(self):
861 """Debug dump string output of connect table"""
862 return '\n'.join(str(x) for x in self.connect_table)
864 def new_connection(self, options, system_flags, transport, from_dnstr,
865 sched):
866 """Set up a new connection for the DSA based on input
867 parameters. Connection will be added to the DSA
868 connect_table and will be marked as "to be added" pending
869 a call to commit_connections()
871 dnstr = "CN=%s," % str(uuid.uuid4()) + self.dsa_dnstr
873 connect = NTDSConnection(dnstr)
874 connect.to_be_added = True
875 connect.enabled = True
876 connect.from_dnstr = from_dnstr
877 connect.options = options
878 connect.system_flags = system_flags
880 if transport is not None:
881 connect.transport_dnstr = transport.dnstr
882 connect.transport_guid = transport.guid
884 if sched is not None:
885 connect.schedule = sched
886 else:
887 # Create schedule. Attribute value set according to MS-TECH
888 # intra-site connection creation document
889 connect.schedule = new_connection_schedule()
891 self.add_connection(dnstr, connect)
892 return connect
895 class NTDSConnection(object):
896 """Class defines a nTDSConnection found under a DSA
898 def __init__(self, dnstr):
899 self.dnstr = dnstr
900 self.guid = None
901 self.enabled = False
902 self.whenCreated = 0
903 self.to_be_added = False # new connection needs to be added
904 self.to_be_deleted = False # old connection needs to be deleted
905 self.to_be_modified = False
906 self.options = 0
907 self.system_flags = 0
908 self.transport_dnstr = None
909 self.transport_guid = None
910 self.from_dnstr = None
911 self.schedule = None
913 def __str__(self):
914 """Debug dump string output of NTDSConnection object"""
916 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr) +\
917 "\n\tenabled=%s" % self.enabled +\
918 "\n\tto_be_added=%s" % self.to_be_added +\
919 "\n\tto_be_deleted=%s" % self.to_be_deleted +\
920 "\n\tto_be_modified=%s" % self.to_be_modified +\
921 "\n\toptions=0x%08X" % self.options +\
922 "\n\tsystem_flags=0x%08X" % self.system_flags +\
923 "\n\twhenCreated=%d" % self.whenCreated +\
924 "\n\ttransport_dn=%s" % self.transport_dnstr
926 if self.guid is not None:
927 text += "\n\tguid=%s" % str(self.guid)
929 if self.transport_guid is not None:
930 text += "\n\ttransport_guid=%s" % str(self.transport_guid)
932 text = text + "\n\tfrom_dn=%s" % self.from_dnstr
934 if self.schedule is not None:
935 text += "\n\tschedule.size=%s" % self.schedule.size +\
936 "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth +\
937 ("\n\tschedule.numberOfSchedules=%s" %
938 self.schedule.numberOfSchedules)
940 for i, header in enumerate(self.schedule.headerArray):
941 text += ("\n\tschedule.headerArray[%d].type=%d" %
942 (i, header.type)) +\
943 ("\n\tschedule.headerArray[%d].offset=%d" %
944 (i, header.offset)) +\
945 "\n\tschedule.dataArray[%d].slots[ " % i +\
946 "".join("0x%X " % slot for slot in self.schedule.dataArray[i].slots) +\
949 return text
951 def load_connection(self, samdb):
952 """Given a NTDSConnection object with an prior initialization
953 for the object's DN, search for the DN and load attributes
954 from the samdb.
956 attrs = ["options",
957 "enabledConnection",
958 "schedule",
959 "whenCreated",
960 "objectGUID",
961 "transportType",
962 "fromServer",
963 "systemFlags"]
964 try:
965 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
966 attrs=attrs)
968 except ldb.LdbError as e8:
969 (enum, estr) = e8.args
970 raise KCCError("Unable to find nTDSConnection for (%s) - (%s)" %
971 (self.dnstr, estr))
973 msg = res[0]
975 if "options" in msg:
976 self.options = int(msg["options"][0])
978 if "enabledConnection" in msg:
979 if str(msg["enabledConnection"][0]).upper().lstrip().rstrip() == "TRUE":
980 self.enabled = True
982 if "systemFlags" in msg:
983 self.system_flags = int(msg["systemFlags"][0])
985 try:
986 self.guid = \
987 misc.GUID(samdb.schema_format_value("objectGUID",
988 msg["objectGUID"][0]))
989 except KeyError:
990 raise KCCError("Unable to find objectGUID in nTDSConnection "
991 "for (%s)" % (self.dnstr))
993 if "transportType" in msg:
994 dsdn = dsdb_Dn(samdb, msg["transportType"][0].decode('utf8'))
995 self.load_connection_transport(samdb, str(dsdn.dn))
997 if "schedule" in msg:
998 self.schedule = ndr_unpack(drsblobs.schedule, msg["schedule"][0])
1000 if "whenCreated" in msg:
1001 self.whenCreated = ldb.string_to_time(str(msg["whenCreated"][0]))
1003 if "fromServer" in msg:
1004 dsdn = dsdb_Dn(samdb, msg["fromServer"][0].decode('utf8'))
1005 self.from_dnstr = str(dsdn.dn)
1006 assert self.from_dnstr is not None
1008 def load_connection_transport(self, samdb, tdnstr):
1009 """Given a NTDSConnection object which enumerates a transport
1010 DN, load the transport information for the connection object
1012 :param tdnstr: transport DN to load
1014 attrs = ["objectGUID"]
1015 try:
1016 res = samdb.search(base=tdnstr,
1017 scope=ldb.SCOPE_BASE, attrs=attrs)
1019 except ldb.LdbError as e9:
1020 (enum, estr) = e9.args
1021 raise KCCError("Unable to find transport (%s) - (%s)" %
1022 (tdnstr, estr))
1024 if "objectGUID" in res[0]:
1025 msg = res[0]
1026 self.transport_dnstr = tdnstr
1027 self.transport_guid = \
1028 misc.GUID(samdb.schema_format_value("objectGUID",
1029 msg["objectGUID"][0]))
1030 assert self.transport_dnstr is not None
1031 assert self.transport_guid is not None
1033 def commit_deleted(self, samdb, ro=False):
1034 """Local helper routine for commit_connections() which
1035 handles committed connections that are to be deleted from
1036 the database database
1038 assert self.to_be_deleted
1039 self.to_be_deleted = False
1041 # No database modification requested
1042 if ro:
1043 return
1045 try:
1046 samdb.delete(self.dnstr)
1047 except ldb.LdbError as e10:
1048 (enum, estr) = e10.args
1049 raise KCCError("Could not delete nTDSConnection for (%s) - (%s)" %
1050 (self.dnstr, estr))
1052 def commit_added(self, samdb, ro=False):
1053 """Local helper routine for commit_connections() which
1054 handles committed connections that are to be added to the
1055 database
1057 assert self.to_be_added
1058 self.to_be_added = False
1060 # No database modification requested
1061 if ro:
1062 return
1064 # First verify we don't have this entry to ensure nothing
1065 # is programmatically amiss
1066 found = False
1067 try:
1068 msg = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1069 if len(msg) != 0:
1070 found = True
1072 except ldb.LdbError as e11:
1073 (enum, estr) = e11.args
1074 if enum != ldb.ERR_NO_SUCH_OBJECT:
1075 raise KCCError("Unable to search for (%s) - (%s)" %
1076 (self.dnstr, estr))
1077 if found:
1078 raise KCCError("nTDSConnection for (%s) already exists!" %
1079 self.dnstr)
1081 if self.enabled:
1082 enablestr = "TRUE"
1083 else:
1084 enablestr = "FALSE"
1086 # Prepare a message for adding to the samdb
1087 m = ldb.Message()
1088 m.dn = ldb.Dn(samdb, self.dnstr)
1090 m["objectClass"] = \
1091 ldb.MessageElement("nTDSConnection", ldb.FLAG_MOD_ADD,
1092 "objectClass")
1093 m["showInAdvancedViewOnly"] = \
1094 ldb.MessageElement("TRUE", ldb.FLAG_MOD_ADD,
1095 "showInAdvancedViewOnly")
1096 m["enabledConnection"] = \
1097 ldb.MessageElement(enablestr, ldb.FLAG_MOD_ADD,
1098 "enabledConnection")
1099 m["fromServer"] = \
1100 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_ADD, "fromServer")
1101 m["options"] = \
1102 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_ADD, "options")
1103 m["systemFlags"] = \
1104 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_ADD,
1105 "systemFlags")
1107 if self.transport_dnstr is not None:
1108 m["transportType"] = \
1109 ldb.MessageElement(str(self.transport_dnstr), ldb.FLAG_MOD_ADD,
1110 "transportType")
1112 if self.schedule is not None:
1113 m["schedule"] = \
1114 ldb.MessageElement(ndr_pack(self.schedule),
1115 ldb.FLAG_MOD_ADD, "schedule")
1116 try:
1117 samdb.add(m)
1118 except ldb.LdbError as e12:
1119 (enum, estr) = e12.args
1120 raise KCCError("Could not add nTDSConnection for (%s) - (%s)" %
1121 (self.dnstr, estr))
1123 def commit_modified(self, samdb, ro=False):
1124 """Local helper routine for commit_connections() which
1125 handles committed connections that are to be modified to the
1126 database
1128 assert self.to_be_modified
1129 self.to_be_modified = False
1131 # No database modification requested
1132 if ro:
1133 return
1135 # First verify we have this entry to ensure nothing
1136 # is programmatically amiss
1137 try:
1138 # we don't use the search result, but it tests the status
1139 # of self.dnstr in the database.
1140 samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE)
1142 except ldb.LdbError as e13:
1143 (enum, estr) = e13.args
1144 if enum == ldb.ERR_NO_SUCH_OBJECT:
1145 raise KCCError("nTDSConnection for (%s) doesn't exist!" %
1146 self.dnstr)
1147 raise KCCError("Unable to search for (%s) - (%s)" %
1148 (self.dnstr, estr))
1150 if self.enabled:
1151 enablestr = "TRUE"
1152 else:
1153 enablestr = "FALSE"
1155 # Prepare a message for modifying the samdb
1156 m = ldb.Message()
1157 m.dn = ldb.Dn(samdb, self.dnstr)
1159 m["enabledConnection"] = \
1160 ldb.MessageElement(enablestr, ldb.FLAG_MOD_REPLACE,
1161 "enabledConnection")
1162 m["fromServer"] = \
1163 ldb.MessageElement(self.from_dnstr, ldb.FLAG_MOD_REPLACE,
1164 "fromServer")
1165 m["options"] = \
1166 ldb.MessageElement(str(self.options), ldb.FLAG_MOD_REPLACE,
1167 "options")
1168 m["systemFlags"] = \
1169 ldb.MessageElement(str(self.system_flags), ldb.FLAG_MOD_REPLACE,
1170 "systemFlags")
1172 if self.transport_dnstr is not None:
1173 m["transportType"] = \
1174 ldb.MessageElement(str(self.transport_dnstr),
1175 ldb.FLAG_MOD_REPLACE, "transportType")
1176 else:
1177 m["transportType"] = \
1178 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "transportType")
1180 if self.schedule is not None:
1181 m["schedule"] = \
1182 ldb.MessageElement(ndr_pack(self.schedule),
1183 ldb.FLAG_MOD_REPLACE, "schedule")
1184 else:
1185 m["schedule"] = \
1186 ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "schedule")
1187 try:
1188 samdb.modify(m)
1189 except ldb.LdbError as e14:
1190 (enum, estr) = e14.args
1191 raise KCCError("Could not modify nTDSConnection for (%s) - (%s)" %
1192 (self.dnstr, estr))
1194 def set_modified(self, truefalse):
1195 self.to_be_modified = truefalse
1197 def is_schedule_minimum_once_per_week(self):
1198 """Returns True if our schedule includes at least one
1199 replication interval within the week. False otherwise
1201 # replinfo schedule is None means "always", while
1202 # NTDSConnection schedule is None means "never".
1203 if self.schedule is None or self.schedule.dataArray[0] is None:
1204 return False
1206 for slot in self.schedule.dataArray[0].slots:
1207 if (slot & 0x0F) != 0x0:
1208 return True
1209 return False
1211 def is_equivalent_schedule(self, sched):
1212 """Returns True if our schedule is equivalent to the input
1213 comparison schedule.
1215 :param shed: schedule to compare to
1217 # There are 4 cases, where either self.schedule or sched can be None
1219 # | self. is None | self. is not None
1220 # --------------+-----------------+--------------------
1221 # sched is None | True | False
1222 # --------------+-----------------+--------------------
1223 # sched is not None | False | do calculations
1225 if self.schedule is None:
1226 return sched is None
1228 if sched is None:
1229 return False
1231 if ((self.schedule.size != sched.size or
1232 self.schedule.bandwidth != sched.bandwidth or
1233 self.schedule.numberOfSchedules != sched.numberOfSchedules)):
1234 return False
1236 for i, header in enumerate(self.schedule.headerArray):
1238 if self.schedule.headerArray[i].type != sched.headerArray[i].type:
1239 return False
1241 if self.schedule.headerArray[i].offset != \
1242 sched.headerArray[i].offset:
1243 return False
1245 for a, b in zip(self.schedule.dataArray[i].slots,
1246 sched.dataArray[i].slots):
1247 if a != b:
1248 return False
1249 return True
1251 def is_rodc_topology(self):
1252 """Returns True if NTDS Connection specifies RODC
1253 topology only
1255 if self.options & dsdb.NTDSCONN_OPT_RODC_TOPOLOGY == 0:
1256 return False
1257 return True
1259 def is_generated(self):
1260 """Returns True if NTDS Connection was generated by the
1261 KCC topology algorithm as opposed to set by the administrator
1263 if self.options & dsdb.NTDSCONN_OPT_IS_GENERATED == 0:
1264 return False
1265 return True
1267 def is_override_notify_default(self):
1268 """Returns True if NTDS Connection should override notify default
1270 if self.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT == 0:
1271 return False
1272 return True
1274 def is_use_notify(self):
1275 """Returns True if NTDS Connection should use notify
1277 if self.options & dsdb.NTDSCONN_OPT_USE_NOTIFY == 0:
1278 return False
1279 return True
1281 def is_twoway_sync(self):
1282 """Returns True if NTDS Connection should use twoway sync
1284 if self.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC == 0:
1285 return False
1286 return True
1288 def is_intersite_compression_disabled(self):
1289 """Returns True if NTDS Connection intersite compression
1290 is disabled
1292 if self.options & dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION == 0:
1293 return False
1294 return True
1296 def is_user_owned_schedule(self):
1297 """Returns True if NTDS Connection has a user owned schedule
1299 if self.options & dsdb.NTDSCONN_OPT_USER_OWNED_SCHEDULE == 0:
1300 return False
1301 return True
1303 def is_enabled(self):
1304 """Returns True if NTDS Connection is enabled
1306 return self.enabled
1308 def get_from_dnstr(self):
1309 """Return fromServer dn string attribute"""
1310 return self.from_dnstr
1313 class Partition(NamingContext):
1314 """A naming context discovered thru Partitions DN of the config schema.
1316 This is a more specific form of NamingContext class (inheriting from that
1317 class) and it identifies unique attributes enumerated in the Partitions
1318 such as which nTDSDSAs are cross referenced for replicas
1320 def __init__(self, partstr):
1321 self.partstr = partstr
1322 self.enabled = True
1323 self.system_flags = 0
1324 self.rw_location_list = []
1325 self.ro_location_list = []
1327 # We don't have enough info to properly
1328 # fill in the naming context yet. We'll get that
1329 # fully set up with load_partition().
1330 NamingContext.__init__(self, None)
1332 def load_partition(self, samdb):
1333 """Given a Partition class object that has been initialized with its
1334 partition dn string, load the partition from the sam database, identify
1335 the type of the partition (schema, domain, etc) and record the list of
1336 nTDSDSAs that appear in the cross reference attributes
1337 msDS-NC-Replica-Locations and msDS-NC-RO-Replica-Locations.
1339 :param samdb: sam database to load partition from
1341 attrs = ["nCName",
1342 "Enabled",
1343 "systemFlags",
1344 "msDS-NC-Replica-Locations",
1345 "msDS-NC-RO-Replica-Locations"]
1346 try:
1347 res = samdb.search(base=self.partstr, scope=ldb.SCOPE_BASE,
1348 attrs=attrs)
1350 except ldb.LdbError as e15:
1351 (enum, estr) = e15.args
1352 raise KCCError("Unable to find partition for (%s) - (%s)" %
1353 (self.partstr, estr))
1354 msg = res[0]
1355 for k in msg.keys():
1356 if k == "dn":
1357 continue
1359 if k == "Enabled":
1360 if str(msg[k][0]).upper().lstrip().rstrip() == "TRUE":
1361 self.enabled = True
1362 else:
1363 self.enabled = False
1364 continue
1366 if k == "systemFlags":
1367 self.system_flags = int(msg[k][0])
1368 continue
1370 for value in msg[k]:
1371 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
1372 dnstr = str(dsdn.dn)
1374 if k == "nCName":
1375 self.nc_dnstr = dnstr
1376 continue
1378 if k == "msDS-NC-Replica-Locations":
1379 self.rw_location_list.append(dnstr)
1380 continue
1382 if k == "msDS-NC-RO-Replica-Locations":
1383 self.ro_location_list.append(dnstr)
1384 continue
1386 # Now identify what type of NC this partition
1387 # enumerated
1388 self.identify_by_basedn(samdb)
1390 def is_enabled(self):
1391 """Returns True if partition is enabled
1393 return self.is_enabled
1395 def is_foreign(self):
1396 """Returns True if this is not an Active Directory NC in our
1397 forest but is instead something else (e.g. a foreign NC)
1399 if (self.system_flags & dsdb.SYSTEM_FLAG_CR_NTDS_NC) == 0:
1400 return True
1401 else:
1402 return False
1404 def should_be_present(self, target_dsa):
1405 """Tests whether this partition should have an NC replica
1406 on the target dsa. This method returns a tuple of
1407 needed=True/False, ro=True/False, partial=True/False
1409 :param target_dsa: should NC be present on target dsa
1411 ro = False
1412 partial = False
1414 # If this is the config, schema, or default
1415 # domain NC for the target dsa then it should
1416 # be present
1417 needed = (self.nc_type == NCType.config or
1418 self.nc_type == NCType.schema or
1419 (self.nc_type == NCType.domain and
1420 self.nc_dnstr == target_dsa.default_dnstr))
1422 # A writable replica of an application NC should be present
1423 # if there a cross reference to the target DSA exists. Depending
1424 # on whether the DSA is ro we examine which type of cross reference
1425 # to look for (msDS-NC-Replica-Locations or
1426 # msDS-NC-RO-Replica-Locations
1427 if self.nc_type == NCType.application:
1428 if target_dsa.is_ro():
1429 if target_dsa.dsa_dnstr in self.ro_location_list:
1430 needed = True
1431 else:
1432 if target_dsa.dsa_dnstr in self.rw_location_list:
1433 needed = True
1435 # If the target dsa is a gc then a partial replica of a
1436 # domain NC (other than the DSAs default domain) should exist
1437 # if there is also a cross reference for the DSA
1438 if (target_dsa.is_gc() and
1439 self.nc_type == NCType.domain and
1440 self.nc_dnstr != target_dsa.default_dnstr and
1441 (target_dsa.dsa_dnstr in self.ro_location_list or
1442 target_dsa.dsa_dnstr in self.rw_location_list)):
1443 needed = True
1444 partial = True
1446 # partial NCs are always readonly
1447 if needed and (target_dsa.is_ro() or partial):
1448 ro = True
1450 return needed, ro, partial
1452 def __str__(self):
1453 """Debug dump string output of class"""
1454 text = "%s" % NamingContext.__str__(self) +\
1455 "\n\tpartdn=%s" % self.partstr +\
1456 "".join("\n\tmsDS-NC-Replica-Locations=%s" % k for k in self.rw_location_list) +\
1457 "".join("\n\tmsDS-NC-RO-Replica-Locations=%s" % k for k in self.ro_location_list)
1458 return text
1461 class Site(object):
1462 """An individual site object discovered thru the configuration
1463 naming context. Contains all DSAs that exist within the site
1465 def __init__(self, site_dnstr, nt_now):
1466 self.site_dnstr = site_dnstr
1467 self.site_guid = None
1468 self.site_options = 0
1469 self.site_topo_generator = None
1470 self.site_topo_failover = 0 # appears to be in minutes
1471 self.dsa_table = {}
1472 self.rw_dsa_table = {}
1473 self.nt_now = nt_now
1475 def load_site(self, samdb):
1476 """Loads the NTDS Site Settings options attribute for the site
1477 as well as querying and loading all DSAs that appear within
1478 the site.
1480 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1481 attrs = ["options",
1482 "interSiteTopologyFailover",
1483 "interSiteTopologyGenerator"]
1484 try:
1485 res = samdb.search(base=ssdn, scope=ldb.SCOPE_BASE,
1486 attrs=attrs)
1487 self_res = samdb.search(base=self.site_dnstr, scope=ldb.SCOPE_BASE,
1488 attrs=['objectGUID'])
1489 except ldb.LdbError as e16:
1490 (enum, estr) = e16.args
1491 raise KCCError("Unable to find site settings for (%s) - (%s)" %
1492 (ssdn, estr))
1494 msg = res[0]
1495 if "options" in msg:
1496 self.site_options = int(msg["options"][0])
1498 if "interSiteTopologyGenerator" in msg:
1499 self.site_topo_generator = \
1500 str(msg["interSiteTopologyGenerator"][0])
1502 if "interSiteTopologyFailover" in msg:
1503 self.site_topo_failover = int(msg["interSiteTopologyFailover"][0])
1505 msg = self_res[0]
1506 if "objectGUID" in msg:
1507 self.site_guid = misc.GUID(samdb.schema_format_value("objectGUID",
1508 msg["objectGUID"][0]))
1510 self.load_all_dsa(samdb)
1512 def load_all_dsa(self, samdb):
1513 """Discover all nTDSDSA thru the sites entry and
1514 instantiate and load the DSAs. Each dsa is inserted
1515 into the dsa_table by dn string.
1517 try:
1518 res = samdb.search(self.site_dnstr,
1519 scope=ldb.SCOPE_SUBTREE,
1520 expression="(objectClass=nTDSDSA)")
1521 except ldb.LdbError as e17:
1522 (enum, estr) = e17.args
1523 raise KCCError("Unable to find nTDSDSAs - (%s)" % estr)
1525 for msg in res:
1526 dnstr = str(msg.dn)
1528 # already loaded
1529 if dnstr in self.dsa_table:
1530 continue
1532 dsa = DirectoryServiceAgent(dnstr)
1534 dsa.load_dsa(samdb)
1536 # Assign this dsa to my dsa table
1537 # and index by dsa dn
1538 self.dsa_table[dnstr] = dsa
1539 if not dsa.is_ro():
1540 self.rw_dsa_table[dnstr] = dsa
1542 def get_dsa(self, dnstr):
1543 """Return a previously loaded DSA object by consulting
1544 the sites dsa_table for the provided DSA dn string
1546 :return: None if DSA doesn't exist
1548 return self.dsa_table.get(dnstr)
1550 def select_istg(self, samdb, mydsa, ro):
1551 """Determine if my DC should be an intersite topology
1552 generator. If my DC is the istg and is both a writeable
1553 DC and the database is opened in write mode then we perform
1554 an originating update to set the interSiteTopologyGenerator
1555 attribute in the NTDS Site Settings object. An RODC always
1556 acts as an ISTG for itself.
1558 # The KCC on an RODC always acts as an ISTG for itself
1559 if mydsa.dsa_is_ro:
1560 mydsa.dsa_is_istg = True
1561 self.site_topo_generator = mydsa.dsa_dnstr
1562 return True
1564 c_rep = get_dsa_config_rep(mydsa)
1566 # Load repsFrom and replUpToDateVector if not already loaded
1567 # so we can get the current state of the config replica and
1568 # whether we are getting updates from the istg
1569 c_rep.load_repsFrom(samdb)
1571 c_rep.load_replUpToDateVector(samdb)
1573 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1574 # First, the KCC on a writable DC determines whether it acts
1575 # as an ISTG for its site
1577 # Let s be the object such that s!lDAPDisplayName = nTDSDSA
1578 # and classSchema in s!objectClass.
1580 # Let D be the sequence of objects o in the site of the local
1581 # DC such that o!objectCategory = s. D is sorted in ascending
1582 # order by objectGUID.
1584 # Which is a fancy way of saying "sort all the nTDSDSA objects
1585 # in the site by guid in ascending order". Place sorted list
1586 # in D_sort[]
1587 D_sort = sorted(
1588 self.rw_dsa_table.values(),
1589 key=lambda dsa: ndr_pack(dsa.dsa_guid))
1591 # double word number of 100 nanosecond intervals since 1600s
1593 # Let f be the duration o!interSiteTopologyFailover seconds, or 2 hours
1594 # if o!interSiteTopologyFailover is 0 or has no value.
1596 # Note: lastSuccess and ntnow are in 100 nanosecond intervals
1597 # so it appears we have to turn f into the same interval
1599 # interSiteTopologyFailover (if set) appears to be in minutes
1600 # so we'll need to convert to seconds and then 100 nanosecond
1601 # intervals
1602 # XXX [MS-ADTS] 6.2.2.3.1 says it is seconds, not minutes.
1604 # 10,000,000 is number of 100 nanosecond intervals in a second
1605 if self.site_topo_failover == 0:
1606 f = 2 * 60 * 60 * 10000000
1607 else:
1608 f = self.site_topo_failover * 60 * 10000000
1610 # Let o be the site settings object for the site of the local
1611 # DC, or NULL if no such o exists.
1612 d_dsa = self.dsa_table.get(self.site_topo_generator)
1614 # From MS-ADTS 6.2.2.3.1 ISTG selection:
1615 # If o != NULL and o!interSiteTopologyGenerator is not the
1616 # nTDSDSA object for the local DC and
1617 # o!interSiteTopologyGenerator is an element dj of sequence D:
1619 if d_dsa is not None and d_dsa is not mydsa:
1620 # From MS-ADTS 6.2.2.3.1 ISTG Selection:
1621 # Let c be the cursor in the replUpToDateVector variable
1622 # associated with the NC replica of the config NC such
1623 # that c.uuidDsa = dj!invocationId. If no such c exists
1624 # (No evidence of replication from current ITSG):
1625 # Let i = j.
1626 # Let t = 0.
1628 # Else if the current time < c.timeLastSyncSuccess - f
1629 # (Evidence of time sync problem on current ISTG):
1630 # Let i = 0.
1631 # Let t = 0.
1633 # Else (Evidence of replication from current ITSG):
1634 # Let i = j.
1635 # Let t = c.timeLastSyncSuccess.
1637 # last_success appears to be a double word containing
1638 # number of 100 nanosecond intervals since the 1600s
1639 j_idx = D_sort.index(d_dsa)
1641 found = False
1642 for cursor in c_rep.rep_replUpToDateVector_cursors:
1643 if d_dsa.dsa_ivid == cursor.source_dsa_invocation_id:
1644 found = True
1645 break
1647 if not found:
1648 i_idx = j_idx
1649 t_time = 0
1651 # XXX doc says current time < c.timeLastSyncSuccess - f
1652 # which is true only if f is negative or clocks are wrong.
1653 # f is not negative in the default case (2 hours).
1654 elif self.nt_now - cursor.last_sync_success > f:
1655 i_idx = 0
1656 t_time = 0
1657 else:
1658 i_idx = j_idx
1659 t_time = cursor.last_sync_success
1661 # Otherwise (Nominate local DC as ISTG):
1662 # Let i be the integer such that di is the nTDSDSA
1663 # object for the local DC.
1664 # Let t = the current time.
1665 else:
1666 i_idx = D_sort.index(mydsa)
1667 t_time = self.nt_now
1669 # Compute a function that maintains the current ISTG if
1670 # it is alive, cycles through other candidates if not.
1672 # Let k be the integer (i + ((current time - t) /
1673 # o!interSiteTopologyFailover)) MOD |D|.
1675 # Note: We don't want to divide by zero here so they must
1676 # have meant "f" instead of "o!interSiteTopologyFailover"
1677 k_idx = (i_idx + ((self.nt_now - t_time) // f)) % len(D_sort)
1679 # The local writable DC acts as an ISTG for its site if and
1680 # only if dk is the nTDSDSA object for the local DC. If the
1681 # local DC does not act as an ISTG, the KCC skips the
1682 # remainder of this task.
1683 d_dsa = D_sort[k_idx]
1684 d_dsa.dsa_is_istg = True
1686 # Update if we are the ISTG, otherwise return
1687 if d_dsa is not mydsa:
1688 return False
1690 # Nothing to do
1691 if self.site_topo_generator == mydsa.dsa_dnstr:
1692 return True
1694 self.site_topo_generator = mydsa.dsa_dnstr
1696 # If readonly database then do not perform a
1697 # persistent update
1698 if ro:
1699 return True
1701 # Perform update to the samdb
1702 ssdn = "CN=NTDS Site Settings,%s" % self.site_dnstr
1704 m = ldb.Message()
1705 m.dn = ldb.Dn(samdb, ssdn)
1707 m["interSiteTopologyGenerator"] = \
1708 ldb.MessageElement(mydsa.dsa_dnstr, ldb.FLAG_MOD_REPLACE,
1709 "interSiteTopologyGenerator")
1710 try:
1711 samdb.modify(m)
1713 except ldb.LdbError as estr:
1714 raise KCCError(
1715 "Could not set interSiteTopologyGenerator for (%s) - (%s)" %
1716 (ssdn, estr))
1717 return True
1719 def is_intrasite_topology_disabled(self):
1720 """Returns True if intra-site topology is disabled for site"""
1721 return (self.site_options &
1722 dsdb.DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED) != 0
1724 def is_intersite_topology_disabled(self):
1725 """Returns True if inter-site topology is disabled for site"""
1726 return ((self.site_options &
1727 dsdb.DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED)
1728 != 0)
1730 def is_random_bridgehead_disabled(self):
1731 """Returns True if selection of random bridgehead is disabled"""
1732 return (self.site_options &
1733 dsdb.DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED) != 0
1735 def is_detect_stale_disabled(self):
1736 """Returns True if detect stale is disabled for site"""
1737 return (self.site_options &
1738 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED) != 0
1740 def is_cleanup_ntdsconn_disabled(self):
1741 """Returns True if NTDS Connection cleanup is disabled for site"""
1742 return (self.site_options &
1743 dsdb.DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED) != 0
1745 def same_site(self, dsa):
1746 """Return True if dsa is in this site"""
1747 if self.get_dsa(dsa.dsa_dnstr):
1748 return True
1749 return False
1751 def is_rodc_site(self):
1752 if len(self.dsa_table) > 0 and len(self.rw_dsa_table) == 0:
1753 return True
1754 return False
1756 def __str__(self):
1757 """Debug dump string output of class"""
1758 text = "%s:" % self.__class__.__name__ +\
1759 "\n\tdn=%s" % self.site_dnstr +\
1760 "\n\toptions=0x%X" % self.site_options +\
1761 "\n\ttopo_generator=%s" % self.site_topo_generator +\
1762 "\n\ttopo_failover=%d" % self.site_topo_failover
1763 for key, dsa in self.dsa_table.items():
1764 text = text + "\n%s" % dsa
1765 return text
1768 class GraphNode(object):
1769 """A graph node describing a set of edges that should be directed to it.
1771 Each edge is a connection for a particular naming context replica directed
1772 from another node in the forest to this node.
1775 def __init__(self, dsa_dnstr, max_node_edges):
1776 """Instantiate the graph node according to a DSA dn string
1778 :param max_node_edges: maximum number of edges that should ever
1779 be directed to the node
1781 self.max_edges = max_node_edges
1782 self.dsa_dnstr = dsa_dnstr
1783 self.edge_from = []
1785 def __str__(self):
1786 text = "%s:" % self.__class__.__name__ +\
1787 "\n\tdsa_dnstr=%s" % self.dsa_dnstr +\
1788 "\n\tmax_edges=%d" % self.max_edges
1790 for i, edge in enumerate(self.edge_from):
1791 if isinstance(edge, str):
1792 text += "\n\tedge_from[%d]=%s" % (i, edge)
1794 return text
1796 def add_edge_from(self, from_dsa_dnstr):
1797 """Add an edge from the dsa to our graph nodes edge from list
1799 :param from_dsa_dnstr: the dsa that the edge emanates from
1801 assert isinstance(from_dsa_dnstr, str)
1803 # No edges from myself to myself
1804 if from_dsa_dnstr == self.dsa_dnstr:
1805 return False
1806 # Only one edge from a particular node
1807 if from_dsa_dnstr in self.edge_from:
1808 return False
1809 # Not too many edges
1810 if len(self.edge_from) >= self.max_edges:
1811 return False
1812 self.edge_from.append(from_dsa_dnstr)
1813 return True
1815 def add_edges_from_connections(self, dsa):
1816 """For each nTDSConnection object associated with a particular
1817 DSA, we test if it implies an edge to this graph node (i.e.
1818 the "fromServer" attribute). If it does then we add an
1819 edge from the server unless we are over the max edges for this
1820 graph node
1822 :param dsa: dsa with a dnstr equivalent to his graph node
1824 for connect in dsa.connect_table.values():
1825 self.add_edge_from(connect.from_dnstr)
1827 def add_connections_from_edges(self, dsa, transport):
1828 """For each edge directed to this graph node, ensure there
1829 is a corresponding nTDSConnection object in the dsa.
1831 for edge_dnstr in self.edge_from:
1832 connections = dsa.get_connection_by_from_dnstr(edge_dnstr)
1834 # For each edge directed to the NC replica that
1835 # "should be present" on the local DC, the KCC determines
1836 # whether an object c exists such that:
1838 # c is a child of the DC's nTDSDSA object.
1839 # c.objectCategory = nTDSConnection
1841 # Given the NC replica ri from which the edge is directed,
1842 # c.fromServer is the dsname of the nTDSDSA object of
1843 # the DC on which ri "is present".
1845 # c.options does not contain NTDSCONN_OPT_RODC_TOPOLOGY
1847 found_valid = False
1848 for connect in connections:
1849 if connect.is_rodc_topology():
1850 continue
1851 found_valid = True
1853 if found_valid:
1854 continue
1856 # if no such object exists then the KCC adds an object
1857 # c with the following attributes
1859 # Generate a new dnstr for this nTDSConnection
1860 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1861 flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1862 dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1864 dsa.new_connection(opt, flags, transport, edge_dnstr, None)
1866 def has_sufficient_edges(self):
1867 """Return True if we have met the maximum "from edges" criteria"""
1868 if len(self.edge_from) >= self.max_edges:
1869 return True
1870 return False
1873 class Transport(object):
1874 """Class defines a Inter-site transport found under Sites
1877 def __init__(self, dnstr):
1878 self.dnstr = dnstr
1879 self.options = 0
1880 self.guid = None
1881 self.name = None
1882 self.address_attr = None
1883 self.bridgehead_list = []
1885 def __str__(self):
1886 """Debug dump string output of Transport object"""
1888 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr) +\
1889 "\n\tguid=%s" % str(self.guid) +\
1890 "\n\toptions=%d" % self.options +\
1891 "\n\taddress_attr=%s" % self.address_attr +\
1892 "\n\tname=%s" % self.name +\
1893 "".join("\n\tbridgehead_list=%s" % dnstr for dnstr in self.bridgehead_list)
1895 return text
1897 def load_transport(self, samdb):
1898 """Given a Transport object with an prior initialization
1899 for the object's DN, search for the DN and load attributes
1900 from the samdb.
1902 attrs = ["objectGUID",
1903 "options",
1904 "name",
1905 "bridgeheadServerListBL",
1906 "transportAddressAttribute"]
1907 try:
1908 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
1909 attrs=attrs)
1911 except ldb.LdbError as e18:
1912 (enum, estr) = e18.args
1913 raise KCCError("Unable to find Transport for (%s) - (%s)" %
1914 (self.dnstr, estr))
1916 msg = res[0]
1917 self.guid = misc.GUID(samdb.schema_format_value("objectGUID",
1918 msg["objectGUID"][0]))
1920 if "options" in msg:
1921 self.options = int(msg["options"][0])
1923 if "transportAddressAttribute" in msg:
1924 self.address_attr = str(msg["transportAddressAttribute"][0])
1926 if "name" in msg:
1927 self.name = str(msg["name"][0])
1929 if "bridgeheadServerListBL" in msg:
1930 for value in msg["bridgeheadServerListBL"]:
1931 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
1932 dnstr = str(dsdn.dn)
1933 if dnstr not in self.bridgehead_list:
1934 self.bridgehead_list.append(dnstr)
1937 class RepsFromTo(object):
1938 """Class encapsulation of the NDR repsFromToBlob.
1940 Removes the necessity of external code having to
1941 understand about other_info or manipulation of
1942 update flags.
1944 def __init__(self, nc_dnstr=None, ndr_blob=None):
1946 self.__dict__['to_be_deleted'] = False
1947 self.__dict__['nc_dnstr'] = nc_dnstr
1948 self.__dict__['update_flags'] = 0x0
1949 # XXX the following sounds dubious and/or better solved
1950 # elsewhere, but lets leave it for now. In particular, there
1951 # seems to be no reason for all the non-ndr generated
1952 # attributes to be handled in the round about way (e.g.
1953 # self.__dict__['to_be_deleted'] = False above). On the other
1954 # hand, it all seems to work. Hooray! Hands off!.
1956 # WARNING:
1958 # There is a very subtle bug here with python
1959 # and our NDR code. If you assign directly to
1960 # a NDR produced struct (e.g. t_repsFrom.ctr.other_info)
1961 # then a proper python GC reference count is not
1962 # maintained.
1964 # To work around this we maintain an internal
1965 # reference to "dns_name(x)" and "other_info" elements
1966 # of repsFromToBlob. This internal reference
1967 # is hidden within this class but it is why you
1968 # see statements like this below:
1970 # self.__dict__['ndr_blob'].ctr.other_info = \
1971 # self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1973 # That would appear to be a redundant assignment but
1974 # it is necessary to hold a proper python GC reference
1975 # count.
1976 if ndr_blob is None:
1977 self.__dict__['ndr_blob'] = drsblobs.repsFromToBlob()
1978 self.__dict__['ndr_blob'].version = 0x1
1979 self.__dict__['dns_name1'] = None
1980 self.__dict__['dns_name2'] = None
1982 self.__dict__['ndr_blob'].ctr.other_info = \
1983 self.__dict__['other_info'] = drsblobs.repsFromTo1OtherInfo()
1985 else:
1986 self.__dict__['ndr_blob'] = ndr_blob
1987 self.__dict__['other_info'] = ndr_blob.ctr.other_info
1989 if ndr_blob.version == 0x1:
1990 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name
1991 self.__dict__['dns_name2'] = None
1992 else:
1993 self.__dict__['dns_name1'] = ndr_blob.ctr.other_info.dns_name1
1994 self.__dict__['dns_name2'] = ndr_blob.ctr.other_info.dns_name2
1996 def __str__(self):
1997 """Debug dump string output of class"""
1999 text = "%s:" % self.__class__.__name__ +\
2000 "\n\tdnstr=%s" % self.nc_dnstr +\
2001 "\n\tupdate_flags=0x%X" % self.update_flags +\
2002 "\n\tversion=%d" % self.version +\
2003 "\n\tsource_dsa_obj_guid=%s" % self.source_dsa_obj_guid +\
2004 ("\n\tsource_dsa_invocation_id=%s" %
2005 self.source_dsa_invocation_id) +\
2006 "\n\ttransport_guid=%s" % self.transport_guid +\
2007 "\n\treplica_flags=0x%X" % self.replica_flags +\
2008 ("\n\tconsecutive_sync_failures=%d" %
2009 self.consecutive_sync_failures) +\
2010 "\n\tlast_success=%s" % self.last_success +\
2011 "\n\tlast_attempt=%s" % self.last_attempt +\
2012 "\n\tdns_name1=%s" % self.dns_name1 +\
2013 "\n\tdns_name2=%s" % self.dns_name2 +\
2014 "\n\tschedule[ " +\
2015 "".join("0x%X " % slot for slot in self.schedule) +\
2018 return text
2020 def __setattr__(self, item, value):
2021 """Set an attribute and change update flag.
2023 Be aware that setting any RepsFromTo attribute will set the
2024 drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS update flag.
2026 if item in ['schedule', 'replica_flags', 'transport_guid',
2027 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2028 'consecutive_sync_failures', 'last_success',
2029 'last_attempt']:
2031 if item in ['replica_flags']:
2032 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
2033 elif item in ['schedule']:
2034 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
2036 setattr(self.__dict__['ndr_blob'].ctr, item, value)
2038 elif item in ['dns_name1']:
2039 self.__dict__['dns_name1'] = value
2041 if self.__dict__['ndr_blob'].version == 0x1:
2042 self.__dict__['ndr_blob'].ctr.other_info.dns_name = \
2043 self.__dict__['dns_name1']
2044 else:
2045 self.__dict__['ndr_blob'].ctr.other_info.dns_name1 = \
2046 self.__dict__['dns_name1']
2048 elif item in ['dns_name2']:
2049 self.__dict__['dns_name2'] = value
2051 if self.__dict__['ndr_blob'].version == 0x1:
2052 raise AttributeError(item)
2053 else:
2054 self.__dict__['ndr_blob'].ctr.other_info.dns_name2 = \
2055 self.__dict__['dns_name2']
2057 elif item in ['nc_dnstr']:
2058 self.__dict__['nc_dnstr'] = value
2060 elif item in ['to_be_deleted']:
2061 self.__dict__['to_be_deleted'] = value
2063 elif item in ['version']:
2064 raise AttributeError("Attempt to set readonly attribute %s" % item)
2065 else:
2066 raise AttributeError("Unknown attribute %s" % item)
2068 self.__dict__['update_flags'] |= drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
2070 def __getattr__(self, item):
2071 """Overload of RepsFromTo attribute retrieval.
2073 Allows external code to ignore substructures within the blob
2075 if item in ['schedule', 'replica_flags', 'transport_guid',
2076 'source_dsa_obj_guid', 'source_dsa_invocation_id',
2077 'consecutive_sync_failures', 'last_success',
2078 'last_attempt']:
2079 return getattr(self.__dict__['ndr_blob'].ctr, item)
2081 elif item in ['version']:
2082 return self.__dict__['ndr_blob'].version
2084 elif item in ['dns_name1']:
2085 if self.__dict__['ndr_blob'].version == 0x1:
2086 return self.__dict__['ndr_blob'].ctr.other_info.dns_name
2087 else:
2088 return self.__dict__['ndr_blob'].ctr.other_info.dns_name1
2090 elif item in ['dns_name2']:
2091 if self.__dict__['ndr_blob'].version == 0x1:
2092 raise AttributeError(item)
2093 else:
2094 return self.__dict__['ndr_blob'].ctr.other_info.dns_name2
2096 elif item in ['to_be_deleted']:
2097 return self.__dict__['to_be_deleted']
2099 elif item in ['nc_dnstr']:
2100 return self.__dict__['nc_dnstr']
2102 elif item in ['update_flags']:
2103 return self.__dict__['update_flags']
2105 raise AttributeError("Unknown attribute %s" % item)
2107 def is_modified(self):
2108 return (self.update_flags != 0x0)
2110 def set_unmodified(self):
2111 self.__dict__['update_flags'] = 0x0
2114 class SiteLink(object):
2115 """Class defines a site link found under sites
2118 def __init__(self, dnstr):
2119 self.dnstr = dnstr
2120 self.options = 0
2121 self.system_flags = 0
2122 self.cost = 0
2123 self.schedule = None
2124 self.interval = None
2125 self.site_list = []
2127 def __str__(self):
2128 """Debug dump string output of Transport object"""
2130 text = "%s:\n\tdn=%s" % (self.__class__.__name__, self.dnstr) +\
2131 "\n\toptions=%d" % self.options +\
2132 "\n\tsystem_flags=%d" % self.system_flags +\
2133 "\n\tcost=%d" % self.cost +\
2134 "\n\tinterval=%s" % self.interval
2136 if self.schedule is not None:
2137 text += "\n\tschedule.size=%s" % self.schedule.size +\
2138 "\n\tschedule.bandwidth=%s" % self.schedule.bandwidth +\
2139 ("\n\tschedule.numberOfSchedules=%s" %
2140 self.schedule.numberOfSchedules)
2142 for i, header in enumerate(self.schedule.headerArray):
2143 text += ("\n\tschedule.headerArray[%d].type=%d" %
2144 (i, header.type)) +\
2145 ("\n\tschedule.headerArray[%d].offset=%d" %
2146 (i, header.offset)) +\
2147 "\n\tschedule.dataArray[%d].slots[ " % i +\
2148 "".join("0x%X " % slot for slot in self.schedule.dataArray[i].slots) +\
2151 for guid, dn in self.site_list:
2152 text = text + "\n\tsite_list=%s (%s)" % (guid, dn)
2153 return text
2155 def load_sitelink(self, samdb):
2156 """Given a siteLink object with an prior initialization
2157 for the object's DN, search for the DN and load attributes
2158 from the samdb.
2160 attrs = ["options",
2161 "systemFlags",
2162 "cost",
2163 "schedule",
2164 "replInterval",
2165 "siteList"]
2166 try:
2167 res = samdb.search(base=self.dnstr, scope=ldb.SCOPE_BASE,
2168 attrs=attrs, controls=['extended_dn:0'])
2170 except ldb.LdbError as e19:
2171 (enum, estr) = e19.args
2172 raise KCCError("Unable to find SiteLink for (%s) - (%s)" %
2173 (self.dnstr, estr))
2175 msg = res[0]
2177 if "options" in msg:
2178 self.options = int(msg["options"][0])
2180 if "systemFlags" in msg:
2181 self.system_flags = int(msg["systemFlags"][0])
2183 if "cost" in msg:
2184 self.cost = int(msg["cost"][0])
2186 if "replInterval" in msg:
2187 self.interval = int(msg["replInterval"][0])
2189 if "siteList" in msg:
2190 for value in msg["siteList"]:
2191 dsdn = dsdb_Dn(samdb, value.decode('utf8'))
2192 guid = misc.GUID(dsdn.dn.get_extended_component('GUID'))
2193 dnstr = str(dsdn.dn)
2194 if (guid, dnstr) not in self.site_list:
2195 self.site_list.append((guid, dnstr))
2197 if "schedule" in msg:
2198 self.schedule = ndr_unpack(drsblobs.schedule, value)
2199 else:
2200 self.schedule = new_connection_schedule()
2203 class KCCFailedObject(object):
2204 def __init__(self, uuid, failure_count, time_first_failure,
2205 last_result, dns_name):
2206 self.uuid = uuid
2207 self.failure_count = failure_count
2208 self.time_first_failure = time_first_failure
2209 self.last_result = last_result
2210 self.dns_name = dns_name
2213 ##################################################
2214 # Global Functions and Variables
2215 ##################################################
2217 def get_dsa_config_rep(dsa):
2218 # Find configuration NC replica for the DSA
2219 for c_rep in dsa.current_rep_table.values():
2220 if c_rep.is_config():
2221 return c_rep
2223 raise KCCError("Unable to find config NC replica for (%s)" %
2224 dsa.dsa_dnstr)
2227 def new_connection_schedule():
2228 """Create a default schedule for an NTDSConnection or Sitelink. This
2229 is packed differently from the repltimes schedule used elsewhere
2230 in KCC (where the 168 nibbles are packed into 84 bytes).
2232 # 168 byte instances of the 0x01 value. The low order 4 bits
2233 # of the byte equate to 15 minute intervals within a single hour.
2234 # There are 168 bytes because there are 168 hours in a full week
2235 # Effectively we are saying to perform replication at the end of
2236 # each hour of the week
2237 schedule = drsblobs.schedule()
2239 schedule.size = 188
2240 schedule.bandwidth = 0
2241 schedule.numberOfSchedules = 1
2243 header = drsblobs.scheduleHeader()
2244 header.type = 0
2245 header.offset = 20
2247 schedule.headerArray = [header]
2249 data = drsblobs.scheduleSlots()
2250 data.slots = [0x01] * 168
2252 schedule.dataArray = [data]
2253 return schedule
2256 ##################################################
2257 # DNS related calls
2258 ##################################################
2260 def uncovered_sites_to_cover(samdb, site_name):
2262 Discover which sites have no DCs and whose lowest single-hop cost
2263 distance for any link attached to that site is linked to the site supplied.
2265 We compare the lowest cost of your single-hop link to this site to all of
2266 those available (if it exists). This means that a lower ranked siteLink
2267 with only the uncovered site can trump any available links (but this can
2268 only be done with specific, poorly enacted user configuration).
2270 If the site is connected to more than one other site with the same
2271 siteLink, only the largest site (failing that sorted alphabetically)
2272 creates the DNS records.
2274 :param samdb database
2275 :param site_name origin site (with a DC)
2277 :return a list of sites this site should be covering (for DNS)
2279 sites_to_cover = []
2281 server_res = samdb.search(base=samdb.get_config_basedn(),
2282 scope=ldb.SCOPE_SUBTREE,
2283 expression="(&(objectClass=server)"
2284 "(serverReference=*))")
2286 site_res = samdb.search(base=samdb.get_config_basedn(),
2287 scope=ldb.SCOPE_SUBTREE,
2288 expression="(objectClass=site)")
2290 sites_in_use = Counter()
2291 dc_count = 0
2293 # Assume server is of form DC,Servers,Site-ABCD because of schema
2294 for msg in server_res:
2295 site_dn = msg.dn.parent().parent()
2296 sites_in_use[site_dn.canonical_str()] += 1
2298 if site_dn.get_rdn_value().lower() == site_name.lower():
2299 dc_count += 1
2301 if len(sites_in_use) != len(site_res):
2302 # There is a possible uncovered site
2303 sites_uncovered = []
2305 for msg in site_res:
2306 if msg.dn.canonical_str() not in sites_in_use:
2307 sites_uncovered.append(msg)
2309 own_site_dn = "CN={},CN=Sites,{}".format(
2310 ldb.binary_encode(site_name),
2311 ldb.binary_encode(str(samdb.get_config_basedn()))
2314 for site in sites_uncovered:
2315 encoded_dn = ldb.binary_encode(str(site.dn))
2317 # Get a sorted list of all siteLinks featuring the uncovered site
2318 link_res1 = samdb.search(base=samdb.get_config_basedn(),
2319 scope=ldb.SCOPE_SUBTREE, attrs=["cost"],
2320 expression="(&(objectClass=siteLink)"
2321 "(siteList={}))".format(encoded_dn),
2322 controls=["server_sort:1:0:cost"])
2324 # Get a sorted list of all siteLinks connecting this an the
2325 # uncovered site
2326 link_res2 = samdb.search(base=samdb.get_config_basedn(),
2327 scope=ldb.SCOPE_SUBTREE,
2328 attrs=["cost", "siteList"],
2329 expression="(&(objectClass=siteLink)"
2330 "(siteList={})(siteList={}))".format(
2331 own_site_dn,
2332 encoded_dn),
2333 controls=["server_sort:1:0:cost"])
2335 # Add to list if your link is equal in cost to lowest cost link
2336 if len(link_res1) > 0 and len(link_res2) > 0:
2337 cost1 = int(link_res1[0]['cost'][0])
2338 cost2 = int(link_res2[0]['cost'][0])
2340 # Own siteLink must match the lowest cost link
2341 if cost1 != cost2:
2342 continue
2344 # In a siteLink with more than 2 sites attached, only pick the
2345 # largest site, and if there are multiple, the earliest
2346 # alphabetically.
2347 to_cover = True
2348 for site_val in link_res2[0]['siteList']:
2349 site_dn = ldb.Dn(samdb, str(site_val))
2350 site_dn_str = site_dn.canonical_str()
2351 site_rdn = site_dn.get_rdn_value().lower()
2352 if sites_in_use[site_dn_str] > dc_count:
2353 to_cover = False
2354 break
2355 elif (sites_in_use[site_dn_str] == dc_count and
2356 site_rdn < site_name.lower()):
2357 to_cover = False
2358 break
2360 if to_cover:
2361 site_cover_rdn = site.dn.get_rdn_value()
2362 sites_to_cover.append(site_cover_rdn.lower())
2364 return sites_to_cover