s4:librpc/rpc: don't do async requests if gensec doesn't support async replies (bug...
[Samba/gebeck_regimport.git] / source4 / scripting / bin / samba_kcc
blob2f169a8273bff9673934f577f21d7dbcb13791cf
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 cn_conn.is_generated() == False:
305 continue
307 if self.my_site.is_cleanup_ntdsconn_disabled() == True:
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 == True:
332 break
334 if lesser and cn_conn.is_rodc_topology() == False:
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 mydsa.is_istg() == False:
363 continue
365 if cn_conn.is_generated() == False:
366 continue
368 if self.keep_connection(cn_conn) == True:
369 continue
371 # XXX - To be implemented
373 if all_connected == False:
374 continue
376 if cn_conn.is_rodc_topology() == False:
377 cn_conn.to_be_deleted = True
380 if opts.readonly:
381 for dnstr, connect in mydsa.connect_table.items():
382 if connect.to_be_deleted == True:
383 logger.info("TO BE DELETED:\n%s" % connect)
384 if connect.to_be_added == True:
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.option & 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 same_site == False:
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 (same_site == False 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 cn_conn.is_enabled() == False:
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 == True or
590 cn_conn.transport_dnstr == None or
591 cn_conn.transport_dnstr.find("CN=IP") == 0 or
592 is_smtp_replication_available() == False):
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() == True and \
700 cn_conn.is_rodc_topology() == False:
702 s_dnstr = cn_conn.get_from_dnstr()
703 if s_dnstr is not None:
704 s_dsa = self.get_dsa(s_dnstr)
706 # No DSA matching this source DN string?
707 if s_dsa == None:
708 return False, None
710 # To imply a repsFrom tuple is needed, each of these
711 # must be True:
713 # An NC replica of the NC "is present" on the DC to
714 # which the nTDSDSA object referenced by cn!fromServer
715 # corresponds.
717 # An NC replica of the NC "should be present" on
718 # the local DC
719 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
721 if s_rep is None or s_rep.is_present() == False:
722 return False, None
724 # To imply a repsFrom tuple is needed, each of these
725 # must be True:
727 # The NC replica on the DC referenced by cn!fromServer is
728 # a writable replica or the NC replica that "should be
729 # present" on the local DC is a partial replica.
731 # The NC is not a domain NC, the NC replica that
732 # "should be present" on the local DC is a partial
733 # replica, cn!transportType has no value, or
734 # cn!transportType has an RDN of CN=IP.
736 implied = (s_rep.is_ro() == False or n_rep.is_partial() == True) and \
737 (n_rep.is_domain() == False or
738 n_rep.is_partial() == True or
739 cn_conn.transport_dnstr == None or
740 cn_conn.transport_dnstr.find("CN=IP") == 0)
742 if implied:
743 return True, s_dsa
744 else:
745 return False, None
747 def translate_ntdsconn(self):
748 """This function adjusts values of repsFrom abstract attributes of NC
749 replicas on the local DC to match those implied by
750 nTDSConnection objects.
752 logger.debug("translate_ntdsconn(): enter")
754 if self.my_dsa.is_translate_ntdsconn_disabled():
755 return
757 current_rep_table, needed_rep_table = self.my_dsa.get_rep_tables()
759 # Filled in with replicas we currently have that need deleting
760 delete_rep_table = {}
762 # We're using the MS notation names here to allow
763 # correlation back to the published algorithm.
765 # n_rep - NC replica (n)
766 # t_repsFrom - tuple (t) in n!repsFrom
767 # s_dsa - Source DSA of the replica. Defined as nTDSDSA
768 # object (s) such that (s!objectGUID = t.uuidDsa)
769 # In our IDL representation of repsFrom the (uuidDsa)
770 # attribute is called (source_dsa_obj_guid)
771 # cn_conn - (cn) is nTDSConnection object and child of the local DC's
772 # nTDSDSA object and (cn!fromServer = s)
773 # s_rep - source DSA replica of n
775 # If we have the replica and its not needed
776 # then we add it to the "to be deleted" list.
777 for dnstr, n_rep in current_rep_table.items():
778 if dnstr not in needed_rep_table.keys():
779 delete_rep_table[dnstr] = n_rep
781 # Now perform the scan of replicas we'll need
782 # and compare any current repsFrom against the
783 # connections
784 for dnstr, n_rep in needed_rep_table.items():
786 # load any repsFrom and fsmo roles as we'll
787 # need them during connection translation
788 n_rep.load_repsFrom(self.samdb)
789 n_rep.load_fsmo_roles(self.samdb)
791 # Loop thru the existing repsFrom tupples (if any)
792 for i, t_repsFrom in enumerate(n_rep.rep_repsFrom):
794 # for each tuple t in n!repsFrom, let s be the nTDSDSA
795 # object such that s!objectGUID = t.uuidDsa
796 guidstr = str(t_repsFrom.source_dsa_obj_guid)
797 s_dsa = self.get_dsa_by_guidstr(guidstr)
799 # Source dsa is gone from config (strange)
800 # so cleanup stale repsFrom for unlisted DSA
801 if s_dsa is None:
802 logger.debug("repsFrom source DSA guid (%s) not found" %
803 guidstr)
804 t_repsFrom.to_be_deleted = True
805 continue
807 s_dnstr = s_dsa.dsa_dnstr
809 # Retrieve my DSAs connection object (if it exists)
810 # that specifies the fromServer equivalent to
811 # the DSA that is specified in the repsFrom source
812 cn_conn = self.my_dsa.get_connection_by_from_dnstr(s_dnstr)
814 # Let (cn) be the nTDSConnection object such that (cn)
815 # is a child of the local DC's nTDSDSA object and
816 # (cn!fromServer = s) and (cn!options) does not contain
817 # NTDSCONN_OPT_RODC_TOPOLOGY or NULL if no such (cn) exists.
818 if cn_conn and cn_conn.is_rodc_topology() == True:
819 cn_conn = None
821 # KCC removes this repsFrom tuple if any of the following
822 # is true:
823 # cn = NULL.
825 # No NC replica of the NC "is present" on DSA that
826 # would be source of replica
828 # A writable replica of the NC "should be present" on
829 # the local DC, but a partial replica "is present" on
830 # the source DSA
831 s_rep = s_dsa.get_current_replica(n_rep.nc_dnstr)
833 if cn_conn is None or \
834 s_rep is None or s_rep.is_present() == False or \
835 (n_rep.is_ro() == False and s_rep.is_partial() == True):
837 t_repsFrom.to_be_deleted = True
838 continue
840 # If the KCC did not remove t from n!repsFrom, it updates t
841 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
843 # Loop thru connections and add implied repsFrom tuples
844 # for each NTDSConnection under our local DSA if the
845 # repsFrom is not already present
846 for cn_dnstr, cn_conn in self.my_dsa.connect_table.items():
848 implied, s_dsa = self.is_repsFrom_implied(n_rep, cn_conn)
849 if implied == False:
850 continue
852 # Loop thru the existing repsFrom tupples (if any) and
853 # if we already have a tuple for this connection then
854 # no need to proceed to add. It will have been changed
855 # to have the correct attributes above
856 for i, t_repsFrom in enumerate(n_rep.rep_repsFrom):
858 guidstr = str(t_repsFrom.source_dsa_obj_guid)
859 if s_dsa is self.get_dsa_by_guidstr(guidstr):
860 s_dsa = None
861 break
863 if s_dsa == None:
864 continue
866 # Create a new RepsFromTo and proceed to modify
867 # it according to specification
868 t_repsFrom = RepsFromTo(n_rep.nc_dnstr)
870 t_repsFrom.source_dsa_obj_guid = s_dsa.dsa_guid
872 self.modify_repsFrom(n_rep, t_repsFrom, s_rep, s_dsa, cn_conn)
874 # Add to our NC repsFrom as this is newly computed
875 if t_repsFrom.is_modified():
876 n_rep.rep_repsFrom.append(t_repsFrom)
878 if opts.readonly:
879 # Display any to be deleted or modified repsFrom
880 text = n_rep.dumpstr_to_be_deleted()
881 if text:
882 logger.info("TO BE DELETED:\n%s" % text)
883 text = n_rep.dumpstr_to_be_modified()
884 if text:
885 logger.info("TO BE MODIFIED:\n%s" % text)
887 # Peform deletion from our tables but perform
888 # no database modification
889 n_rep.commit_repsFrom(self.samdb, ro=True)
890 else:
891 # Commit any modified repsFrom to the NC replica
892 n_rep.commit_repsFrom(self.samdb)
894 def keep_connection(self, cn_conn):
895 """Determines if the connection is meant to be kept during the
896 pruning of unneeded connections operation.
898 Consults the keep_connection_list[] which was built during
899 intersite NC replica graph computation.
901 ::returns (True or False): if (True) connection should not be pruned
903 if cn_conn in self.keep_connection_list:
904 return True
905 return False
907 def merge_failed_links(self):
908 """Merge of kCCFailedLinks and kCCFailedLinks from bridgeheads.
909 The KCC on a writable DC attempts to merge the link and connection
910 failure information from bridgehead DCs in its own site to help it
911 identify failed bridgehead DCs.
913 # MS-TECH Ref 6.2.2.3.2 Merge of kCCFailedLinks and kCCFailedLinks
914 # from Bridgeheads
916 # XXX - not implemented yet
918 def setup_graph(self):
919 """Set up a GRAPH, populated with a VERTEX for each site
920 object, a MULTIEDGE for each siteLink object, and a
921 MUTLIEDGESET for each siteLinkBridge object (or implied
922 siteLinkBridge).
924 ::returns: a new graph
926 # XXX - not implemented yet
927 return None
929 def get_bridgehead(self, site, part, transport, partial_ok, detect_failed):
930 """Get a bridghead DC.
932 :param site: site object representing for which a bridgehead
933 DC is desired.
934 :param part: crossRef for NC to replicate.
935 :param transport: interSiteTransport object for replication
936 traffic.
937 :param partial_ok: True if a DC containing a partial
938 replica or a full replica will suffice, False if only
939 a full replica will suffice.
940 :param detect_failed: True to detect failed DCs and route
941 replication traffic around them, False to assume no DC
942 has failed.
943 ::returns: dsa object for the bridgehead DC or None
946 bhs = self.get_all_bridgeheads(site, part, transport,
947 partial_ok, detect_failed)
948 if len(bhs) == 0:
949 logger.debug("get_bridgehead: exit\n\tsitedn=%s\n\tbhdn=None" %
950 site.site_dnstr)
951 return None
952 else:
953 logger.debug("get_bridgehead: exit\n\tsitedn=%s\n\tbhdn=%s" %
954 (site.site_dnstr, bhs[0].dsa_dnstr))
955 return bhs[0]
957 def get_all_bridgeheads(self, site, part, transport,
958 partial_ok, detect_failed):
959 """Get all bridghead DCs satisfying the given criteria
961 :param site: site object representing the site for which
962 bridgehead DCs are desired.
963 :param part: partition for NC to replicate.
964 :param transport: interSiteTransport object for
965 replication traffic.
966 :param partial_ok: True if a DC containing a partial
967 replica or a full replica will suffice, False if
968 only a full replica will suffice.
969 :param detect_ok: True to detect failed DCs and route
970 replication traffic around them, FALSE to assume
971 no DC has failed.
972 ::returns: list of dsa object for available bridgehead
973 DCs or None
976 bhs = []
978 logger.debug("get_all_bridgeheads: %s" % transport)
980 for key, dsa in site.dsa_table.items():
982 pdnstr = dsa.get_parent_dnstr()
984 # IF t!bridgeheadServerListBL has one or more values and
985 # t!bridgeheadServerListBL does not contain a reference
986 # to the parent object of dc then skip dc
987 if (len(transport.bridgehead_list) != 0 and
988 pdnstr not in transport.bridgehead_list):
989 continue
991 # IF dc is in the same site as the local DC
992 # IF a replica of cr!nCName is not in the set of NC replicas
993 # that "should be present" on dc or a partial replica of the
994 # NC "should be present" but partialReplicasOkay = FALSE
995 # Skip dc
996 if self.my_site.same_site(dsa):
997 needed, ro, partial = part.should_be_present(dsa)
998 if needed == False or (partial == True and partial_ok == False):
999 continue
1001 # ELSE
1002 # IF an NC replica of cr!nCName is not in the set of NC
1003 # replicas that "are present" on dc or a partial replica of
1004 # the NC "is present" but partialReplicasOkay = FALSE
1005 # Skip dc
1006 else:
1007 rep = dsa.get_current_replica(part.nc_dnstr)
1008 if rep is None or (rep.is_partial() and partial_ok == False):
1009 continue
1011 # IF AmIRODC() and cr!nCName corresponds to default NC then
1012 # Let dsaobj be the nTDSDSA object of the dc
1013 # IF dsaobj.msDS-Behavior-Version < DS_BEHAVIOR_WIN2008
1014 # Skip dc
1015 if self.my_dsa.is_ro() and part.is_default():
1016 if dsa.is_minimum_behavior(DS_BEHAVIOR_WIN2008) == False:
1017 continue
1019 # IF t!name != "IP" and the parent object of dc has no value for
1020 # the attribute specified by t!transportAddressAttribute
1021 # Skip dc
1022 if transport.name != "IP":
1023 # MS tech specification says we retrieve the named
1024 # attribute in "transportAddressAttribute" from the parent
1025 # of the DSA object
1026 try:
1027 attrs = [ transport.address_attr ]
1029 res = self.samdb.search(base=pdnstr, scope=ldb.SCOPE_BASE,
1030 attrs=attrs)
1031 except ldb.ldbError, (enum, estr):
1032 continue
1034 msg = res[0]
1035 nastr = str(msg[transport.address_attr][0])
1037 # IF BridgeheadDCFailed(dc!objectGUID, detectFailedDCs) = TRUE
1038 # Skip dc
1039 if self.is_bridgehead_failed(dsa, detect_failed) == True:
1040 continue
1042 logger.debug("get_all_bridgeheads: dsadn=%s" % dsa.dsa_dnstr)
1043 bhs.append(dsa)
1045 # IF bit NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED is set in
1046 # s!options
1047 # SORT bhs such that all GC servers precede DCs that are not GC
1048 # servers, and otherwise by ascending objectGUID
1049 # ELSE
1050 # SORT bhs in a random order
1051 if site.is_random_bridgehead_disabled() == True:
1052 bhs.sort(sort_dsa_by_gc_and_guid)
1053 else:
1054 random.shuffle(bhs)
1056 return bhs
1059 def is_bridgehead_failed(self, dsa, detect_failed):
1060 """Determine whether a given DC is known to be in a failed state
1061 ::returns: True if and only if the DC should be considered failed
1063 # XXX - not implemented yet
1064 return False
1066 def create_connection(self, part, rbh, rsite, transport,
1067 lbh, lsite, link_opt, link_sched,
1068 partial_ok, detect_failed):
1069 """Create an nTDSConnection object with the given parameters
1070 if one does not already exist.
1072 :param part: crossRef object for the NC to replicate.
1073 :param rbh: nTDSDSA object for DC to act as the
1074 IDL_DRSGetNCChanges server (which is in a site other
1075 than the local DC's site).
1076 :param rsite: site of the rbh
1077 :param transport: interSiteTransport object for the transport
1078 to use for replication traffic.
1079 :param lbh: nTDSDSA object for DC to act as the
1080 IDL_DRSGetNCChanges client (which is in the local DC's site).
1081 :param lsite: site of the lbh
1082 :param link_opt: Replication parameters (aggregated siteLink options, etc.)
1083 :param link_sched: Schedule specifying the times at which
1084 to begin replicating.
1085 :partial_ok: True if bridgehead DCs containing partial
1086 replicas of the NC are acceptable.
1087 :param detect_failed: True to detect failed DCs and route
1088 replication traffic around them, FALSE to assume no DC
1089 has failed.
1091 rbhs_all = self.get_all_bridgeheads(rsite, part, transport,
1092 partial_ok, False)
1094 # MS-TECH says to compute rbhs_avail but then doesn't use it
1095 # rbhs_avail = self.get_all_bridgeheads(rsite, part, transport,
1096 # partial_ok, detect_failed)
1098 lbhs_all = self.get_all_bridgeheads(lsite, part, transport,
1099 partial_ok, False)
1101 # MS-TECH says to compute lbhs_avail but then doesn't use it
1102 # lbhs_avail = self.get_all_bridgeheads(lsite, part, transport,
1103 # partial_ok, detect_failed)
1105 # FOR each nTDSConnection object cn such that the parent of cn is
1106 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1107 for ldsa in lbhs_all:
1108 for cn in ldsa.connect_table.values():
1110 rdsa = None
1111 for rdsa in rbhs_all:
1112 if cn.from_dnstr == rdsa.dsa_dnstr:
1113 break
1115 if rdsa is None:
1116 continue
1118 # IF bit NTDSCONN_OPT_IS_GENERATED is set in cn!options and
1119 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options and
1120 # cn!transportType references t
1121 if (cn.is_generated() and not cn.is_rodc_topology() and
1122 cn.transport_dnstr == transport.dnstr):
1124 # IF bit NTDSCONN_OPT_USER_OWNED_SCHEDULE is clear in
1125 # cn!options and cn!schedule != sch
1126 # Perform an originating update to set cn!schedule to
1127 # sched
1128 if (not cn.is_user_owned_schedule() and
1129 not cn.is_equivalent_schedule(link_sched)):
1130 cn.schedule = link_sched
1131 cn.set_modified(True)
1133 # IF bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1134 # NTDSCONN_OPT_USE_NOTIFY are set in cn
1135 if cn.is_override_notify_default() == True and \
1136 cn.is_use_notify() == True:
1138 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is clear in
1139 # ri.Options
1140 # Perform an originating update to clear bits
1141 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1142 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1143 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) == 0:
1144 cn.options &= \
1145 ~(dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT | \
1146 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1147 cn.set_modified(True)
1149 # ELSE
1150 else:
1152 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in
1153 # ri.Options
1154 # Perform an originating update to set bits
1155 # NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1156 # NTDSCONN_OPT_USE_NOTIFY in cn!options
1157 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1158 cn.options |= \
1159 (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT | \
1160 dsdb.NTDSCONN_OPT_USE_NOTIFY)
1161 cn.set_modified(True)
1164 # IF bit NTDSCONN_OPT_TWOWAY_SYNC is set in cn!options
1165 if cn.is_twoway_sync() == True:
1167 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is clear in
1168 # ri.Options
1169 # Perform an originating update to clear bit
1170 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1171 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) == 0:
1172 cn.options &= ~dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1173 cn.set_modified(True)
1175 # ELSE
1176 else:
1178 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in
1179 # ri.Options
1180 # Perform an originating update to set bit
1181 # NTDSCONN_OPT_TWOWAY_SYNC in cn!options
1182 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1183 cn.options |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1184 cn.set_modified(True)
1187 # IF bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION is set
1188 # in cn!options
1189 if cn.is_intersite_compression_disabled() == True:
1191 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is clear
1192 # in ri.Options
1193 # Perform an originating update to clear bit
1194 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1195 # cn!options
1196 if (link_opt & \
1197 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) == 0:
1198 cn.options &= \
1199 ~dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1200 cn.set_modified(True)
1202 # ELSE
1203 else:
1204 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1205 # ri.Options
1206 # Perform an originating update to set bit
1207 # NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in
1208 # cn!options
1209 if (link_opt & \
1210 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
1211 cn.options |= \
1212 dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1213 cn.set_modified(True)
1215 # Display any modified connection
1216 if opts.readonly:
1217 if cn.to_be_modified == True:
1218 logger.info("TO BE MODIFIED:\n%s" % cn)
1220 ldsa.commit_connections(self.samdb, ro=True)
1221 else:
1222 ldsa.commit_connections(self.samdb)
1223 # ENDFOR
1225 valid_connections = 0
1227 # FOR each nTDSConnection object cn such that cn!parent is
1228 # a DC in lbhsAll and cn!fromServer references a DC in rbhsAll
1229 for ldsa in lbhs_all:
1230 for cn in ldsa.connect_table.values():
1232 rdsa = None
1233 for rdsa in rbhs_all:
1234 if cn.from_dnstr == rdsa.dsa_dnstr:
1235 break
1237 if rdsa is None:
1238 continue
1240 # IF (bit NTDSCONN_OPT_IS_GENERATED is clear in cn!options or
1241 # cn!transportType references t) and
1242 # NTDSCONN_OPT_RODC_TOPOLOGY is clear in cn!options
1243 if ((not cn.is_generated() or
1244 cn.transport_dnstr == transport.dnstr) and
1245 not cn.is_rodc_topology()):
1247 # LET rguid be the objectGUID of the nTDSDSA object
1248 # referenced by cn!fromServer
1249 # LET lguid be (cn!parent)!objectGUID
1251 # IF BridgeheadDCFailed(rguid, detectFailedDCs) = FALSE and
1252 # BridgeheadDCFailed(lguid, detectFailedDCs) = FALSE
1253 # Increment cValidConnections by 1
1254 if (not self.is_bridgehead_failed(rdsa, detect_failed) and
1255 not self.is_bridgehead_failed(ldsa, detect_failed)):
1256 valid_connections += 1
1258 # IF keepConnections does not contain cn!objectGUID
1259 # APPEND cn!objectGUID to keepConnections
1260 if not self.keep_connection(cn):
1261 self.keep_connection_list.append(cn)
1263 # ENDFOR
1265 # IF cValidConnections = 0
1266 if valid_connections == 0:
1268 # LET opt be NTDSCONN_OPT_IS_GENERATED
1269 opt = dsdb.NTDSCONN_OPT_IS_GENERATED
1271 # IF bit NTDSSITELINK_OPT_USE_NOTIFY is set in ri.Options
1272 # SET bits NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT and
1273 # NTDSCONN_OPT_USE_NOTIFY in opt
1274 if (link_opt & dsdb.NTDSSITELINK_OPT_USE_NOTIFY) != 0:
1275 opt |= (dsdb.NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT |
1276 dsdb.NTDSCONN_USE_NOTIFY)
1278 # IF bit NTDSSITELINK_OPT_TWOWAY_SYNC is set in ri.Options
1279 # SET bit NTDSCONN_OPT_TWOWAY_SYNC opt
1280 if (link_opt & dsdb.NTDSSITELINK_OPT_TWOWAY_SYNC) != 0:
1281 opt |= dsdb.NTDSCONN_OPT_TWOWAY_SYNC
1283 # IF bit NTDSSITELINK_OPT_DISABLE_COMPRESSION is set in
1284 # ri.Options
1285 # SET bit NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION in opt
1286 if (link_opt &
1287 dsdb.NTDSSITELINK_OPT_DISABLE_COMPRESSION) != 0:
1288 opt |= dsdb.NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION
1290 # Perform an originating update to create a new nTDSConnection
1291 # object cn that is a child of lbh, cn!enabledConnection = TRUE,
1292 # cn!options = opt, cn!transportType is a reference to t,
1293 # cn!fromServer is a reference to rbh, and cn!schedule = sch
1294 cn = lbh.new_connection(opt, 0, transport, lbh.dsa_dnstr, link_sched)
1296 # Display any added connection
1297 if opts.readonly:
1298 if cn.to_be_added == True:
1299 logger.info("TO BE ADDED:\n%s" % cn)
1301 lbh.commit_connections(self.samdb, ro=True)
1302 else:
1303 lbh.commit_connections(self.samdb)
1305 # APPEND cn!objectGUID to keepConnections
1306 if not self.keep_connection(cn):
1307 self.keep_connection_list.append(cn)
1309 def create_connections(self, graph, part, detect_failed):
1310 """Construct an NC replica graph for the NC identified by
1311 the given crossRef, then create any additional nTDSConnection
1312 objects required.
1314 :param graph: site graph.
1315 :param part: crossRef object for NC.
1316 :param detect_failed: True to detect failed DCs and route
1317 replication traffic around them, False to assume no DC
1318 has failed.
1320 Modifies self.keep_connection_list by adding any connections
1321 deemed to be "in use".
1323 ::returns: (all_connected, found_failed_dc)
1324 (all_connected) True if the resulting NC replica graph
1325 connects all sites that need to be connected.
1326 (found_failed_dc) True if one or more failed DCs were
1327 detected.
1329 all_connected = True
1330 found_failed = False
1332 logger.debug("create_connections(): enter\n\tpartdn=%s\n\tdetect_failed=%s" %
1333 (part.nc_dnstr, detect_failed))
1335 # XXX - This is a highly abbreviated function from the MS-TECH
1336 # ref. It creates connections between bridgeheads to all
1337 # sites that have appropriate replicas. Thus we are not
1338 # creating a minimum cost spanning tree but instead
1339 # producing a fully connected tree. This should produce
1340 # a full (albeit not optimal cost) replication topology.
1341 my_vertex = Vertex(self.my_site, part)
1342 my_vertex.color_vertex()
1344 # No NC replicas for this NC in the site of the local DC,
1345 # so no nTDSConnection objects need be created
1346 if my_vertex.is_white():
1347 return all_connected, found_failed
1349 # LET partialReplicaOkay be TRUE if and only if
1350 # localSiteVertex.Color = COLOR.BLACK
1351 if my_vertex.is_black():
1352 partial_ok = True
1353 else:
1354 partial_ok = False
1356 # Utilize the IP transport only for now
1357 transport = None
1358 for transport in self.transport_table.values():
1359 if transport.name == "IP":
1360 break
1362 if transport is None:
1363 raise Exception("Unable to find inter-site transport for IP")
1365 for rsite in self.site_table.values():
1367 # We don't make connections to our own site as that
1368 # is intrasite topology generator's job
1369 if rsite is self.my_site:
1370 continue
1372 # Determine bridgehead server in remote site
1373 rbh = self.get_bridgehead(rsite, part, transport,
1374 partial_ok, detect_failed)
1376 # RODC acts as an BH for itself
1377 # IF AmIRODC() then
1378 # LET lbh be the nTDSDSA object of the local DC
1379 # ELSE
1380 # LET lbh be the result of GetBridgeheadDC(localSiteVertex.ID,
1381 # cr, t, partialReplicaOkay, detectFailedDCs)
1382 if self.my_dsa.is_ro():
1383 lsite = self.my_site
1384 lbh = self.my_dsa
1385 else:
1386 lsite = self.my_site
1387 lbh = self.get_bridgehead(lsite, part, transport,
1388 partial_ok, detect_failed)
1390 # Find the siteLink object that enumerates the connection
1391 # between the two sites if it is present
1392 sitelink = self.get_sitelink(lsite.site_dnstr, rsite.site_dnstr)
1393 if sitelink is None:
1394 link_opt = 0x0
1395 link_sched = None
1396 else:
1397 link_opt = sitelink.options
1398 link_sched = sitelink.schedule
1400 self.create_connection(part, rbh, rsite, transport,
1401 lbh, lsite, link_opt, link_sched,
1402 partial_ok, detect_failed)
1404 return all_connected, found_failed
1406 def create_intersite_connections(self):
1407 """Computes an NC replica graph for each NC replica that "should be
1408 present" on the local DC or "is present" on any DC in the same site
1409 as the local DC. For each edge directed to an NC replica on such a
1410 DC from an NC replica on a DC in another site, the KCC creates an
1411 nTDSConnection object to imply that edge if one does not already
1412 exist.
1414 Modifies self.keep_connection_list - A list of nTDSConnection
1415 objects for edges that are directed
1416 to the local DC's site in one or more NC replica graphs.
1418 returns: True if spanning trees were created for all NC replica
1419 graphs, otherwise False.
1421 all_connected = True
1422 self.keep_connection_list = []
1424 # LET crossRefList be the set containing each object o of class
1425 # crossRef such that o is a child of the CN=Partitions child of the
1426 # config NC
1428 # FOR each crossRef object cr in crossRefList
1429 # IF cr!enabled has a value and is false, or if FLAG_CR_NTDS_NC
1430 # is clear in cr!systemFlags, skip cr.
1431 # LET g be the GRAPH return of SetupGraph()
1433 for part in self.part_table.values():
1435 if not part.is_enabled():
1436 continue
1438 if part.is_foreign():
1439 continue
1441 graph = self.setup_graph()
1443 # Create nTDSConnection objects, routing replication traffic
1444 # around "failed" DCs.
1445 found_failed = False
1447 connected, found_failed = self.create_connections(graph, part, True)
1449 if not connected:
1450 all_connected = False
1452 if found_failed:
1453 # One or more failed DCs preclude use of the ideal NC
1454 # replica graph. Add connections for the ideal graph.
1455 self.create_connections(graph, part, False)
1457 return all_connected
1459 def intersite(self):
1460 """The head method for generating the inter-site KCC replica
1461 connection graph and attendant nTDSConnection objects
1462 in the samdb.
1464 Produces self.keep_connection_list[] of NTDS Connections
1465 that should be kept during subsequent pruning process.
1467 ::return (True or False): (True) if the produced NC replica
1468 graph connects all sites that need to be connected
1471 # Retrieve my DSA
1472 mydsa = self.my_dsa
1473 mysite = self.my_site
1474 all_connected = True
1476 logger.debug("intersite(): enter")
1478 # Determine who is the ISTG
1479 if opts.readonly:
1480 mysite.select_istg(self.samdb, mydsa, ro=True)
1481 else:
1482 mysite.select_istg(self.samdb, mydsa, ro=False)
1484 # Test whether local site has topology disabled
1485 if mysite.is_intersite_topology_disabled():
1486 logger.debug("intersite(): exit disabled all_connected=%d" %
1487 all_connected)
1488 return all_connected
1490 if not mydsa.is_istg():
1491 logger.debug("intersite(): exit not istg all_connected=%d" %
1492 all_connected)
1493 return all_connected
1495 self.merge_failed_links()
1497 # For each NC with an NC replica that "should be present" on the
1498 # local DC or "is present" on any DC in the same site as the
1499 # local DC, the KCC constructs a site graph--a precursor to an NC
1500 # replica graph. The site connectivity for a site graph is defined
1501 # by objects of class interSiteTransport, siteLink, and
1502 # siteLinkBridge in the config NC.
1504 all_connected = self.create_intersite_connections()
1506 logger.debug("intersite(): exit all_connected=%d" % all_connected)
1507 return all_connected
1509 def update_rodc_connection(self):
1510 """Runs when the local DC is an RODC and updates the RODC NTFRS
1511 connection object.
1513 # Given an nTDSConnection object cn1, such that cn1.options contains
1514 # NTDSCONN_OPT_RODC_TOPOLOGY, and another nTDSConnection object cn2,
1515 # does not contain NTDSCONN_OPT_RODC_TOPOLOGY, modify cn1 to ensure
1516 # that the following is true:
1518 # cn1.fromServer = cn2.fromServer
1519 # cn1.schedule = cn2.schedule
1521 # If no such cn2 can be found, cn1 is not modified.
1522 # If no such cn1 can be found, nothing is modified by this task.
1524 # XXX - not implemented yet
1526 def intrasite_max_node_edges(self, node_count):
1527 """Returns the maximum number of edges directed to a node in
1528 the intrasite replica graph.
1530 The KCC does not create more
1531 than 50 edges directed to a single DC. To optimize replication,
1532 we compute that each node should have n+2 total edges directed
1533 to it such that (n) is the smallest non-negative integer
1534 satisfying (node_count <= 2*(n*n) + 6*n + 7)
1536 :param node_count: total number of nodes in the replica graph
1538 n = 0
1539 while True:
1540 if node_count <= (2 * (n * n) + (6 * n) + 7):
1541 break
1542 n = n + 1
1543 n = n + 2
1544 if n < 50:
1545 return n
1546 return 50
1548 def construct_intrasite_graph(self, site_local, dc_local,
1549 nc_x, gc_only, detect_stale):
1551 # We're using the MS notation names here to allow
1552 # correlation back to the published algorithm.
1554 # nc_x - naming context (x) that we are testing if it
1555 # "should be present" on the local DC
1556 # f_of_x - replica (f) found on a DC (s) for NC (x)
1557 # dc_s - DC where f_of_x replica was found
1558 # dc_local - local DC that potentially needs a replica
1559 # (f_of_x)
1560 # r_list - replica list R
1561 # p_of_x - replica (p) is partial and found on a DC (s)
1562 # for NC (x)
1563 # l_of_x - replica (l) is the local replica for NC (x)
1564 # that should appear on the local DC
1565 # r_len = is length of replica list |R|
1567 # If the DSA doesn't need a replica for this
1568 # partition (NC x) then continue
1569 needed, ro, partial = nc_x.should_be_present(dc_local)
1571 logger.debug("construct_intrasite_graph(): enter" +
1572 "\n\tgc_only=%d" % gc_only +
1573 "\n\tdetect_stale=%d" % detect_stale +
1574 "\n\tneeded=%s" % needed +
1575 "\n\tro=%s" % ro +
1576 "\n\tpartial=%s" % partial +
1577 "\n%s" % nc_x)
1579 if not needed:
1580 return
1582 # Create a NCReplica that matches what the local replica
1583 # should say. We'll use this below in our r_list
1584 l_of_x = NCReplica(dc_local.dsa_dnstr, dc_local.dsa_guid,
1585 nc_x.nc_dnstr)
1587 l_of_x.identify_by_basedn(self.samdb)
1589 l_of_x.rep_partial = partial
1590 l_of_x.rep_ro = ro
1592 # Add this replica that "should be present" to the
1593 # needed replica table for this DSA
1594 dc_local.add_needed_replica(l_of_x)
1596 # Empty replica sequence list
1597 r_list = []
1599 # We'll loop thru all the DSAs looking for
1600 # writeable NC replicas that match the naming
1601 # context dn for (nc_x)
1603 for dc_s_dn, dc_s in self.my_site.dsa_table.items():
1605 # If this partition (nc_x) doesn't appear as a
1606 # replica (f_of_x) on (dc_s) then continue
1607 if not nc_x.nc_dnstr in dc_s.current_rep_table.keys():
1608 continue
1610 # Pull out the NCReplica (f) of (x) with the dn
1611 # that matches NC (x) we are examining.
1612 f_of_x = dc_s.current_rep_table[nc_x.nc_dnstr]
1614 # Replica (f) of NC (x) must be writable
1615 if f_of_x.is_ro():
1616 continue
1618 # Replica (f) of NC (x) must satisfy the
1619 # "is present" criteria for DC (s) that
1620 # it was found on
1621 if not f_of_x.is_present():
1622 continue
1624 # DC (s) must be a writable DSA other than
1625 # my local DC. In other words we'd only replicate
1626 # from other writable DC
1627 if dc_s.is_ro() or dc_s is dc_local:
1628 continue
1630 # Certain replica graphs are produced only
1631 # for global catalogs, so test against
1632 # method input parameter
1633 if gc_only and not dc_s.is_gc():
1634 continue
1636 # DC (s) must be in the same site as the local DC
1637 # as this is the intra-site algorithm. This is
1638 # handled by virtue of placing DSAs in per
1639 # site objects (see enclosing for() loop)
1641 # If NC (x) is intended to be read-only full replica
1642 # for a domain NC on the target DC then the source
1643 # DC should have functional level at minimum WIN2008
1645 # Effectively we're saying that in order to replicate
1646 # to a targeted RODC (which was introduced in Windows 2008)
1647 # then we have to replicate from a DC that is also minimally
1648 # at that level.
1650 # You can also see this requirement in the MS special
1651 # considerations for RODC which state that to deploy
1652 # an RODC, at least one writable domain controller in
1653 # the domain must be running Windows Server 2008
1654 if ro and not partial and nc_x.nc_type == NCType.domain:
1655 if not dc_s.is_minimum_behavior(DS_BEHAVIOR_WIN2008):
1656 continue
1658 # If we haven't been told to turn off stale connection
1659 # detection and this dsa has a stale connection then
1660 # continue
1661 if detect_stale and self.is_stale_link_connection(dc_s) == True:
1662 continue
1664 # Replica meets criteria. Add it to table indexed
1665 # by the GUID of the DC that it appears on
1666 r_list.append(f_of_x)
1668 # If a partial (not full) replica of NC (x) "should be present"
1669 # on the local DC, append to R each partial replica (p of x)
1670 # such that p "is present" on a DC satisfying the same
1671 # criteria defined above for full replica DCs.
1672 if partial == True:
1674 # Now we loop thru all the DSAs looking for
1675 # partial NC replicas that match the naming
1676 # context dn for (NC x)
1677 for dc_s_dn, dc_s in self.my_site.dsa_table.items():
1679 # If this partition NC (x) doesn't appear as a
1680 # replica (p) of NC (x) on the dsa DC (s) then
1681 # continue
1682 if not nc_x.nc_dnstr in dc_s.current_rep_table.keys():
1683 continue
1685 # Pull out the NCReplica with the dn that
1686 # matches NC (x) we are examining.
1687 p_of_x = dsa.current_rep_table[nc_x.nc_dnstr]
1689 # Replica (p) of NC (x) must be partial
1690 if not p_of_x.is_partial():
1691 continue
1693 # Replica (p) of NC (x) must satisfy the
1694 # "is present" criteria for DC (s) that
1695 # it was found on
1696 if not p_of_x.is_present():
1697 continue
1699 # DC (s) must be a writable DSA other than
1700 # my DSA. In other words we'd only replicate
1701 # from other writable DSA
1702 if dc_s.is_ro() or dc_s is dc_local:
1703 continue
1705 # Certain replica graphs are produced only
1706 # for global catalogs, so test against
1707 # method input parameter
1708 if gc_only and not dc_s.is_gc():
1709 continue
1711 # DC (s) must be in the same site as the local DC
1712 # as this is the intra-site algorithm. This is
1713 # handled by virtue of placing DSAs in per
1714 # site objects (see enclosing for() loop)
1716 # This criteria is moot (a no-op) for this case
1717 # because we are scanning for (partial = True). The
1718 # MS algorithm statement says partial replica scans
1719 # should adhere to the "same" criteria as full replica
1720 # scans so the criteria doesn't change here...its just
1721 # rendered pointless.
1723 # The case that is occurring would be a partial domain
1724 # replica is needed on a local DC global catalog. There
1725 # is no minimum windows behavior for those since GCs
1726 # have always been present.
1727 if ro and not partial and nc_x.nc_type == NCType.domain:
1728 if not dc_s.is_minimum_behavior(DS_BEHAVIOR_WIN2008):
1729 continue
1731 # If we haven't been told to turn off stale connection
1732 # detection and this dsa has a stale connection then
1733 # continue
1734 if detect_stale and self.is_stale_link_connection(dc_s) == True:
1735 continue
1737 # Replica meets criteria. Add it to table indexed
1738 # by the GUID of the DSA that it appears on
1739 r_list.append(p_of_x)
1741 # Append to R the NC replica that "should be present"
1742 # on the local DC
1743 r_list.append(l_of_x)
1745 r_list.sort(sort_replica_by_dsa_guid)
1747 r_len = len(r_list)
1749 max_node_edges = self.intrasite_max_node_edges(r_len)
1751 # Add a node for each r_list element to the replica graph
1752 graph_list = []
1753 for rep in r_list:
1754 node = GraphNode(rep.rep_dsa_dnstr, max_node_edges)
1755 graph_list.append(node)
1757 # For each r(i) from (0 <= i < |R|-1)
1758 i = 0
1759 while i < (r_len-1):
1760 # Add an edge from r(i) to r(i+1) if r(i) is a full
1761 # replica or r(i+1) is a partial replica
1762 if not r_list[i].is_partial() or r_list[i+1].is_partial():
1763 graph_list[i+1].add_edge_from(r_list[i].rep_dsa_dnstr)
1765 # Add an edge from r(i+1) to r(i) if r(i+1) is a full
1766 # replica or ri is a partial replica.
1767 if not r_list[i+1].is_partial() or r_list[i].is_partial():
1768 graph_list[i].add_edge_from(r_list[i+1].rep_dsa_dnstr)
1769 i = i + 1
1771 # Add an edge from r|R|-1 to r0 if r|R|-1 is a full replica
1772 # or r0 is a partial replica.
1773 if not r_list[r_len-1].is_partial() or r_list[0].is_partial():
1774 graph_list[0].add_edge_from(r_list[r_len-1].rep_dsa_dnstr)
1776 # Add an edge from r0 to r|R|-1 if r0 is a full replica or
1777 # r|R|-1 is a partial replica.
1778 if not r_list[0].is_partial() or r_list[r_len-1].is_partial():
1779 graph_list[r_len-1].add_edge_from(r_list[0].rep_dsa_dnstr)
1781 # For each existing nTDSConnection object implying an edge
1782 # from rj of R to ri such that j != i, an edge from rj to ri
1783 # is not already in the graph, and the total edges directed
1784 # to ri is less than n+2, the KCC adds that edge to the graph.
1785 i = 0
1786 while i < r_len:
1787 dsa = self.my_site.dsa_table[graph_list[i].dsa_dnstr]
1788 graph_list[i].add_edges_from_connections(dsa)
1789 i = i + 1
1791 i = 0
1792 while i < r_len:
1793 tnode = graph_list[i]
1795 # To optimize replication latency in sites with many NC replicas, the
1796 # KCC adds new edges directed to ri to bring the total edges to n+2,
1797 # where the NC replica rk of R from which the edge is directed
1798 # is chosen at random such that k != i and an edge from rk to ri
1799 # is not already in the graph.
1801 # Note that the KCC tech ref does not give a number for the definition
1802 # of "sites with many NC replicas". At a bare minimum to satisfy
1803 # n+2 edges directed at a node we have to have at least three replicas
1804 # in |R| (i.e. if n is zero then at least replicas from two other graph
1805 # nodes may direct edges to us).
1806 if r_len >= 3:
1807 # pick a random index
1808 findex = rindex = random.randint(0, r_len-1)
1810 # while this node doesn't have sufficient edges
1811 while tnode.has_sufficient_edges() == False:
1812 # If this edge can be successfully added (i.e. not
1813 # the same node and edge doesn't already exist) then
1814 # select a new random index for the next round
1815 if tnode.add_edge_from(graph_list[rindex].dsa_dnstr) == True:
1816 findex = rindex = random.randint(0, r_len-1)
1817 else:
1818 # Otherwise continue looking against each node
1819 # after the random selection
1820 rindex = rindex + 1
1821 if rindex >= r_len:
1822 rindex = 0
1824 if rindex == findex:
1825 logger.error("Unable to satisfy max edge criteria!")
1826 break
1828 # Print the graph node in debug mode
1829 logger.debug("%s" % tnode)
1831 # For each edge directed to the local DC, ensure a nTDSConnection
1832 # points to us that satisfies the KCC criteria
1833 if graph_list[i].dsa_dnstr == dc_local.dsa_dnstr:
1834 graph_list[i].add_connections_from_edges(dc_local)
1836 i = i + 1
1838 def intrasite(self):
1839 """The head method for generating the intra-site KCC replica
1840 connection graph and attendant nTDSConnection objects
1841 in the samdb
1843 # Retrieve my DSA
1844 mydsa = self.my_dsa
1846 logger.debug("intrasite(): enter")
1848 # Test whether local site has topology disabled
1849 mysite = self.site_table[self.my_site_dnstr]
1850 if mysite.is_intrasite_topology_disabled():
1851 return
1853 detect_stale = (mysite.is_detect_stale_disabled() == False)
1855 # Loop thru all the partitions.
1856 for partdn, part in self.part_table.items():
1857 self.construct_intrasite_graph(mysite, mydsa, part, False,
1858 detect_stale)
1860 # If the DC is a GC server, the KCC constructs an additional NC
1861 # replica graph (and creates nTDSConnection objects) for the
1862 # config NC as above, except that only NC replicas that "are present"
1863 # on GC servers are added to R.
1864 for partdn, part in self.part_table.items():
1865 if part.is_config():
1866 self.construct_intrasite_graph(mysite, mydsa, part, True,
1867 detect_stale)
1869 # The DC repeats the NC replica graph computation and nTDSConnection
1870 # creation for each of the NC replica graphs, this time assuming
1871 # that no DC has failed. It does so by re-executing the steps as
1872 # if the bit NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED were
1873 # set in the options attribute of the site settings object for
1874 # the local DC's site. (ie. we set "detec_stale" flag to False)
1876 # Loop thru all the partitions.
1877 for partdn, part in self.part_table.items():
1878 self.construct_intrasite_graph(mysite, mydsa, part, False,
1879 False) # don't detect stale
1881 # If the DC is a GC server, the KCC constructs an additional NC
1882 # replica graph (and creates nTDSConnection objects) for the
1883 # config NC as above, except that only NC replicas that "are present"
1884 # on GC servers are added to R.
1885 for partdn, part in self.part_table.items():
1886 if part.is_config():
1887 self.construct_intrasite_graph(mysite, mydsa, part, True,
1888 False) # don't detect stale
1890 if opts.readonly:
1891 # Display any to be added or modified repsFrom
1892 for dnstr, connect in mydsa.connect_table.items():
1893 if connect.to_be_deleted == True:
1894 logger.info("TO BE DELETED:\n%s" % connect)
1895 if connect.to_be_modified == True:
1896 logger.info("TO BE MODIFIED:\n%s" % connect)
1897 if connect.to_be_added == True:
1898 logger.info("TO BE ADDED:\n%s" % connect)
1900 mydsa.commit_connections(self.samdb, ro=True)
1901 else:
1902 # Commit any newly created connections to the samdb
1903 mydsa.commit_connections(self.samdb)
1905 def run(self, dburl, lp, creds):
1906 """Method to perform a complete run of the KCC and
1907 produce an updated topology for subsequent NC replica
1908 syncronization between domain controllers
1910 # We may already have a samdb setup if we are
1911 # currently importing an ldif for a test run
1912 if self.samdb is None:
1913 try:
1914 self.samdb = SamDB(url=lp.samdb_url(),
1915 session_info=system_session(),
1916 credentials=creds, lp=lp)
1918 except ldb.LdbError, (num, msg):
1919 logger.error("Unable to open sam database %s : %s" %
1920 (lp.samdb_url(), msg))
1921 return 1
1923 try:
1924 # Setup
1925 self.load_my_site()
1926 self.load_my_dsa()
1928 self.load_all_sites()
1929 self.load_all_partitions()
1930 self.load_all_transports()
1931 self.load_all_sitelinks()
1933 # These are the published steps (in order) for the
1934 # MS-TECH description of the KCC algorithm
1936 # Step 1
1937 self.refresh_failed_links_connections()
1939 # Step 2
1940 self.intrasite()
1942 # Step 3
1943 all_connected = self.intersite()
1945 # Step 4
1946 self.remove_unneeded_ntdsconn(all_connected)
1948 # Step 5
1949 self.translate_ntdsconn()
1951 # Step 6
1952 self.remove_unneeded_failed_links_connections()
1954 # Step 7
1955 self.update_rodc_connection()
1957 except Exception, estr:
1958 logger.error("%s" % estr)
1959 return 1
1961 return 0
1963 def import_ldif(self, dburl, lp, creds, ldif_file):
1964 """Routine to import all objects and attributes that are relevent
1965 to the KCC algorithms from a previously exported LDIF file.
1967 The point of this function is to allow a programmer/debugger to
1968 import an LDIF file with non-security relevent information that
1969 was previously extracted from a DC database. The LDIF file is used
1970 to create a temporary abbreviated database. The KCC algorithm can
1971 then run against this abbreviated database for debug or test
1972 verification that the topology generated is computationally the
1973 same between different OSes and algorithms.
1975 :param dburl: path to the temporary abbreviated db to create
1976 :param ldif_file: path to the ldif file to import
1978 if os.path.exists(dburl):
1979 logger.error("Specify a database (%s) that doesn't already exist." %
1980 dburl)
1981 return 1
1983 # Use ["modules:"] as we are attempting to build a sam
1984 # database as opposed to start it here.
1985 self.samdb = Ldb(url=dburl, session_info=system_session(),
1986 lp=lp, options=["modules:"])
1988 self.samdb.transaction_start()
1989 try:
1990 data = read_and_sub_file(ldif_file, None)
1991 self.samdb.add_ldif(data, None)
1993 except Exception, estr:
1994 logger.error("%s" % estr)
1995 self.samdb.transaction_cancel()
1996 return 1
1997 else:
1998 self.samdb.transaction_commit()
2000 self.samdb = None
2002 # We have an abbreviated list of options here because we have built
2003 # an abbreviated database. We use the rootdse and extended-dn
2004 # modules only during this re-open
2005 self.samdb = SamDB(url=dburl, session_info=system_session(),
2006 credentials=creds, lp=lp,
2007 options=["modules:rootdse,extended_dn_out_ldb"])
2008 return 0
2010 def export_ldif(self, dburl, lp, creds, ldif_file):
2011 """Routine to extract all objects and attributes that are relevent
2012 to the KCC algorithms from a DC database.
2014 The point of this function is to allow a programmer/debugger to
2015 extract an LDIF file with non-security relevent information from
2016 a DC database. The LDIF file can then be used to "import" via
2017 the import_ldif() function this file into a temporary abbreviated
2018 database. The KCC algorithm can then run against this abbreviated
2019 database for debug or test verification that the topology generated
2020 is computationally the same between different OSes and algorithms.
2022 :param dburl: LDAP database URL to extract info from
2023 :param ldif_file: output LDIF file name to create
2025 try:
2026 self.samdb = SamDB(url=dburl,
2027 session_info=system_session(),
2028 credentials=creds, lp=lp)
2029 except ldb.LdbError, (enum, estr):
2030 logger.error("Unable to open sam database (%s) : %s" %
2031 (lp.samdb_url(), estr))
2032 return 1
2034 if os.path.exists(ldif_file):
2035 logger.error("Specify a file (%s) that doesn't already exist." %
2036 ldif_file)
2037 return 1
2039 try:
2040 f = open(ldif_file, "w")
2041 except (enum, estr):
2042 logger.error("Unable to open (%s) : %s" % (ldif_file, estr))
2043 return 1
2045 try:
2046 # Query Partitions
2047 attrs = [ "objectClass",
2048 "objectGUID",
2049 "cn",
2050 "whenChanged",
2051 "objectSid",
2052 "Enabled",
2053 "systemFlags",
2054 "dnsRoot",
2055 "nCName",
2056 "msDS-NC-Replica-Locations",
2057 "msDS-NC-RO-Replica-Locations" ]
2059 sstr = "CN=Partitions,%s" % self.samdb.get_config_basedn()
2060 res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
2061 attrs=attrs,
2062 expression="(objectClass=crossRef)")
2064 # Write partitions output
2065 write_search_result(self.samdb, f, res)
2067 # Query cross reference container
2068 attrs = [ "objectClass",
2069 "objectGUID",
2070 "cn",
2071 "whenChanged",
2072 "fSMORoleOwner",
2073 "systemFlags",
2074 "msDS-Behavior-Version",
2075 "msDS-EnabledFeature" ]
2077 sstr = "CN=Partitions,%s" % self.samdb.get_config_basedn()
2078 res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
2079 attrs=attrs,
2080 expression="(objectClass=crossRefContainer)")
2082 # Write cross reference container output
2083 write_search_result(self.samdb, f, res)
2085 # Query Sites
2086 attrs = [ "objectClass",
2087 "objectGUID",
2088 "cn",
2089 "whenChanged",
2090 "systemFlags" ]
2092 sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
2093 sites = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
2094 attrs=attrs,
2095 expression="(objectClass=site)")
2097 # Write sites output
2098 write_search_result(self.samdb, f, sites)
2100 # Query NTDS Site Settings
2101 for msg in sites:
2102 sitestr = str(msg.dn)
2104 attrs = [ "objectClass",
2105 "objectGUID",
2106 "cn",
2107 "whenChanged",
2108 "interSiteTopologyGenerator",
2109 "interSiteTopologyFailover",
2110 "schedule",
2111 "options" ]
2113 sstr = "CN=NTDS Site Settings,%s" % sitestr
2114 res = self.samdb.search(base=sstr, scope=ldb.SCOPE_BASE,
2115 attrs=attrs)
2117 # Write Site Settings output
2118 write_search_result(self.samdb, f, res)
2120 # Naming context list
2121 nclist = []
2123 # Query Directory Service Agents
2124 for msg in sites:
2125 sstr = str(msg.dn)
2127 ncattrs = [ "hasMasterNCs",
2128 "msDS-hasMasterNCs",
2129 "hasPartialReplicaNCs",
2130 "msDS-HasDomainNCs",
2131 "msDS-hasFullReplicaNCs",
2132 "msDS-HasInstantiatedNCs" ]
2133 attrs = [ "objectClass",
2134 "objectGUID",
2135 "cn",
2136 "whenChanged",
2137 "invocationID",
2138 "options",
2139 "msDS-isRODC",
2140 "msDS-Behavior-Version" ]
2142 res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
2143 attrs=attrs + ncattrs,
2144 expression="(objectClass=nTDSDSA)")
2146 # Spin thru all the DSAs looking for NC replicas
2147 # and build a list of all possible Naming Contexts
2148 # for subsequent retrieval below
2149 for msg in res:
2150 for k in msg.keys():
2151 if k in ncattrs:
2152 for value in msg[k]:
2153 # Some of these have binary DNs so
2154 # use dsdb_Dn to split out relevent parts
2155 dsdn = dsdb_Dn(self.samdb, value)
2156 dnstr = str(dsdn.dn)
2157 if dnstr not in nclist:
2158 nclist.append(dnstr)
2160 # Write DSA output
2161 write_search_result(self.samdb, f, res)
2163 # Query NTDS Connections
2164 for msg in sites:
2165 sstr = str(msg.dn)
2167 attrs = [ "objectClass",
2168 "objectGUID",
2169 "cn",
2170 "whenChanged",
2171 "options",
2172 "whenCreated",
2173 "enabledConnection",
2174 "schedule",
2175 "transportType",
2176 "fromServer",
2177 "systemFlags" ]
2179 res = self.samdb.search(base=sstr, scope=ldb.SCOPE_SUBTREE,
2180 attrs=attrs,
2181 expression="(objectClass=nTDSConnection)")
2182 # Write NTDS Connection output
2183 write_search_result(self.samdb, f, res)
2186 # Query Intersite transports
2187 attrs = [ "objectClass",
2188 "objectGUID",
2189 "cn",
2190 "whenChanged",
2191 "options",
2192 "name",
2193 "bridgeheadServerListBL",
2194 "transportAddressAttribute" ]
2196 sstr = "CN=Inter-Site Transports,CN=Sites,%s" % \
2197 self.samdb.get_config_basedn()
2198 res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
2199 attrs=attrs,
2200 expression="(objectClass=interSiteTransport)")
2202 # Write inter-site transport output
2203 write_search_result(self.samdb, f, res)
2205 # Query siteLink
2206 attrs = [ "objectClass",
2207 "objectGUID",
2208 "cn",
2209 "whenChanged",
2210 "systemFlags",
2211 "options",
2212 "schedule",
2213 "replInterval",
2214 "siteList",
2215 "cost" ]
2217 sstr = "CN=Sites,%s" % \
2218 self.samdb.get_config_basedn()
2219 res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
2220 attrs=attrs,
2221 expression="(objectClass=siteLink)")
2223 # Write siteLink output
2224 write_search_result(self.samdb, f, res)
2226 # Query siteLinkBridge
2227 attrs = [ "objectClass",
2228 "objectGUID",
2229 "cn",
2230 "whenChanged",
2231 "siteLinkList" ]
2233 sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
2234 res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
2235 attrs=attrs,
2236 expression="(objectClass=siteLinkBridge)")
2238 # Write siteLinkBridge output
2239 write_search_result(self.samdb, f, res)
2241 # Query servers containers
2242 # Needed for samdb.server_site_name()
2243 attrs = [ "objectClass",
2244 "objectGUID",
2245 "cn",
2246 "whenChanged",
2247 "systemFlags" ]
2249 sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
2250 res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
2251 attrs=attrs,
2252 expression="(objectClass=serversContainer)")
2254 # Write servers container output
2255 write_search_result(self.samdb, f, res)
2257 # Query servers
2258 # Needed because some transport interfaces refer back to
2259 # attributes found in the server object. Also needed
2260 # so extended-dn will be happy with dsServiceName in rootDSE
2261 attrs = [ "objectClass",
2262 "objectGUID",
2263 "cn",
2264 "whenChanged",
2265 "systemFlags",
2266 "dNSHostName",
2267 "mailAddress" ]
2269 sstr = "CN=Sites,%s" % self.samdb.get_config_basedn()
2270 res = self.samdb.search(sstr, scope=ldb.SCOPE_SUBTREE,
2271 attrs=attrs,
2272 expression="(objectClass=server)")
2274 # Write server output
2275 write_search_result(self.samdb, f, res)
2277 # Query Naming Context replicas
2278 attrs = [ "objectClass",
2279 "objectGUID",
2280 "cn",
2281 "whenChanged",
2282 "objectSid",
2283 "fSMORoleOwner",
2284 "msDS-Behavior-Version",
2285 "repsFrom",
2286 "repsTo" ]
2288 for sstr in nclist:
2289 res = self.samdb.search(sstr, scope=ldb.SCOPE_BASE,
2290 attrs=attrs)
2292 # Write naming context output
2293 write_search_result(self.samdb, f, res)
2295 # Query rootDSE replicas
2296 attrs=[ "objectClass",
2297 "objectGUID",
2298 "cn",
2299 "whenChanged",
2300 "rootDomainNamingContext",
2301 "configurationNamingContext",
2302 "schemaNamingContext",
2303 "defaultNamingContext",
2304 "dsServiceName" ]
2306 sstr = ""
2307 res = self.samdb.search(sstr, scope=ldb.SCOPE_BASE,
2308 attrs=attrs)
2310 # Record the rootDSE object as a dn as it
2311 # would appear in the base ldb file. We have
2312 # to save it this way because we are going to
2313 # be importing as an abbreviated database.
2314 res[0].dn = ldb.Dn(self.samdb, "@ROOTDSE")
2316 # Write rootdse output
2317 write_search_result(self.samdb, f, res)
2319 except ldb.LdbError, (enum, estr):
2320 logger.error("Error processing (%s) : %s" % (sstr, estr))
2321 return 1
2323 f.close()
2324 return 0
2326 ##################################################
2327 # Global Functions
2328 ##################################################
2329 def sort_replica_by_dsa_guid(rep1, rep2):
2330 return cmp(rep1.rep_dsa_guid, rep2.rep_dsa_guid)
2332 def sort_dsa_by_gc_and_guid(dsa1, dsa2):
2333 if dsa1.is_gc() == True and dsa2.is_gc() == False:
2334 return -1
2335 if dsa1.is_gc() == False and dsa2.is_gc() == True:
2336 return +1
2337 return cmp(dsa1.dsa_guid, dsa2.dsa_guid)
2339 def is_smtp_replication_available():
2340 """Currently always returns false because Samba
2341 doesn't implement SMTP transfer for NC changes
2342 between DCs
2344 return False
2346 def write_search_result(samdb, f, res):
2347 for msg in res:
2348 lstr = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
2349 f.write("%s" % lstr)
2351 ##################################################
2352 # samba_kcc entry point
2353 ##################################################
2355 parser = optparse.OptionParser("samba_kcc [options]")
2356 sambaopts = options.SambaOptions(parser)
2357 credopts = options.CredentialsOptions(parser)
2359 parser.add_option_group(sambaopts)
2360 parser.add_option_group(credopts)
2361 parser.add_option_group(options.VersionOptions(parser))
2363 parser.add_option("--readonly",
2364 help="compute topology but do not update database",
2365 action="store_true")
2367 parser.add_option("--debug",
2368 help="debug output",
2369 action="store_true")
2371 parser.add_option("--seed",
2372 help="random number seed",
2373 type=str, metavar="<number>")
2375 parser.add_option("--importldif",
2376 help="import topology ldif file",
2377 type=str, metavar="<file>")
2379 parser.add_option("--exportldif",
2380 help="export topology ldif file",
2381 type=str, metavar="<file>")
2383 parser.add_option("-H", "--URL" ,
2384 help="LDB URL for database or target server",
2385 type=str, metavar="<URL>", dest="dburl")
2387 parser.add_option("--tmpdb",
2388 help="schemaless database file to create for ldif import",
2389 type=str, metavar="<file>")
2391 logger = logging.getLogger("samba_kcc")
2392 logger.addHandler(logging.StreamHandler(sys.stdout))
2394 lp = sambaopts.get_loadparm()
2395 creds = credopts.get_credentials(lp, fallback_machine=True)
2397 opts, args = parser.parse_args()
2399 if opts.readonly is None:
2400 opts.readonly = False
2402 if opts.debug:
2403 logger.setLevel(logging.DEBUG)
2404 elif opts.readonly:
2405 logger.setLevel(logging.INFO)
2406 else:
2407 logger.setLevel(logging.WARNING)
2409 # initialize seed from optional input parameter
2410 if opts.seed:
2411 random.seed(int(opts.seed))
2412 else:
2413 random.seed(0xACE5CA11)
2415 if opts.dburl is None:
2416 opts.dburl = lp.samdb_url()
2418 # Instantiate Knowledge Consistency Checker and perform run
2419 kcc = KCC()
2421 if opts.exportldif:
2422 rc = kcc.export_ldif(opts.dburl, lp, creds, opts.exportldif)
2423 sys.exit(rc)
2425 if opts.importldif:
2426 if opts.tmpdb is None or opts.tmpdb.startswith('ldap'):
2427 logger.error("Specify a target temp database file with --tmpdb option.")
2428 sys.exit(1)
2430 rc = kcc.import_ldif(opts.tmpdb, lp, creds, opts.importldif)
2431 if rc != 0:
2432 sys.exit(rc)
2434 rc = kcc.run(opts.dburl, lp, creds)
2435 sys.exit(rc)