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