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 (
41 class LDAPBase(object):
43 def __init__(self, host, creds, lp,
44 two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
45 view="section", base="", scope="SUB",
46 outf=sys.stdout, errf=sys.stderr, skip_missing_dn=True):
50 if os.path.isfile(host):
51 samdb_url = "tdb://%s" % host
53 samdb_url = "ldap://%s" % host
54 # use 'paged_search' module when connecting remotely
55 if samdb_url.lower().startswith("ldap://"):
56 ldb_options = ["modules:paged_searches"]
59 self.ldb = Ldb(url=samdb_url,
63 self.search_base = base
64 self.search_scope = scope
65 self.two_domains = two
67 self.descriptor = descriptor
68 self.sort_aces = sort_aces
70 self.verbose = verbose
72 self.skip_missing_dn = skip_missing_dn
73 self.base_dn = str(self.ldb.get_default_basedn())
74 self.root_dn = str(self.ldb.get_root_basedn())
75 self.config_dn = str(self.ldb.get_config_basedn())
76 self.schema_dn = str(self.ldb.get_schema_basedn())
77 self.domain_netbios = self.find_netbios()
78 self.server_names = self.find_servers()
79 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
80 self.domain_sid = self.find_domain_sid()
83 # Log some domain controller specific place-holers that are being used
84 # when compare content of two DCs. Uncomment for DEBUG purposes.
85 if self.two_domains and not self.quiet:
86 self.outf.write("\n* Place-holders for %s:\n" % self.host)
87 self.outf.write(4 * " " + "${DOMAIN_DN} => %s\n" %
89 self.outf.write(4 * " " + "${DOMAIN_NETBIOS} => %s\n" %
91 self.outf.write(4 * " " + "${SERVER_NAME} => %s\n" %
93 self.outf.write(4 * " " + "${DOMAIN_NAME} => %s\n" %
96 def find_domain_sid(self):
97 res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
98 return ndr_unpack(security.dom_sid, res[0]["objectSid"][0])
100 def find_servers(self):
103 res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn,
104 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
108 srv.append(str(x["cn"][0]))
111 def find_netbios(self):
112 res = self.ldb.search(base="CN=Partitions,%s" % self.config_dn,
113 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
116 if "nETBIOSName" in x.keys():
117 return x["nETBIOSName"][0]
119 def object_exists(self, object_dn):
122 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
123 except LdbError as e2:
124 (enum, estr) = e2.args
125 if enum == ERR_NO_SUCH_OBJECT:
130 def delete_force(self, object_dn):
132 self.ldb.delete(object_dn)
133 except Ldb.LdbError as e:
134 assert "No such object" in str(e)
136 def get_attribute_name(self, key):
137 """ Returns the real attribute name
138 It resolved ranged results e.g. member;range=0-1499
141 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
149 def get_attribute_values(self, object_dn, key, vals):
150 """ Returns list with all attribute values
151 It resolved ranged results e.g. member;range=0-1499
154 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
158 # no range, just return the values
164 # get additional values in a loop
165 # until we get a response with '*' at the end
168 n = "%s;range=%d-*" % (attr, hi + 1)
169 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
177 for key in res.keys():
183 if m.group(1) != attr:
187 fvals = list(res[key])
194 if fm.group(3) == "*":
195 # if we got "*" we're done
198 assert int(fm.group(2)) == hi + 1
199 hi = int(fm.group(3))
203 def get_attributes(self, object_dn):
204 """ Returns dict with all default visible attributes
206 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
209 # 'Dn' element is not iterable and we have it as 'distinguishedName'
213 for key, vals in res.items():
214 name = self.get_attribute_name(key)
215 # sort vals and return a list, help to compare
217 attributes[name] = self.get_attribute_values(object_dn, key, vals)
221 def get_descriptor_sddl(self, object_dn):
222 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
223 desc = res[0]["nTSecurityDescriptor"][0]
224 desc = ndr_unpack(security.descriptor, desc)
225 return desc.as_sddl(self.domain_sid)
227 def guid_as_string(self, guid_blob):
228 """ Translate binary representation of schemaIDGUID to standard string representation.
229 @gid_blob: binary schemaIDGUID
231 blob = "%s" % guid_blob
232 stops = [4, 2, 2, 2, 6]
236 while x < len(stops):
240 c = hex(ord(blob[index])).replace("0x", "")
241 c = [None, "0" + c, c][len(c)]
242 if 2 * index < len(blob):
250 assert index == len(blob)
251 return res.strip().replace(" ", "-")
253 def get_sid_map(self):
254 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
257 res = self.ldb.search(base=self.base_dn,
258 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
261 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = str(item["sAMAccountName"][0])
266 class Descriptor(object):
267 def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr):
270 self.con = connection
272 self.sddl = self.con.get_descriptor_sddl(self.dn)
273 self.dacl_list = self.extract_dacl()
274 if self.con.sort_aces:
275 self.dacl_list.sort()
277 def extract_dacl(self):
278 """ Extracts the DACL as a list of ACE string (with the brakets).
281 if "S:" in self.sddl:
282 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
284 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
285 except AttributeError:
287 return re.findall("(\(.*?\))", res)
289 def fix_sid(self, ace):
291 sids = re.findall("S-[-0-9]+", res)
292 # If there are not SIDs to replace return the same ACE
297 name = self.con.sid_map[sid]
298 res = res.replace(sid, name)
300 # Do not bother if the SID is not found in baseDN
304 def diff_1(self, other):
306 if len(self.dacl_list) != len(other.dacl_list):
307 res += 4 * " " + "Difference in ACE count:\n"
308 res += 8 * " " + "=> %s\n" % len(self.dacl_list)
309 res += 8 * " " + "=> %s\n" % len(other.dacl_list)
317 self_ace = "%s" % self.dacl_list[i]
322 other_ace = "%s" % other.dacl_list[i]
325 if len(self_ace) + len(other_ace) == 0:
327 self_ace_fixed = "%s" % self.fix_sid(self_ace)
328 other_ace_fixed = "%s" % other.fix_sid(other_ace)
329 if self_ace_fixed != other_ace_fixed:
330 res += "%60s * %s\n" % (self_ace_fixed, other_ace_fixed)
333 res += "%60s | %s\n" % (self_ace_fixed, other_ace_fixed)
337 def diff_2(self, other):
339 if len(self.dacl_list) != len(other.dacl_list):
340 res += 4 * " " + "Difference in ACE count:\n"
341 res += 8 * " " + "=> %s\n" % len(self.dacl_list)
342 res += 8 * " " + "=> %s\n" % len(other.dacl_list)
347 self_dacl_list_fixed = []
348 other_dacl_list_fixed = []
349 [self_dacl_list_fixed.append(self.fix_sid(ace)) for ace in self.dacl_list]
350 [other_dacl_list_fixed.append(other.fix_sid(ace)) for ace in other.dacl_list]
351 for ace in self_dacl_list_fixed:
353 other_dacl_list_fixed.index(ace)
355 self_aces.append(ace)
357 common_aces.append(ace)
358 self_aces = sorted(self_aces)
359 if len(self_aces) > 0:
360 res += 4 * " " + "ACEs found only in %s:\n" % self.con.host
361 for ace in self_aces:
362 res += 8 * " " + ace + "\n"
364 for ace in other_dacl_list_fixed:
366 self_dacl_list_fixed.index(ace)
368 other_aces.append(ace)
370 common_aces.append(ace)
371 other_aces = sorted(other_aces)
372 if len(other_aces) > 0:
373 res += 4 * " " + "ACEs found only in %s:\n" % other.con.host
374 for ace in other_aces:
375 res += 8 * " " + ace + "\n"
377 common_aces = sorted(list(set(common_aces)))
379 res += 4 * " " + "ACEs found in both:\n"
380 for ace in common_aces:
381 res += 8 * " " + ace + "\n"
382 return (self_aces == [] and other_aces == [], res)
385 class LDAPObject(object):
386 def __init__(self, connection, dn, summary, filter_list,
387 outf=sys.stdout, errf=sys.stderr):
390 self.con = connection
391 self.two_domains = self.con.two_domains
392 self.quiet = self.con.quiet
393 self.verbose = self.con.verbose
394 self.summary = summary
395 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
396 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
397 for x in self.con.server_names:
398 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
399 self.attributes = self.con.get_attributes(self.dn)
400 # One domain - two domain controllers
402 # Some attributes are defined as FLAG_ATTR_NOT_REPLICATED
404 # The following list was generated by
405 # egrep '^systemFlags: |^ldapDisplayName: |^linkID: ' \
406 # source4/setup/ad-schema/MS-AD_Schema_2K8_R2_Attributes.txt | \
407 # grep -B1 FLAG_ATTR_NOT_REPLICATED | \
408 # grep ldapDisplayName | \
410 self.non_replicated_attributes = [
413 "dSCorePropagationData",
418 "msDS-Cached-Membership",
419 "msDS-Cached-Membership-Time-Stamp",
420 "msDS-EnabledFeatureBL",
421 "msDS-ExecuteScriptPassword",
423 "msDS-ReplicationEpoch",
424 "msDS-RetiredReplNCSignatures",
425 "msDS-USNLastSyncSuccess",
426 # "distinguishedName", # This is implicitly replicated
427 # "objectGUID", # This is implicitly replicated
428 "partialAttributeDeletionList",
429 "partialAttributeSet",
432 "replPropertyMetaData",
433 "replUpToDateVector",
437 "rIDPreviousAllocationPool",
444 "whenChanged", # This is implicitly replicated, but may diverge on updates of non-replicated attributes
446 self.ignore_attributes = self.non_replicated_attributes
447 self.ignore_attributes += ["msExchServer1HighestUSN"]
449 self.ignore_attributes += filter_list
451 self.dn_attributes = []
452 self.domain_attributes = []
453 self.servername_attributes = []
454 self.netbios_attributes = []
455 self.other_attributes = []
456 # Two domains - two domain controllers
459 self.ignore_attributes += [
460 "objectCategory", "objectGUID", "objectSid", "whenCreated",
461 "whenChanged", "pwdLastSet", "uSNCreated", "creationTime",
462 "modifiedCount", "priorSetTime", "rIDManagerReference",
463 "gPLink", "ipsecNFAReference", "fRSPrimaryMember",
464 "fSMORoleOwner", "masteredBy", "ipsecOwnersReference",
465 "wellKnownObjects", "otherWellKnownObjects", "badPwdCount",
466 "ipsecISAKMPReference", "ipsecFilterReference",
467 "msDs-masteredBy", "lastSetTime",
468 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath",
469 "accountExpires", "invocationId", "operatingSystemVersion",
471 # After Exchange preps
472 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
474 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
475 self.dn_attributes = [
476 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
477 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
478 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
479 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
480 # After Exchange preps
481 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
482 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
483 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
484 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",
485 # After 2012 R2 functional preparation
486 "msDS-MembersOfResourcePropertyListBL",
487 "msDS-ValueTypeReference",
488 "msDS-MembersOfResourcePropertyList",
489 "msDS-ValueTypeReferenceBL",
490 "msDS-ClaimTypeAppliesToClass",
492 self.dn_attributes = [x.upper() for x in self.dn_attributes]
494 # Attributes that contain the Domain name e.g. 'samba.org'
495 self.domain_attributes = [
496 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
497 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName", ]
498 self.domain_attributes = [x.upper() for x in self.domain_attributes]
500 # May contain DOMAIN_NETBIOS and SERVER_NAME
501 self.servername_attributes = ["distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
502 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
503 "msDS-IsDomainFor", "interSiteTopologyGenerator", ]
504 self.servername_attributes = [x.upper() for x in self.servername_attributes]
506 self.netbios_attributes = ["servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name", ]
507 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
509 self.other_attributes = ["name", "DC", ]
510 self.other_attributes = [x.upper() for x in self.other_attributes]
512 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
516 Log on the screen if there is no --quiet option set
519 self.outf.write(msg +"\n")
523 if not self.two_domains:
525 if res.upper().endswith(self.con.base_dn.upper()):
526 res = res[:len(res) - len(self.con.base_dn)] + "${DOMAIN_DN}"
529 def fix_domain_name(self, s):
531 if not self.two_domains:
533 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
534 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
537 def fix_domain_netbios(self, s):
539 if not self.two_domains:
541 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
542 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
545 def fix_server_name(self, s):
547 if not self.two_domains or len(self.con.server_names) > 1:
549 for x in self.con.server_names:
550 res = res.upper().replace(x, "${SERVER_NAME}")
553 def __eq__(self, other):
554 if self.con.descriptor:
555 return self.cmp_desc(other)
556 return self.cmp_attrs(other)
558 def cmp_desc(self, other):
559 d1 = Descriptor(self.con, self.dn, outf=self.outf, errf=self.errf)
560 d2 = Descriptor(other.con, other.dn, outf=self.outf, errf=self.errf)
561 if self.con.view == "section":
563 elif self.con.view == "collision":
566 raise Exception("Unknown --view option value.")
568 self.screen_output = res[1]
569 other.screen_output = res[1]
573 def cmp_attrs(self, other):
575 self.unique_attrs = []
576 self.df_value_attrs = []
577 other.unique_attrs = []
578 if self.attributes.keys() != other.attributes.keys():
580 title = 4 * " " + "Attributes found only in %s:" % self.con.host
581 for x in self.attributes.keys():
582 if x not in other.attributes.keys() and \
583 not x.upper() in [q.upper() for q in other.ignore_attributes]:
587 res += 8 * " " + x + "\n"
588 self.unique_attrs.append(x)
590 title = 4 * " " + "Attributes found only in %s:" % other.con.host
591 for x in other.attributes.keys():
592 if x not in self.attributes.keys() and \
593 not x.upper() in [q.upper() for q in self.ignore_attributes]:
597 res += 8 * " " + x + "\n"
598 other.unique_attrs.append(x)
600 missing_attrs = [x.upper() for x in self.unique_attrs]
601 missing_attrs += [x.upper() for x in other.unique_attrs]
602 title = 4 * " " + "Difference in attribute values:"
603 for x in self.attributes.keys():
604 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
606 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
607 self.attributes[x] = sorted(self.attributes[x])
608 other.attributes[x] = sorted(other.attributes[x])
609 if self.attributes[x] != other.attributes[x]:
614 # First check if the difference can be fixed but shunting the first part
615 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
616 if x.upper() in self.other_attributes:
617 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
618 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
621 # Attribute values that are list that contain DN based values that may differ
622 elif x.upper() in self.dn_attributes:
626 m = self.attributes[x]
627 n = other.attributes[x]
628 p = [self.fix_dn(j) for j in m]
629 q = [other.fix_dn(j) for j in n]
632 # Attributes that contain the Domain name in them
633 if x.upper() in self.domain_attributes:
637 m = self.attributes[x]
638 n = other.attributes[x]
639 p = [self.fix_domain_name(j) for j in m]
640 q = [other.fix_domain_name(j) for j in n]
644 if x.upper() in self.servername_attributes:
645 # Attributes with SERVER_NAME
649 m = self.attributes[x]
650 n = other.attributes[x]
651 p = [self.fix_server_name(j) for j in m]
652 q = [other.fix_server_name(j) for j in n]
656 if x.upper() in self.netbios_attributes:
657 # Attributes with NETBIOS Domain name
661 m = self.attributes[x]
662 n = other.attributes[x]
663 p = [self.fix_domain_netbios(j) for j in m]
664 q = [other.fix_domain_netbios(j) for j in n]
672 res += 8 * " " + x + " => \n%s\n%s" % (p, q) + "\n"
674 res += 8 * " " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
675 self.df_value_attrs.append(x)
677 if self.unique_attrs + other.unique_attrs != []:
678 assert self.unique_attrs != other.unique_attrs
679 self.summary["unique_attrs"] += self.unique_attrs
680 self.summary["df_value_attrs"] += self.df_value_attrs
681 other.summary["unique_attrs"] += other.unique_attrs
682 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
684 self.screen_output = res
685 other.screen_output = res
690 class LDAPBundle(object):
692 def __init__(self, connection, context, dn_list=None, filter_list=None,
693 outf=sys.stdout, errf=sys.stderr):
696 self.con = connection
697 self.two_domains = self.con.two_domains
698 self.quiet = self.con.quiet
699 self.verbose = self.con.verbose
700 self.search_base = self.con.search_base
701 self.search_scope = self.con.search_scope
702 self.skip_missing_dn = self.con.skip_missing_dn
704 self.summary["unique_attrs"] = []
705 self.summary["df_value_attrs"] = []
706 self.summary["known_ignored_dn"] = []
707 self.summary["abnormal_ignored_dn"] = []
708 self.filter_list = filter_list
710 self.dn_list = dn_list
711 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
712 self.context = context.upper()
713 self.dn_list = self.get_dn_list(context)
715 raise Exception("Unknown initialization data for LDAPBundle().")
717 while counter < len(self.dn_list) and self.two_domains:
718 # Use alias reference
719 tmp = self.dn_list[counter]
720 tmp = tmp[:len(tmp) - len(self.con.base_dn)] + "${DOMAIN_DN}"
721 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
722 if len(self.con.server_names) == 1:
723 for x in self.con.server_names:
724 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
725 self.dn_list[counter] = tmp
727 self.dn_list = list(set(self.dn_list))
728 self.dn_list = sorted(self.dn_list)
729 self.size = len(self.dn_list)
733 Log on the screen if there is no --quiet option set
736 self.outf.write(msg + "\n")
738 def update_size(self):
739 self.size = len(self.dn_list)
740 self.dn_list = sorted(self.dn_list)
742 def diff(self, other):
744 if self.size != other.size:
745 self.log("\n* DN lists have different size: %s != %s" % (self.size, other.size))
746 if not self.skip_missing_dn:
749 self_dns = set([q.upper() for q in self.dn_list])
750 other_dns = set([q.upper() for q in other.dn_list])
753 # This is the case where we want to explicitly compare two objects with different DNs.
754 # It does not matter if they are in the same DC, in two DC in one domain or in two
756 if self.search_scope != SCOPE_BASE and not self.skip_missing_dn:
758 self_only = self_dns - other_dns # missing in other
761 self.log("\n* DNs found only in %s:" % self.con.host)
763 self.log(4 * " " + x)
765 other_only = other_dns - self_dns # missing in self
768 self.log("\n* DNs found only in %s:" % other.con.host)
770 self.log(4 * " " + x)
772 common_dns = self_dns & other_dns
773 self.log("\n* Objects to be compared: %d" % len(common_dns))
775 for dn in common_dns:
778 object1 = LDAPObject(connection=self.con,
780 summary=self.summary,
781 filter_list=self.filter_list,
782 outf=self.outf, errf=self.errf)
783 except LdbError as e:
784 self.log("LdbError for dn %s: %s" % (dn, e))
788 object2 = LDAPObject(connection=other.con,
790 summary=other.summary,
791 filter_list=self.filter_list,
792 outf=self.outf, errf=self.errf)
793 except LdbError as e:
794 self.log("LdbError for dn %s: %s" % (dn, e))
797 if object1 == object2:
799 self.log("\nComparing:")
800 self.log("'%s' [%s]" % (object1.dn, object1.con.host))
801 self.log("'%s' [%s]" % (object2.dn, object2.con.host))
802 self.log(4 * " " + "OK")
804 self.log("\nComparing:")
805 self.log("'%s' [%s]" % (object1.dn, object1.con.host))
806 self.log("'%s' [%s]" % (object2.dn, object2.con.host))
807 self.log(object1.screen_output)
808 self.log(4 * " " + "FAILED")
810 self.summary = object1.summary
811 other.summary = object2.summary
815 def get_dn_list(self, context):
816 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
817 Parse all DNs and filter those that are 'strange' or abnormal.
819 if context.upper() == "DOMAIN":
820 search_base = self.con.base_dn
821 elif context.upper() == "CONFIGURATION":
822 search_base = self.con.config_dn
823 elif context.upper() == "SCHEMA":
824 search_base = self.con.schema_dn
825 elif context.upper() == "DNSDOMAIN":
826 search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
827 elif context.upper() == "DNSFOREST":
828 search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
831 if not self.search_base:
832 self.search_base = search_base
833 self.search_scope = self.search_scope.upper()
834 if self.search_scope == "SUB":
835 self.search_scope = SCOPE_SUBTREE
836 elif self.search_scope == "BASE":
837 self.search_scope = SCOPE_BASE
838 elif self.search_scope == "ONE":
839 self.search_scope = SCOPE_ONELEVEL
841 raise ValueError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
843 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
844 except LdbError as e3:
845 (enum, estr) = e3.args
846 self.outf.write("Failed search of base=%s\n" % self.search_base)
849 dn_list.append(x["dn"].get_linearized())
852 def print_summary(self):
853 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
854 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
856 if self.summary["unique_attrs"]:
857 self.log("\nAttributes found only in %s:" % self.con.host)
858 self.log("".join([str("\n" + 4 * " " + x) for x in self.summary["unique_attrs"]]))
860 if self.summary["df_value_attrs"]:
861 self.log("\nAttributes with different values:")
862 self.log("".join([str("\n" + 4 * " " + x) for x in self.summary["df_value_attrs"]]))
863 self.summary["df_value_attrs"] = []
866 class cmd_ldapcmp(Command):
867 """Compare two ldap databases."""
868 synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
870 takes_optiongroups = {
871 "sambaopts": options.SambaOptions,
872 "versionopts": options.VersionOptions,
873 "credopts": options.CredentialsOptionsDouble,
876 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?", "context4?", "context5?"]
879 Option("-w", "--two", dest="two", action="store_true", default=False,
880 help="Hosts are in two different domains"),
881 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
882 help="Do not print anything but relay on just exit code"),
883 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
884 help="Print all DN pairs that have been compared"),
885 Option("--sd", dest="descriptor", action="store_true", default=False,
886 help="Compare nTSecurityDescriptor attibutes only"),
887 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
888 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
889 Option("--view", dest="view", default="section", choices=["section", "collision"],
890 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
891 Option("--base", dest="base", default="",
892 help="Pass search base that will build DN list for the first DC."),
893 Option("--base2", dest="base2", default="",
894 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
895 Option("--scope", dest="scope", default="SUB", choices=["SUB", "ONE", "BASE"],
896 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
897 Option("--filter", dest="filter", default="",
898 help="List of comma separated attributes to ignore in the comparision"),
899 Option("--skip-missing-dn", dest="skip_missing_dn", action="store_true", default=False,
900 help="Skip report and failure due to missing DNs in one server or another"),
903 def run(self, URL1, URL2,
904 context1=None, context2=None, context3=None, context4=None, context5=None,
905 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
906 view="section", base="", base2="", scope="SUB", filter="",
907 credopts=None, sambaopts=None, versionopts=None, skip_missing_dn=False):
909 lp = sambaopts.get_loadparm()
911 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
914 creds = credopts.get_credentials(lp, fallback_machine=True)
917 creds2 = credopts.get_credentials2(lp, guess=False)
918 if creds2.is_anonymous():
921 creds2.set_domain("")
922 creds2.set_workstation("")
923 if using_ldap and not creds.authentication_requested():
924 raise CommandError("You must supply at least one username/password pair")
926 # make a list of contexts to compare in
930 # If search bases are specified context is defaulted to
931 # DOMAIN so the given search bases can be verified.
932 contexts = ["DOMAIN"]
934 # if no argument given, we compare all contexts
935 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
937 for c in [context1, context2, context3, context4, context5]:
940 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
941 raise CommandError("Incorrect argument: %s" % c)
942 contexts.append(c.upper())
944 if verbose and quiet:
945 raise CommandError("You cannot set --verbose and --quiet together")
946 if (not base and base2) or (base and not base2):
947 raise CommandError("You need to specify both --base and --base2 at the same time")
949 con1 = LDAPBase(URL1, creds, lp,
950 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
951 verbose=verbose, view=view, base=base, scope=scope,
952 outf=self.outf, errf=self.errf, skip_missing_dn=skip_missing_dn)
953 assert len(con1.base_dn) > 0
955 con2 = LDAPBase(URL2, creds2, lp,
956 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
957 verbose=verbose, view=view, base=base2, scope=scope,
958 outf=self.outf, errf=self.errf, skip_missing_dn=skip_missing_dn)
959 assert len(con2.base_dn) > 0
961 filter_list = filter.split(",")
964 for context in contexts:
966 self.outf.write("\n* Comparing [%s] context...\n" % context)
968 b1 = LDAPBundle(con1, context=context, filter_list=filter_list,
969 outf=self.outf, errf=self.errf)
970 b2 = LDAPBundle(con2, context=context, filter_list=filter_list,
971 outf=self.outf, errf=self.errf)
975 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
979 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
981 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
982 b2.summary["df_value_attrs"] = []
983 self.outf.write("\nSUMMARY\n")
984 self.outf.write("---------\n")
987 # mark exit status as FAILURE if a least one comparison failed
990 raise CommandError("Compare failed: %d" % status)