s4:scripting/python: always treat the highwatermark as opaque (bug #9508)
[sfrench/samba-autobuild/.git] / source4 / scripting / bin / samba_kcc
1 #!/usr/bin/env python
2 #
3 # Compute our KCC topology
4 #
5 # Copyright (C) Dave Craft 2011
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import os
21 import sys
22 import random
23
24 # ensure we get messages out immediately, so they get in the samba logs,
25 # and don't get swallowed by a timeout
26 os.environ['PYTHONUNBUFFERED'] = '1'
27
28 # forcing GMT avoids a problem in some timezones with kerberos. Both MIT
29 # heimdal can get mutual authentication errors due to the 24 second difference
30 # between UTC and GMT when using some zone files (eg. the PDT zone from
31 # the US)
32 os.environ["TZ"] = "GMT"
33
34 # Find right directory when running from source tree
35 sys.path.insert(0, "bin/python")
36
37 import optparse
38 import logging
39
40 from samba import (
41     getopt as options,
42     Ldb,
43     ldb,
44     dsdb,
45     read_and_sub_file)
46 from samba.auth import system_session
47 from samba.samdb import SamDB
48 from samba.dcerpc import drsuapi
49 from samba.kcc_utils import *
50
51 class KCC(object):
52     """The Knowledge Consistency Checker class.
53
54     A container for objects and methods allowing a run of the KCC.  Produces a
55     set of connections in the samdb for which the Distributed Replication
56     Service can then utilize to replicate naming contexts
57     """
58     def __init__(self):
59         """Initializes the partitions class which can hold
60         our local DCs partitions or all the partitions in
61         the forest
62         """
63         self.part_table = {}    # partition objects
64         self.site_table = {}
65         self.transport_table = {}
66         self.sitelink_table = {}
67
68         # Used in inter-site topology computation.  A list
69         # of connections (by NTDSConnection object) that are
70         # to be kept when pruning un-needed NTDS Connections
71         self.keep_connection_list = []
72
73         self.my_dsa_dnstr = None  # My dsa DN
74         self.my_dsa = None  # My dsa object
75
76         self.my_site_dnstr = None
77         self.my_site = None
78
79         self.samdb = None
80
81     def load_all_transports(self):
82         """Loads the inter-site transport objects for Sites
83
84         ::returns: Raises an Exception on error
85         """
86         try:
87             res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
88                                     self.samdb.get_config_basedn(),
89                                     scope=ldb.SCOPE_SUBTREE,
90                                     expression="(objectClass=interSiteTransport)")
91         except ldb.LdbError, (enum, estr):
92             raise Exception("Unable to find inter-site transports - (%s)" %
93                     estr)
94
95         for msg in res:
96             dnstr = str(msg.dn)
97
98             # already loaded
99             if dnstr in self.transport_table.keys():
100                 continue
101
102             transport = Transport(dnstr)
103
104             transport.load_transport(self.samdb)
105
106             # Assign this transport to table
107             # and index by dn
108             self.transport_table[dnstr] = transport
109
110     def load_all_sitelinks(self):
111         """Loads the inter-site siteLink objects
112
113         ::returns: Raises an Exception on error
114         """
115         try:
116             res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
117                                     self.samdb.get_config_basedn(),
118                                     scope=ldb.SCOPE_SUBTREE,
119                                     expression="(objectClass=siteLink)")
120         except ldb.LdbError, (enum, estr):
121             raise Exception("Unable to find inter-site siteLinks - (%s)" % estr)
122
123         for msg in res:
124             dnstr = str(msg.dn)
125
126             # already loaded
127             if dnstr in self.sitelink_table.keys():
128                 continue
129
130             sitelink = SiteLink(dnstr)
131
132             sitelink.load_sitelink(self.samdb)
133
134             # Assign this siteLink to table
135             # and index by dn
136             self.sitelink_table[dnstr] = sitelink
137
138     def get_sitelink(self, site1_dnstr, site2_dnstr):
139         """Return the siteLink (if it exists) that connects the
140         two input site DNs
141         """
142         for sitelink in self.sitelink_table.values():
143             if sitelink.is_sitelink(site1_dnstr, site2_dnstr):
144                 return sitelink
145         return None
146
147     def load_my_site(self):
148         """Loads the Site class for the local DSA
149
150         ::returns: Raises an Exception on error
151         """
152         self.my_site_dnstr = "CN=%s,CN=Sites,%s" % (
153                               self.samdb.server_site_name(),
154                               self.samdb.get_config_basedn())
155         site = Site(self.my_site_dnstr)
156         site.load_site(self.samdb)
157
158         self.site_table[self.my_site_dnstr] = site
159         self.my_site = site
160
161     def load_all_sites(self):
162         """Discover all sites and instantiate and load each
163         NTDS Site settings.
164
165         ::returns: Raises an Exception on error
166         """
167         try:
168             res = self.samdb.search("CN=Sites,%s" %
169                                     self.samdb.get_config_basedn(),
170                                     scope=ldb.SCOPE_SUBTREE,
171                                     expression="(objectClass=site)")
172         except ldb.LdbError, (enum, estr):
173             raise Exception("Unable to find sites - (%s)" % estr)
174
175         for msg in res:
176             sitestr = str(msg.dn)
177
178             # already loaded
179             if sitestr in self.site_table.keys():
180                 continue
181
182             site = Site(sitestr)
183             site.load_site(self.samdb)
184
185             self.site_table[sitestr] = site
186
187     def load_my_dsa(self):
188         """Discover my nTDSDSA dn thru the rootDSE entry
189
190         ::returns: Raises an Exception on error.
191         """
192         dn = ldb.Dn(self.samdb, "")
193         try:
194             res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
195                                     attrs=["dsServiceName"])
196         except ldb.LdbError, (enum, estr):
197             raise Exception("Unable to find my nTDSDSA - (%s)" % estr)
198
199         self.my_dsa_dnstr = res[0]["dsServiceName"][0]
200         self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
201
202     def load_all_partitions(self):
203         """Discover all NCs thru the Partitions dn and
204         instantiate and load the NCs.
205
206         Each NC is inserted into the part_table by partition
207         dn string (not the nCName dn string)
208
209         ::returns: Raises an Exception on error
210         """
211         try:
212             res = self.samdb.search("CN=Partitions,%s" %
213                                     self.samdb.get_config_basedn(),
214                                     scope=ldb.SCOPE_SUBTREE,
215                                     expression="(objectClass=crossRef)")
216         except ldb.LdbError, (enum, estr):
217             raise Exception("Unable to find partitions - (%s)" % estr)
218
219         for msg in res:
220             partstr = str(msg.dn)
221
222             # already loaded
223             if partstr in self.part_table.keys():
224                 continue
225
226             part = Partition(partstr)
227
228             part.load_partition(self.samdb)
229             self.part_table[partstr] = part
230
231     def should_be_present_test(self):
232         """Enumerate all loaded partitions and DSAs in local
233         site and test if NC should be present as replica
234         """
235         for partdn, part in self.part_table.items():
236             for dsadn, dsa in self.my_site.dsa_table.items():
237                needed, ro, partial = part.should_be_present(dsa)
238                logger.info("dsadn:%s\nncdn:%s\nneeded=%s:ro=%s:partial=%s\n" %
239                            (dsadn, part.nc_dnstr, needed, ro, partial))
240
241     def refresh_failed_links_connections(self):
242         # XXX - not implemented yet
243         pass
244
245     def is_stale_link_connection(self, target_dsa):
246         """Returns False if no tuple z exists in the kCCFailedLinks or
247         kCCFailedConnections variables such that z.UUIDDsa is the
248         objectGUID of the target dsa, z.FailureCount > 0, and
249         the current time - z.TimeFirstFailure > 2 hours.
250         """
251         # XXX - not implemented yet
252         return False
253
254     def remove_unneeded_failed_links_connections(self):
255         # XXX - not implemented yet
256         pass
257
258     def remove_unneeded_ntdsconn(self, all_connected):
259         """Removes unneeded NTDS Connections after computation
260         of KCC intra and inter-site topology has finished.
261         """
262         mydsa = self.my_dsa
263
264         # Loop thru connections
265         for cn_dnstr, cn_conn in mydsa.connect_table.items():
266
267             s_dnstr = cn_conn.get_from_dnstr()
268             if s_dnstr is None:
269                 cn_conn.to_be_deleted = True
270                 continue
271
272             # Get the source DSA no matter what site
273             s_dsa = self.get_dsa(s_dnstr)
274
275             # Check if the DSA is in our site
276             if self.my_site.same_site(s_dsa):
277                 same_site = True
278             else:
279                 same_site = False
280
281             # Given an nTDSConnection object cn, if the DC with the
282             # nTDSDSA object dc that is the parent object of cn and
283             # the DC with the nTDSDA object referenced by cn!fromServer
284             # are in the same site, the KCC on dc deletes cn if all of
285             # the following are true:
286             #
287             # Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
288             #
289             # No site settings object s exists for the local DC's site, or
290             # bit NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED is clear in
291             # s!options.
292             #
293             # Another nTDSConnection object cn2 exists such that cn and
294             # cn2 have the same parent object, cn!fromServer = cn2!fromServer,
295             # and either
296             #
297             #     cn!whenCreated < cn2!whenCreated
298             #
299             #     cn!whenCreated = cn2!whenCreated and
300             #     cn!objectGUID < cn2!objectGUID
301             #
302             # Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
303             if same_site:
304                 if not cn_conn.is_generated():
305                     continue
306
307                 if self.my_site.is_cleanup_ntdsconn_disabled():
308                     continue
309
310                 # Loop thru connections looking for a duplicate that
311                 # fulfills the previous criteria
312                 lesser = False
313
314                 for cn2_dnstr, cn2_conn in mydsa.connect_table.items():
315                     if cn2_conn is cn_conn:
316                         continue
317
318                     s2_dnstr = cn2_conn.get_from_dnstr()
319                     if s2_dnstr is None:
320                         continue
321
322                     # If the NTDS Connections has a different
323                     # fromServer field then no match
324                     if s2_dnstr != s_dnstr:
325                         continue
326
327                     lesser = (cn_conn.whenCreated < cn2_conn.whenCreated or
328                               (cn_conn.whenCreated == cn2_conn.whenCreated and
329                                cmp(cn_conn.guid, cn2_conn.guid) < 0))
330
331                     if lesser:
332                         break
333
334                 if lesser and not cn_conn.is_rodc_topology():
335                     cn_conn.to_be_deleted = True
336
337             # Given an nTDSConnection object cn, if the DC with the nTDSDSA
338             # object dc that is the parent object of cn and the DC with
339             # the nTDSDSA object referenced by cn!fromServer are in
340             # different sites, a KCC acting as an ISTG in dc's site
341             # deletes cn if all of the following are true:
342             #
343             #     Bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options.
344             #
345             #     cn!fromServer references an nTDSDSA object for a DC
346             #     in a site other than the local DC's site.
347             #
348             #     The keepConnections sequence returned by
349             #     CreateIntersiteConnections() does not contain
350             #     cn!objectGUID, or cn is "superseded by" (see below)
351             #     another nTDSConnection cn2 and keepConnections
352             #     contains cn2!objectGUID.
353             #
354             #     The return value of CreateIntersiteConnections()
355             #     was true.
356             #
357             #     Bit NTDSCONN_OPT_RODC_TOPOLOGY is clear in
358             #     cn!options
359             #
360             else: # different site
361
362                 if not mydsa.is_istg():
363                     continue
364
365                 if not cn_conn.is_generated():
366                     continue
367
368                 if self.keep_connection(cn_conn):
369                     continue
370
371                 # XXX - To be implemented
372
373                 if not all_connected:
374                     continue
375
376                 if not cn_conn.is_rodc_topology():
377                     cn_conn.to_be_deleted = True
378
379
380         if opts.readonly:
381             for dnstr, connect in mydsa.connect_table.items():
382                 if connect.to_be_deleted:
383                     logger.info("TO BE DELETED:\n%s" % connect)
384                 if connect.to_be_added:
385                     logger.info("TO BE ADDED:\n%s" % connect)
386
387             # Peform deletion from our tables but perform
388             # no database modification
389             mydsa.commit_connections(self.samdb, ro=True)
390         else:
391             # Commit any modified connections
392             mydsa.commit_connections(self.samdb)
393
394     def get_dsa_by_guidstr(self, guidstr):
395         """Given a DSA guid string, consule all sites looking
396         for the corresponding DSA and return it.
397         """
398         for site in self.site_table.values():
399             dsa = site.get_dsa_by_guidstr(guidstr)
400             if dsa is not None:
401                 return dsa
402         return None
403
404     def get_dsa(self, dnstr):
405         """Given a DSA dn string, consule all sites looking
406         for the corresponding DSA and return it.
407         """
408         for site in self.site_table.values():
409             dsa = site.get_dsa(dnstr)
410             if dsa is not None:
411                 return dsa
412         return None
413
414     def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
415         """Update t_repsFrom if necessary to satisfy requirements. Such
416         updates are typically required when the IDL_DRSGetNCChanges
417         server has moved from one site to another--for example, to
418         enable compression when the server is moved from the
419         client's site to another site.
420
421         :param n_rep: NC replica we need
422         :param t_repsFrom: repsFrom tuple to modify
423         :param s_rep: NC replica at source DSA
424         :param s_dsa: source DSA
425         :param cn_conn: Local DSA NTDSConnection child
426
427         ::returns: (update) bit field containing which portion of the
428            repsFrom was modified.  This bit field is suitable as input
429            to IDL_DRSReplicaModify ulModifyFields element, as it consists
430            of these bits:
431                drsuapi.DRSUAPI_DRS_UPDATE_SCHEDULE
432                drsuapi.DRSUAPI_DRS_UPDATE_FLAGS
433                drsuapi.DRSUAPI_DRS_UPDATE_ADDRESS
434         """
435         s_dnstr = s_dsa.dsa_dnstr
436         update = 0x0
437
438         if self.my_site.same_site(s_dsa):
439             same_site = True
440         else:
441             same_site = False
442
443         times = cn_conn.convert_schedule_to_repltimes()
444
445         # if schedule doesn't match then update and modify
446         if times != t_repsFrom.schedule:
447             t_repsFrom.schedule = times
448
449         # Bit DRS_PER_SYNC is set in replicaFlags if and only
450         # if nTDSConnection schedule has a value v that specifies
451         # scheduled replication is to be performed at least once
452         # per week.
453         if cn_conn.is_schedule_minimum_once_per_week():
454
455             if (t_repsFrom.replica_flags &
456                 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0:
457                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
458
459         # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
460         # if the source DSA and the local DC's nTDSDSA object are
461         # in the same site or source dsa is the FSMO role owner
462         # of one or more FSMO roles in the NC replica.
463         if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
464
465             if (t_repsFrom.replica_flags &
466                 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0:
467                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
468
469         # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
470         # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
471         # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
472         # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
473         # t.replicaFlags if and only if s and the local DC's
474         # nTDSDSA object are in different sites.
475         if (cn_conn.options & dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0:
476
477             if (cn_conn.option & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
478
479                 if (t_repsFrom.replica_flags &
480                     drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
481                     t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
482
483         elif not same_site:
484
485             if (t_repsFrom.replica_flags &
486                 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0:
487                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
488
489         # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
490         # and only if s and the local DC's nTDSDSA object are
491         # not in the same site and the
492         # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
493         # clear in cn!options
494         if (not same_site and
495            (cn_conn.options &
496             dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
497
498             if (t_repsFrom.replica_flags &
499                 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0:
500                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
501
502         # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
503         # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
504         if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
505
506             if (t_repsFrom.replica_flags &
507                 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0:
508                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
509
510         # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
511         # set in t.replicaFlags if and only if cn!enabledConnection = false.
512         if not cn_conn.is_enabled():
513
514             if (t_repsFrom.replica_flags &
515                 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0:
516                 t_repsFrom.replica_flags |= \
517                     drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
518
519             if (t_repsFrom.replica_flags &
520                 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0:
521                 t_repsFrom.replica_flags |= \
522                     drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
523
524         # If s and the local DC's nTDSDSA object are in the same site,
525         # cn!transportType has no value, or the RDN of cn!transportType
526         # is CN=IP:
527         #
528         #     Bit DRS_MAIL_REP in t.replicaFlags is clear.
529         #
530         #     t.uuidTransport = NULL GUID.
531         #
532         #     t.uuidDsa = The GUID-based DNS name of s.
533         #
534         # Otherwise:
535         #
536         #     Bit DRS_MAIL_REP in t.replicaFlags is set.
537         #
538         #     If x is the object with dsname cn!transportType,
539         #     t.uuidTransport = x!objectGUID.
540         #
541         #     Let a be the attribute identified by
542         #     x!transportAddressAttribute. If a is
543         #     the dNSHostName attribute, t.uuidDsa = the GUID-based
544         #      DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
545         #
546         # It appears that the first statement i.e.
547         #
548         #     "If s and the local DC's nTDSDSA object are in the same
549         #      site, cn!transportType has no value, or the RDN of
550         #      cn!transportType is CN=IP:"
551         #
552         # could be a slightly tighter statement if it had an "or"
553         # between each condition.  I believe this should
554         # be interpreted as:
555         #
556         #     IF (same-site) OR (no-value) OR (type-ip)
557         #
558         # because IP should be the primary transport mechanism
559         # (even in inter-site) and the absense of the transportType
560         # attribute should always imply IP no matter if its multi-site
561         #
562         # NOTE MS-TECH INCORRECT:
563         #
564         #     All indications point to these statements above being
565         #     incorrectly stated:
566         #
567         #         t.uuidDsa = The GUID-based DNS name of s.
568         #
569         #         Let a be the attribute identified by
570         #         x!transportAddressAttribute. If a is
571         #         the dNSHostName attribute, t.uuidDsa = the GUID-based
572         #         DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
573         #
574         #     because the uuidDSA is a GUID and not a GUID-base DNS
575         #     name.  Nor can uuidDsa hold (s!parent)!a if not
576         #     dNSHostName.  What should have been said is:
577         #
578         #         t.naDsa = The GUID-based DNS name of s
579         #
580         #     That would also be correct if transportAddressAttribute
581         #     were "mailAddress" because (naDsa) can also correctly
582         #     hold the SMTP ISM service address.
583         #
584         nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
585
586         # We're not currently supporting SMTP replication
587         # so is_smtp_replication_available() is currently
588         # always returning False
589         if (same_site or
590             cn_conn.transport_dnstr is None or
591             cn_conn.transport_dnstr.find("CN=IP") == 0 or
592             not is_smtp_replication_available()):
593
594             if (t_repsFrom.replica_flags &
595                 drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0:
596                 t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
597
598             null_guid = misc.GUID()
599             if (t_repsFrom.transport_guid is None or
600                 t_repsFrom.transport_guid != null_guid):
601                 t_repsFrom.transport_guid = null_guid
602
603             # See (NOTE MS-TECH INCORRECT) above
604             if t_repsFrom.version == 0x1:
605                 if t_repsFrom.dns_name1 is None or \
606                    t_repsFrom.dns_name1 != nastr:
607                     t_repsFrom.dns_name1 = nastr
608             else:
609                 if t_repsFrom.dns_name1 is None or \
610                    t_repsFrom.dns_name2 is None or \
611                    t_repsFrom.dns_name1 != nastr or \
612                    t_repsFrom.dns_name2 != nastr:
613                     t_repsFrom.dns_name1 = nastr
614                     t_repsFrom.dns_name2 = nastr
615
616         else:
617             if (t_repsFrom.replica_flags &
618                 drsuapi.DRSUAPI_DRS_MAIL_REP) == 0x0:
619                 t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_MAIL_REP
620
621             # We have a transport type but its not an
622             # object in the database
623             if cn_conn.transport_dnstr not in self.transport_table.keys():
624                 raise Exception("Missing inter-site transport - (%s)" %
625                                 cn_conn.transport_dnstr)
626
627             x_transport = self.transport_table[cn_conn.transport_dnstr]
628
629             if t_repsFrom.transport_guid != x_transport.guid:
630                 t_repsFrom.transport_guid = x_transport.guid
631
632             # See (NOTE MS-TECH INCORRECT) above
633             if x_transport.address_attr == "dNSHostName":
634
635                 if t_repsFrom.version == 0x1:
636                     if t_repsFrom.dns_name1 is None or \
637                        t_repsFrom.dns_name1 != nastr:
638                         t_repsFrom.dns_name1 = nastr
639                 else:
640                     if t_repsFrom.dns_name1 is None or \
641                        t_repsFrom.dns_name2 is None or \
642                        t_repsFrom.dns_name1 != nastr or \
643                        t_repsFrom.dns_name2 != nastr:
644                         t_repsFrom.dns_name1 = nastr
645                         t_repsFrom.dns_name2 = nastr
646
647             else:
648                 # MS tech specification says we retrieve the named
649                 # attribute in "transportAddressAttribute" from the parent of
650                 # the DSA object
651                 try:
652                     pdnstr = s_dsa.get_parent_dnstr()
653                     attrs = [ x_transport.address_attr ]
654
655                     res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
656                                             attrs=attrs)
657                 except ldb.ldbError, (enum, estr):
658                     raise Exception \
659                         ("Unable to find attr (%s) for (%s) - (%s)" %
660                          (x_transport.address_attr, pdnstr, estr))
661
662                 msg = res[0]
663                 nastr = str(msg[x_transport.address_attr][0])
664
665                 # See (NOTE MS-TECH INCORRECT) above
666                 if t_repsFrom.version == 0x1:
667                     if t_repsFrom.dns_name1 is None or \
668                        t_repsFrom.dns_name1 != nastr:
669                         t_repsFrom.dns_name1 = nastr
670                 else:
671                     if t_repsFrom.dns_name1 is None or \
672                        t_repsFrom.dns_name2 is None or \
673                        t_repsFrom.dns_name1 != nastr or \
674                        t_repsFrom.dns_name2 != nastr:
675
676                         t_repsFrom.dns_name1 = nastr
677                         t_repsFrom.dns_name2 = nastr
678
679         if t_repsFrom.is_modified():
680             logger.debug("modify_repsFrom(): %s" % t_repsFrom)
681
682     def is_repsFrom_implied(self, n_rep, cn_conn):
683         """Given a NC replica and NTDS Connection, determine if the connection
684         implies a repsFrom tuple should be present from the source DSA listed
685         in the connection to the naming context
686
687         :param n_rep: NC replica
688         :param conn: NTDS Connection
689         ::returns (True || False), source DSA:
690         """
691         # NTDS Connection must satisfy all the following criteria
692         # to imply a repsFrom tuple is needed:
693         #
694         #    cn!enabledConnection = true.
695         #    cn!options does not contain NTDSCONN_OPT_RODC_TOPOLOGY.
696         #    cn!fromServer references an nTDSDSA object.
697         s_dsa = None
698
699         if cn_conn.is_enabled() and not cn_conn.is_rodc_topology():
700
701             s_dnstr = cn_conn.get_from_dnstr()
702             if s_dnstr is not None:
703                 s_dsa = self.get_dsa(s_dnstr)
704
705             # No DSA matching this source DN string?
706             if s_dsa is None:
707                 return False, None
708
709         # To imply a repsFrom tuple is needed, each of these
710         # must be True:
711         #
712         #     An NC replica of the NC "is present" on the DC to
713         #     which the nTDSDSA object referenced by cn!fromServer
714         #     corresponds.
715         #
716         #     An NC replica of the NC "should be present" on
717         #     the local DC
718         s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
719
720         if s_rep is None or not s_rep.is_present():
721             return False, None
722
723         # To imply a repsFrom tuple is needed, each of these
724         # must be True:
725         #
726         #     The NC replica on the DC referenced by cn!fromServer is
727         #     a writable replica or the NC replica that "should be
728         #     present" on the local DC is a partial replica.
729         #
730         #     The NC is not a domain NC, the NC replica that
731         #     "should be present" on the local DC is a partial
732         #     replica, cn!transportType has no value, or
733         #     cn!transportType has an RDN of CN=IP.
734         #
735         implied = (not s_rep.is_ro() or n_rep.is_partial()) and \
736                   (not n_rep.is_domain() or
737                    n_rep.is_partial() or
738                    cn_conn.transport_dnstr is None or
739                    cn_conn.transport_dnstr.find("CN=IP") == 0)
740
741         if implied:
742             return True, s_dsa
743         else:
744             return False, None
745
746     def translate_ntdsconn(self):
747         """This function adjusts values of repsFrom abstract attributes of NC
748         replicas on the local DC to match those implied by
749         nTDSConnection objects.
750         """
751         logger.debug("translate_ntdsconn(): enter")
752
753         if self.my_dsa.is_translate_ntdsconn_disabled():
754             return
755
756         current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
757
758         # Filled in with replicas we currently have that need deleting
759         delete_rep_table = {}
760
761         # We're using the MS notation names here to allow
762         # correlation back to the published algorithm.
763         #
764         # n_rep      - NC replica (n)
765         # t_repsFrom - tuple (t) in n!repsFrom
766         # s_dsa      - Source DSA of the replica. Defined as nTDSDSA
767         #              object (s) such that (s!objectGUID = t.uuidDsa)
768         #              In our IDL representation of repsFrom the (uuidDsa)
769         #              attribute is called (source_dsa_obj_guid)
770         # cn_conn    - (cn) is nTDSConnection object and child of the local DC's
771         #              nTDSDSA object and (cn!fromServer = s)
772         # s_rep      - source DSA replica of n
773         #
774         # If we have the replica and its not needed
775         # then we add it to the "to be deleted" list.
776         for dnstr, n_rep in current_rep_table.items():
777             if dnstr not in needed_rep_table.keys():
778                 delete_rep_table[dnstr] = n_rep
779
780         # Now perform the scan of replicas we'll need
781         # and compare any current repsFrom against the
782         # connections
783         for dnstr, n_rep in needed_rep_table.items():
784
785             # load any repsFrom and fsmo roles as we'll
786             # need them during connection translation
787             n_rep.load_repsFrom(self.samdb)
788             n_rep.load_fsmo_roles(self.samdb)
789
790             # Loop thru the existing repsFrom tupples (if any)
791             for i, t_repsFrom in enumerate(n_rep.rep_repsFrom):
792
793                 # for each tuple t in n!repsFrom, let s be the nTDSDSA
794                 # object such that s!objectGUID = t.uuidDsa
795                 guidstr = str(t_repsFrom.source_dsa_obj_guid)
796                 s_dsa = self.get_dsa_by_guidstr(guidstr)
797
798                 # Source dsa is gone from config (strange)
799                 # so cleanup stale repsFrom for unlisted DSA
800                 if s_dsa is None:
801                     logger.debug("repsFrom source DSA guid (%s) not found" %
802                                  guidstr)
803                     t_repsFrom.to_be_deleted = True
804                     continue
805
806                 s_dnstr = s_dsa.dsa_dnstr
807
808                 # Retrieve my DSAs connection object (if it exists)
809                 # that specifies the fromServer equivalent to
810                 # the DSA that is specified in the repsFrom source
811                 cn_conn = self.my_dsa.get_connection_by_from_dnstr(s_dnstr)
812
813                 # Let (cn) be the nTDSConnection object such that (cn)
814                 # is a child of the local DC's nTDSDSA object and
815                 # (cn!fromServer = s) and (cn!options) does not contain
816                 # NTDSCONN_OPT_RODC_TOPOLOGY or NULL if no such (cn) exists.
817                 if cn_conn and cn_conn.is_rodc_topology():
818                     cn_conn = None
819
820                 # KCC removes this repsFrom tuple if any of the following
821                 # is true:
822                 #     cn = NULL.
823                 #
824                 #     No NC replica of the NC "is present" on DSA that
825                 #     would be source of replica
826                 #
827                 #     A writable replica of the NC "should be present" on
828                 #     the local DC, but a partial replica "is present" on
829                 #     the source DSA
830                 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
831
832                 if cn_conn is None or \
833                    s_rep is None or not s_rep.is_present() or \
834                    (not n_rep.is_ro() and s_rep.is_partial()):
835
836                     t_repsFrom.to_be_deleted = True
837                     continue
838
839                 # If the KCC did not remove t from n!repsFrom, it updates t
840                 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
841
842             # Loop thru connections and add implied repsFrom tuples
843             # for each NTDSConnection under our local DSA if the
844             # repsFrom is not already present
845             for cn_dnstr, cn_conn in self.my_dsa.connect_table.items():
846
847                 implied, s_dsa = self.is_repsFrom_implied(n_rep, cn_conn)
848                 if not implied:
849                     continue
850
851                 # Loop thru the existing repsFrom tupples (if any) and
852                 # if we already have a tuple for this connection then
853                 # no need to proceed to add.  It will have been changed
854                 # to have the correct attributes above
855                 for i, t_repsFrom in enumerate(n_rep.rep_repsFrom):
856
857                      guidstr = str(t_repsFrom.source_dsa_obj_guid)
858                      if s_dsa is self.get_dsa_by_guidstr(guidstr):
859                          s_dsa = None
860                          break
861
862                 if s_dsa is None:
863                     continue
864
865                 # Create a new RepsFromTo and proceed to modify
866                 # it according to specification
867                 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
868
869                 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
870
871                 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
872
873                 # Add to our NC repsFrom as this is newly computed
874                 if t_repsFrom.is_modified():
875                     n_rep.rep_repsFrom.append(t_repsFrom)
876
877             if opts.readonly:
878                 # Display any to be deleted or modified repsFrom
879                 text = n_rep.dumpstr_to_be_deleted()
880                 if text:
881                     logger.info("TO BE DELETED:\n%s" % text)
882                 text = n_rep.dumpstr_to_be_modified()
883                 if text:
884                     logger.info("TO BE MODIFIED:\n%s" % text)
885
886                 # Peform deletion from our tables but perform
887                 # no database modification
888                 n_rep.commit_repsFrom(self.samdb, ro=True)
889             else:
890                 # Commit any modified repsFrom to the NC replica
891                 n_rep.commit_repsFrom(self.samdb)
892
893     def keep_connection(self, cn_conn):
894         """Determines if the connection is meant to be kept during the
895         pruning of unneeded connections operation.
896
897         Consults the keep_connection_list[] which was built during
898         intersite NC replica graph computation.
899
900         ::returns (True or False): if (True) connection should not be pruned
901         """
902         if cn_conn in self.keep_connection_list:
903             return True
904         return False
905
906     def merge_failed_links(self):
907         """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
908         The KCC on a writable DC attempts to merge the link and connection
909         failure information from bridgehead DCs in its own site to help it
910         identify failed bridgehead DCs.
911         """
912         # MS-TECH Ref 6.2.2.3.2 Merge of kCCFailedLinks and kCCFailedLinks
913         #     from Bridgeheads
914
915         # XXX - not implemented yet
916
917     def setup_graph(self):
918         """Set up a GRAPH, populated with a VERTEX for each site
919         object, a MULTIEDGE for each siteLink object, and a
920         MUTLIEDGESET for each siteLinkBridge object (or implied
921         siteLinkBridge).
922
923         ::returns: a new graph
924         """
925         # XXX - not implemented yet
926         return None
927
928     def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
929         """Get a bridghead DC.
930
931         :param site: site object representing for which a bridgehead
932             DC is desired.
933         :param part: crossRef for NC to replicate.
934         :param transport: interSiteTransport object for replication
935             traffic.
936         :param partial_ok: True if a DC containing a partial
937             replica or a full replica will suffice, False if only
938             a full replica will suffice.
939         :param detect_failed: True to detect failed DCs and route
940             replication traffic around them, False to assume no DC
941             has failed.
942         ::returns: dsa object for the bridgehead DC or None
943         """
944
945         bhs = self.get_all_bridgeheads(site, part, transport,
946                                        partial_ok, detect_failed)
947         if len(bhs) == 0:
948             logger.debug("get_bridgehead: exit\n\tsitedn=%s\n\tbhdn=None" %
949                          site.site_dnstr)
950             return None
951         else:
952             logger.debug("get_bridgehead: exit\n\tsitedn=%s\n\tbhdn=%s" %
953                          (site.site_dnstr, bhs[0].dsa_dnstr))
954             return bhs[0]
955
956     def get_all_bridgeheads(self, site, part, transport,
957                             partial_ok, detect_failed):
958         """Get all bridghead DCs satisfying the given criteria
959
960         :param site: site object representing the site for which
961             bridgehead DCs are desired.
962         :param part: partition for NC to replicate.
963         :param transport: interSiteTransport object for
964             replication traffic.
965         :param partial_ok: True if a DC containing a partial
966             replica or a full replica will suffice, False if
967             only a full replica will suffice.
968         :param detect_ok: True to detect failed DCs and route
969             replication traffic around them, FALSE to assume
970             no DC has failed.
971         ::returns: list of dsa object for available bridgehead
972             DCs or None
973         """
974
975         bhs = []
976
977         logger.debug("get_all_bridgeheads: %s" % transport)
978
979         for key, dsa in site.dsa_table.items():
980
981             pdnstr = dsa.get_parent_dnstr()
982
983             # IF t!bridgeheadServerListBL has one or more values and
984             # t!bridgeheadServerListBL does not contain a reference
985             # to the parent object of dc then skip dc
986             if (len(transport.bridgehead_list) != 0 and
987                 pdnstr not in transport.bridgehead_list):
988                 continue
989
990             # IF dc is in the same site as the local DC
991             #    IF a replica of cr!nCName is not in the set of NC replicas
992             #    that "should be present" on dc or a partial replica of the
993             #    NC "should be present" but partialReplicasOkay = FALSE
994             #        Skip dc
995             if self.my_site.same_site(dsa):
996                 needed, ro, partial = part.should_be_present(dsa)
997                 if not needed or (partial and not partial_ok):
998                     continue
999
1000             # ELSE
1001             #     IF an NC replica of cr!nCName is not in the set of NC
1002             #     replicas that "are present" on dc or a partial replica of
1003             #     the NC "is present" but partialReplicasOkay = FALSE
1004             #          Skip dc
1005             else:
1006                 rep = dsa.get_current_replica(part.nc_dnstr)
1007                 if rep is None or (rep.is_partial() and not partial_ok):
1008                     continue
1009
1010             # IF AmIRODC() and cr!nCName corresponds to default NC then
1011             #     Let dsaobj be the nTDSDSA object of the dc
1012             #     IF  dsaobj.msDS-Behavior-Version < DS_BEHAVIOR_WIN2008
1013             #         Skip dc
1014             if self.my_dsa.is_ro() and part.is_default():
1015                 if not dsa.is_minimum_behavior(DS_BEHAVIOR_WIN2008):
1016                     continue
1017
1018             # IF t!name != "IP" and the parent object of dc has no value for
1019             # the attribute specified by t!transportAddressAttribute
1020             #     Skip dc
1021             if transport.name != "IP":
1022                 # MS tech specification says we retrieve the named
1023                 # attribute in "transportAddressAttribute" from the parent
1024                 # of the DSA object
1025                 try:
1026                     attrs = [ transport.address_attr ]
1027
1028                     res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
1029                                             attrs=attrs)
1030                 except ldb.ldbError, (enum, estr):
1031                     continue
1032
1033                 msg = res[0]
1034                 nastr = str(msg[transport.address_attr][0])
1035
1036             # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1037             #     Skip dc
1038             if self.is_bridgehead_failed(dsa, detect_failed):
1039                 continue
1040
1041             logger.debug("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1042             bhs.append(dsa)
1043
1044         # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1045         # s!options
1046         #    SORT bhs such that all GC servers precede DCs that are not GC
1047         #    servers, and otherwise by ascending objectGUID
1048         # ELSE
1049         #    SORT bhs in a random order
1050         if site.is_random_bridgehead_disabled():
1051             bhs.sort(sort_dsa_by_gc_and_guid)
1052         else:
1053             random.shuffle(bhs)
1054
1055         return bhs
1056
1057
1058     def is_bridgehead_failed(self, dsa, detect_failed):
1059         """Determine whether a given DC is known to be in a failed state
1060         ::returns: True if and only if the DC should be considered failed
1061         """
1062         # XXX - not implemented yet
1063         return False
1064
1065     def create_connection(self, part, rbh, rsite, transport,
1066                           lbh, lsite, link_opt, link_sched,
1067                           partial_ok, detect_failed):
1068         """Create an nTDSConnection object with the given parameters
1069         if one does not already exist.
1070
1071         :param part: crossRef object for the NC to replicate.
1072         :param rbh: nTDSDSA object for DC to act as the
1073             IDL_DRSGetNCChanges server (which is in a site other
1074             than the local DC's site).
1075         :param rsite: site of the rbh
1076         :param transport: interSiteTransport object for the transport
1077             to use for replication traffic.
1078         :param lbh: nTDSDSA object for DC to act as the
1079             IDL_DRSGetNCChanges client (which is in the local DC's site).
1080         :param lsite: site of the lbh
1081         :param link_opt: Replication parameters (aggregated siteLink options, etc.)
1082         :param link_sched: Schedule specifying the times at which
1083             to begin replicating.
1084         :partial_ok: True if bridgehead DCs containing partial
1085             replicas of the NC are acceptable.
1086         :param detect_failed: True to detect failed DCs and route
1087             replication traffic around them, FALSE to assume no DC
1088             has failed.
1089         """
1090         rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1091                                             partial_ok, False)
1092
1093         # MS-TECH says to compute rbhs_avail but then doesn't use it
1094         # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1095         #                                        partial_ok, detect_failed)
1096
1097         lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1098                                             partial_ok, False)
1099
1100         # MS-TECH says to compute lbhs_avail but then doesn't use it
1101         # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1102         #                                       partial_ok, detect_failed)
1103
1104         # FOR each nTDSConnection object cn such that the parent of cn is
1105         # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1106         for ldsa in lbhs_all:
1107             for cn in ldsa.connect_table.values():
1108
1109                 rdsa = None
1110                 for rdsa in rbhs_all:
1111                     if cn.from_dnstr == rdsa.dsa_dnstr:
1112                         break
1113
1114                 if rdsa is None:
1115                     continue
1116
1117                 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1118                 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1119                 # cn!transportType references t
1120                 if (cn.is_generated() and not cn.is_rodc_topology() and
1121                     cn.transport_dnstr == transport.dnstr):
1122
1123                     # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1124                     # cn!options and cn!schedule != sch
1125                     #     Perform an originating update to set cn!schedule to
1126                     #     sched
1127                     if (not cn.is_user_owned_schedule() and
1128                         not cn.is_equivalent_schedule(link_sched)):
1129                         cn.schedule = link_sched
1130                         cn.set_modified(True)
1131
1132                     # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1133                     # NTDSCONN_OPT_USE_NOTIFY are set in cn
1134                     if cn.is_override_notify_default() and \
1135                        cn.is_use_notify():
1136
1137                         # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1138                         # ri.Options
1139                         #    Perform an originating update to clear bits
1140                         #    NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1141                         #    NTDSCONN_OPT_USE_NOTIFY in cn!options
1142                         if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1143                             cn.options &= \
1144                                 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1145                                   dsdb.NTDSCONN_OPT_USE_NOTIFY)
1146                             cn.set_modified(True)
1147
1148                     # ELSE
1149                     else:
1150
1151                         # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1152                         # ri.Options
1153                         #     Perform an originating update to set bits
1154                         #     NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1155                         #     NTDSCONN_OPT_USE_NOTIFY in cn!options
1156                         if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1157                             cn.options |= \
1158                                 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1159                                  dsdb.NTDSCONN_OPT_USE_NOTIFY)
1160                             cn.set_modified(True)
1161
1162
1163                     # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1164                     if cn.is_twoway_sync():
1165
1166                         # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1167                         # ri.Options
1168                         #     Perform an originating update to clear bit
1169                         #     NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1170                         if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1171                             cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1172                             cn.set_modified(True)
1173
1174                     # ELSE
1175                     else:
1176
1177                         # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1178                         # ri.Options
1179                         #     Perform an originating update to set bit
1180                         #     NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1181                         if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1182                             cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1183                             cn.set_modified(True)
1184
1185
1186                     # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1187                     # in cn!options
1188                     if cn.is_intersite_compression_disabled():
1189
1190                         # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1191                         # in ri.Options
1192                         #     Perform an originating update to clear bit
1193                         #     NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1194                         #     cn!options
1195                         if (link_opt &
1196                             dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0:
1197                             cn.options &= \
1198                                 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1199                             cn.set_modified(True)
1200
1201                     # ELSE
1202                     else:
1203                         # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1204                         # ri.Options
1205                         #     Perform an originating update to set bit
1206                         #     NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1207                         #     cn!options
1208                         if (link_opt &
1209                             dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
1210                             cn.options |= \
1211                                 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1212                             cn.set_modified(True)
1213
1214                     # Display any modified connection
1215                     if opts.readonly:
1216                         if cn.to_be_modified:
1217                             logger.info("TO BE MODIFIED:\n%s" % cn)
1218
1219                         ldsa.commit_connections(self.samdb, ro=True)
1220                     else:
1221                         ldsa.commit_connections(self.samdb)
1222         # ENDFOR
1223
1224         valid_connections = 0
1225
1226         # FOR each nTDSConnection object cn such that cn!parent is
1227         # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1228         for ldsa in lbhs_all:
1229             for cn in ldsa.connect_table.values():
1230
1231                 rdsa = None
1232                 for rdsa in rbhs_all:
1233                     if cn.from_dnstr == rdsa.dsa_dnstr:
1234                         break
1235
1236                 if rdsa is None:
1237                     continue
1238
1239                 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1240                 # cn!transportType references t) and
1241                 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1242                 if ((not cn.is_generated() or
1243                      cn.transport_dnstr == transport.dnstr) and
1244                      not cn.is_rodc_topology()):
1245
1246                     # LET rguid be the objectGUID of the nTDSDSA object
1247                     # referenced by cn!fromServer
1248                     # LET lguid be (cn!parent)!objectGUID
1249
1250                     # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1251                     # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1252                     #     Increment cValidConnections by 1
1253                     if (not self.is_bridgehead_failed(rdsa, detect_failed) and
1254                         not self.is_bridgehead_failed(ldsa, detect_failed)):
1255                         valid_connections += 1
1256
1257                     # IF keepConnections does not contain cn!objectGUID
1258                     #     APPEND cn!objectGUID to keepConnections
1259                     if not self.keep_connection(cn):
1260                         self.keep_connection_list.append(cn)
1261
1262         # ENDFOR
1263
1264         # IF cValidConnections = 0
1265         if valid_connections == 0:
1266
1267             # LET opt be NTDSCONN_OPT_IS_GENERATED
1268             opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1269
1270             # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1271             #     SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1272             #     NTDSCONN_OPT_USE_NOTIFY in opt
1273             if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1274                 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1275                         dsdb.NTDSCONN_USE_NOTIFY)
1276
1277             # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1278             #     SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1279             if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1280                 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1281
1282             # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1283             # ri.Options
1284             #     SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1285             if (link_opt &
1286                 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
1287                 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1288
1289             # Perform an originating update to create a new nTDSConnection
1290             # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1291             # cn!options = opt, cn!transportType is a reference to t,
1292             # cn!fromServer is a reference to rbh, and cn!schedule = sch
1293             cn = lbh.new_connection(opt, 0, transport, lbh.dsa_dnstr, link_sched)
1294
1295             # Display any added connection
1296             if opts.readonly:
1297                 if cn.to_be_added:
1298                     logger.info("TO BE ADDED:\n%s" % cn)
1299
1300                     lbh.commit_connections(self.samdb, ro=True)
1301             else:
1302                 lbh.commit_connections(self.samdb)
1303
1304             # APPEND cn!objectGUID to keepConnections
1305             if not self.keep_connection(cn):
1306                 self.keep_connection_list.append(cn)
1307
1308     def create_connections(self, graph, part, detect_failed):
1309         """Construct an NC replica graph for the NC identified by
1310         the given crossRef, then create any additional nTDSConnection
1311         objects required.
1312
1313         :param graph: site graph.
1314         :param part: crossRef object for NC.
1315         :param detect_failed:  True to detect failed DCs and route
1316             replication traffic around them, False to assume no DC
1317             has failed.
1318
1319         Modifies self.keep_connection_list by adding any connections
1320         deemed to be "in use".
1321
1322         ::returns: (all_connected, found_failed_dc)
1323         (all_connected) True if the resulting NC replica graph
1324             connects all sites that need to be connected.
1325         (found_failed_dc) True if one or more failed DCs were
1326             detected.
1327         """
1328         all_connected = True
1329         found_failed = False
1330
1331         logger.debug("create_connections(): enter\n\tpartdn=%s\n\tdetect_failed=%s" %
1332                      (part.nc_dnstr, detect_failed))
1333
1334         # XXX - This is a highly abbreviated function from the MS-TECH
1335         #       ref.  It creates connections between bridgeheads to all
1336         #       sites that have appropriate replicas.  Thus we are not
1337         #       creating a minimum cost spanning tree but instead
1338         #       producing a fully connected tree.  This should produce
1339         #       a full (albeit not optimal cost) replication topology.
1340         my_vertex = Vertex(self.my_site, part)
1341         my_vertex.color_vertex()
1342
1343         # No NC replicas for this NC in the site of the local DC,
1344         # so no nTDSConnection objects need be created
1345         if my_vertex.is_white():
1346             return all_connected, found_failed
1347
1348         # LET partialReplicaOkay be TRUE if and only if
1349         # localSiteVertex.Color = COLOR.BLACK
1350         if my_vertex.is_black():
1351             partial_ok = True
1352         else:
1353             partial_ok = False
1354
1355         # Utilize the IP transport only for now
1356         transport = None
1357         for transport in self.transport_table.values():
1358             if transport.name == "IP":
1359                break
1360
1361         if transport is None:
1362             raise Exception("Unable to find inter-site transport for IP")
1363
1364         for rsite in self.site_table.values():
1365
1366             # We don't make connections to our own site as that
1367             # is intrasite topology generator's job
1368             if rsite is self.my_site:
1369                 continue
1370
1371             # Determine bridgehead server in remote site
1372             rbh = self.get_bridgehead(rsite, part, transport,
1373                                       partial_ok, detect_failed)
1374
1375             # RODC acts as an BH for itself
1376             # IF AmIRODC() then
1377             #     LET lbh be the nTDSDSA object of the local DC
1378             # ELSE
1379             #     LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1380             #     cr, t, partialReplicaOkay, detectFailedDCs)
1381             if self.my_dsa.is_ro():
1382                lsite = self.my_site
1383                lbh = self.my_dsa
1384             else:
1385                lsite = self.my_site
1386                lbh = self.get_bridgehead(lsite, part, transport,
1387                                            partial_ok, detect_failed)
1388
1389             # Find the siteLink object that enumerates the connection
1390             # between the two sites if it is present
1391             sitelink = self.get_sitelink(lsite.site_dnstr, rsite.site_dnstr)
1392             if sitelink is None:
1393                 link_opt = 0x0
1394                 link_sched = None
1395             else:
1396                 link_opt = sitelink.options
1397                 link_sched = sitelink.schedule
1398
1399             self.create_connection(part, rbh, rsite, transport,
1400                                    lbh, lsite, link_opt, link_sched,
1401                                    partial_ok, detect_failed)
1402
1403         return all_connected, found_failed
1404
1405     def create_intersite_connections(self):
1406         """Computes an NC replica graph for each NC replica that "should be
1407         present" on the local DC or "is present" on any DC in the same site
1408         as the local DC. For each edge directed to an NC replica on such a
1409         DC from an NC replica on a DC in another site, the KCC creates an
1410         nTDSConnection object to imply that edge if one does not already
1411         exist.
1412
1413         Modifies self.keep_connection_list - A list of nTDSConnection
1414         objects for edges that are directed
1415         to the local DC's site in one or more NC replica graphs.
1416
1417         returns: True if spanning trees were created for all NC replica
1418             graphs, otherwise False.
1419         """
1420         all_connected = True
1421         self.keep_connection_list = []
1422
1423         # LET crossRefList be the set containing each object o of class
1424         # crossRef such that o is a child of the CN=Partitions child of the
1425         # config NC
1426
1427         # FOR each crossRef object cr in crossRefList
1428         #    IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1429         #        is clear in cr!systemFlags, skip cr.
1430         #    LET g be the GRAPH return of SetupGraph()
1431
1432         for part in self.part_table.values():
1433
1434             if not part.is_enabled():
1435                 continue
1436
1437             if part.is_foreign():
1438                 continue
1439
1440             graph = self.setup_graph()
1441
1442             # Create nTDSConnection objects, routing replication traffic
1443             # around "failed" DCs.
1444             found_failed = False
1445
1446             connected, found_failed = self.create_connections(graph, part, True)
1447
1448             if not connected:
1449                 all_connected = False
1450
1451                 if found_failed:
1452                     # One or more failed DCs preclude use of the ideal NC
1453                     # replica graph. Add connections for the ideal graph.
1454                     self.create_connections(graph, part, False)
1455
1456         return all_connected
1457
1458     def intersite(self):
1459         """The head method for generating the inter-site KCC replica
1460         connection graph and attendant nTDSConnection objects
1461         in the samdb.
1462
1463         Produces self.keep_connection_list[] of NTDS Connections
1464         that should be kept during subsequent pruning process.
1465
1466         ::return (True or False):  (True) if the produced NC replica
1467             graph connects all sites that need to be connected
1468         """
1469
1470         # Retrieve my DSA
1471         mydsa = self.my_dsa
1472         mysite = self.my_site
1473         all_connected = True
1474
1475         logger.debug("intersite(): enter")
1476
1477         # Determine who is the ISTG
1478         if opts.readonly:
1479             mysite.select_istg(self.samdb, mydsa, ro=True)
1480         else:
1481             mysite.select_istg(self.samdb, mydsa, ro=False)
1482
1483         # Test whether local site has topology disabled
1484         if mysite.is_intersite_topology_disabled():
1485             logger.debug("intersite(): exit disabled all_connected=%d" %
1486                          all_connected)
1487             return all_connected
1488
1489         if not mydsa.is_istg():
1490             logger.debug("intersite(): exit not istg all_connected=%d" %
1491                          all_connected)
1492             return all_connected
1493
1494         self.merge_failed_links()
1495
1496         # For each NC with an NC replica that "should be present" on the
1497         # local DC or "is present" on any DC in the same site as the
1498         # local DC, the KCC constructs a site graph--a precursor to an NC
1499         # replica graph. The site connectivity for a site graph is defined
1500         # by objects of class interSiteTransport, siteLink, and
1501         # siteLinkBridge in the config NC.
1502
1503         all_connected = self.create_intersite_connections()
1504
1505         logger.debug("intersite(): exit all_connected=%d" % all_connected)
1506         return all_connected
1507
1508     def update_rodc_connection(self):
1509         """Runs when the local DC is an RODC and updates the RODC NTFRS
1510         connection object.
1511         """
1512         # Given an nTDSConnection object cn1, such that cn1.options contains
1513         # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1514         # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1515         # that the following is true:
1516         #
1517         #     cn1.fromServer = cn2.fromServer
1518         #     cn1.schedule = cn2.schedule
1519         #
1520         # If no such cn2 can be found, cn1 is not modified.
1521         # If no such cn1 can be found, nothing is modified by this task.
1522
1523         # XXX - not implemented yet
1524
1525     def intrasite_max_node_edges(self, node_count):
1526         """Returns the maximum number of edges directed to a node in
1527         the intrasite replica graph.
1528
1529         The KCC does not create more
1530         than 50 edges directed to a single DC. To optimize replication,
1531         we compute that each node should have n+2 total edges directed
1532         to it such that (n) is the smallest non-negative integer
1533         satisfying (node_count <= 2*(n*n) + 6*n + 7)
1534
1535         :param node_count: total number of nodes in the replica graph
1536         """
1537         n = 0
1538         while True:
1539             if node_count <= (2 * (n * n) + (6 * n) + 7):
1540                 break
1541             n = n + 1
1542         n = n + 2
1543         if n < 50:
1544             return n
1545         return 50
1546
1547     def construct_intrasite_graph(self, site_local, dc_local,
1548                                   nc_x, gc_only, detect_stale):
1549
1550         # We're using the MS notation names here to allow
1551         # correlation back to the published algorithm.
1552         #
1553         # nc_x     - naming context (x) that we are testing if it
1554         #            "should be present" on the local DC
1555         # f_of_x   - replica (f) found on a DC (s) for NC (x)
1556         # dc_s     - DC where f_of_x replica was found
1557         # dc_local - local DC that potentially needs a replica
1558         #            (f_of_x)
1559         # r_list   - replica list R
1560         # p_of_x   - replica (p) is partial and found on a DC (s)
1561         #            for NC (x)
1562         # l_of_x   - replica (l) is the local replica for NC (x)
1563         #            that should appear on the local DC
1564         # r_len = is length of replica list |R|
1565         #
1566         # If the DSA doesn't need a replica for this
1567         # partition (NC x) then continue
1568         needed, ro, partial = nc_x.should_be_present(dc_local)
1569
1570         logger.debug("construct_intrasite_graph(): enter" +
1571                      "\n\tgc_only=%d" % gc_only +
1572                      "\n\tdetect_stale=%d" % detect_stale +
1573                      "\n\tneeded=%s" % needed +
1574                      "\n\tro=%s" % ro +
1575                      "\n\tpartial=%s" % partial +
1576                      "\n%s" % nc_x)
1577
1578         if not needed:
1579             return
1580
1581         # Create a NCReplica that matches what the local replica
1582         # should say.  We'll use this below in our r_list
1583         l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
1584                            nc_x.nc_dnstr)
1585
1586         l_of_x.identify_by_basedn(self.samdb)
1587
1588         l_of_x.rep_partial = partial
1589         l_of_x.rep_ro = ro
1590
1591         # Add this replica that "should be present" to the
1592         # needed replica table for this DSA
1593         dc_local.add_needed_replica(l_of_x)
1594
1595         # Empty replica sequence list
1596         r_list = []
1597
1598         # We'll loop thru all the DSAs looking for
1599         # writeable NC replicas that match the naming
1600         # context dn for (nc_x)
1601         #
1602         for dc_s_dn, dc_s in self.my_site.dsa_table.items():
1603
1604             # If this partition (nc_x) doesn't appear as a
1605             # replica (f_of_x) on (dc_s) then continue
1606             if not nc_x.nc_dnstr in dc_s.current_rep_table.keys():
1607                 continue
1608
1609             # Pull out the NCReplica (f) of (x) with the dn
1610             # that matches NC (x) we are examining.
1611             f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
1612
1613             # Replica (f) of NC (x) must be writable
1614             if f_of_x.is_ro():
1615                 continue
1616
1617             # Replica (f) of NC (x) must satisfy the
1618             # "is present" criteria for DC (s) that
1619             # it was found on
1620             if not f_of_x.is_present():
1621                 continue
1622
1623             # DC (s) must be a writable DSA other than
1624             # my local DC.  In other words we'd only replicate
1625             # from other writable DC
1626             if dc_s.is_ro() or dc_s is dc_local:
1627                 continue
1628
1629             # Certain replica graphs are produced only
1630             # for global catalogs, so test against
1631             # method input parameter
1632             if gc_only and not dc_s.is_gc():
1633                 continue
1634
1635             # DC (s) must be in the same site as the local DC
1636             # as this is the intra-site algorithm. This is
1637             # handled by virtue of placing DSAs in per
1638             # site objects (see enclosing for() loop)
1639
1640             # If NC (x) is intended to be read-only full replica
1641             # for a domain NC on the target DC then the source
1642             # DC should have functional level at minimum WIN2008
1643             #
1644             # Effectively we're saying that in order to replicate
1645             # to a targeted RODC (which was introduced in Windows 2008)
1646             # then we have to replicate from a DC that is also minimally
1647             # at that level.
1648             #
1649             # You can also see this requirement in the MS special
1650             # considerations for RODC which state that to deploy
1651             # an RODC, at least one writable domain controller in
1652             # the domain must be running Windows Server 2008
1653             if ro and not partial and nc_x.nc_type == NCType.domain:
1654                 if not dc_s.is_minimum_behavior(DS_BEHAVIOR_WIN2008):
1655                     continue
1656
1657             # If we haven't been told to turn off stale connection
1658             # detection and this dsa has a stale connection then
1659             # continue
1660             if detect_stale and self.is_stale_link_connection(dc_s):
1661                continue
1662
1663             # Replica meets criteria.  Add it to table indexed
1664             # by the GUID of the DC that it appears on
1665             r_list.append(f_of_x)
1666
1667         # If a partial (not full) replica of NC (x) "should be present"
1668         # on the local DC, append to R each partial replica (p of x)
1669         # such that p "is present" on a DC satisfying the same
1670         # criteria defined above for full replica DCs.
1671         if partial:
1672
1673             # Now we loop thru all the DSAs looking for
1674             # partial NC replicas that match the naming
1675             # context dn for (NC x)
1676             for dc_s_dn, dc_s in self.my_site.dsa_table.items():
1677
1678                 # If this partition NC (x) doesn't appear as a
1679                 # replica (p) of NC (x) on the dsa DC (s) then
1680                 # continue
1681                 if not nc_x.nc_dnstr in dc_s.current_rep_table.keys():
1682                     continue
1683
1684                 # Pull out the NCReplica with the dn that
1685                 # matches NC (x) we are examining.
1686                 p_of_x = dsa.current_rep_table[nc_x.nc_dnstr]
1687
1688                 # Replica (p) of NC (x) must be partial
1689                 if not p_of_x.is_partial():
1690                     continue
1691
1692                 # Replica (p) of NC (x) must satisfy the
1693                 # "is present" criteria for DC (s) that
1694                 # it was found on
1695                 if not p_of_x.is_present():
1696                     continue
1697
1698                 # DC (s) must be a writable DSA other than
1699                 # my DSA.  In other words we'd only replicate
1700                 # from other writable DSA
1701                 if dc_s.is_ro() or dc_s is dc_local:
1702                     continue
1703
1704                 # Certain replica graphs are produced only
1705                 # for global catalogs, so test against
1706                 # method input parameter
1707                 if gc_only and not dc_s.is_gc():
1708                     continue
1709
1710                 # DC (s) must be in the same site as the local DC
1711                 # as this is the intra-site algorithm. This is
1712                 # handled by virtue of placing DSAs in per
1713                 # site objects (see enclosing for() loop)
1714
1715                 # This criteria is moot (a no-op) for this case
1716                 # because we are scanning for (partial = True).  The
1717                 # MS algorithm statement says partial replica scans
1718                 # should adhere to the "same" criteria as full replica
1719                 # scans so the criteria doesn't change here...its just
1720                 # rendered pointless.
1721                 #
1722                 # The case that is occurring would be a partial domain
1723                 # replica is needed on a local DC global catalog.  There
1724                 # is no minimum windows behavior for those since GCs
1725                 # have always been present.
1726                 if ro and not partial and nc_x.nc_type == NCType.domain:
1727                     if not dc_s.is_minimum_behavior(DS_BEHAVIOR_WIN2008):
1728                         continue
1729
1730                 # If we haven't been told to turn off stale connection
1731                 # detection and this dsa has a stale connection then
1732                 # continue
1733                 if detect_stale and self.is_stale_link_connection(dc_s):
1734                     continue
1735
1736                 # Replica meets criteria.  Add it to table indexed
1737                 # by the GUID of the DSA that it appears on
1738                 r_list.append(p_of_x)
1739
1740         # Append to R the NC replica that "should be present"
1741         # on the local DC
1742         r_list.append(l_of_x)
1743
1744         r_list.sort(sort_replica_by_dsa_guid)
1745
1746         r_len = len(r_list)
1747
1748         max_node_edges = self.intrasite_max_node_edges(r_len)
1749
1750         # Add a node for each r_list element to the replica graph
1751         graph_list = []
1752         for rep in r_list:
1753             node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
1754             graph_list.append(node)
1755
1756         # For each r(i) from (0 <= i < |R|-1)
1757         i = 0
1758         while i < (r_len-1):
1759             # Add an edge from r(i) to r(i+1) if r(i) is a full
1760             # replica or r(i+1) is a partial replica
1761             if not r_list[i].is_partial() or r_list[i+1].is_partial():
1762                 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
1763
1764             # Add an edge from r(i+1) to r(i) if r(i+1) is a full
1765             # replica or ri is a partial replica.
1766             if not r_list[i+1].is_partial() or r_list[i].is_partial():
1767                 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
1768             i = i + 1
1769
1770         # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
1771         # or r0 is a partial replica.
1772         if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
1773             graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
1774
1775         # Add an edge from r0 to r|R|-1 if r0 is a full replica or
1776         # r|R|-1 is a partial replica.
1777         if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
1778             graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
1779
1780         # For each existing nTDSConnection object implying an edge
1781         # from rj of R to ri such that j != i, an edge from rj to ri
1782         # is not already in the graph, and the total edges directed
1783         # to ri is less than n+2, the KCC adds that edge to the graph.
1784         i = 0
1785         while i < r_len:
1786             dsa = self.my_site.dsa_table[graph_list[i].dsa_dnstr]
1787             graph_list[i].add_edges_from_connections(dsa)
1788             i = i + 1
1789
1790         i = 0
1791         while i < r_len:
1792             tnode = graph_list[i]
1793
1794             # To optimize replication latency in sites with many NC replicas, the
1795             # KCC adds new edges directed to ri to bring the total edges to n+2,
1796             # where the NC replica rk of R from which the edge is directed
1797             # is chosen at random such that k != i and an edge from rk to ri
1798             # is not already in the graph.
1799             #
1800             # Note that the KCC tech ref does not give a number for the definition
1801             # of "sites with many NC replicas".   At a bare minimum to satisfy
1802             # n+2 edges directed at a node we have to have at least three replicas
1803             # in |R| (i.e. if n is zero then at least replicas from two other graph
1804             # nodes may direct edges to us).
1805             if r_len >= 3:
1806                 # pick a random index
1807                 findex = rindex = random.randint(0, r_len-1)
1808
1809                 # while this node doesn't have sufficient edges
1810                 while not tnode.has_sufficient_edges():
1811                     # If this edge can be successfully added (i.e. not
1812                     # the same node and edge doesn't already exist) then
1813                     # select a new random index for the next round
1814                     if tnode.add_edge_from(graph_list[rindex].dsa_dnstr):
1815                         findex = rindex = random.randint(0, r_len-1)
1816                     else:
1817                         # Otherwise continue looking against each node
1818                         # after the random selection
1819                         rindex = rindex + 1
1820                         if rindex >= r_len:
1821                             rindex = 0
1822
1823                         if rindex == findex:
1824                             logger.error("Unable to satisfy max edge criteria!")
1825                             break
1826
1827             # Print the graph node in debug mode
1828             logger.debug("%s" % tnode)
1829
1830             # For each edge directed to the local DC, ensure a nTDSConnection
1831             # points to us that satisfies the KCC criteria
1832             if graph_list[i].dsa_dnstr == dc_local.dsa_dnstr:
1833                 graph_list[i].add_connections_from_edges(dc_local)
1834
1835             i = i + 1
1836
1837     def intrasite(self):
1838         """The head method for generating the intra-site KCC replica
1839         connection graph and attendant nTDSConnection objects
1840         in the samdb
1841         """
1842         # Retrieve my DSA
1843         mydsa = self.my_dsa
1844
1845         logger.debug("intrasite(): enter")
1846
1847         # Test whether local site has topology disabled
1848         mysite = self.site_table[self.my_site_dnstr]
1849         if mysite.is_intrasite_topology_disabled():
1850             return
1851
1852         detect_stale = (not mysite.is_detect_stale_disabled())
1853
1854         # Loop thru all the partitions.
1855         for partdn, part in self.part_table.items():
1856             self.construct_intrasite_graph(mysite, mydsa, part, False,
1857                                            detect_stale)
1858
1859         # If the DC is a GC server, the KCC constructs an additional NC
1860         # replica graph (and creates nTDSConnection objects) for the
1861         # config NC as above, except that only NC replicas that "are present"
1862         # on GC servers are added to R.
1863         for partdn, part in self.part_table.items():
1864             if part.is_config():
1865                 self.construct_intrasite_graph(mysite, mydsa, part, True,
1866                                                detect_stale)
1867
1868         # The DC repeats the NC replica graph computation and nTDSConnection
1869         # creation for each of the NC replica graphs, this time assuming
1870         # that no DC has failed. It does so by re-executing the steps as
1871         # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
1872         # set in the options attribute of the site settings object for
1873         # the local DC's site.  (ie. we set "detec_stale" flag to False)
1874
1875         # Loop thru all the partitions.
1876         for partdn, part in self.part_table.items():
1877             self.construct_intrasite_graph(mysite, mydsa, part, False,
1878                                            False) # don't detect stale
1879
1880         # If the DC is a GC server, the KCC constructs an additional NC
1881         # replica graph (and creates nTDSConnection objects) for the
1882         # config NC as above, except that only NC replicas that "are present"
1883         # on GC servers are added to R.
1884         for partdn, part in self.part_table.items():
1885             if part.is_config():
1886                 self.construct_intrasite_graph(mysite, mydsa, part, True,
1887                                                False)  # don't detect stale
1888
1889         if opts.readonly:
1890             # Display any to be added or modified repsFrom
1891             for dnstr, connect in mydsa.connect_table.items():
1892                 if connect.to_be_deleted:
1893                     logger.info("TO BE DELETED:\n%s" % connect)
1894                 if connect.to_be_modified:
1895                     logger.info("TO BE MODIFIED:\n%s" % connect)
1896                 if connect.to_be_added:
1897                     logger.info("TO BE ADDED:\n%s" % connect)
1898
1899             mydsa.commit_connections(self.samdb, ro=True)
1900         else:
1901             # Commit any newly created connections to the samdb
1902             mydsa.commit_connections(self.samdb)
1903
1904     def run(self, dburl, lp, creds):
1905         """Method to perform a complete run of the KCC and
1906         produce an updated topology for subsequent NC replica
1907         syncronization between domain controllers
1908         """
1909         # We may already have a samdb setup if we are
1910         # currently importing an ldif for a test run
1911         if self.samdb is None:
1912             try:
1913                 self.samdb = SamDB(url=lp.samdb_url(),
1914                                    session_info=system_session(),
1915                                    credentials=creds, lp=lp)
1916
1917             except ldb.LdbError, (num, msg):
1918                 logger.error("Unable to open sam database %s : %s" %
1919                              (lp.samdb_url(), msg))
1920                 return 1
1921
1922         try:
1923             # Setup
1924             self.load_my_site()
1925             self.load_my_dsa()
1926
1927             self.load_all_sites()
1928             self.load_all_partitions()
1929             self.load_all_transports()
1930             self.load_all_sitelinks()
1931
1932             # These are the published steps (in order) for the
1933             # MS-TECH description of the KCC algorithm
1934
1935             # Step 1
1936             self.refresh_failed_links_connections()
1937
1938             # Step 2
1939             self.intrasite()
1940
1941             # Step 3
1942             all_connected = self.intersite()
1943
1944             # Step 4
1945             self.remove_unneeded_ntdsconn(all_connected)
1946
1947             # Step 5
1948             self.translate_ntdsconn()
1949
1950             # Step 6
1951             self.remove_unneeded_failed_links_connections()
1952
1953             # Step 7
1954             self.update_rodc_connection()
1955
1956         except Exception, estr:
1957             logger.error("%s" % estr)
1958             return 1
1959
1960         return 0
1961
1962     def import_ldif(self, dburl, lp, creds, ldif_file):
1963         """Routine to import all objects and attributes that are relevent
1964         to the KCC algorithms from a previously exported LDIF file.
1965
1966         The point of this function is to allow a programmer/debugger to
1967         import an LDIF file with non-security relevent information that
1968         was previously extracted from a DC database.  The LDIF file is used
1969         to create a temporary abbreviated database.  The KCC algorithm can
1970         then run against this abbreviated database for debug or test
1971         verification that the topology generated is computationally the
1972         same between different OSes and algorithms.
1973
1974         :param dburl: path to the temporary abbreviated db to create
1975         :param ldif_file: path to the ldif file to import
1976         """
1977         if os.path.exists(dburl):
1978             logger.error("Specify a database (%s) that doesn't already exist." %
1979                          dburl)
1980             return 1
1981
1982         # Use ["modules:"] as we are attempting to build a sam
1983         # database as opposed to start it here.
1984         self.samdb = Ldb(url=dburl, session_info=system_session(),
1985                          lp=lp, options=["modules:"])
1986
1987         self.samdb.transaction_start()
1988         try:
1989             data = read_and_sub_file(ldif_file, None)
1990             self.samdb.add_ldif(data, None)
1991
1992         except Exception, estr:
1993             logger.error("%s" % estr)
1994             self.samdb.transaction_cancel()
1995             return 1
1996         else:
1997             self.samdb.transaction_commit()
1998
1999         self.samdb = None
2000
2001         # We have an abbreviated list of options here because we have built
2002         # an abbreviated database.  We use the rootdse and extended-dn
2003         # modules only during this re-open
2004         self.samdb = SamDB(url=dburl, session_info=system_session(),
2005                            credentials=creds, lp=lp,
2006                            options=["modules:rootdse,extended_dn_out_ldb"])
2007         return 0
2008
2009     def export_ldif(self, dburl, lp, creds, ldif_file):
2010         """Routine to extract all objects and attributes that are relevent
2011         to the KCC algorithms from a DC database.
2012
2013         The point of this function is to allow a programmer/debugger to
2014         extract an LDIF file with non-security relevent information from
2015         a DC database.  The LDIF file can then be used to "import" via
2016         the import_ldif() function this file into a temporary abbreviated
2017         database.  The KCC algorithm can then run against this abbreviated
2018         database for debug or test verification that the topology generated
2019         is computationally the same between different OSes and algorithms.
2020
2021         :param dburl: LDAP database URL to extract info from
2022         :param ldif_file: output LDIF file name to create
2023         """
2024         try:
2025             self.samdb = SamDB(url=dburl,
2026                                session_info=system_session(),
2027                                credentials=creds, lp=lp)
2028         except ldb.LdbError, (enum, estr):
2029             logger.error("Unable to open sam database (%s) : %s" %
2030                          (lp.samdb_url(), estr))
2031             return 1
2032
2033         if os.path.exists(ldif_file):
2034             logger.error("Specify a file (%s) that doesn't already exist." %
2035                          ldif_file)
2036             return 1
2037
2038         try:
2039             f = open(ldif_file, "w")
2040         except (enum, estr):
2041             logger.error("Unable to open (%s) : %s" % (ldif_file, estr))
2042             return 1
2043
2044         try:
2045             # Query Partitions
2046             attrs = [ "objectClass",
2047                       "objectGUID",
2048                       "cn",
2049                       "whenChanged",
2050                       "objectSid",
2051                       "Enabled",
2052                       "systemFlags",
2053                       "dnsRoot",
2054                       "nCName",
2055                       "msDS-NC-Replica-Locations",
2056                       "msDS-NC-RO-Replica-Locations" ]
2057
2058             sstr = "CN=Partitions,%s" % self.samdb.get_config_basedn()
2059             res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
2060                                      attrs=attrs,
2061                                      expression="(objectClass=crossRef)")
2062
2063             # Write partitions output
2064             write_search_result(self.samdb, f, res)
2065
2066             # Query cross reference container
2067             attrs = [ "objectClass",
2068                       "objectGUID",
2069                       "cn",
2070                       "whenChanged",
2071                       "fSMORoleOwner",
2072                       "systemFlags",
2073                       "msDS-Behavior-Version",
2074                       "msDS-EnabledFeature" ]
2075
2076             sstr = "CN=Partitions,%s" % self.samdb.get_config_basedn()
2077             res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
2078                                      attrs=attrs,
2079                                      expression="(objectClass=crossRefContainer)")
2080
2081             # Write cross reference container output
2082             write_search_result(self.samdb, f, res)
2083
2084             # Query Sites
2085             attrs = [ "objectClass",
2086                       "objectGUID",
2087                       "cn",
2088                       "whenChanged",
2089                       "systemFlags" ]
2090
2091             sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
2092             sites = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
2093                                       attrs=attrs,
2094                                       expression="(objectClass=site)")
2095
2096             # Write sites output
2097             write_search_result(self.samdb, f, sites)
2098
2099             # Query NTDS Site Settings
2100             for msg in sites:
2101                 sitestr = str(msg.dn)
2102
2103                 attrs = [ "objectClass",
2104                           "objectGUID",
2105                           "cn",
2106                           "whenChanged",
2107                           "interSiteTopologyGenerator",
2108                           "interSiteTopologyFailover",
2109                           "schedule",
2110                           "options" ]
2111
2112                 sstr = "CN=NTDS Site Settings,%s" % sitestr
2113                 res = self.samdb.search(base=sstr, scope=ldb.SCOPE_BASE,
2114                                          attrs=attrs)
2115
2116                 # Write Site Settings output
2117                 write_search_result(self.samdb, f, res)
2118
2119             # Naming context list
2120             nclist = []
2121
2122             # Query Directory Service Agents
2123             for msg in sites:
2124                 sstr = str(msg.dn)
2125
2126                 ncattrs = [ "hasMasterNCs",
2127                             "msDS-hasMasterNCs",
2128                             "hasPartialReplicaNCs",
2129                             "msDS-HasDomainNCs",
2130                             "msDS-hasFullReplicaNCs",
2131                             "msDS-HasInstantiatedNCs" ]
2132                 attrs = [ "objectClass",
2133                             "objectGUID",
2134                             "cn",
2135                             "whenChanged",
2136                             "invocationID",
2137                             "options",
2138                             "msDS-isRODC",
2139                             "msDS-Behavior-Version" ]
2140
2141                 res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
2142                                         attrs=attrs + ncattrs,
2143                                         expression="(objectClass=nTDSDSA)")
2144
2145                 # Spin thru all the DSAs looking for NC replicas
2146                 # and build a list of all possible Naming Contexts
2147                 # for subsequent retrieval below
2148                 for msg in res:
2149                     for k in msg.keys():
2150                         if k in ncattrs:
2151                             for value in msg[k]:
2152                                 # Some of these have binary DNs so
2153                                 # use dsdb_Dn to split out relevent parts
2154                                 dsdn = dsdb_Dn(self.samdb, value)
2155                                 dnstr = str(dsdn.dn)
2156                                 if dnstr not in nclist:
2157                                     nclist.append(dnstr)
2158
2159                 # Write DSA output
2160                 write_search_result(self.samdb, f, res)
2161
2162             # Query NTDS Connections
2163             for msg in sites:
2164                 sstr = str(msg.dn)
2165
2166                 attrs = [ "objectClass",
2167                           "objectGUID",
2168                           "cn",
2169                           "whenChanged",
2170                           "options",
2171                           "whenCreated",
2172                           "enabledConnection",
2173                           "schedule",
2174                           "transportType",
2175                           "fromServer",
2176                           "systemFlags" ]
2177
2178                 res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
2179                                         attrs=attrs,
2180                                         expression="(objectClass=nTDSConnection)")
2181                 # Write NTDS Connection output
2182                 write_search_result(self.samdb, f, res)
2183
2184
2185             # Query Intersite transports
2186             attrs = [ "objectClass",
2187                       "objectGUID",
2188                       "cn",
2189                       "whenChanged",
2190                       "options",
2191                       "name",
2192                       "bridgeheadServerListBL",
2193                       "transportAddressAttribute" ]
2194
2195             sstr = "CN=Inter-Site Transports,CN=Sites,%s" % \
2196                    self.samdb.get_config_basedn()
2197             res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
2198                                      attrs=attrs,
2199                                      expression="(objectClass=interSiteTransport)")
2200
2201             # Write inter-site transport output
2202             write_search_result(self.samdb, f, res)
2203
2204             # Query siteLink
2205             attrs = [ "objectClass",
2206                       "objectGUID",
2207                       "cn",
2208                       "whenChanged",
2209                       "systemFlags",
2210                       "options",
2211                       "schedule",
2212                       "replInterval",
2213                       "siteList",
2214                       "cost" ]
2215
2216             sstr = "CN=Sites,%s" % \
2217                    self.samdb.get_config_basedn()
2218             res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
2219                                      attrs=attrs,
2220                                      expression="(objectClass=siteLink)")
2221
2222             # Write siteLink output
2223             write_search_result(self.samdb, f, res)
2224
2225             # Query siteLinkBridge
2226             attrs = [ "objectClass",
2227                       "objectGUID",
2228                       "cn",
2229                       "whenChanged",
2230                       "siteLinkList" ]
2231
2232             sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
2233             res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
2234                                      attrs=attrs,
2235                                      expression="(objectClass=siteLinkBridge)")
2236
2237             # Write siteLinkBridge output
2238             write_search_result(self.samdb, f, res)
2239
2240             # Query servers containers
2241             # Needed for samdb.server_site_name()
2242             attrs = [ "objectClass",
2243                       "objectGUID",
2244                       "cn",
2245                       "whenChanged",
2246                       "systemFlags" ]
2247
2248             sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
2249             res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
2250                                      attrs=attrs,
2251                                      expression="(objectClass=serversContainer)")
2252
2253             # Write servers container output
2254             write_search_result(self.samdb, f, res)
2255
2256             # Query servers
2257             # Needed because some transport interfaces refer back to
2258             # attributes found in the server object.   Also needed
2259             # so extended-dn will be happy with dsServiceName in rootDSE
2260             attrs = [ "objectClass",
2261                       "objectGUID",
2262                       "cn",
2263                       "whenChanged",
2264                       "systemFlags",
2265                       "dNSHostName",
2266                       "mailAddress" ]
2267
2268             sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
2269             res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
2270                                      attrs=attrs,
2271                                      expression="(objectClass=server)")
2272
2273             # Write server output
2274             write_search_result(self.samdb, f, res)
2275
2276             # Query Naming Context replicas
2277             attrs = [ "objectClass",
2278                       "objectGUID",
2279                       "cn",
2280                       "whenChanged",
2281                       "objectSid",
2282                       "fSMORoleOwner",
2283                       "msDS-Behavior-Version",
2284                       "repsFrom",
2285                       "repsTo" ]
2286
2287             for sstr in nclist:
2288                 res = self.samdb.search(sstr, scope=ldb.SCOPE_BASE,
2289                                         attrs=attrs)
2290
2291                 # Write naming context output
2292                 write_search_result(self.samdb, f, res)
2293
2294             # Query rootDSE replicas
2295             attrs=[ "objectClass",
2296                     "objectGUID",
2297                     "cn",
2298                     "whenChanged",
2299                     "rootDomainNamingContext",
2300                     "configurationNamingContext",
2301                     "schemaNamingContext",
2302                     "defaultNamingContext",
2303                     "dsServiceName" ]
2304
2305             sstr = ""
2306             res = self.samdb.search(sstr, scope=ldb.SCOPE_BASE,
2307                                      attrs=attrs)
2308
2309             # Record the rootDSE object as a dn as it
2310             # would appear in the base ldb file.  We have
2311             # to save it this way because we are going to
2312             # be importing as an abbreviated database.
2313             res[0].dn = ldb.Dn(self.samdb, "@ROOTDSE")
2314
2315             # Write rootdse output
2316             write_search_result(self.samdb, f, res)
2317
2318         except ldb.LdbError, (enum, estr):
2319             logger.error("Error processing (%s) : %s" % (sstr, estr))
2320             return 1
2321
2322         f.close()
2323         return 0
2324
2325 ##################################################
2326 # Global Functions
2327 ##################################################
2328 def sort_replica_by_dsa_guid(rep1, rep2):
2329     return cmp(rep1.rep_dsa_guid, rep2.rep_dsa_guid)
2330
2331 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
2332     if dsa1.is_gc() and not dsa2.is_gc():
2333         return -1
2334     if not dsa1.is_gc() and dsa2.is_gc():
2335         return +1
2336     return cmp(dsa1.dsa_guid, dsa2.dsa_guid)
2337
2338 def is_smtp_replication_available():
2339     """Currently always returns false because Samba
2340     doesn't implement SMTP transfer for NC changes
2341     between DCs
2342     """
2343     return False
2344
2345 def write_search_result(samdb, f, res):
2346     for msg in res:
2347         lstr = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2348         f.write("%s" % lstr)
2349
2350 ##################################################
2351 # samba_kcc entry point
2352 ##################################################
2353
2354 parser = optparse.OptionParser("samba_kcc [options]")
2355 sambaopts = options.SambaOptions(parser)
2356 credopts = options.CredentialsOptions(parser)
2357
2358 parser.add_option_group(sambaopts)
2359 parser.add_option_group(credopts)
2360 parser.add_option_group(options.VersionOptions(parser))
2361
2362 parser.add_option("--readonly",
2363                   help="compute topology but do not update database",
2364                   action="store_true")
2365
2366 parser.add_option("--debug",
2367                   help="debug output",
2368                   action="store_true")
2369
2370 parser.add_option("--seed",
2371                   help="random number seed",
2372                   type=str, metavar="<number>")
2373
2374 parser.add_option("--importldif",
2375                   help="import topology ldif file",
2376                   type=str, metavar="<file>")
2377
2378 parser.add_option("--exportldif",
2379                   help="export topology ldif file",
2380                   type=str, metavar="<file>")
2381
2382 parser.add_option("-H", "--URL" ,
2383                   help="LDB URL for database or target server",
2384                   type=str, metavar="<URL>", dest="dburl")
2385
2386 parser.add_option("--tmpdb",
2387                   help="schemaless database file to create for ldif import",
2388                   type=str, metavar="<file>")
2389
2390 logger = logging.getLogger("samba_kcc")
2391 logger.addHandler(logging.StreamHandler(sys.stdout))
2392
2393 lp = sambaopts.get_loadparm()
2394 creds = credopts.get_credentials(lp, fallback_machine=True)
2395
2396 opts, args = parser.parse_args()
2397
2398 if opts.readonly is None:
2399     opts.readonly = False
2400
2401 if opts.debug:
2402     logger.setLevel(logging.DEBUG)
2403 elif opts.readonly:
2404     logger.setLevel(logging.INFO)
2405 else:
2406     logger.setLevel(logging.WARNING)
2407
2408 # initialize seed from optional input parameter
2409 if opts.seed:
2410     random.seed(int(opts.seed))
2411 else:
2412     random.seed(0xACE5CA11)
2413
2414 if opts.dburl is None:
2415     opts.dburl = lp.samdb_url()
2416
2417 # Instantiate Knowledge Consistency Checker and perform run
2418 kcc = KCC()
2419
2420 if opts.exportldif:
2421     rc = kcc.export_ldif(opts.dburl, lp, creds, opts.exportldif)
2422     sys.exit(rc)
2423
2424 if opts.importldif:
2425     if opts.tmpdb is None or opts.tmpdb.startswith('ldap'):
2426         logger.error("Specify a target temp database file with --tmpdb option.")
2427         sys.exit(1)
2428
2429     rc = kcc.import_ldif(opts.tmpdb, lp, creds, opts.importldif)
2430     if rc != 0:
2431         sys.exit(rc)
2432
2433 rc = kcc.run(opts.dburl, lp, creds)
2434 sys.exit(rc)