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