1# define the KCC object
2#
3# Copyright (C) Dave Craft 2011
4# Copyright (C) Andrew Bartlett 2015
5#
6# Andrew Bartlett's alleged work performed by his underlings Douglas
7# Bagnall and Garming Sam.
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
22import random
23import uuid
24
25import itertools
26from samba import unix2nttime, nttime2unix
27from samba import ldb, dsdb, drs_utils
28from samba.auth import system_session
29from samba.samdb import SamDB
30from samba.dcerpc import drsuapi, misc
31
32from samba.kcc.kcc_utils import Site, Partition, Transport, SiteLink
33from samba.kcc.kcc_utils import NCReplica, NCType, nctype_lut, GraphNode
34from samba.kcc.kcc_utils import RepsFromTo, KCCError, KCCFailedObject
35from samba.kcc.graph import convert_schedule_to_repltimes
36
37from samba.ndr import ndr_pack
38
39from samba.kcc.graph_utils import verify_and_dot
40
41from samba.kcc import ldif_import_export
42from samba.kcc.graph import setup_graph, get_spanning_tree_edges
43from samba.kcc.graph import Vertex
44
45from samba.kcc.debug import DEBUG, DEBUG_FN, logger
46from samba.kcc import debug
47from samba.compat import cmp_fn
48
49
50def sort_dsa_by_gc_and_guid(dsa1, dsa2):
51    """Helper to sort DSAs by guid global catalog status
52
53    GC DSAs come before non-GC DSAs, other than that, the guids are
54    sorted in NDR form.
55
56    :param dsa1: A DSA object
57    :param dsa2: Another DSA
58    :return: -1, 0, or 1, indicating sort order.
59    """
60    if dsa1.is_gc() and not dsa2.is_gc():
61        return -1
62    if not dsa1.is_gc() and dsa2.is_gc():
63        return +1
64    return cmp_fn(ndr_pack(dsa1.dsa_guid), ndr_pack(dsa2.dsa_guid))
65
66
67def is_smtp_replication_available():
68    """Can the KCC use SMTP replication?
69
70    Currently always returns false because Samba doesn't implement
71    SMTP transfer for NC changes between DCs.
72
73    :return: Boolean (always False)
74    """
75    return False
76
77
78class KCC(object):
79    """The Knowledge Consistency Checker class.
80
81    A container for objects and methods allowing a run of the KCC.  Produces a
82    set of connections in the samdb for which the Distributed Replication
83    Service can then utilize to replicate naming contexts
84
85    :param unix_now: The putative current time in seconds since 1970.
86    :param readonly: Don't write to the database.
87    :param verify: Check topological invariants for the generated graphs
88    :param debug: Write verbosely to stderr.
89    :param dot_file_dir: write diagnostic Graphviz files in this directory
90    """
91    def __init__(self, unix_now, readonly=False, verify=False, debug=False,
92                 dot_file_dir=None):
93        """Initializes the partitions class which can hold
94        our local DCs partitions or all the partitions in
95        the forest
96        """
97        self.part_table = {}    # partition objects
98        self.site_table = {}
99        self.ip_transport = None
100        self.sitelink_table = {}
101        self.dsa_by_dnstr = {}
102        self.dsa_by_guid = {}
103
104        self.get_dsa_by_guidstr = self.dsa_by_guid.get
105        self.get_dsa = self.dsa_by_dnstr.get
106
107        # TODO: These should be backed by a 'permanent' store so that when
108        # calling DRSGetReplInfo with DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES,
109        # the failure information can be returned
110        self.kcc_failed_links = {}
111        self.kcc_failed_connections = set()
112
113        # Used in inter-site topology computation.  A list
114        # of connections (by NTDSConnection object) that are
115        # to be kept when pruning un-needed NTDS Connections
116        self.kept_connections = set()
117
118        self.my_dsa_dnstr = None  # My dsa DN
119        self.my_dsa = None  # My dsa object
120
121        self.my_site_dnstr = None
122        self.my_site = None
123
124        self.samdb = None
125
126        self.unix_now = unix_now
127        self.nt_now = unix2nttime(unix_now)
128        self.readonly = readonly
129        self.verify = verify
130        self.debug = debug
131        self.dot_file_dir = dot_file_dir
132
133    def load_ip_transport(self):
134        """Loads the inter-site transport objects for Sites
135
136        :return: None
137        :raise KCCError: if no IP transport is found
138        """
139        try:
140            res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
141                                    self.samdb.get_config_basedn(),
142                                    scope=ldb.SCOPE_SUBTREE,
143                                    expression="(objectClass=interSiteTransport)")
144        except ldb.LdbError as e2:
145            (enum, estr) = e2.args
146            raise KCCError("Unable to find inter-site transports - (%s)" %
147                           estr)
148
149        for msg in res:
150            dnstr = str(msg.dn)
151
152            transport = Transport(dnstr)
153
154            transport.load_transport(self.samdb)
155            if transport.name == 'IP':
156                self.ip_transport = transport
157            elif transport.name == 'SMTP':
158                logger.debug("Samba KCC is ignoring the obsolete "
159                             "SMTP transport.")
160
161            else:
162                logger.warning("Samba KCC does not support the transport "
163                               "called %r." % (transport.name,))
164
165        if self.ip_transport is None:
166            raise KCCError("there doesn't seem to be an IP transport")
167
168    def load_all_sitelinks(self):
169        """Loads the inter-site siteLink objects
170
171        :return: None
172        :raise KCCError: if site-links aren't found
173        """
174        try:
175            res = self.samdb.search("CN=Inter-Site Transports,CN=Sites,%s" %
176                                    self.samdb.get_config_basedn(),
177                                    scope=ldb.SCOPE_SUBTREE,
178                                    expression="(objectClass=siteLink)")
179        except ldb.LdbError as e3:
180            (enum, estr) = e3.args
181            raise KCCError("Unable to find inter-site siteLinks - (%s)" % estr)
182
183        for msg in res:
184            dnstr = str(msg.dn)
185
186            # already loaded
187            if dnstr in self.sitelink_table:
188                continue
189
190            sitelink = SiteLink(dnstr)
191
192            sitelink.load_sitelink(self.samdb)
193
194            # Assign this siteLink to table
195            # and index by dn
196            self.sitelink_table[dnstr] = sitelink
197
198    def load_site(self, dn_str):
199        """Helper for load_my_site and load_all_sites.
200
201        Put all the site's DSAs into the KCC indices.
202
203        :param dn_str: a site dn_str
204        :return: the Site object pertaining to the dn_str
205        """
206        site = Site(dn_str, self.unix_now)
207        site.load_site(self.samdb)
208
209        # We avoid replacing the site with an identical copy in case
210        # somewhere else has a reference to the old one, which would
211        # lead to all manner of confusion and chaos.
212        guid = str(site.site_guid)
213        if guid not in self.site_table:
214            self.site_table[guid] = site
215            self.dsa_by_dnstr.update(site.dsa_table)
216            self.dsa_by_guid.update((str(x.dsa_guid), x)
217                                    for x in site.dsa_table.values())
218
219        return self.site_table[guid]
220
221    def load_my_site(self):
222        """Load the Site object for the local DSA.
223
224        :return: None
225        """
226        self.my_site_dnstr = ("CN=%s,CN=Sites,%s" % (
227            self.samdb.server_site_name(),
228            self.samdb.get_config_basedn()))
229
230        self.my_site = self.load_site(self.my_site_dnstr)
231
232    def load_all_sites(self):
233        """Discover all sites and create Site objects.
234
235        :return: None
236        :raise: KCCError if sites can't be found
237        """
238        try:
239            res = self.samdb.search("CN=Sites,%s" %
240                                    self.samdb.get_config_basedn(),
241                                    scope=ldb.SCOPE_SUBTREE,
242                                    expression="(objectClass=site)")
243        except ldb.LdbError as e4:
244            (enum, estr) = e4.args
245            raise KCCError("Unable to find sites - (%s)" % estr)
246
247        for msg in res:
248            sitestr = str(msg.dn)
249            self.load_site(sitestr)
250
251    def load_my_dsa(self):
252        """Discover my nTDSDSA dn thru the rootDSE entry
253
254        :return: None
255        :raise: KCCError if DSA can't be found
256        """
257        dn_query = "<GUID=%s>" % self.samdb.get_ntds_GUID()
258        dn = ldb.Dn(self.samdb, dn_query)
259        try:
260            res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
261                                    attrs=["objectGUID"])
262        except ldb.LdbError as e5:
263            (enum, estr) = e5.args
264            DEBUG_FN("Search for dn '%s' [from %s] failed: %s. "
265                     "This typically happens in --importldif mode due "
266                     "to lack of module support." % (dn, dn_query, estr))
267            try:
268                # We work around the failure above by looking at the
269                # dsServiceName that was put in the fake rootdse by
270                # the --exportldif, rather than the
271                # samdb.get_ntds_GUID(). The disadvantage is that this
272                # mode requires we modify the @ROOTDSE dnq to support
273                # --forced-local-dsa
274                service_name_res = self.samdb.search(base="",
275                                                     scope=ldb.SCOPE_BASE,
276                                                     attrs=["dsServiceName"])
277                dn = ldb.Dn(self.samdb,
278                            service_name_res[0]["dsServiceName"][0].decode('utf8'))
279
280                res = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
281                                        attrs=["objectGUID"])
282            except ldb.LdbError as e:
283                (enum, estr) = e.args
284                raise KCCError("Unable to find my nTDSDSA - (%s)" % estr)
285
286        if len(res) != 1:
287            raise KCCError("Unable to find my nTDSDSA at %s" %
288                           dn.extended_str())
289
290        ntds_guid = misc.GUID(self.samdb.get_ntds_GUID())
291        if misc.GUID(res[0]["objectGUID"][0]) != ntds_guid:
292            raise KCCError("Did not find the GUID we expected,"
293                           " perhaps due to --importldif")
294
295        self.my_dsa_dnstr = str(res[0].dn)
296
297        self.my_dsa = self.my_site.get_dsa(self.my_dsa_dnstr)
298
299        if self.my_dsa_dnstr not in self.dsa_by_dnstr:
300            debug.DEBUG_DARK_YELLOW("my_dsa %s isn't in self.dsas_by_dnstr:"
301                                    " it must be RODC.\n"
302                                    "Let's add it, because my_dsa is special!"
303                                    "\n(likewise for self.dsa_by_guid)" %
304                                    self.my_dsa_dnstr)
305
306            self.dsa_by_dnstr[self.my_dsa_dnstr] = self.my_dsa
307            self.dsa_by_guid[str(self.my_dsa.dsa_guid)] = self.my_dsa
308
309    def load_all_partitions(self):
310        """Discover and load all partitions.
311
312        Each NC is inserted into the part_table by partition
313        dn string (not the nCName dn string)
314
315        :return: None
316        :raise: KCCError if partitions can't be found
317        """
318        try:
319            res = self.samdb.search("CN=Partitions,%s" %
320                                    self.samdb.get_config_basedn(),
321                                    scope=ldb.SCOPE_SUBTREE,
322                                    expression="(objectClass=crossRef)")
323        except ldb.LdbError as e6:
324            (enum, estr) = e6.args
325            raise KCCError("Unable to find partitions - (%s)" % estr)
326
327        for msg in res:
328            partstr = str(msg.dn)
329
330            # already loaded
331            if partstr in self.part_table:
332                continue
333
334            part = Partition(partstr)
335
336            part.load_partition(self.samdb)
337            self.part_table[partstr] = part
338
339    def refresh_failed_links_connections(self, ping=None):
340        """Ensure the failed links list is up to date
341
342        Based on MS-ADTS 6.2.2.1
343
344        :param ping: An oracle function of remote site availability
345        :return: None
346        """
347        # LINKS: Refresh failed links
348        self.kcc_failed_links = {}
349        current, needed = self.my_dsa.get_rep_tables()
350        for replica in current.values():
351            # For every possible connection to replicate
352            for reps_from in replica.rep_repsFrom:
353                failure_count = reps_from.consecutive_sync_failures
354                if failure_count <= 0:
355                    continue
356
357                dsa_guid = str(reps_from.source_dsa_obj_guid)
358                time_first_failure = reps_from.last_success
359                last_result = reps_from.last_attempt
360                dns_name = reps_from.dns_name1
361
362                f = self.kcc_failed_links.get(dsa_guid)
363                if f is None:
364                    f = KCCFailedObject(dsa_guid, failure_count,
365                                        time_first_failure, last_result,
366                                        dns_name)
367                    self.kcc_failed_links[dsa_guid] = f
368                else:
369                    f.failure_count = max(f.failure_count, failure_count)
370                    f.time_first_failure = min(f.time_first_failure,
371                                               time_first_failure)
372                    f.last_result = last_result
373
374        # CONNECTIONS: Refresh failed connections
375        restore_connections = set()
376        if ping is not None:
377            DEBUG("refresh_failed_links: checking if links are still down")
378            for connection in self.kcc_failed_connections:
379                if ping(connection.dns_name):
380                    # Failed connection is no longer failing
381                    restore_connections.add(connection)
382                else:
383                    connection.failure_count += 1
384        else:
385            DEBUG("refresh_failed_links: not checking live links because we\n"
386                  "weren't asked to --attempt-live-connections")
387
388        # Remove the restored connections from the failed connections
389        self.kcc_failed_connections.difference_update(restore_connections)
390
391    def is_stale_link_connection(self, target_dsa):
392        """Check whether a link to a remote DSA is stale
393
394        Used in MS-ADTS 6.2.2.2 Intrasite Connection Creation
395
396        Returns True if the remote seems to have been down for at
397        least two hours, otherwise False.
398
399        :param target_dsa: the remote DSA object
400        :return: True if link is stale, otherwise False
401        """
402        failed_link = self.kcc_failed_links.get(str(target_dsa.dsa_guid))
403        if failed_link:
404            # failure_count should be > 0, but check anyways
405            if failed_link.failure_count > 0:
406                unix_first_failure = \
407                    nttime2unix(failed_link.time_first_failure)
408                # TODO guard against future
409                if unix_first_failure > self.unix_now:
410                    logger.error("The last success time attribute for \
411                                 repsFrom is in the future!")
412
413                # Perform calculation in seconds
414                if (self.unix_now - unix_first_failure) > 60 * 60 * 2:
415                    return True
416
417        # TODO connections.
418        # We have checked failed *links*, but we also need to check
419        # *connections*
420
421        return False
422
423    # TODO: This should be backed by some form of local database
424    def remove_unneeded_failed_links_connections(self):
425        # Remove all tuples in kcc_failed_links where failure count = 0
426        # In this implementation, this should never happen.
427
428        # Remove all connections which were not used this run or connections
429        # that became active during this run.
430        pass
431
432    def _ensure_connections_are_loaded(self, connections):
433        """Load or fake-load NTDSConnections lacking GUIDs
434
435        New connections don't have GUIDs and created times which are
436        needed for sorting. If we're in read-only mode, we make fake
437        GUIDs, otherwise we ask SamDB to do it for us.
438
439        :param connections: an iterable of NTDSConnection objects.
440        :return: None
441        """
442        for cn_conn in connections:
443            if cn_conn.guid is None:
444                if self.readonly:
445                    cn_conn.guid = misc.GUID(str(uuid.uuid4()))
446                    cn_conn.whenCreated = self.nt_now
447                else:
448                    cn_conn.load_connection(self.samdb)
449
450    def _mark_broken_ntdsconn(self):
451        """Find NTDS Connections that lack a remote
452
453        I'm not sure how they appear. Let's be rid of them by marking
454        them with the to_be_deleted attribute.
455
456        :return: None
457        """
458        for cn_conn in self.my_dsa.connect_table.values():
459            s_dnstr = cn_conn.get_from_dnstr()
460            if s_dnstr is None:
461                DEBUG_FN("%s has phantom connection %s" % (self.my_dsa,
462                                                           cn_conn))
463                cn_conn.to_be_deleted = True
464
465    def _mark_unneeded_local_ntdsconn(self):
466        """Find unneeded intrasite NTDS Connections for removal
467
468        Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections.
469        Every DC removes its own unnecessary intrasite connections.
470        This function tags them with the to_be_deleted attribute.
471
472        :return: None
473        """
474        # XXX should an RODC be regarded as same site? It isn't part
475        # of the intrasite ring.
476
477        if self.my_site.is_cleanup_ntdsconn_disabled():
478            DEBUG_FN("not doing ntdsconn cleanup for site %s, "
479                     "because it is disabled" % self.my_site)
480            return
481
482        mydsa = self.my_dsa
483
484        try:
485            self._ensure_connections_are_loaded(mydsa.connect_table.values())
486        except KCCError:
487            # RODC never actually added any connections to begin with
488            if mydsa.is_ro():
489                return
490
491        local_connections = []
492
493        for cn_conn in mydsa.connect_table.values():
494            s_dnstr = cn_conn.get_from_dnstr()
495            if s_dnstr in self.my_site.dsa_table:
496                removable = not (cn_conn.is_generated() or
497                                 cn_conn.is_rodc_topology())
498                packed_guid = ndr_pack(cn_conn.guid)
499                local_connections.append((cn_conn, s_dnstr,
500                                          packed_guid, removable))
501
502        # Avoid "ValueError: r cannot be bigger than the iterable" in
503        # for a, b in itertools.permutations(local_connections, 2):
504        if (len(local_connections) < 2):
505            return
506
507        for a, b in itertools.permutations(local_connections, 2):
508            cn_conn, s_dnstr, packed_guid, removable = a
509            cn_conn2, s_dnstr2, packed_guid2, removable2 = b
510            if (removable and
511                s_dnstr == s_dnstr2 and
512                cn_conn.whenCreated < cn_conn2.whenCreated or
513                (cn_conn.whenCreated == cn_conn2.whenCreated and
514                 packed_guid < packed_guid2)):
515                cn_conn.to_be_deleted = True
516
517    def _mark_unneeded_intersite_ntdsconn(self):
518        """find unneeded intersite NTDS Connections for removal
519
520        Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections. The
521        intersite topology generator removes links for all DCs in its
522        site. Here we just tag them with the to_be_deleted attribute.
523
524        :return: None
525        """
526        # TODO Figure out how best to handle the RODC case
527        # The RODC is ISTG, but shouldn't act on anyone's behalf.
528        if self.my_dsa.is_ro():
529            return
530
531        # Find the intersite connections
532        local_dsas = self.my_site.dsa_table
533        connections_and_dsas = []
534        for dsa in local_dsas.values():
535            for cn in dsa.connect_table.values():
536                if cn.to_be_deleted:
537                    continue
538                s_dnstr = cn.get_from_dnstr()
539                if s_dnstr is None:
540                    continue
541                if s_dnstr not in local_dsas:
542                    from_dsa = self.get_dsa(s_dnstr)
543                    # Samba ONLY: ISTG removes connections to dead DCs
544                    if from_dsa is None or '\\0ADEL' in s_dnstr:
545                        logger.info("DSA appears deleted, removing connection %s"
546                                    % s_dnstr)
547                        cn.to_be_deleted = True
548                        continue
549                    connections_and_dsas.append((cn, dsa, from_dsa))
550
551        self._ensure_connections_are_loaded(x[0] for x in connections_and_dsas)
552        for cn, to_dsa, from_dsa in connections_and_dsas:
553            if not cn.is_generated() or cn.is_rodc_topology():
554                continue
555
556            # If the connection is in the kept_connections list, we
557            # only remove it if an endpoint seems down.
558            if (cn in self.kept_connections and
559                not (self.is_bridgehead_failed(to_dsa, True) or
560                     self.is_bridgehead_failed(from_dsa, True))):
561                continue
562
563            # this one is broken and might be superseded by another.
564            # But which other? Let's just say another link to the same
565            # site can supersede.
566            from_dnstr = from_dsa.dsa_dnstr
567            for site in self.site_table.values():
568                if from_dnstr in site.rw_dsa_table:
569                    for cn2, to_dsa2, from_dsa2 in connections_and_dsas:
570                        if (cn is not cn2 and
571                            from_dsa2 in site.rw_dsa_table):
572                            cn.to_be_deleted = True
573
574    def _commit_changes(self, dsa):
575        if dsa.is_ro() or self.readonly:
576            for connect in dsa.connect_table.values():
577                if connect.to_be_deleted:
578                    logger.info("TO BE DELETED:\n%s" % connect)
579                if connect.to_be_added:
580                    logger.info("TO BE ADDED:\n%s" % connect)
581                if connect.to_be_modified:
582                    logger.info("TO BE MODIFIED:\n%s" % connect)
583
584            # Peform deletion from our tables but perform
585            # no database modification
586            dsa.commit_connections(self.samdb, ro=True)
587        else:
588            # Commit any modified connections
589            dsa.commit_connections(self.samdb)
590
591    def remove_unneeded_ntdsconn(self, all_connected):
592        """Remove unneeded NTDS Connections once topology is calculated
593
594        Based on MS-ADTS 6.2.2.4 Removing Unnecessary Connections
595
596        :param all_connected: indicates whether all sites are connected
597        :return: None
598        """
599        self._mark_broken_ntdsconn()
600        self._mark_unneeded_local_ntdsconn()
601        # if we are not the istg, we're done!
602        # if we are the istg, but all_connected is False, we also do nothing.
603        if self.my_dsa.is_istg() and all_connected:
604            self._mark_unneeded_intersite_ntdsconn()
605
606        for dsa in self.my_site.dsa_table.values():
607            self._commit_changes(dsa)
608
609    def modify_repsFrom(self, n_rep, t_repsFrom, s_rep, s_dsa, cn_conn):
610        """Update an repsFrom object if required.
611
612        Part of MS-ADTS 6.2.2.5.
613
614        Update t_repsFrom if necessary to satisfy requirements. Such
615        updates are typically required when the IDL_DRSGetNCChanges
616        server has moved from one site to another--for example, to
617        enable compression when the server is moved from the
618        client's site to another site.
619
620        The repsFrom.update_flags bit field may be modified
621        auto-magically if any changes are made here. See
622        kcc_utils.RepsFromTo for gory details.
623
624
625        :param n_rep: NC replica we need
626        :param t_repsFrom: repsFrom tuple to modify
627        :param s_rep: NC replica at source DSA
628        :param s_dsa: source DSA
629        :param cn_conn: Local DSA NTDSConnection child
630
631        :return: None
632        """
633        s_dnstr = s_dsa.dsa_dnstr
634        same_site = s_dnstr in self.my_site.dsa_table
635
636        # if schedule doesn't match then update and modify
637        times = convert_schedule_to_repltimes(cn_conn.schedule)
638        if times != t_repsFrom.schedule:
639            t_repsFrom.schedule = times
640
641        # Bit DRS_ADD_REF is set in replicaFlags unconditionally
642        # Samba ONLY:
643        if ((t_repsFrom.replica_flags &
644             drsuapi.DRSUAPI_DRS_ADD_REF) == 0x0):
645            t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_ADD_REF
646
647        # Bit DRS_PER_SYNC is set in replicaFlags if and only
648        # if nTDSConnection schedule has a value v that specifies
649        # scheduled replication is to be performed at least once
650        # per week.
651        if cn_conn.is_schedule_minimum_once_per_week():
652
653            if ((t_repsFrom.replica_flags &
654                 drsuapi.DRSUAPI_DRS_PER_SYNC) == 0x0):
655                t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_PER_SYNC
656
657        # Bit DRS_INIT_SYNC is set in t.replicaFlags if and only
658        # if the source DSA and the local DC's nTDSDSA object are
659        # in the same site or source dsa is the FSMO role owner
660        # of one or more FSMO roles in the NC replica.
661        if same_site or n_rep.is_fsmo_role_owner(s_dnstr):
662
663            if ((t_repsFrom.replica_flags &
664                 drsuapi.DRSUAPI_DRS_INIT_SYNC) == 0x0):
665                t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_INIT_SYNC
666
667        # If bit NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT is set in
668        # cn!options, bit DRS_NEVER_NOTIFY is set in t.replicaFlags
669        # if and only if bit NTDSCONN_OPT_USE_NOTIFY is clear in
670        # cn!options. Otherwise, bit DRS_NEVER_NOTIFY is set in
671        # t.replicaFlags if and only if s and the local DC's
672        # nTDSDSA object are in different sites.
673        if ((cn_conn.options &
674             dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT) != 0x0):
675
676            if (cn_conn.options & dsdb.NTDSCONN_OPT_USE_NOTIFY) == 0x0:
677                # WARNING
678                #
679                # it LOOKS as if this next test is a bit silly: it
680                # checks the flag then sets it if it not set; the same
681                # effect could be achieved by unconditionally setting
682                # it. But in fact the repsFrom object has special
683                # magic attached to it, and altering replica_flags has
684                # side-effects. That is bad in my opinion, but there
685                # you go.
686                if ((t_repsFrom.replica_flags &
687                     drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
688                    t_repsFrom.replica_flags |= \
689                        drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
690
691        elif not same_site:
692
693            if ((t_repsFrom.replica_flags &
694                 drsuapi.DRSUAPI_DRS_NEVER_NOTIFY) == 0x0):
695                t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_NEVER_NOTIFY
696
697        # Bit DRS_USE_COMPRESSION is set in t.replicaFlags if
698        # and only if s and the local DC's nTDSDSA object are
699        # not in the same site and the
700        # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION bit is
701        # clear in cn!options
702        if (not same_site and
703            (cn_conn.options &
704             dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION) == 0x0):
705
706            if ((t_repsFrom.replica_flags &
707                 drsuapi.DRSUAPI_DRS_USE_COMPRESSION) == 0x0):
708                t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_USE_COMPRESSION
709
710        # Bit DRS_TWOWAY_SYNC is set in t.replicaFlags if and only
711        # if bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options.
712        if (cn_conn.options & dsdb.NTDSCONN_OPT_TWOWAY_SYNC) != 0x0:
713
714            if ((t_repsFrom.replica_flags &
715                 drsuapi.DRSUAPI_DRS_TWOWAY_SYNC) == 0x0):
716                t_repsFrom.replica_flags |= drsuapi.DRSUAPI_DRS_TWOWAY_SYNC
717
718        # Bits DRS_DISABLE_AUTO_SYNC and DRS_DISABLE_PERIODIC_SYNC are
719        # set in t.replicaFlags if and only if cn!enabledConnection = false.
720        if not cn_conn.is_enabled():
721
722            if ((t_repsFrom.replica_flags &
723                 drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC) == 0x0):
724                t_repsFrom.replica_flags |= \
725                    drsuapi.DRSUAPI_DRS_DISABLE_AUTO_SYNC
726
727            if ((t_repsFrom.replica_flags &
728                 drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC) == 0x0):
729                t_repsFrom.replica_flags |= \
730                    drsuapi.DRSUAPI_DRS_DISABLE_PERIODIC_SYNC
731
732        # If s and the local DC's nTDSDSA object are in the same site,
733        # cn!transportType has no value, or the RDN of cn!transportType
734        # is CN=IP:
735        #
736        #     Bit DRS_MAIL_REP in t.replicaFlags is clear.
737        #
738        #     t.uuidTransport = NULL GUID.
739        #
740        #     t.uuidDsa = The GUID-based DNS name of s.
741        #
742        # Otherwise:
743        #
744        #     Bit DRS_MAIL_REP in t.replicaFlags is set.
745        #
746        #     If x is the object with dsname cn!transportType,
747        #     t.uuidTransport = x!objectGUID.
748        #
749        #     Let a be the attribute identified by
750        #     x!transportAddressAttribute. If a is
751        #     the dNSHostName attribute, t.uuidDsa = the GUID-based
752        #      DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
753        #
754        # It appears that the first statement i.e.
755        #
756        #     "If s and the local DC's nTDSDSA object are in the same
757        #      site, cn!transportType has no value, or the RDN of
758        #      cn!transportType is CN=IP:"
759        #
760        # could be a slightly tighter statement if it had an "or"
761        # between each condition.  I believe this should
762        # be interpreted as:
763        #
764        #     IF (same-site) OR (no-value) OR (type-ip)
765        #
766        # because IP should be the primary transport mechanism
767        # (even in inter-site) and the absense of the transportType
768        # attribute should always imply IP no matter if its multi-site
769        #
770        # NOTE MS-TECH INCORRECT:
771        #
772        #     All indications point to these statements above being
773        #     incorrectly stated:
774        #
775        #         t.uuidDsa = The GUID-based DNS name of s.
776        #
777        #         Let a be the attribute identified by
778        #         x!transportAddressAttribute. If a is
779        #         the dNSHostName attribute, t.uuidDsa = the GUID-based
780        #         DNS name of s. Otherwise, t.uuidDsa = (s!parent)!a.
781        #
782        #     because the uuidDSA is a GUID and not a GUID-base DNS
783        #     name.  Nor can uuidDsa hold (s!parent)!a if not
784        #     dNSHostName.  What should have been said is:
785        #
786        #         t.naDsa = The GUID-based DNS name of s
787        #
788        #     That would also be correct if transportAddressAttribute
789        #     were "mailAddress" because (naDsa) can also correctly
790        #     hold the SMTP ISM service address.
791        #
792        nastr = "%s._msdcs.%s" % (s_dsa.dsa_guid, self.samdb.forest_dns_name())
793
794        if ((t_repsFrom.replica_flags &
795             drsuapi.DRSUAPI_DRS_MAIL_REP) != 0x0):
796            t_repsFrom.replica_flags &= ~drsuapi.DRSUAPI_DRS_MAIL_REP
797
798        t_repsFrom.transport_guid = misc.GUID()
799
800        # See (NOTE MS-TECH INCORRECT) above
801
802        # NOTE: it looks like these conditionals are pointless,
803        # because the state will end up as `t_repsFrom.dns_name1 ==
804        # nastr` in either case, BUT the repsFrom thing is magic and
805        # assigning to it alters some flags. So we try not to update
806        # it unless necessary.
807        if t_repsFrom.dns_name1 != nastr:
808            t_repsFrom.dns_name1 = nastr
809
810        if t_repsFrom.version > 0x1 and t_repsFrom.dns_name2 != nastr:
811            t_repsFrom.dns_name2 = nastr
812
813        if t_repsFrom.is_modified():
814            DEBUG_FN("modify_repsFrom(): %s" % t_repsFrom)
815
816    def get_dsa_for_implied_replica(self, n_rep, cn_conn):
817        """If a connection imply a replica, find the relevant DSA
818
819        Given a NC replica and NTDS Connection, determine if the
820        connection implies a repsFrom tuple should be present from the
821        source DSA listed in the connection to the naming context. If
822        it should be, return the DSA; otherwise return None.
823
824        Based on part of MS-ADTS 6.2.2.5
825
826        :param n_rep: NC replica
827        :param cn_conn: NTDS Connection
828        :return: source DSA or None
829        """
830        # XXX different conditions for "implies" than MS-ADTS 6.2.2
831        # preamble.
832
833        # It boils down to: we want an enabled, non-FRS connections to
834        # a valid remote DSA with a non-RO replica corresponding to
835        # n_rep.
836
837        if not cn_conn.is_enabled() or cn_conn.is_rodc_topology():
838            return None
839
840        s_dnstr = cn_conn.get_from_dnstr()
841        s_dsa = self.get_dsa(s_dnstr)
842
843        # No DSA matching this source DN string?
844        if s_dsa is None:
845            return None
846
847        s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
848
849        if (s_rep is not None and
850            s_rep.is_present() and
851            (not s_rep.is_ro() or n_rep.is_partial())):
852            return s_dsa
853        return None
854
855    def translate_ntdsconn(self, current_dsa=None):
856        """Adjust repsFrom to match NTDSConnections
857
858        This function adjusts values of repsFrom abstract attributes of NC
859        replicas on the local DC to match those implied by
860        nTDSConnection objects.
861
862        Based on [MS-ADTS] 6.2.2.5
863
864        :param current_dsa: optional DSA on whose behalf we are acting.
865        :return: None
866        """
867        ro = False
868        if current_dsa is None:
869            current_dsa = self.my_dsa
870
871        if current_dsa.is_ro():
872            ro = True
873
874        if current_dsa.is_translate_ntdsconn_disabled():
875            DEBUG_FN("skipping translate_ntdsconn() "
876                     "because disabling flag is set")
877            return
878
879        DEBUG_FN("translate_ntdsconn(): enter")
880
881        current_rep_table, needed_rep_table = current_dsa.get_rep_tables()
882
883        # Filled in with replicas we currently have that need deleting
884        delete_reps = set()
885
886        # We're using the MS notation names here to allow
887        # correlation back to the published algorithm.
888        #
889        # n_rep      - NC replica (n)
890        # t_repsFrom - tuple (t) in n!repsFrom
891        # s_dsa      - Source DSA of the replica. Defined as nTDSDSA
892        #              object (s) such that (s!objectGUID = t.uuidDsa)
893        #              In our IDL representation of repsFrom the (uuidDsa)
894        #              attribute is called (source_dsa_obj_guid)
895        # cn_conn    - (cn) is nTDSConnection object and child of the local
896        #               DC's nTDSDSA object and (cn!fromServer = s)
897        # s_rep      - source DSA replica of n
898        #
899        # If we have the replica and its not needed
900        # then we add it to the "to be deleted" list.
901        for dnstr in current_rep_table:
902            # If we're on the RODC, hardcode the update flags
903            if ro:
904                c_rep = current_rep_table[dnstr]
905                c_rep.load_repsFrom(self.samdb)
906                for t_repsFrom in c_rep.rep_repsFrom:
907                    replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC |
908                                     drsuapi.DRSUAPI_DRS_PER_SYNC |
909                                     drsuapi.DRSUAPI_DRS_ADD_REF |
910                                     drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING |
911                                     drsuapi.DRSUAPI_DRS_NONGC_RO_REP)
912                    if t_repsFrom.replica_flags != replica_flags:
913                        t_repsFrom.replica_flags = replica_flags
914                c_rep.commit_repsFrom(self.samdb, ro=self.readonly)
915            else:
916                if dnstr not in needed_rep_table:
917                    delete_reps.add(dnstr)
918
919        DEBUG_FN('current %d needed %d delete %d' % (len(current_rep_table),
920                 len(needed_rep_table), len(delete_reps)))
921
922        if delete_reps:
923            # TODO Must delete repsFrom/repsTo for these replicas
924            DEBUG('deleting these reps: %s' % delete_reps)
925            for dnstr in delete_reps:
926                del current_rep_table[dnstr]
927
928        # HANDLE REPS-FROM
929        #
930        # Now perform the scan of replicas we'll need
931        # and compare any current repsFrom against the
932        # connections
933        for n_rep in needed_rep_table.values():
934
935            # load any repsFrom and fsmo roles as we'll
936            # need them during connection translation
937            n_rep.load_repsFrom(self.samdb)
938            n_rep.load_fsmo_roles(self.samdb)
939
940            # Loop thru the existing repsFrom tuples (if any)
941            # XXX This is a list and could contain duplicates
942            #     (multiple load_repsFrom calls)
943            for t_repsFrom in n_rep.rep_repsFrom:
944
945                # for each tuple t in n!repsFrom, let s be the nTDSDSA
946                # object such that s!objectGUID = t.uuidDsa
947                guidstr = str(t_repsFrom.source_dsa_obj_guid)
948                s_dsa = self.get_dsa_by_guidstr(guidstr)
949
950                # Source dsa is gone from config (strange)
951                # so cleanup stale repsFrom for unlisted DSA
952                if s_dsa is None:
953                    logger.warning("repsFrom source DSA guid (%s) not found" %
954                                   guidstr)
955                    t_repsFrom.to_be_deleted = True
956                    continue
957
958                # Find the connection that this repsFrom would use. If
959                # there isn't a good one (i.e. non-RODC_TOPOLOGY,
960                # meaning non-FRS), we delete the repsFrom.
961                s_dnstr = s_dsa.dsa_dnstr
962                connections = current_dsa.get_connection_by_from_dnstr(s_dnstr)
963                for cn_conn in connections:
964                    if not cn_conn.is_rodc_topology():
965                        break
966                else:
967                    # no break means no non-rodc_topology connection exists
968                    t_repsFrom.to_be_deleted = True
969                    continue
970
971                # KCC removes this repsFrom tuple if any of the following
972                # is true:
973                #     No NC replica of the NC "is present" on DSA that
974                #     would be source of replica
975                #
976                #     A writable replica of the NC "should be present" on
977                #     the local DC, but a partial replica "is present" on
978                #     the source DSA
979                s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
980
981                if s_rep is None or not s_rep.is_present() or \
982                   (not n_rep.is_ro() and s_rep.is_partial()):
983
984                    t_repsFrom.to_be_deleted = True
985                    continue
986
987                # If the KCC did not remove t from n!repsFrom, it updates t
988                self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
989
990            # Loop thru connections and add implied repsFrom tuples
991            # for each NTDSConnection under our local DSA if the
992            # repsFrom is not already present
993            for cn_conn in current_dsa.connect_table.values():
994
995                s_dsa = self.get_dsa_for_implied_replica(n_rep, cn_conn)
996                if s_dsa is None:
997                    continue
998
999                # Loop thru the existing repsFrom tuples (if any) and
1000                # if we already have a tuple for this connection then
1001                # no need to proceed to add.  It will have been changed
1002                # to have the correct attributes above
1003                for t_repsFrom in n_rep.rep_repsFrom:
1004                    guidstr = str(t_repsFrom.source_dsa_obj_guid)
1005                    if s_dsa is self.get_dsa_by_guidstr(guidstr):
1006                        s_dsa = None
1007                        break
1008
1009                if s_dsa is None:
1010                    continue
1011
1012                # Create a new RepsFromTo and proceed to modify
1013                # it according to specification
1014                t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
1015
1016                t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
1017
1018                s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
1019
1020                self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
1021
1022                # Add to our NC repsFrom as this is newly computed
1023                if t_repsFrom.is_modified():
1024                    n_rep.rep_repsFrom.append(t_repsFrom)
1025
1026            if self.readonly or ro:
1027                # Display any to be deleted or modified repsFrom
1028                text = n_rep.dumpstr_to_be_deleted()
1029                if text:
1030                    logger.info("TO BE DELETED:\n%s" % text)
1031                text = n_rep.dumpstr_to_be_modified()
1032                if text:
1033                    logger.info("TO BE MODIFIED:\n%s" % text)
1034
1035                # Peform deletion from our tables but perform
1036                # no database modification
1037                n_rep.commit_repsFrom(self.samdb, ro=True)
1038            else:
1039                # Commit any modified repsFrom to the NC replica
1040                n_rep.commit_repsFrom(self.samdb)
1041
1042        # HANDLE REPS-TO:
1043        #
1044        # Now perform the scan of replicas we'll need
1045        # and compare any current repsTo against the
1046        # connections
1047
1048        # RODC should never push to anybody (should we check this?)
1049        if ro:
1050            return
1051
1052        for n_rep in needed_rep_table.values():
1053
1054            # load any repsTo and fsmo roles as we'll
1055            # need them during connection translation
1056            n_rep.load_repsTo(self.samdb)
1057
1058            # Loop thru the existing repsTo tuples (if any)
1059            # XXX This is a list and could contain duplicates
1060            #     (multiple load_repsTo calls)
1061            for t_repsTo in n_rep.rep_repsTo:
1062
1063                # for each tuple t in n!repsTo, let s be the nTDSDSA
1064                # object such that s!objectGUID = t.uuidDsa
1065                guidstr = str(t_repsTo.source_dsa_obj_guid)
1066                s_dsa = self.get_dsa_by_guidstr(guidstr)
1067
1068                # Source dsa is gone from config (strange)
1069                # so cleanup stale repsTo for unlisted DSA
1070                if s_dsa is None:
1071                    logger.warning("repsTo source DSA guid (%s) not found" %
1072                                   guidstr)
1073                    t_repsTo.to_be_deleted = True
1074                    continue
1075
1076                # Find the connection that this repsTo would use. If
1077                # there isn't a good one (i.e. non-RODC_TOPOLOGY,
1078                # meaning non-FRS), we delete the repsTo.
1079                s_dnstr = s_dsa.dsa_dnstr
1080                if '\\0ADEL' in s_dnstr:
1081                    logger.warning("repsTo source DSA guid (%s) appears deleted" %
1082                                   guidstr)
1083                    t_repsTo.to_be_deleted = True
1084                    continue
1085
1086                connections = s_dsa.get_connection_by_from_dnstr(self.my_dsa_dnstr)
1087                if len(connections) > 0:
1088                    # Then this repsTo is tentatively valid
1089                    continue
1090                else:
1091                    # There is no plausible connection for this repsTo
1092                    t_repsTo.to_be_deleted = True
1093
1094            if self.readonly:
1095                # Display any to be deleted or modified repsTo
1096                for rt in n_rep.rep_repsTo:
1097                    if rt.to_be_deleted:
1098                        logger.info("REMOVING REPS-TO: %s" % rt)
1099
1100                # Peform deletion from our tables but perform
1101                # no database modification
1102                n_rep.commit_repsTo(self.samdb, ro=True)
1103            else:
1104                # Commit any modified repsTo to the NC replica
1105                n_rep.commit_repsTo(self.samdb)
1106
1107        # TODO Remove any duplicate repsTo values. This should never happen in
1108        # any normal situations.
1109
1110    def merge_failed_links(self, ping=None):
1111        """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
1112
1113        The KCC on a writable DC attempts to merge the link and connection
1114        failure information from bridgehead DCs in its own site to help it
1115        identify failed bridgehead DCs.
1116
1117        Based on MS-ADTS 6.2.2.3.2 "Merge of kCCFailedLinks and kCCFailedLinks
1118        from Bridgeheads"
1119
1120        :param ping: An oracle of current bridgehead availability
1121        :return: None
1122        """
1123        # 1. Queries every bridgehead server in your site (other than yourself)
1124        # 2. For every ntDSConnection that references a server in a different
1125        #    site merge all the failure info
1126        #
1127        # XXX - not implemented yet
1128        if ping is not None:
1129            debug.DEBUG_RED("merge_failed_links() is NOT IMPLEMENTED")
1130        else:
1131            DEBUG_FN("skipping merge_failed_links() because it requires "
1132                     "real network connections\n"
1133                     "and we weren't asked to --attempt-live-connections")
1134
1135    def setup_graph(self, part):
1136        """Set up an intersite graph
1137
1138        An intersite graph has a Vertex for each site object, a
1139        MultiEdge for each SiteLink object, and a MutliEdgeSet for
1140        each siteLinkBridge object (or implied siteLinkBridge). It
1141        reflects the intersite topology in a slightly more abstract
1142        graph form.
1143
1144        Roughly corresponds to MS-ADTS 6.2.2.3.4.3
1145
1146        :param part: a Partition object
1147        :returns: an InterSiteGraph object
1148        """
1149        # If 'Bridge all site links' is enabled and Win2k3 bridges required
1150        # is not set
1151        # NTDSTRANSPORT_OPT_BRIDGES_REQUIRED 0x00000002
1152        # No documentation for this however, ntdsapi.h appears to have:
1153        # NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x00001000
1154        bridges_required = self.my_site.site_options & 0x00001002 != 0
1155        transport_guid = str(self.ip_transport.guid)
1156
1157        g = setup_graph(part, self.site_table, transport_guid,
1158                        self.sitelink_table, bridges_required)
1159
1160        if self.verify or self.dot_file_dir is not None:
1161            dot_edges = []
1162            for edge in g.edges:
1163                for a, b in itertools.combinations(edge.vertices, 2):
1164                    dot_edges.append((a.site.site_dnstr, b.site.site_dnstr))
1165            verify_properties = ()
1166            name = 'site_edges_%s' % part.partstr
1167            verify_and_dot(name, dot_edges, directed=False,
1168                           label=self.my_dsa_dnstr,
1169                           properties=verify_properties, debug=DEBUG,
1170                           verify=self.verify,
1171                           dot_file_dir=self.dot_file_dir)
1172
1173        return g
1174
1175    def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
1176        """Get a bridghead DC for a site.
1177
1178        Part of MS-ADTS 6.2.2.3.4.4
1179
1180        :param site: site object representing for which a bridgehead
1181            DC is desired.
1182        :param part: crossRef for NC to replicate.
1183        :param transport: interSiteTransport object for replication
1184            traffic.
1185        :param partial_ok: True if a DC containing a partial
1186            replica or a full replica will suffice, False if only
1187            a full replica will suffice.
1188        :param detect_failed: True to detect failed DCs and route
1189            replication traffic around them, False to assume no DC
1190            has failed.
1191        :return: dsa object for the bridgehead DC or None
1192        """
1193
1194        bhs = self.get_all_bridgeheads(site, part, transport,
1195                                       partial_ok, detect_failed)
1196        if not bhs:
1197            debug.DEBUG_MAGENTA("get_bridgehead FAILED:\nsitedn = %s" %
1198                                site.site_dnstr)
1199            return None
1200
1201        debug.DEBUG_GREEN("get_bridgehead:\n\tsitedn = %s\n\tbhdn = %s" %
1202                          (site.site_dnstr, bhs[0].dsa_dnstr))
1203        return bhs[0]
1204
1205    def get_all_bridgeheads(self, site, part, transport,
1206                            partial_ok, detect_failed):
1207        """Get all bridghead DCs on a site satisfying the given criteria
1208
1209        Part of MS-ADTS 6.2.2.3.4.4
1210
1211        :param site: site object representing the site for which
1212            bridgehead DCs are desired.
1213        :param part: partition for NC to replicate.
1214        :param transport: interSiteTransport object for
1215            replication traffic.
1216        :param partial_ok: True if a DC containing a partial
1217            replica or a full replica will suffice, False if
1218            only a full replica will suffice.
1219        :param detect_failed: True to detect failed DCs and route
1220            replication traffic around them, FALSE to assume
1221            no DC has failed.
1222        :return: list of dsa object for available bridgehead DCs
1223        """
1224        bhs = []
1225
1226        if transport.name != "IP":
1227            raise KCCError("get_all_bridgeheads has run into a "
1228                           "non-IP transport! %r"
1229                           % (transport.name,))
1230
1231        DEBUG_FN(site.rw_dsa_table)
1232        for dsa in site.rw_dsa_table.values():
1233
1234            pdnstr = dsa.get_parent_dnstr()
1235
1236            # IF t!bridgeheadServerListBL has one or more values and
1237            # t!bridgeheadServerListBL does not contain a reference
1238            # to the parent object of dc then skip dc
1239            if ((len(transport.bridgehead_list) != 0 and
1240                 pdnstr not in transport.bridgehead_list)):
1241                continue
1242
1243            # IF dc is in the same site as the local DC
1244            #    IF a replica of cr!nCName is not in the set of NC replicas
1245            #    that "should be present" on dc or a partial replica of the
1246            #    NC "should be present" but partialReplicasOkay = FALSE
1247            #        Skip dc
1248            if self.my_site.same_site(dsa):
1249                needed, ro, partial = part.should_be_present(dsa)
1250                if not needed or (partial and not partial_ok):
1251                    continue
1252                rep = dsa.get_current_replica(part.nc_dnstr)
1253
1254            # ELSE
1255            #     IF an NC replica of cr!nCName is not in the set of NC
1256            #     replicas that "are present" on dc or a partial replica of
1257            #     the NC "is present" but partialReplicasOkay = FALSE
1258            #          Skip dc
1259            else:
1260                rep = dsa.get_current_replica(part.nc_dnstr)
1261                if rep is None or (rep.is_partial() and not partial_ok):
1262                    continue
1263
1264            # IF AmIRODC() and cr!nCName corresponds to default NC then
1265            #     Let dsaobj be the nTDSDSA object of the dc
1266            #     IF  dsaobj.msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
1267            #         Skip dc
1268            if self.my_dsa.is_ro() and rep is not None and rep.is_default():
1269                if not dsa.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
1270                    continue
1271
1272            # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1273            #     Skip dc
1274            if self.is_bridgehead_failed(dsa, detect_failed):
1275                DEBUG("bridgehead is failed")
1276                continue
1277
1278            DEBUG_FN("found a bridgehead: %s" % dsa.dsa_dnstr)
1279            bhs.append(dsa)
1280
1281        # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1282        # s!options
1283        #    SORT bhs such that all GC servers precede DCs that are not GC
1284        #    servers, and otherwise by ascending objectGUID
1285        # ELSE
1286        #    SORT bhs in a random order
1287        if site.is_random_bridgehead_disabled():
1288            bhs.sort(sort_dsa_by_gc_and_guid)
1289        else:
1290            random.shuffle(bhs)
1291        debug.DEBUG_YELLOW(bhs)
1292        return bhs
1293
1294    def is_bridgehead_failed(self, dsa, detect_failed):
1295        """Determine whether a given DC is known to be in a failed state
1296
1297        :param dsa: the bridgehead to test
1298        :param detect_failed: True to really check, False to assume no failure
1299        :return: True if and only if the DC should be considered failed
1300
1301        Here we DEPART from the pseudo code spec which appears to be
1302        wrong. It says, in full:
1303
1304    /***** BridgeheadDCFailed *****/
1305    /* Determine whether a given DC is known to be in a failed state.
1306     * IN: objectGUID - objectGUID of the DC's nTDSDSA object.
1307     * IN: detectFailedDCs - TRUE if and only failed DC detection is
1308     *     enabled.
1309     * RETURNS: TRUE if and only if the DC should be considered to be in a
1310     *          failed state.
1311     */
1312    BridgeheadDCFailed(IN GUID objectGUID, IN bool detectFailedDCs) : bool
1313    {
1314        IF bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set in
1315        the options attribute of the site settings object for the local
1316        DC's site
1317            RETURN FALSE
1318        ELSEIF a tuple z exists in the kCCFailedLinks or
1319        kCCFailedConnections variables such that z.UUIDDsa =
1320        objectGUID, z.FailureCount > 1, and the current time -
1321        z.TimeFirstFailure > 2 hours
1322            RETURN TRUE
1323        ELSE
1324            RETURN detectFailedDCs
1325        ENDIF
1326    }
1327
1328        where you will see detectFailedDCs is not behaving as
1329        advertised -- it is acting as a default return code in the
1330        event that a failure is not detected, not a switch turning
1331        detection on or off. Elsewhere the documentation seems to
1332        concur with the comment rather than the code.
1333        """
1334        if not detect_failed:
1335            return False
1336
1337        # NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x00000008
1338        # When DETECT_STALE_DISABLED, we can never know of if
1339        # it's in a failed state
1340        if self.my_site.site_options & 0x00000008:
1341            return False
1342
1343        return self.is_stale_link_connection(dsa)
1344
1345    def create_connection(self, part, rbh, rsite, transport,
1346                          lbh, lsite, link_opt, link_sched,
1347                          partial_ok, detect_failed):
1348        """Create an nTDSConnection object as specified if it doesn't exist.
1349
1350        Part of MS-ADTS 6.2.2.3.4.5
1351
1352        :param part: crossRef object for the NC to replicate.
1353        :param rbh: nTDSDSA object for DC to act as the
1354            IDL_DRSGetNCChanges server (which is in a site other
1355            than the local DC's site).
1356        :param rsite: site of the rbh
1357        :param transport: interSiteTransport object for the transport
1358            to use for replication traffic.
1359        :param lbh: nTDSDSA object for DC to act as the
1360            IDL_DRSGetNCChanges client (which is in the local DC's site).
1361        :param lsite: site of the lbh
1362        :param link_opt: Replication parameters (aggregated siteLink options,
1363                                                 etc.)
1364        :param link_sched: Schedule specifying the times at which
1365            to begin replicating.
1366        :partial_ok: True if bridgehead DCs containing partial
1367            replicas of the NC are acceptable.
1368        :param detect_failed: True to detect failed DCs and route
1369            replication traffic around them, FALSE to assume no DC
1370            has failed.
1371        """
1372        rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1373                                            partial_ok, False)
1374        rbh_table = dict((x.dsa_dnstr, x) for x in rbhs_all)
1375
1376        debug.DEBUG_GREY("rbhs_all: %s %s" % (len(rbhs_all),
1377                                              [x.dsa_dnstr for x in rbhs_all]))
1378
1379        # MS-TECH says to compute rbhs_avail but then doesn't use it
1380        # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1381        #                                        partial_ok, detect_failed)
1382
1383        lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1384                                            partial_ok, False)
1385        if lbh.is_ro():
1386            lbhs_all.append(lbh)
1387
1388        debug.DEBUG_GREY("lbhs_all: %s %s" % (len(lbhs_all),
1389                                              [x.dsa_dnstr for x in lbhs_all]))
1390
1391        # MS-TECH says to compute lbhs_avail but then doesn't use it
1392        # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1393        #                                       partial_ok, detect_failed)
1394
1395        # FOR each nTDSConnection object cn such that the parent of cn is
1396        # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1397        for ldsa in lbhs_all:
1398            for cn in ldsa.connect_table.values():
1399
1400                rdsa = rbh_table.get(cn.from_dnstr)
1401                if rdsa is None:
1402                    continue
1403
1404                debug.DEBUG_DARK_YELLOW("rdsa is %s" % rdsa.dsa_dnstr)
1405                # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1406                # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1407                # cn!transportType references t
1408                if ((cn.is_generated() and
1409                     not cn.is_rodc_topology() and
1410                     cn.transport_guid == transport.guid)):
1411
1412                    # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1413                    # cn!options and cn!schedule != sch
1414                    #     Perform an originating update to set cn!schedule to
1415                    #     sched
1416                    if ((not cn.is_user_owned_schedule() and
1417                         not cn.is_equivalent_schedule(link_sched))):
1418                        cn.schedule = link_sched
1419                        cn.set_modified(True)
1420
1421                    # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1422                    # NTDSCONN_OPT_USE_NOTIFY are set in cn
1423                    if cn.is_override_notify_default() and \
1424                       cn.is_use_notify():
1425
1426                        # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1427                        # ri.Options
1428                        #    Perform an originating update to clear bits
1429                        #    NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1430                        #    NTDSCONN_OPT_USE_NOTIFY in cn!options
1431                        if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1432                            cn.options &= \
1433                                ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1434                                  dsdb.NTDSCONN_OPT_USE_NOTIFY)
1435                            cn.set_modified(True)
1436
1437                    # ELSE
1438                    else:
1439
1440                        # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1441                        # ri.Options
1442                        #     Perform an originating update to set bits
1443                        #     NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1444                        #     NTDSCONN_OPT_USE_NOTIFY in cn!options
1445                        if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1446                            cn.options |= \
1447                                (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1448                                 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1449                            cn.set_modified(True)
1450
1451                    # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1452                    if cn.is_twoway_sync():
1453
1454                        # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1455                        # ri.Options
1456                        #     Perform an originating update to clear bit
1457                        #     NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1458                        if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1459                            cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1460                            cn.set_modified(True)
1461
1462                    # ELSE
1463                    else:
1464
1465                        # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1466                        # ri.Options
1467                        #     Perform an originating update to set bit
1468                        #     NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1469                        if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1470                            cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1471                            cn.set_modified(True)
1472
1473                    # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1474                    # in cn!options
1475                    if cn.is_intersite_compression_disabled():
1476
1477                        # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1478                        # in ri.Options
1479                        #     Perform an originating update to clear bit
1480                        #     NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1481                        #     cn!options
1482                        if ((link_opt &
1483                             dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0):
1484                            cn.options &= \
1485                                ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1486                            cn.set_modified(True)
1487
1488                    # ELSE
1489                    else:
1490                        # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1491                        # ri.Options
1492                        #     Perform an originating update to set bit
1493                        #     NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1494                        #     cn!options
1495                        if ((link_opt &
1496                             dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1497                            cn.options |= \
1498                                dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1499                            cn.set_modified(True)
1500
1501                    # Display any modified connection
1502                    if self.readonly or ldsa.is_ro():
1503                        if cn.to_be_modified:
1504                            logger.info("TO BE MODIFIED:\n%s" % cn)
1505
1506                        ldsa.commit_connections(self.samdb, ro=True)
1507                    else:
1508                        ldsa.commit_connections(self.samdb)
1509        # ENDFOR
1510
1511        valid_connections = 0
1512
1513        # FOR each nTDSConnection object cn such that cn!parent is
1514        # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1515        for ldsa in lbhs_all:
1516            for cn in ldsa.connect_table.values():
1517
1518                rdsa = rbh_table.get(cn.from_dnstr)
1519                if rdsa is None:
1520                    continue
1521
1522                debug.DEBUG_DARK_YELLOW("round 2: rdsa is %s" % rdsa.dsa_dnstr)
1523
1524                # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1525                # cn!transportType references t) and
1526                # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1527                if (((not cn.is_generated() or
1528                      cn.transport_guid == transport.guid) and
1529                     not cn.is_rodc_topology())):
1530
1531                    # LET rguid be the objectGUID of the nTDSDSA object
1532                    # referenced by cn!fromServer
1533                    # LET lguid be (cn!parent)!objectGUID
1534
1535                    # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1536                    # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1537                    #     Increment cValidConnections by 1
1538                    if ((not self.is_bridgehead_failed(rdsa, detect_failed) and
1539                         not self.is_bridgehead_failed(ldsa, detect_failed))):
1540                        valid_connections += 1
1541
1542                    # IF keepConnections does not contain cn!objectGUID
1543                    #     APPEND cn!objectGUID to keepConnections
1544                    self.kept_connections.add(cn)
1545
1546        # ENDFOR
1547        debug.DEBUG_RED("valid connections %d" % valid_connections)
1548        DEBUG("kept_connections:\n%s" % (self.kept_connections,))
1549        # IF cValidConnections = 0
1550        if valid_connections == 0:
1551
1552            # LET opt be NTDSCONN_OPT_IS_GENERATED
1553            opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1554
1555            # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1556            #     SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1557            #     NTDSCONN_OPT_USE_NOTIFY in opt
1558            if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1559                opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1560                        dsdb.NTDSCONN_OPT_USE_NOTIFY)
1561
1562            # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1563            #     SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1564            if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1565                opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1566
1567            # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1568            # ri.Options
1569            #     SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1570            if ((link_opt &
1571                 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0):
1572                opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1573
1574            # Perform an originating update to create a new nTDSConnection
1575            # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1576            # cn!options = opt, cn!transportType is a reference to t,
1577            # cn!fromServer is a reference to rbh, and cn!schedule = sch
1578            DEBUG_FN("new connection, KCC dsa: %s" % self.my_dsa.dsa_dnstr)
1579            system_flags = (dsdb.SYSTEM_FLAG_CONFIG_ALLOW_RENAME |
1580                            dsdb.SYSTEM_FLAG_CONFIG_ALLOW_MOVE)
1581
1582            cn = lbh.new_connection(opt, system_flags, transport,
1583                                    rbh.dsa_dnstr, link_sched)
1584
1585            # Display any added connection
1586            if self.readonly or lbh.is_ro():
1587                if cn.to_be_added:
1588                    logger.info("TO BE ADDED:\n%s" % cn)
1589
1590                lbh.commit_connections(self.samdb, ro=True)
1591            else:
1592                lbh.commit_connections(self.samdb)
1593
1594            # APPEND cn!objectGUID to keepConnections
1595            self.kept_connections.add(cn)
1596
1597    def add_transports(self, vertex, local_vertex, graph, detect_failed):
1598        """Build a Vertex's transport lists
1599
1600        Each vertex has accept_red_red and accept_black lists that
1601        list what transports they accept under various conditions. The
1602        only transport that is ever accepted is IP, and a dummy extra
1603        transport called "EDGE_TYPE_ALL".
1604
1605        Part of MS-ADTS 6.2.2.3.4.3 -- ColorVertices
1606
1607        :param vertex: the remote vertex we are thinking about
1608        :param local_vertex: the vertex relating to the local site.
1609        :param graph: the intersite graph
1610        :param detect_failed: whether to detect failed links
1611        :return: True if some bridgeheads were not found
1612        """
1613        # The docs ([MS-ADTS] 6.2.2.3.4.3) say to use local_vertex
1614        # here, but using vertex seems to make more sense. That is,
1615        # the docs want this:
1616        #
1617        # bh = self.get_bridgehead(local_vertex.site, vertex.part, transport,
1618        #                         local_vertex.is_black(), detect_failed)
1619        #
1620        # TODO WHY?????
1621
1622        vertex.accept_red_red = []
1623        vertex.accept_black = []
1624        found_failed = False
1625
1626        if vertex in graph.connected_vertices:
1627            t_guid = str(self.ip_transport.guid)
1628
1629            bh = self.get_bridgehead(vertex.site, vertex.part,
1630                                     self.ip_transport,
1631                                     vertex.is_black(), detect_failed)
1632            if bh is None:
1633                if vertex.site.is_rodc_site():
1634                    vertex.accept_red_red.append(t_guid)
1635                else:
1636                    found_failed = True
1637            else:
1638                vertex.accept_red_red.append(t_guid)
1639                vertex.accept_black.append(t_guid)
1640
1641        # Add additional transport to ensure another run of Dijkstra
1642        vertex.accept_red_red.append("EDGE_TYPE_ALL")
1643        vertex.accept_black.append("EDGE_TYPE_ALL")
1644
1645        return found_failed
1646
1647    def create_connections(self, graph, part, detect_failed):
1648        """Create intersite NTDSConnections as needed by a partition
1649
1650        Construct an NC replica graph for the NC identified by
1651        the given crossRef, then create any additional nTDSConnection
1652        objects required.
1653
1654        :param graph: site graph.
1655        :param part: crossRef object for NC.
1656        :param detect_failed:  True to detect failed DCs and route
1657            replication traffic around them, False to assume no DC
1658            has failed.
1659
1660        Modifies self.kept_connections by adding any connections
1661        deemed to be "in use".
1662
1663        :return: (all_connected, found_failed_dc)
1664        (all_connected) True if the resulting NC replica graph
1665            connects all sites that need to be connected.
1666        (found_failed_dc) True if one or more failed DCs were
1667            detected.
1668        """
1669        all_connected = True
1670        found_failed = False
1671
1672        DEBUG_FN("create_connections(): enter\n"
1673                 "\tpartdn=%s\n\tdetect_failed=%s" %
1674                 (part.nc_dnstr, detect_failed))
1675
1676        # XXX - This is a highly abbreviated function from the MS-TECH
1677        #       ref.  It creates connections between bridgeheads to all
1678        #       sites that have appropriate replicas.  Thus we are not
1679        #       creating a minimum cost spanning tree but instead
1680        #       producing a fully connected tree.  This should produce
1681        #       a full (albeit not optimal cost) replication topology.
1682
1683        my_vertex = Vertex(self.my_site, part)
1684        my_vertex.color_vertex()
1685
1686        for v in graph.vertices:
1687            v.color_vertex()
1688            if self.add_transports(v, my_vertex, graph, detect_failed):
1689                found_failed = True
1690
1691        # No NC replicas for this NC in the site of the local DC,
1692        # so no nTDSConnection objects need be created
1693        if my_vertex.is_white():
1694            return all_connected, found_failed
1695
1696        edge_list, n_components = get_spanning_tree_edges(graph,
1697                                                          self.my_site,
1698                                                          label=part.partstr)
1699
1700        DEBUG_FN("%s Number of components: %d" %
1701                 (part.nc_dnstr, n_components))
1702        if n_components > 1:
1703            all_connected = False
1704
1705        # LET partialReplicaOkay be TRUE if and only if
1706        # localSiteVertex.Color = COLOR.BLACK
1707        partial_ok = my_vertex.is_black()
1708
1709        # Utilize the IP transport only for now
1710        transport = self.ip_transport
1711
1712        DEBUG("edge_list %s" % edge_list)
1713        for e in edge_list:
1714            # XXX more accurate comparison?
1715            if e.directed and e.vertices[0].site is self.my_site:
1716                continue
1717
1718            if e.vertices[0].site is self.my_site:
1719                rsite = e.vertices[1].site
1720            else:
1721                rsite = e.vertices[0].site
1722
1723            # We don't make connections to our own site as that
1724            # is intrasite topology generator's job
1725            if rsite is self.my_site:
1726                DEBUG("rsite is my_site")
1727                continue
1728
1729            # Determine bridgehead server in remote site
1730            rbh = self.get_bridgehead(rsite, part, transport,
1731                                      partial_ok, detect_failed)
1732            if rbh is None:
1733                continue
1734
1735            # RODC acts as an BH for itself
1736            # IF AmIRODC() then
1737            #     LET lbh be the nTDSDSA object of the local DC
1738            # ELSE
1739            #     LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1740            #     cr, t, partialReplicaOkay, detectFailedDCs)
1741            if self.my_dsa.is_ro():
1742                lsite = self.my_site
1743                lbh = self.my_dsa
1744            else:
1745                lsite = self.my_site
1746                lbh = self.get_bridgehead(lsite, part, transport,
1747                                          partial_ok, detect_failed)
1748            # TODO
1749            if lbh is None:
1750                debug.DEBUG_RED("DISASTER! lbh is None")
1751                return False, True
1752
1753            DEBUG_FN("lsite: %s\nrsite: %s" % (lsite, rsite))
1754            DEBUG_FN("vertices %s" % (e.vertices,))
1755            debug.DEBUG_BLUE("bridgeheads\n%s\n%s\n%s" % (lbh, rbh, "-" * 70))
1756
1757            sitelink = e.site_link
1758            if sitelink is None:
1759                link_opt = 0x0
1760                link_sched = None
1761            else:
1762                link_opt = sitelink.options
1763                link_sched = sitelink.schedule
1764
1765            self.create_connection(part, rbh, rsite, transport,
1766                                   lbh, lsite, link_opt, link_sched,
1767                                   partial_ok, detect_failed)
1768
1769        return all_connected, found_failed
1770
1771    def create_intersite_connections(self):
1772        """Create NTDSConnections as necessary for all partitions.
1773
1774        Computes an NC replica graph for each NC replica that "should be
1775        present" on the local DC or "is present" on any DC in the same site
1776        as the local DC. For each edge directed to an NC replica on such a
1777        DC from an NC replica on a DC in another site, the KCC creates an
1778        nTDSConnection object to imply that edge if one does not already
1779        exist.
1780
1781        Modifies self.kept_connections - A set of nTDSConnection
1782        objects for edges that are directed
1783        to the local DC's site in one or more NC replica graphs.
1784
1785        :return: True if spanning trees were created for all NC replica
1786                 graphs, otherwise False.
1787        """
1788        all_connected = True
1789        self.kept_connections = set()
1790
1791        # LET crossRefList be the set containing each object o of class
1792        # crossRef such that o is a child of the CN=Partitions child of the
1793        # config NC
1794
1795        # FOR each crossRef object cr in crossRefList
1796        #    IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1797        #        is clear in cr!systemFlags, skip cr.
1798        #    LET g be the GRAPH return of SetupGraph()
1799
1800        for part in self.part_table.values():
1801
1802            if not part.is_enabled():
1803                continue
1804
1805            if part.is_foreign():
1806                continue
1807
1808            graph = self.setup_graph(part)
1809
1810            # Create nTDSConnection objects, routing replication traffic
1811            # around "failed" DCs.
1812            found_failed = False
1813
1814            connected, found_failed = self.create_connections(graph,
1815                                                              part, True)
1816
1817            DEBUG("with detect_failed: connected %s Found failed %s" %
1818                  (connected, found_failed))
1819            if not connected:
1820                all_connected = False
1821
1822                if found_failed:
1823                    # One or more failed DCs preclude use of the ideal NC
1824                    # replica graph. Add connections for the ideal graph.
1825                    self.create_connections(graph, part, False)
1826
1827        return all_connected
1828
1829    def intersite(self, ping):
1830        """Generate the inter-site KCC replica graph and nTDSConnections
1831
1832        As per MS-ADTS 6.2.2.3.
1833
1834        If self.readonly is False, the connections are added to self.samdb.
1835
1836        Produces self.kept_connections which is a set of NTDS
1837        Connections that should be kept during subsequent pruning
1838        process.
1839
1840        After this has run, all sites should be connected in a minimum
1841        spanning tree.
1842
1843        :param ping: An oracle function of remote site availability
1844        :return (True or False):  (True) if the produced NC replica
1845            graph connects all sites that need to be connected
1846        """
1847
1848        # Retrieve my DSA
1849        mydsa = self.my_dsa
1850        mysite = self.my_site
1851        all_connected = True
1852
1853        DEBUG_FN("intersite(): enter")
1854
1855        # Determine who is the ISTG
1856        if self.readonly:
1857            mysite.select_istg(self.samdb, mydsa, ro=True)
1858        else:
1859            mysite.select_istg(self.samdb, mydsa, ro=False)
1860
1861        # Test whether local site has topology disabled
1862        if mysite.is_intersite_topology_disabled():
1863            DEBUG_FN("intersite(): exit disabled all_connected=%d" %
1864                     all_connected)
1865            return all_connected
1866
1867        if not mydsa.is_istg():
1868            DEBUG_FN("intersite(): exit not istg all_connected=%d" %
1869                     all_connected)
1870            return all_connected
1871
1872        self.merge_failed_links(ping)
1873
1874        # For each NC with an NC replica that "should be present" on the
1875        # local DC or "is present" on any DC in the same site as the
1876        # local DC, the KCC constructs a site graph--a precursor to an NC
1877        # replica graph. The site connectivity for a site graph is defined
1878        # by objects of class interSiteTransport, siteLink, and
1879        # siteLinkBridge in the config NC.
1880
1881        all_connected = self.create_intersite_connections()
1882
1883        DEBUG_FN("intersite(): exit all_connected=%d" % all_connected)
1884        return all_connected
1885
1886    # This function currently does no actions. The reason being that we cannot
1887    # perform modifies in this way on the RODC.
1888    def update_rodc_connection(self, ro=True):
1889        """Updates the RODC NTFRS connection object.
1890
1891        If the local DSA is not an RODC, this does nothing.
1892        """
1893        if not self.my_dsa.is_ro():
1894            return
1895
1896        # Given an nTDSConnection object cn1, such that cn1.options contains
1897        # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1898        # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1899        # that the following is true:
1900        #
1901        #     cn1.fromServer = cn2.fromServer
1902        #     cn1.schedule = cn2.schedule
1903        #
1904        # If no such cn2 can be found, cn1 is not modified.
1905        # If no such cn1 can be found, nothing is modified by this task.
1906
1907        all_connections = self.my_dsa.connect_table.values()
1908        ro_connections = [x for x in all_connections if x.is_rodc_topology()]
1909        rw_connections = [x for x in all_connections
1910                          if x not in ro_connections]
1911
1912        # XXX here we are dealing with multiple RODC_TOPO connections,
1913        # if they exist. It is not clear whether the spec means that
1914        # or if it ever arises.
1915        if rw_connections and ro_connections:
1916            for con in ro_connections:
1917                cn2 = rw_connections[0]
1918                con.from_dnstr = cn2.from_dnstr
1919                con.schedule = cn2.schedule
1920                con.to_be_modified = True
1921
1922            self.my_dsa.commit_connections(self.samdb, ro=ro)
1923
1924    def intrasite_max_node_edges(self, node_count):
1925        """Find the maximum number of edges directed to an intrasite node
1926
1927        The KCC does not create more than 50 edges directed to a
1928        single DC. To optimize replication, we compute that each node
1929        should have n+2 total edges directed to it such that (n) is
1930        the smallest non-negative integer satisfying
1931        (node_count <= 2*(n*n) + 6*n + 7)
1932
1933        (If the number of edges is m (i.e. n + 2), that is the same as
1934        2 * m*m - 2 * m + 3). We think in terms of n because that is
1935        the number of extra connections over the double directed ring
1936        that exists by default.
1937
1938        edges  n   nodecount
1939          2    0    7
1940          3    1   15
1941          4    2   27
1942          5    3   43
1943                  ...
1944         50   48 4903
1945
1946        :param node_count: total number of nodes in the replica graph
1947
1948        The intention is that there should be no more than 3 hops
1949        between any two DSAs at a site. With up to 7 nodes the 2 edges
1950        of the ring are enough; any configuration of extra edges with
1951        8 nodes will be enough. It is less clear that the 3 hop
1952        guarantee holds at e.g. 15 nodes in degenerate cases, but
1953        those are quite unlikely given the extra edges are randomly
1954        arranged.
1955
1956        :param node_count: the number of nodes in the site
1957        "return: The desired maximum number of connections
1958        """
1959        n = 0
1960        while True:
1961            if node_count <= (2 * (n * n) + (6 * n) + 7):
1962                break
1963            n = n + 1
1964        n = n + 2
1965        if n < 50:
1966            return n
1967        return 50
1968
1969    def construct_intrasite_graph(self, site_local, dc_local,
1970                                  nc_x, gc_only, detect_stale):
1971        """Create an intrasite graph using given parameters
1972
1973        This might be called a number of times per site with different
1974        parameters.
1975
1976        Based on [MS-ADTS] 6.2.2.2
1977
1978        :param site_local: site for which we are working
1979        :param dc_local: local DC that potentially needs a replica
1980        :param nc_x:  naming context (x) that we are testing if it
1981                    "should be present" on the local DC
1982        :param gc_only: Boolean - only consider global catalog servers
1983        :param detect_stale: Boolean - check whether links seems down
1984        :return: None
1985        """
1986        # We're using the MS notation names here to allow
1987        # correlation back to the published algorithm.
1988        #
1989        # nc_x     - naming context (x) that we are testing if it
1990        #            "should be present" on the local DC
1991        # f_of_x   - replica (f) found on a DC (s) for NC (x)
1992        # dc_s     - DC where f_of_x replica was found
1993        # dc_local - local DC that potentially needs a replica
1994        #            (f_of_x)
1995        # r_list   - replica list R
1996        # p_of_x   - replica (p) is partial and found on a DC (s)
1997        #            for NC (x)
1998        # l_of_x   - replica (l) is the local replica for NC (x)
1999        #            that should appear on the local DC
2000        # r_len = is length of replica list |R|
2001        #
2002        # If the DSA doesn't need a replica for this
2003        # partition (NC x) then continue
2004        needed, ro, partial = nc_x.should_be_present(dc_local)
2005
2006        debug.DEBUG_YELLOW("construct_intrasite_graph(): enter" +
2007                           "\n\tgc_only=%d" % gc_only +
2008                           "\n\tdetect_stale=%d" % detect_stale +
2009                           "\n\tneeded=%s" % needed +
2010                           "\n\tro=%s" % ro +
2011                           "\n\tpartial=%s" % partial +
2012                           "\n%s" % nc_x)
2013
2014        if not needed:
2015            debug.DEBUG_RED("%s lacks 'should be present' status, "
2016                            "aborting construct_intrasite_graph!" %
2017                            nc_x.nc_dnstr)
2018            return
2019
2020        # Create a NCReplica that matches what the local replica
2021        # should say.  We'll use this below in our r_list
2022        l_of_x = NCReplica(dc_local, nc_x.nc_dnstr)
2023
2024        l_of_x.identify_by_basedn(self.samdb)
2025
2026        l_of_x.rep_partial = partial
2027        l_of_x.rep_ro = ro
2028
2029        # Add this replica that "should be present" to the
2030        # needed replica table for this DSA
2031        dc_local.add_needed_replica(l_of_x)
2032
2033        # Replica list
2034        #
2035        # Let R be a sequence containing each writable replica f of x
2036        # such that f "is present" on a DC s satisfying the following
2037        # criteria:
2038        #
2039        #  * s is a writable DC other than the local DC.
2040        #
2041        #  * s is in the same site as the local DC.
2042        #
2043        #  * If x is a read-only full replica and x is a domain NC,
2044        #    then the DC's functional level is at least
2045        #    DS_BEHAVIOR_WIN2008.
2046        #
2047        #  * Bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED is set
2048        #    in the options attribute of the site settings object for
2049        #    the local DC's site, or no tuple z exists in the
2050        #    kCCFailedLinks or kCCFailedConnections variables such
2051        #    that z.UUIDDsa is the objectGUID of the nTDSDSA object
2052        #    for s, z.FailureCount > 0, and the current time -
2053        #    z.TimeFirstFailure > 2 hours.
2054
2055        r_list = []
2056
2057        # We'll loop thru all the DSAs looking for
2058        # writeable NC replicas that match the naming
2059        # context dn for (nc_x)
2060        #
2061        for dc_s in self.my_site.dsa_table.values():
2062            # If this partition (nc_x) doesn't appear as a
2063            # replica (f_of_x) on (dc_s) then continue
2064            if nc_x.nc_dnstr not in dc_s.current_rep_table:
2065                continue
2066
2067            # Pull out the NCReplica (f) of (x) with the dn
2068            # that matches NC (x) we are examining.
2069            f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2070
2071            # Replica (f) of NC (x) must be writable
2072            if f_of_x.is_ro():
2073                continue
2074
2075            # Replica (f) of NC (x) must satisfy the
2076            # "is present" criteria for DC (s) that
2077            # it was found on
2078            if not f_of_x.is_present():
2079                continue
2080
2081            # DC (s) must be a writable DSA other than
2082            # my local DC.  In other words we'd only replicate
2083            # from other writable DC
2084            if dc_s.is_ro() or dc_s is dc_local:
2085                continue
2086
2087            # Certain replica graphs are produced only
2088            # for global catalogs, so test against
2089            # method input parameter
2090            if gc_only and not dc_s.is_gc():
2091                continue
2092
2093            # DC (s) must be in the same site as the local DC
2094            # as this is the intra-site algorithm. This is
2095            # handled by virtue of placing DSAs in per
2096            # site objects (see enclosing for() loop)
2097
2098            # If NC (x) is intended to be read-only full replica
2099            # for a domain NC on the target DC then the source
2100            # DC should have functional level at minimum WIN2008
2101            #
2102            # Effectively we're saying that in order to replicate
2103            # to a targeted RODC (which was introduced in Windows 2008)
2104            # then we have to replicate from a DC that is also minimally
2105            # at that level.
2106            #
2107            # You can also see this requirement in the MS special
2108            # considerations for RODC which state that to deploy
2109            # an RODC, at least one writable domain controller in
2110            # the domain must be running Windows Server 2008
2111            if ro and not partial and nc_x.nc_type == NCType.domain:
2112                if not dc_s.is_minimum_behavior(dsdb.DS_DOMAIN_FUNCTION_2008):
2113                    continue
2114
2115            # If we haven't been told to turn off stale connection
2116            # detection and this dsa has a stale connection then
2117            # continue
2118            if detect_stale and self.is_stale_link_connection(dc_s):
2119                continue
2120
2121            # Replica meets criteria.  Add it to table indexed
2122            # by the GUID of the DC that it appears on
2123            r_list.append(f_of_x)
2124
2125        # If a partial (not full) replica of NC (x) "should be present"
2126        # on the local DC, append to R each partial replica (p of x)
2127        # such that p "is present" on a DC satisfying the same
2128        # criteria defined above for full replica DCs.
2129        #
2130        # XXX This loop and the previous one differ only in whether
2131        # the replica is partial or not. here we only accept partial
2132        # (because we're partial); before we only accepted full. Order
2133        # doen't matter (the list is sorted a few lines down) so these
2134        # loops could easily be merged. Or this could be a helper
2135        # function.
2136
2137        if partial:
2138            # Now we loop thru all the DSAs looking for
2139            # partial NC replicas that match the naming
2140            # context dn for (NC x)
2141            for dc_s in self.my_site.dsa_table.values():
2142
2143                # If this partition NC (x) doesn't appear as a
2144                # replica (p) of NC (x) on the dsa DC (s) then
2145                # continue
2146                if nc_x.nc_dnstr not in dc_s.current_rep_table:
2147                    continue
2148
2149                # Pull out the NCReplica with the dn that
2150                # matches NC (x) we are examining.
2151                p_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
2152
2153                # Replica (p) of NC (x) must be partial
2154                if not p_of_x.is_partial():
2155                    continue
2156
2157                # Replica (p) of NC (x) must satisfy the
2158                # "is present" criteria for DC (s) that
2159                # it was found on
2160                if not p_of_x.is_present():
2161                    continue
2162
2163                # DC (s) must be a writable DSA other than
2164                # my DSA.  In other words we'd only replicate
2165                # from other writable DSA
2166                if dc_s.is_ro() or dc_s is dc_local:
2167                    continue
2168
2169                # Certain replica graphs are produced only
2170                # for global catalogs, so test against
2171                # method input parameter
2172                if gc_only and not dc_s.is_gc():
2173                    continue
2174
2175                # If we haven't been told to turn off stale connection
2176                # detection and this dsa has a stale connection then
2177                # continue
2178                if detect_stale and self.is_stale_link_connection(dc_s):
2179                    continue
2180
2181                # Replica meets criteria.  Add it to table indexed
2182                # by the GUID of the DSA that it appears on
2183                r_list.append(p_of_x)
2184
2185        # Append to R the NC replica that "should be present"
2186        # on the local DC
2187        r_list.append(l_of_x)
2188
2189        r_list.sort(key=lambda rep: ndr_pack(rep.rep_dsa_guid))
2190        r_len = len(r_list)
2191
2192        max_node_edges = self.intrasite_max_node_edges(r_len)
2193
2194        # Add a node for each r_list element to the replica graph
2195        graph_list = []
2196        for rep in r_list:
2197            node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
2198            graph_list.append(node)
2199
2200        # For each r(i) from (0 <= i < |R|-1)
2201        i = 0
2202        while i < (r_len - 1):
2203            # Add an edge from r(i) to r(i+1) if r(i) is a full
2204            # replica or r(i+1) is a partial replica
2205            if not r_list[i].is_partial() or r_list[i +1].is_partial():
2206                graph_list[i + 1].add_edge_from(r_list[i].rep_dsa_dnstr)
2207
2208            # Add an edge from r(i+1) to r(i) if r(i+1) is a full
2209            # replica or ri is a partial replica.
2210            if not r_list[i + 1].is_partial() or r_list[i].is_partial():
2211                graph_list[i].add_edge_from(r_list[i + 1].rep_dsa_dnstr)
2212            i = i + 1
2213
2214        # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
2215        # or r0 is a partial replica.
2216        if not r_list[r_len - 1].is_partial() or r_list[0].is_partial():
2217            graph_list[0].add_edge_from(r_list[r_len - 1].rep_dsa_dnstr)
2218
2219        # Add an edge from r0 to r|R|-1 if r0 is a full replica or
2220        # r|R|-1 is a partial replica.
2221        if not r_list[0].is_partial() or r_list[r_len -1].is_partial():
2222            graph_list[r_len - 1].add_edge_from(r_list[0].rep_dsa_dnstr)
2223
2224        DEBUG("r_list is length %s" % len(r_list))
2225        DEBUG('\n'.join(str((x.rep_dsa_guid, x.rep_dsa_dnstr))
2226                        for x in r_list))
2227
2228        do_dot_files = self.dot_file_dir is not None and self.debug
2229        if self.verify or do_dot_files:
2230            dot_edges = []
2231            dot_vertices = set()
2232            for v1 in graph_list:
2233                dot_vertices.add(v1.dsa_dnstr)
2234                for v2 in v1.edge_from:
2235                    dot_edges.append((v2, v1.dsa_dnstr))
2236                    dot_vertices.add(v2)
2237
2238            verify_properties = ('connected',)
2239            verify_and_dot('intrasite_pre_ntdscon', dot_edges, dot_vertices,
2240                           label='%s__%s__%s' % (site_local.site_dnstr,
2241                                                 nctype_lut[nc_x.nc_type],
2242                                                 nc_x.nc_dnstr),
2243                           properties=verify_properties, debug=DEBUG,
2244                           verify=self.verify,
2245                           dot_file_dir=self.dot_file_dir,
2246                           directed=True)
2247
2248            rw_dot_vertices = set(x for x in dot_vertices
2249                                  if not self.get_dsa(x).is_ro())
2250            rw_dot_edges = [(a, b) for a, b in dot_edges if
2251                            a in rw_dot_vertices and b in rw_dot_vertices]
2252            rw_verify_properties = ('connected',
2253                                    'directed_double_ring_or_small')
2254            verify_and_dot('intrasite_rw_pre_ntdscon', rw_dot_edges,
2255                           rw_dot_vertices,
2256                           label='%s__%s__%s' % (site_local.site_dnstr,
2257                                                 nctype_lut[nc_x.nc_type],
2258                                                 nc_x.nc_dnstr),
2259                           properties=rw_verify_properties, debug=DEBUG,
2260                           verify=self.verify,
2261                           dot_file_dir=self.dot_file_dir,
2262                           directed=True)
2263
2264        # For each existing nTDSConnection object implying an edge
2265        # from rj of R to ri such that j != i, an edge from rj to ri
2266        # is not already in the graph, and the total edges directed
2267        # to ri is less than n+2, the KCC adds that edge to the graph.
2268        for vertex in graph_list:
2269            dsa = self.my_site.dsa_table[vertex.dsa_dnstr]
2270            for connect in dsa.connect_table.values():
2271                remote = connect.from_dnstr
2272                if remote in self.my_site.dsa_table:
2273                    vertex.add_edge_from(remote)
2274
2275        DEBUG('reps are:  %s' % '   '.join(x.rep_dsa_dnstr for x in r_list))
2276        DEBUG('dsas are:  %s' % '   '.join(x.dsa_dnstr for x in graph_list))
2277
2278        for tnode in graph_list:
2279            # To optimize replication latency in sites with many NC
2280            # replicas, the KCC adds new edges directed to ri to bring
2281            # the total edges to n+2, where the NC replica rk of R
2282            # from which the edge is directed is chosen at random such
2283            # that k != i and an edge from rk to ri is not already in
2284            # the graph.
2285            #
2286            # Note that the KCC tech ref does not give a number for
2287            # the definition of "sites with many NC replicas". At a
2288            # bare minimum to satisfy n+2 edges directed at a node we
2289            # have to have at least three replicas in |R| (i.e. if n
2290            # is zero then at least replicas from two other graph
2291            # nodes may direct edges to us).
2292            if r_len >= 3 and not tnode.has_sufficient_edges():
2293                candidates = [x for x in graph_list if
2294                              (x is not tnode and
2295                               x.dsa_dnstr not in tnode.edge_from)]
2296
2297                debug.DEBUG_BLUE("looking for random link for %s. r_len %d, "
2298                                 "graph len %d candidates %d"
2299                                 % (tnode.dsa_dnstr, r_len, len(graph_list),
2300                                    len(candidates)))
2301
2302                DEBUG("candidates %s" % [x.dsa_dnstr for x in candidates])
2303
2304                while candidates and not tnode.has_sufficient_edges():
2305                    other = random.choice(candidates)
2306                    DEBUG("trying to add candidate %s" % other.dsa_dnstr)
2307                    if not tnode.add_edge_from(other.dsa_dnstr):
2308                        debug.DEBUG_RED("could not add %s" % other.dsa_dnstr)
2309                    candidates.remove(other)
2310            else:
2311                DEBUG_FN("not adding links to %s: nodes %s, links is %s/%s" %
2312                         (tnode.dsa_dnstr, r_len, len(tnode.edge_from),
2313                          tnode.max_edges))
2314
2315            # Print the graph node in debug mode
2316            DEBUG_FN("%s" % tnode)
2317
2318            # For each edge directed to the local DC, ensure a nTDSConnection
2319            # points to us that satisfies the KCC criteria
2320
2321            if tnode.dsa_dnstr == dc_local.dsa_dnstr:
2322                tnode.add_connections_from_edges(dc_local, self.ip_transport)
2323
2324        if self.verify or do_dot_files:
2325            dot_edges = []
2326            dot_vertices = set()
2327            for v1 in graph_list:
2328                dot_vertices.add(v1.dsa_dnstr)
2329                for v2 in v1.edge_from:
2330                    dot_edges.append((v2, v1.dsa_dnstr))
2331                    dot_vertices.add(v2)
2332
2333            verify_properties = ('connected',)
2334            verify_and_dot('intrasite_post_ntdscon', dot_edges, dot_vertices,
2335                           label='%s__%s__%s' % (site_local.site_dnstr,
2336                                                 nctype_lut[nc_x.nc_type],
2337                                                 nc_x.nc_dnstr),
2338                           properties=verify_properties, debug=DEBUG,
2339                           verify=self.verify,
2340                           dot_file_dir=self.dot_file_dir,
2341                           directed=True)
2342
2343            rw_dot_vertices = set(x for x in dot_vertices
2344                                  if not self.get_dsa(x).is_ro())
2345            rw_dot_edges = [(a, b) for a, b in dot_edges if
2346                            a in rw_dot_vertices and b in rw_dot_vertices]
2347            rw_verify_properties = ('connected',
2348                                    'directed_double_ring_or_small')
2349            verify_and_dot('intrasite_rw_post_ntdscon', rw_dot_edges,
2350                           rw_dot_vertices,
2351                           label='%s__%s__%s' % (site_local.site_dnstr,
2352                                                 nctype_lut[nc_x.nc_type],
2353                                                 nc_x.nc_dnstr),
2354                           properties=rw_verify_properties, debug=DEBUG,
2355                           verify=self.verify,
2356                           dot_file_dir=self.dot_file_dir,
2357                           directed=True)
2358
2359    def intrasite(self):
2360        """Generate the intrasite KCC connections
2361
2362        As per MS-ADTS 6.2.2.2.
2363
2364        If self.readonly is False, the connections are added to self.samdb.
2365
2366        After this call, all DCs in each site with more than 3 DCs
2367        should be connected in a bidirectional ring. If a site has 2
2368        DCs, they will bidirectionally connected. Sites with many DCs
2369        may have arbitrary extra connections.
2370
2371        :return: None
2372        """
2373        mydsa = self.my_dsa
2374
2375        DEBUG_FN("intrasite(): enter")
2376
2377        # Test whether local site has topology disabled
2378        mysite = self.my_site
2379        if mysite.is_intrasite_topology_disabled():
2380            return
2381
2382        detect_stale = (not mysite.is_detect_stale_disabled())
2383        for connect in mydsa.connect_table.values():
2384            if connect.to_be_added:
2385                debug.DEBUG_CYAN("TO BE ADDED:\n%s" % connect)
2386
2387        # Loop thru all the partitions, with gc_only False
2388        for partdn, part in self.part_table.items():
2389            self.construct_intrasite_graph(mysite, mydsa, part, False,
2390                                           detect_stale)
2391            for connect in mydsa.connect_table.values():
2392                if connect.to_be_added:
2393                    debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2394
2395        # If the DC is a GC server, the KCC constructs an additional NC
2396        # replica graph (and creates nTDSConnection objects) for the
2397        # config NC as above, except that only NC replicas that "are present"
2398        # on GC servers are added to R.
2399        for connect in mydsa.connect_table.values():
2400            if connect.to_be_added:
2401                debug.DEBUG_YELLOW("TO BE ADDED:\n%s" % connect)
2402
2403        # Do it again, with gc_only True
2404        for partdn, part in self.part_table.items():
2405            if part.is_config():
2406                self.construct_intrasite_graph(mysite, mydsa, part, True,
2407                                               detect_stale)
2408
2409        # The DC repeats the NC replica graph computation and nTDSConnection
2410        # creation for each of the NC replica graphs, this time assuming
2411        # that no DC has failed. It does so by re-executing the steps as
2412        # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
2413        # set in the options attribute of the site settings object for
2414        # the local DC's site.  (ie. we set "detec_stale" flag to False)
2415        for connect in mydsa.connect_table.values():
2416            if connect.to_be_added:
2417                debug.DEBUG_BLUE("TO BE ADDED:\n%s" % connect)
2418
2419        # Loop thru all the partitions.
2420        for partdn, part in self.part_table.items():
2421            self.construct_intrasite_graph(mysite, mydsa, part, False,
2422                                           False)  # don't detect stale
2423
2424        # If the DC is a GC server, the KCC constructs an additional NC
2425        # replica graph (and creates nTDSConnection objects) for the
2426        # config NC as above, except that only NC replicas that "are present"
2427        # on GC servers are added to R.
2428        for connect in mydsa.connect_table.values():
2429            if connect.to_be_added:
2430                debug.DEBUG_RED("TO BE ADDED:\n%s" % connect)
2431
2432        for partdn, part in self.part_table.items():
2433            if part.is_config():
2434                self.construct_intrasite_graph(mysite, mydsa, part, True,
2435                                               False)  # don't detect stale
2436
2437        self._commit_changes(mydsa)
2438
2439    def list_dsas(self):
2440        """Compile a comprehensive list of DSA DNs
2441
2442        These are all the DSAs on all the sites that KCC would be
2443        dealing with.
2444
2445        This method is not idempotent and may not work correctly in
2446        sequence with KCC.run().
2447
2448        :return: a list of DSA DN strings.
2449        """
2450        self.load_my_site()
2451        self.load_my_dsa()
2452
2453        self.load_all_sites()
2454        self.load_all_partitions()
2455        self.load_ip_transport()
2456        self.load_all_sitelinks()
2457        dsas = []
2458        for site in self.site_table.values():
2459            dsas.extend([dsa.dsa_dnstr.replace('CN=NTDS Settings,', '', 1)
2460                         for dsa in site.dsa_table.values()])
2461        return dsas
2462
2463    def load_samdb(self, dburl, lp, creds, force=False):
2464        """Load the database using an url, loadparm, and credentials
2465
2466        If force is False, the samdb won't be reloaded if it already
2467        exists.
2468
2469        :param dburl: a database url.
2470        :param lp: a loadparm object.
2471        :param creds: a Credentials object.
2472        :param force: a boolean indicating whether to overwrite.
2473
2474        """
2475        if force or self.samdb is None:
2476            try:
2477                self.samdb = SamDB(url=dburl,
2478                                   session_info=system_session(),
2479                                   credentials=creds, lp=lp)
2480            except ldb.LdbError as e1:
2481                (num, msg) = e1.args
2482                raise KCCError("Unable to open sam database %s : %s" %
2483                               (dburl, msg))
2484
2485    def plot_all_connections(self, basename, verify_properties=()):
2486        """Helper function to plot and verify NTDSConnections
2487
2488        :param basename: an identifying string to use in filenames and logs.
2489        :param verify_properties: properties to verify (default empty)
2490        """
2491        verify = verify_properties and self.verify
2492        if not verify and self.dot_file_dir is None:
2493            return
2494
2495        dot_edges = []
2496        dot_vertices = []
2497        edge_colours = []
2498        vertex_colours = []
2499
2500        for dsa in self.dsa_by_dnstr.values():
2501            dot_vertices.append(dsa.dsa_dnstr)
2502            if dsa.is_ro():
2503                vertex_colours.append('#cc0000')
2504            else:
2505                vertex_colours.append('#0000cc')
2506            for con in dsa.connect_table.values():
2507                if con.is_rodc_topology():
2508                    edge_colours.append('red')
2509                else:
2510                    edge_colours.append('blue')
2511                dot_edges.append((con.from_dnstr, dsa.dsa_dnstr))
2512
2513        verify_and_dot(basename, dot_edges, vertices=dot_vertices,
2514                       label=self.my_dsa_dnstr,
2515                       properties=verify_properties, debug=DEBUG,
2516                       verify=verify, dot_file_dir=self.dot_file_dir,
2517                       directed=True, edge_colors=edge_colours,
2518                       vertex_colors=vertex_colours)
2519
2520    def run(self, dburl, lp, creds, forced_local_dsa=None,
2521            forget_local_links=False, forget_intersite_links=False,
2522            attempt_live_connections=False):
2523        """Perform a KCC run, possibly updating repsFrom topology
2524
2525        :param dburl: url of the database to work with.
2526        :param lp: a loadparm object.
2527        :param creds: a Credentials object.
2528        :param forced_local_dsa: pretend to be on the DSA with this dn_str
2529        :param forget_local_links: calculate as if no connections existed
2530               (boolean, default False)
2531        :param forget_intersite_links: calculate with only intrasite connection
2532               (boolean, default False)
2533        :param attempt_live_connections: attempt to connect to remote DSAs to
2534               determine link availability (boolean, default False)
2535        :return: 1 on error, 0 otherwise
2536        """
2537        if self.samdb is None:
2538            DEBUG_FN("samdb is None; let's load it from %s" % (dburl,))
2539            self.load_samdb(dburl, lp, creds, force=False)
2540
2541        if forced_local_dsa:
2542            self.samdb.set_ntds_settings_dn("CN=NTDS Settings,%s" %
2543                                            forced_local_dsa)
2544
2545        try:
2546            # Setup
2547            self.load_my_site()
2548            self.load_my_dsa()
2549
2550            self.load_all_sites()
2551            self.load_all_partitions()
2552            self.load_ip_transport()
2553            self.load_all_sitelinks()
2554
2555            if self.verify or self.dot_file_dir is not None:
2556                guid_to_dnstr = {}
2557                for site in self.site_table.values():
2558                    guid_to_dnstr.update((str(dsa.dsa_guid), dnstr)
2559                                         for dnstr, dsa
2560                                         in site.dsa_table.items())
2561
2562                self.plot_all_connections('dsa_initial')
2563
2564                dot_edges = []
2565                current_reps, needed_reps = self.my_dsa.get_rep_tables()
2566                for dnstr, c_rep in current_reps.items():
2567                    DEBUG("c_rep %s" % c_rep)
2568                    dot_edges.append((self.my_dsa.dsa_dnstr, dnstr))
2569
2570                verify_and_dot('dsa_repsFrom_initial', dot_edges,
2571                               directed=True, label=self.my_dsa_dnstr,
2572                               properties=(), debug=DEBUG, verify=self.verify,
2573                               dot_file_dir=self.dot_file_dir)
2574
2575                dot_edges = []
2576                for site in self.site_table.values():
2577                    for dsa in site.dsa_table.values():
2578                        current_reps, needed_reps = dsa.get_rep_tables()
2579                        for dn_str, rep in current_reps.items():
2580                            for reps_from in rep.rep_repsFrom:
2581                                DEBUG("rep %s" % rep)
2582                                dsa_guid = str(reps_from.source_dsa_obj_guid)
2583                                dsa_dn = guid_to_dnstr[dsa_guid]
2584                                dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2585
2586                verify_and_dot('dsa_repsFrom_initial_all', dot_edges,
2587                               directed=True, label=self.my_dsa_dnstr,
2588                               properties=(), debug=DEBUG, verify=self.verify,
2589                               dot_file_dir=self.dot_file_dir)
2590
2591                dot_edges = []
2592                dot_colours = []
2593                for link in self.sitelink_table.values():
2594                    from hashlib import md5
2595                    tmp_str = link.dnstr.encode('utf8')
2596                    colour = '#' + md5(tmp_str).hexdigest()[:6]
2597                    for a, b in itertools.combinations(link.site_list, 2):
2598                        dot_edges.append((a[1], b[1]))
2599                        dot_colours.append(colour)
2600                properties = ('connected',)
2601                verify_and_dot('dsa_sitelink_initial', dot_edges,
2602                               directed=False,
2603                               label=self.my_dsa_dnstr, properties=properties,
2604                               debug=DEBUG, verify=self.verify,
2605                               dot_file_dir=self.dot_file_dir,
2606                               edge_colors=dot_colours)
2607
2608            if forget_local_links:
2609                for dsa in self.my_site.dsa_table.values():
2610                    dsa.connect_table = dict((k, v) for k, v in
2611                                             dsa.connect_table.items()
2612                                             if v.is_rodc_topology() or
2613                                             (v.from_dnstr not in
2614                                              self.my_site.dsa_table))
2615                self.plot_all_connections('dsa_forgotten_local')
2616
2617            if forget_intersite_links:
2618                for site in self.site_table.values():
2619                    for dsa in site.dsa_table.values():
2620                        dsa.connect_table = dict((k, v) for k, v in
2621                                                 dsa.connect_table.items()
2622                                                 if site is self.my_site and
2623                                                 v.is_rodc_topology())
2624
2625                self.plot_all_connections('dsa_forgotten_all')
2626
2627            if attempt_live_connections:
2628                # Encapsulates lp and creds in a function that
2629                # attempts connections to remote DSAs.
2630                def ping(self, dnsname):
2631                    try:
2632                        drs_utils.drsuapi_connect(dnsname, self.lp, self.creds)
2633                    except drs_utils.drsException:
2634                        return False
2635                    return True
2636            else:
2637                ping = None
2638            # These are the published steps (in order) for the
2639            # MS-TECH description of the KCC algorithm ([MS-ADTS] 6.2.2)
2640
2641            # Step 1
2642            self.refresh_failed_links_connections(ping)
2643
2644            # Step 2
2645            self.intrasite()
2646
2647            # Step 3
2648            all_connected = self.intersite(ping)
2649
2650            # Step 4
2651            self.remove_unneeded_ntdsconn(all_connected)
2652
2653            # Step 5
2654            self.translate_ntdsconn()
2655
2656            # Step 6
2657            self.remove_unneeded_failed_links_connections()
2658
2659            # Step 7
2660            self.update_rodc_connection()
2661
2662            if self.verify or self.dot_file_dir is not None:
2663                self.plot_all_connections('dsa_final',
2664                                          ('connected',))
2665
2666                debug.DEBUG_MAGENTA("there are %d dsa guids" %
2667                                    len(guid_to_dnstr))
2668
2669                dot_edges = []
2670                edge_colors = []
2671                my_dnstr = self.my_dsa.dsa_dnstr
2672                current_reps, needed_reps = self.my_dsa.get_rep_tables()
2673                for dnstr, n_rep in needed_reps.items():
2674                    for reps_from in n_rep.rep_repsFrom:
2675                        guid_str = str(reps_from.source_dsa_obj_guid)
2676                        dot_edges.append((my_dnstr, guid_to_dnstr[guid_str]))
2677                        edge_colors.append('#' + str(n_rep.nc_guid)[:6])
2678
2679                verify_and_dot('dsa_repsFrom_final', dot_edges, directed=True,
2680                               label=self.my_dsa_dnstr,
2681                               properties=(), debug=DEBUG, verify=self.verify,
2682                               dot_file_dir=self.dot_file_dir,
2683                               edge_colors=edge_colors)
2684
2685                dot_edges = []
2686
2687                for site in self.site_table.values():
2688                    for dsa in site.dsa_table.values():
2689                        current_reps, needed_reps = dsa.get_rep_tables()
2690                        for n_rep in needed_reps.values():
2691                            for reps_from in n_rep.rep_repsFrom:
2692                                dsa_guid = str(reps_from.source_dsa_obj_guid)
2693                                dsa_dn = guid_to_dnstr[dsa_guid]
2694                                dot_edges.append((dsa.dsa_dnstr, dsa_dn))
2695
2696                verify_and_dot('dsa_repsFrom_final_all', dot_edges,
2697                               directed=True, label=self.my_dsa_dnstr,
2698                               properties=(), debug=DEBUG, verify=self.verify,
2699                               dot_file_dir=self.dot_file_dir)
2700
2701        except:
2702            raise
2703
2704        return 0
2705
2706    def import_ldif(self, dburl, lp, ldif_file, forced_local_dsa=None):
2707        """Import relevant objects and attributes from an LDIF file.
2708
2709        The point of this function is to allow a programmer/debugger to
2710        import an LDIF file with non-security relevent information that
2711        was previously extracted from a DC database.  The LDIF file is used
2712        to create a temporary abbreviated database.  The KCC algorithm can
2713        then run against this abbreviated database for debug or test
2714        verification that the topology generated is computationally the
2715        same between different OSes and algorithms.
2716
2717        :param dburl: path to the temporary abbreviated db to create
2718        :param lp: a loadparm object.
2719        :param ldif_file: path to the ldif file to import
2720        :param forced_local_dsa: perform KCC from this DSA's point of view
2721        :return: zero on success, 1 on error
2722        """
2723        try:
2724            self.samdb = ldif_import_export.ldif_to_samdb(dburl, lp, ldif_file,
2725                                                          forced_local_dsa)
2726        except ldif_import_export.LdifError as e:
2727            logger.critical(e)
2728            return 1
2729        return 0
2730
2731    def export_ldif(self, dburl, lp, creds, ldif_file):
2732        """Save KCC relevant details to an ldif file
2733
2734        The point of this function is to allow a programmer/debugger to
2735        extract an LDIF file with non-security relevent information from
2736        a DC database.  The LDIF file can then be used to "import" via
2737        the import_ldif() function this file into a temporary abbreviated
2738        database.  The KCC algorithm can then run against this abbreviated
2739        database for debug or test verification that the topology generated
2740        is computationally the same between different OSes and algorithms.
2741
2742        :param dburl: LDAP database URL to extract info from
2743        :param lp: a loadparm object.
2744        :param cred: a Credentials object.
2745        :param ldif_file: output LDIF file name to create
2746        :return: zero on success, 1 on error
2747        """
2748        try:
2749            ldif_import_export.samdb_to_ldif_file(self.samdb, dburl, lp, creds,
2750                                                  ldif_file)
2751        except ldif_import_export.LdifError as e:
2752            logger.critical(e)
2753            return 1
2754        return 0
2755