1 # Unix SMB/CIFS implementation.
2 # A command to compare differences of objects and attributes between
3 # two LDAP servers both running at the same time. It generally compares
4 # one of the three pratitions DOMAIN, CONFIGURATION or SCHEMA. Users
5 # that have to be provided sheould be able to read objects in any of the
8 # Copyright (C) Zahari Zahariev <zahari.zahariev@postpath.com> 2009, 2010
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
29 import samba.getopt as options
31 from samba.ndr import ndr_unpack
32 from samba.dcerpc import security
33 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, ERR_NO_SUCH_OBJECT, LdbError
34 from samba.netcmd import (
40 RE_RANGED_RESULT = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
43 class LDAPBase(object):
45 def __init__(self, host, creds, lp,
46 two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
47 view="section", base="", scope="SUB",
48 outf=sys.stdout, errf=sys.stderr, skip_missing_dn=True):
52 if os.path.isfile(host):
53 samdb_url = "tdb://%s" % host
55 samdb_url = "ldap://%s" % host
56 # use 'paged_search' module when connecting remotely
57 if samdb_url.lower().startswith("ldap://"):
58 ldb_options = ["modules:paged_searches"]
61 self.ldb = Ldb(url=samdb_url,
65 self.search_base = base
66 self.search_scope = scope
67 self.two_domains = two
69 self.descriptor = descriptor
70 self.sort_aces = sort_aces
72 self.verbose = verbose
74 self.skip_missing_dn = skip_missing_dn
75 self.base_dn = str(self.ldb.get_default_basedn())
76 self.root_dn = str(self.ldb.get_root_basedn())
77 self.config_dn = str(self.ldb.get_config_basedn())
78 self.schema_dn = str(self.ldb.get_schema_basedn())
79 self.domain_netbios = self.find_netbios()
80 self.server_names = self.find_servers()
81 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
82 self.domain_sid = self.find_domain_sid()
85 # Log some domain controller specific place-holers that are being used
86 # when compare content of two DCs. Uncomment for DEBUG purposes.
87 if self.two_domains and not self.quiet:
88 self.outf.write("\n* Place-holders for %s:\n" % self.host)
89 self.outf.write(4 * " " + "${DOMAIN_DN} => %s\n" %
91 self.outf.write(4 * " " + "${DOMAIN_NETBIOS} => %s\n" %
93 self.outf.write(4 * " " + "${SERVER_NAME} => %s\n" %
95 self.outf.write(4 * " " + "${DOMAIN_NAME} => %s\n" %
98 def find_domain_sid(self):
99 res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
100 return ndr_unpack(security.dom_sid, res[0]["objectSid"][0])
102 def find_servers(self):
105 res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn,
106 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
108 return [str(x["cn"][0]) for x in res]
110 def find_netbios(self):
111 res = self.ldb.search(base="CN=Partitions,%s" % self.config_dn,
112 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
115 if "nETBIOSName" in x:
116 return x["nETBIOSName"][0]
118 def object_exists(self, object_dn):
121 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
122 except LdbError as e2:
123 (enum, estr) = e2.args
124 if enum == ERR_NO_SUCH_OBJECT:
129 def delete_force(self, object_dn):
131 self.ldb.delete(object_dn)
132 except Ldb.LdbError as e:
133 assert "No such object" in str(e)
135 def get_attribute_name(self, key):
136 """ Returns the real attribute name
137 It resolved ranged results e.g. member;range=0-1499
140 m = RE_RANGED_RESULT.match(key)
146 def get_attribute_values(self, object_dn, key, vals):
147 """ Returns list with all attribute values
148 It resolved ranged results e.g. member;range=0-1499
151 m = RE_RANGED_RESULT.match(key)
153 # no range, just return the values
159 # get additional values in a loop
160 # until we get a response with '*' at the end
163 n = "%s;range=%d-*" % (attr, hi + 1)
164 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
173 m = RE_RANGED_RESULT.match(key)
178 if m.group(1) != attr:
182 fvals = list(res[key])
189 if fm.group(3) == "*":
190 # if we got "*" we're done
193 assert int(fm.group(2)) == hi + 1
194 hi = int(fm.group(3))
198 def get_attributes(self, object_dn):
199 """ Returns dict with all default visible attributes
201 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
204 # 'Dn' element is not iterable and we have it as 'distinguishedName'
208 for key, vals in res.items():
209 name = self.get_attribute_name(key)
210 # sort vals and return a list, help to compare
212 attributes[name] = self.get_attribute_values(object_dn, key, vals)
216 def get_descriptor_sddl(self, object_dn):
217 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
218 desc = res[0]["nTSecurityDescriptor"][0]
219 desc = ndr_unpack(security.descriptor, desc)
220 return desc.as_sddl(self.domain_sid)
222 def guid_as_string(self, guid_blob):
223 """ Translate binary representation of schemaIDGUID to standard string representation.
224 @gid_blob: binary schemaIDGUID
226 blob = "%s" % guid_blob
227 stops = [4, 2, 2, 2, 6]
231 while x < len(stops):
235 c = hex(ord(blob[index])).replace("0x", "")
236 c = [None, "0" + c, c][len(c)]
237 if 2 * index < len(blob):
245 assert index == len(blob)
246 return res.strip().replace(" ", "-")
248 def get_sid_map(self):
249 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
252 res = self.ldb.search(base=self.base_dn,
253 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
256 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = str(item["sAMAccountName"][0])
261 class Descriptor(object):
262 def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr):
265 self.con = connection
267 self.sddl = self.con.get_descriptor_sddl(self.dn)
268 self.dacl_list = self.extract_dacl()
269 if self.con.sort_aces:
270 self.dacl_list.sort()
272 def extract_dacl(self):
273 """ Extracts the DACL as a list of ACE string (with the brakets).
276 if "S:" in self.sddl:
277 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
279 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
280 except AttributeError:
282 return re.findall("(\(.*?\))", res)
284 def fix_sid(self, ace):
286 sids = re.findall("S-[-0-9]+", res)
287 # If there are not SIDs to replace return the same ACE
292 name = self.con.sid_map[sid]
293 res = res.replace(sid, name)
295 # Do not bother if the SID is not found in baseDN
299 def diff_1(self, other):
301 if len(self.dacl_list) != len(other.dacl_list):
302 res += 4 * " " + "Difference in ACE count:\n"
303 res += 8 * " " + "=> %s\n" % len(self.dacl_list)
304 res += 8 * " " + "=> %s\n" % len(other.dacl_list)
312 self_ace = "%s" % self.dacl_list[i]
317 other_ace = "%s" % other.dacl_list[i]
320 if len(self_ace) + len(other_ace) == 0:
322 self_ace_fixed = "%s" % self.fix_sid(self_ace)
323 other_ace_fixed = "%s" % other.fix_sid(other_ace)
324 if self_ace_fixed != other_ace_fixed:
325 res += "%60s * %s\n" % (self_ace_fixed, other_ace_fixed)
328 res += "%60s | %s\n" % (self_ace_fixed, other_ace_fixed)
332 def diff_2(self, other):
334 if len(self.dacl_list) != len(other.dacl_list):
335 res += 4 * " " + "Difference in ACE count:\n"
336 res += 8 * " " + "=> %s\n" % len(self.dacl_list)
337 res += 8 * " " + "=> %s\n" % len(other.dacl_list)
342 self_dacl_list_fixed = [self.fix_sid(ace) for ace in self.dacl_list]
343 other_dacl_list_fixed = [other.fix_sid(ace) for ace in other.dacl_list]
344 for ace in self_dacl_list_fixed:
346 other_dacl_list_fixed.index(ace)
348 self_aces.append(ace)
350 common_aces.append(ace)
351 self_aces = sorted(self_aces)
352 if len(self_aces) > 0:
353 res += 4 * " " + "ACEs found only in %s:\n" % self.con.host
354 for ace in self_aces:
355 res += 8 * " " + ace + "\n"
357 for ace in other_dacl_list_fixed:
359 self_dacl_list_fixed.index(ace)
361 other_aces.append(ace)
363 common_aces.append(ace)
364 other_aces = sorted(other_aces)
365 if len(other_aces) > 0:
366 res += 4 * " " + "ACEs found only in %s:\n" % other.con.host
367 for ace in other_aces:
368 res += 8 * " " + ace + "\n"
370 common_aces = sorted(list(set(common_aces)))
372 res += 4 * " " + "ACEs found in both:\n"
373 for ace in common_aces:
374 res += 8 * " " + ace + "\n"
375 return (self_aces == [] and other_aces == [], res)
378 class LDAPObject(object):
379 def __init__(self, connection, dn, summary, filter_list,
380 outf=sys.stdout, errf=sys.stderr):
383 self.con = connection
384 self.two_domains = self.con.two_domains
385 self.quiet = self.con.quiet
386 self.verbose = self.con.verbose
387 self.summary = summary
388 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
389 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
390 for x in self.con.server_names:
391 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
392 self.attributes = self.con.get_attributes(self.dn)
393 # One domain - two domain controllers
395 # Some attributes are defined as FLAG_ATTR_NOT_REPLICATED
397 # The following list was generated by
398 # egrep '^systemFlags: |^ldapDisplayName: |^linkID: ' \
399 # source4/setup/ad-schema/MS-AD_Schema_2K8_R2_Attributes.txt | \
400 # grep -B1 FLAG_ATTR_NOT_REPLICATED | \
401 # grep ldapDisplayName | \
403 self.non_replicated_attributes = [
406 "dSCorePropagationData",
411 "msDS-Cached-Membership",
412 "msDS-Cached-Membership-Time-Stamp",
413 "msDS-EnabledFeatureBL",
414 "msDS-ExecuteScriptPassword",
416 "msDS-ReplicationEpoch",
417 "msDS-RetiredReplNCSignatures",
418 "msDS-USNLastSyncSuccess",
419 # "distinguishedName", # This is implicitly replicated
420 # "objectGUID", # This is implicitly replicated
421 "partialAttributeDeletionList",
422 "partialAttributeSet",
425 "replPropertyMetaData",
426 "replUpToDateVector",
430 "rIDPreviousAllocationPool",
437 "whenChanged", # This is implicitly replicated, but may diverge on updates of non-replicated attributes
439 self.ignore_attributes = self.non_replicated_attributes
440 self.ignore_attributes += ["msExchServer1HighestUSN"]
442 self.ignore_attributes += filter_list
444 self.dn_attributes = []
445 self.domain_attributes = []
446 self.servername_attributes = []
447 self.netbios_attributes = []
448 self.other_attributes = []
449 # Two domains - two domain controllers
452 self.ignore_attributes += [
453 "objectCategory", "objectGUID", "objectSid", "whenCreated",
454 "whenChanged", "pwdLastSet", "uSNCreated", "creationTime",
455 "modifiedCount", "priorSetTime", "rIDManagerReference",
456 "gPLink", "ipsecNFAReference", "fRSPrimaryMember",
457 "fSMORoleOwner", "masteredBy", "ipsecOwnersReference",
458 "wellKnownObjects", "otherWellKnownObjects", "badPwdCount",
459 "ipsecISAKMPReference", "ipsecFilterReference",
460 "msDs-masteredBy", "lastSetTime",
461 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath",
462 "accountExpires", "invocationId", "operatingSystemVersion",
463 "oEMInformation", "schemaInfo",
464 # After Exchange preps
465 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
467 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
468 self.dn_attributes = [
469 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
470 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
471 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
472 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
473 # After Exchange preps
474 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
475 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
476 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
477 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",
478 # After 2012 R2 functional preparation
479 "msDS-MembersOfResourcePropertyListBL",
480 "msDS-ValueTypeReference",
481 "msDS-MembersOfResourcePropertyList",
482 "msDS-ValueTypeReferenceBL",
483 "msDS-ClaimTypeAppliesToClass",
485 self.dn_attributes = [x.upper() for x in self.dn_attributes]
487 # Attributes that contain the Domain name e.g. 'samba.org'
488 self.domain_attributes = [
489 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
490 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName", ]
491 self.domain_attributes = [x.upper() for x in self.domain_attributes]
493 # May contain DOMAIN_NETBIOS and SERVER_NAME
494 self.servername_attributes = ["distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
495 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
496 "msDS-IsDomainFor", "interSiteTopologyGenerator", ]
497 self.servername_attributes = [x.upper() for x in self.servername_attributes]
499 self.netbios_attributes = ["servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name", ]
500 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
502 self.other_attributes = ["name", "DC", ]
503 self.other_attributes = [x.upper() for x in self.other_attributes]
505 self.ignore_attributes = set([x.upper() for x in self.ignore_attributes])
509 Log on the screen if there is no --quiet option set
512 self.outf.write(msg +"\n")
516 if not self.two_domains:
518 if res.upper().endswith(self.con.base_dn.upper()):
519 res = res[:len(res) - len(self.con.base_dn)] + "${DOMAIN_DN}"
522 def fix_domain_name(self, s):
524 if not self.two_domains:
526 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
527 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
530 def fix_domain_netbios(self, s):
532 if not self.two_domains:
534 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
535 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
538 def fix_server_name(self, s):
540 if not self.two_domains or len(self.con.server_names) > 1:
542 for x in self.con.server_names:
543 res = res.upper().replace(x, "${SERVER_NAME}")
546 def __eq__(self, other):
547 if self.con.descriptor:
548 return self.cmp_desc(other)
549 return self.cmp_attrs(other)
551 def cmp_desc(self, other):
552 d1 = Descriptor(self.con, self.dn, outf=self.outf, errf=self.errf)
553 d2 = Descriptor(other.con, other.dn, outf=self.outf, errf=self.errf)
554 if self.con.view == "section":
556 elif self.con.view == "collision":
559 raise Exception("Unknown --view option value.")
561 self.screen_output = res[1]
562 other.screen_output = res[1]
566 def cmp_attrs(self, other):
568 self.df_value_attrs = []
570 self_attrs = set([attr.upper() for attr in self.attributes])
571 other_attrs = set([attr.upper() for attr in other.attributes])
573 self_unique_attrs = self_attrs - other_attrs - other.ignore_attributes
574 if self_unique_attrs:
575 res += 4 * " " + "Attributes found only in %s:" % self.con.host
576 for x in self_unique_attrs:
577 res += 8 * " " + x + "\n"
579 other_unique_attrs = other_attrs - self_attrs - self.ignore_attributes
580 if other_unique_attrs:
581 res += 4 * " " + "Attributes found only in %s:" % other.con.host
582 for x in other_unique_attrs:
583 res += 8 * " " + x + "\n"
585 missing_attrs = self_unique_attrs & other_unique_attrs
586 title = 4 * " " + "Difference in attribute values:"
587 for x in self.attributes:
588 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
590 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
591 self.attributes[x] = sorted(self.attributes[x])
592 other.attributes[x] = sorted(other.attributes[x])
593 if self.attributes[x] != other.attributes[x]:
598 # First check if the difference can be fixed but shunting the first part
599 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
600 if x.upper() in self.other_attributes:
601 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
602 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
605 # Attribute values that are list that contain DN based values that may differ
606 elif x.upper() in self.dn_attributes:
610 m = self.attributes[x]
611 n = other.attributes[x]
612 p = [self.fix_dn(j) for j in m]
613 q = [other.fix_dn(j) for j in n]
616 # Attributes that contain the Domain name in them
617 if x.upper() in self.domain_attributes:
621 m = self.attributes[x]
622 n = other.attributes[x]
623 p = [self.fix_domain_name(j) for j in m]
624 q = [other.fix_domain_name(j) for j in n]
628 if x.upper() in self.servername_attributes:
629 # Attributes with SERVER_NAME
633 m = self.attributes[x]
634 n = other.attributes[x]
635 p = [self.fix_server_name(j) for j in m]
636 q = [other.fix_server_name(j) for j in n]
640 if x.upper() in self.netbios_attributes:
641 # Attributes with NETBIOS Domain name
645 m = self.attributes[x]
646 n = other.attributes[x]
647 p = [self.fix_domain_netbios(j) for j in m]
648 q = [other.fix_domain_netbios(j) for j in n]
656 res += 8 * " " + x + " => \n%s\n%s" % (p, q) + "\n"
658 res += 8 * " " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
659 self.df_value_attrs.append(x)
662 assert self_unique_attrs != other_unique_attrs
663 self.summary["unique_attrs"] += list(self_unique_attrs)
664 self.summary["df_value_attrs"] += self.df_value_attrs
665 other.summary["unique_attrs"] += list(other_unique_attrs)
666 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
668 self.screen_output = res
669 other.screen_output = res
674 class LDAPBundle(object):
676 def __init__(self, connection, context, dn_list=None, filter_list=None,
677 outf=sys.stdout, errf=sys.stderr):
680 self.con = connection
681 self.two_domains = self.con.two_domains
682 self.quiet = self.con.quiet
683 self.verbose = self.con.verbose
684 self.search_base = self.con.search_base
685 self.search_scope = self.con.search_scope
686 self.skip_missing_dn = self.con.skip_missing_dn
688 self.summary["unique_attrs"] = []
689 self.summary["df_value_attrs"] = []
690 self.summary["known_ignored_dn"] = []
691 self.summary["abnormal_ignored_dn"] = []
692 self.filter_list = filter_list
694 self.dn_list = dn_list
695 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
696 self.context = context.upper()
697 self.dn_list = self.get_dn_list(context)
699 raise Exception("Unknown initialization data for LDAPBundle().")
701 while counter < len(self.dn_list) and self.two_domains:
702 # Use alias reference
703 tmp = self.dn_list[counter]
704 tmp = tmp[:len(tmp) - len(self.con.base_dn)] + "${DOMAIN_DN}"
705 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
706 if len(self.con.server_names) == 1:
707 for x in self.con.server_names:
708 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
709 self.dn_list[counter] = tmp
711 self.dn_list = list(set(self.dn_list))
712 self.dn_list = sorted(self.dn_list)
713 self.size = len(self.dn_list)
717 Log on the screen if there is no --quiet option set
720 self.outf.write(msg + "\n")
722 def update_size(self):
723 self.size = len(self.dn_list)
724 self.dn_list = sorted(self.dn_list)
726 def diff(self, other):
728 if self.size != other.size:
729 self.log("\n* DN lists have different size: %s != %s" % (self.size, other.size))
730 if not self.skip_missing_dn:
733 self_dns = set([q.upper() for q in self.dn_list])
734 other_dns = set([q.upper() for q in other.dn_list])
737 # This is the case where we want to explicitly compare two objects with different DNs.
738 # It does not matter if they are in the same DC, in two DC in one domain or in two
740 if self.search_scope != SCOPE_BASE and not self.skip_missing_dn:
742 self_only = self_dns - other_dns # missing in other
745 self.log("\n* DNs found only in %s:" % self.con.host)
746 for x in sorted(self_only):
747 self.log(4 * " " + x)
749 other_only = other_dns - self_dns # missing in self
752 self.log("\n* DNs found only in %s:" % other.con.host)
753 for x in sorted(other_only):
754 self.log(4 * " " + x)
756 common_dns = self_dns & other_dns
757 self.log("\n* Objects to be compared: %d" % len(common_dns))
759 for dn in common_dns:
762 object1 = LDAPObject(connection=self.con,
764 summary=self.summary,
765 filter_list=self.filter_list,
766 outf=self.outf, errf=self.errf)
767 except LdbError as e:
768 self.log("LdbError for dn %s: %s" % (dn, e))
772 object2 = LDAPObject(connection=other.con,
774 summary=other.summary,
775 filter_list=self.filter_list,
776 outf=self.outf, errf=self.errf)
777 except LdbError as e:
778 self.log("LdbError for dn %s: %s" % (dn, e))
781 if object1 == object2:
783 self.log("\nComparing:")
784 self.log("'%s' [%s]" % (object1.dn, object1.con.host))
785 self.log("'%s' [%s]" % (object2.dn, object2.con.host))
786 self.log(4 * " " + "OK")
788 self.log("\nComparing:")
789 self.log("'%s' [%s]" % (object1.dn, object1.con.host))
790 self.log("'%s' [%s]" % (object2.dn, object2.con.host))
791 self.log(object1.screen_output)
792 self.log(4 * " " + "FAILED")
794 self.summary = object1.summary
795 other.summary = object2.summary
799 def get_dn_list(self, context):
800 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
801 Parse all DNs and filter those that are 'strange' or abnormal.
803 if context.upper() == "DOMAIN":
804 search_base = self.con.base_dn
805 elif context.upper() == "CONFIGURATION":
806 search_base = self.con.config_dn
807 elif context.upper() == "SCHEMA":
808 search_base = self.con.schema_dn
809 elif context.upper() == "DNSDOMAIN":
810 search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
811 elif context.upper() == "DNSFOREST":
812 search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
815 if not self.search_base:
816 self.search_base = search_base
817 self.search_scope = self.search_scope.upper()
818 if self.search_scope == "SUB":
819 self.search_scope = SCOPE_SUBTREE
820 elif self.search_scope == "BASE":
821 self.search_scope = SCOPE_BASE
822 elif self.search_scope == "ONE":
823 self.search_scope = SCOPE_ONELEVEL
825 raise ValueError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
827 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
828 except LdbError as e3:
829 (enum, estr) = e3.args
830 self.outf.write("Failed search of base=%s\n" % self.search_base)
833 dn_list.append(x["dn"].get_linearized())
836 def print_summary(self):
837 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
838 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
840 if self.summary["unique_attrs"]:
841 self.log("\nAttributes found only in %s:" % self.con.host)
842 self.log("".join([str("\n" + 4 * " " + x) for x in self.summary["unique_attrs"]]))
844 if self.summary["df_value_attrs"]:
845 self.log("\nAttributes with different values:")
846 self.log("".join([str("\n" + 4 * " " + x) for x in self.summary["df_value_attrs"]]))
847 self.summary["df_value_attrs"] = []
850 class cmd_ldapcmp(Command):
851 """Compare two ldap databases."""
852 synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
854 takes_optiongroups = {
855 "sambaopts": options.SambaOptions,
856 "versionopts": options.VersionOptions,
857 "credopts": options.CredentialsOptionsDouble,
860 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?", "context4?", "context5?"]
863 Option("-w", "--two", dest="two", action="store_true", default=False,
864 help="Hosts are in two different domains"),
865 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
866 help="Do not print anything but relay on just exit code"),
867 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
868 help="Print all DN pairs that have been compared"),
869 Option("--sd", dest="descriptor", action="store_true", default=False,
870 help="Compare nTSecurityDescriptor attibutes only"),
871 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
872 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
873 Option("--view", dest="view", default="section", choices=["section", "collision"],
874 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
875 Option("--base", dest="base", default="",
876 help="Pass search base that will build DN list for the first DC."),
877 Option("--base2", dest="base2", default="",
878 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
879 Option("--scope", dest="scope", default="SUB", choices=["SUB", "ONE", "BASE"],
880 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
881 Option("--filter", dest="filter", default="",
882 help="List of comma separated attributes to ignore in the comparision"),
883 Option("--skip-missing-dn", dest="skip_missing_dn", action="store_true", default=False,
884 help="Skip report and failure due to missing DNs in one server or another"),
887 def run(self, URL1, URL2,
888 context1=None, context2=None, context3=None, context4=None, context5=None,
889 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
890 view="section", base="", base2="", scope="SUB", filter="",
891 credopts=None, sambaopts=None, versionopts=None, skip_missing_dn=False):
893 lp = sambaopts.get_loadparm()
895 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
898 creds = credopts.get_credentials(lp, fallback_machine=True)
901 creds2 = credopts.get_credentials2(lp, guess=False)
902 if creds2.is_anonymous():
905 creds2.set_domain("")
906 creds2.set_workstation("")
907 if using_ldap and not creds.authentication_requested():
908 raise CommandError("You must supply at least one username/password pair")
910 # make a list of contexts to compare in
914 # If search bases are specified context is defaulted to
915 # DOMAIN so the given search bases can be verified.
916 contexts = ["DOMAIN"]
918 # if no argument given, we compare all contexts
919 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
921 for c in [context1, context2, context3, context4, context5]:
924 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
925 raise CommandError("Incorrect argument: %s" % c)
926 contexts.append(c.upper())
928 if verbose and quiet:
929 raise CommandError("You cannot set --verbose and --quiet together")
930 if (not base and base2) or (base and not base2):
931 raise CommandError("You need to specify both --base and --base2 at the same time")
933 con1 = LDAPBase(URL1, creds, lp,
934 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
935 verbose=verbose, view=view, base=base, scope=scope,
936 outf=self.outf, errf=self.errf, skip_missing_dn=skip_missing_dn)
937 assert len(con1.base_dn) > 0
939 con2 = LDAPBase(URL2, creds2, lp,
940 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
941 verbose=verbose, view=view, base=base2, scope=scope,
942 outf=self.outf, errf=self.errf, skip_missing_dn=skip_missing_dn)
943 assert len(con2.base_dn) > 0
945 filter_list = filter.split(",")
948 for context in contexts:
950 self.outf.write("\n* Comparing [%s] context...\n" % context)
952 b1 = LDAPBundle(con1, context=context, filter_list=filter_list,
953 outf=self.outf, errf=self.errf)
954 b2 = LDAPBundle(con2, context=context, filter_list=filter_list,
955 outf=self.outf, errf=self.errf)
959 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
963 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
965 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
966 b2.summary["df_value_attrs"] = []
967 self.outf.write("\nSUMMARY\n")
968 self.outf.write("---------\n")
971 # mark exit status as FAILURE if a least one comparison failed
974 raise CommandError("Compare failed: %d" % status)