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'
211 for key in list(res.keys()):
212 vals = list(res[key])
214 name = self.get_attribute_name(key)
215 res[name] = self.get_attribute_values(object_dn, key, vals)
219 def get_descriptor_sddl(self, object_dn):
220 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
221 desc = res[0]["nTSecurityDescriptor"][0]
222 desc = ndr_unpack(security.descriptor, desc)
223 return desc.as_sddl(self.domain_sid)
225 def guid_as_string(self, guid_blob):
226 """ Translate binary representation of schemaIDGUID to standard string representation.
227 @gid_blob: binary schemaIDGUID
229 blob = "%s" % guid_blob
230 stops = [4, 2, 2, 2, 6]
234 while x < len(stops):
238 c = hex(ord(blob[index])).replace("0x", "")
239 c = [None, "0" + c, c][len(c)]
240 if 2 * index < len(blob):
248 assert index == len(blob)
249 return res.strip().replace(" ", "-")
251 def get_sid_map(self):
252 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
255 res = self.ldb.search(base=self.base_dn,
256 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
259 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = str(item["sAMAccountName"][0])
264 class Descriptor(object):
265 def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr):
268 self.con = connection
270 self.sddl = self.con.get_descriptor_sddl(self.dn)
271 self.dacl_list = self.extract_dacl()
272 if self.con.sort_aces:
273 self.dacl_list.sort()
275 def extract_dacl(self):
276 """ Extracts the DACL as a list of ACE string (with the brakets).
279 if "S:" in self.sddl:
280 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
282 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
283 except AttributeError:
285 return re.findall("(\(.*?\))", res)
287 def fix_sid(self, ace):
289 sids = re.findall("S-[-0-9]+", res)
290 # If there are not SIDs to replace return the same ACE
295 name = self.con.sid_map[sid]
296 res = res.replace(sid, name)
298 # Do not bother if the SID is not found in baseDN
302 def diff_1(self, other):
304 if len(self.dacl_list) != len(other.dacl_list):
305 res += 4 * " " + "Difference in ACE count:\n"
306 res += 8 * " " + "=> %s\n" % len(self.dacl_list)
307 res += 8 * " " + "=> %s\n" % len(other.dacl_list)
315 self_ace = "%s" % self.dacl_list[i]
320 other_ace = "%s" % other.dacl_list[i]
323 if len(self_ace) + len(other_ace) == 0:
325 self_ace_fixed = "%s" % self.fix_sid(self_ace)
326 other_ace_fixed = "%s" % other.fix_sid(other_ace)
327 if self_ace_fixed != other_ace_fixed:
328 res += "%60s * %s\n" % (self_ace_fixed, other_ace_fixed)
331 res += "%60s | %s\n" % (self_ace_fixed, other_ace_fixed)
335 def diff_2(self, other):
337 if len(self.dacl_list) != len(other.dacl_list):
338 res += 4 * " " + "Difference in ACE count:\n"
339 res += 8 * " " + "=> %s\n" % len(self.dacl_list)
340 res += 8 * " " + "=> %s\n" % len(other.dacl_list)
345 self_dacl_list_fixed = []
346 other_dacl_list_fixed = []
347 [self_dacl_list_fixed.append(self.fix_sid(ace)) for ace in self.dacl_list]
348 [other_dacl_list_fixed.append(other.fix_sid(ace)) for ace in other.dacl_list]
349 for ace in self_dacl_list_fixed:
351 other_dacl_list_fixed.index(ace)
353 self_aces.append(ace)
355 common_aces.append(ace)
356 self_aces = sorted(self_aces)
357 if len(self_aces) > 0:
358 res += 4 * " " + "ACEs found only in %s:\n" % self.con.host
359 for ace in self_aces:
360 res += 8 * " " + ace + "\n"
362 for ace in other_dacl_list_fixed:
364 self_dacl_list_fixed.index(ace)
366 other_aces.append(ace)
368 common_aces.append(ace)
369 other_aces = sorted(other_aces)
370 if len(other_aces) > 0:
371 res += 4 * " " + "ACEs found only in %s:\n" % other.con.host
372 for ace in other_aces:
373 res += 8 * " " + ace + "\n"
375 common_aces = sorted(list(set(common_aces)))
377 res += 4 * " " + "ACEs found in both:\n"
378 for ace in common_aces:
379 res += 8 * " " + ace + "\n"
380 return (self_aces == [] and other_aces == [], res)
383 class LDAPObject(object):
384 def __init__(self, connection, dn, summary, filter_list,
385 outf=sys.stdout, errf=sys.stderr):
388 self.con = connection
389 self.two_domains = self.con.two_domains
390 self.quiet = self.con.quiet
391 self.verbose = self.con.verbose
392 self.summary = summary
393 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
394 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
395 for x in self.con.server_names:
396 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
397 self.attributes = self.con.get_attributes(self.dn)
398 # One domain - two domain controllers
400 # Some attributes are defined as FLAG_ATTR_NOT_REPLICATED
402 # The following list was generated by
403 # egrep '^systemFlags: |^ldapDisplayName: |^linkID: ' \
404 # source4/setup/ad-schema/MS-AD_Schema_2K8_R2_Attributes.txt | \
405 # grep -B1 FLAG_ATTR_NOT_REPLICATED | \
406 # grep ldapDisplayName | \
408 self.non_replicated_attributes = [
411 "dSCorePropagationData",
416 "msDS-Cached-Membership",
417 "msDS-Cached-Membership-Time-Stamp",
418 "msDS-EnabledFeatureBL",
419 "msDS-ExecuteScriptPassword",
421 "msDS-ReplicationEpoch",
422 "msDS-RetiredReplNCSignatures",
423 "msDS-USNLastSyncSuccess",
424 # "distinguishedName", # This is implicitly replicated
425 # "objectGUID", # This is implicitly replicated
426 "partialAttributeDeletionList",
427 "partialAttributeSet",
430 "replPropertyMetaData",
431 "replUpToDateVector",
435 "rIDPreviousAllocationPool",
442 "whenChanged", # This is implicitly replicated, but may diverge on updates of non-replicated attributes
444 self.ignore_attributes = self.non_replicated_attributes
445 self.ignore_attributes += ["msExchServer1HighestUSN"]
447 self.ignore_attributes += filter_list
449 self.dn_attributes = []
450 self.domain_attributes = []
451 self.servername_attributes = []
452 self.netbios_attributes = []
453 self.other_attributes = []
454 # Two domains - two domain controllers
457 self.ignore_attributes += [
458 "objectCategory", "objectGUID", "objectSid", "whenCreated",
459 "whenChanged", "pwdLastSet", "uSNCreated", "creationTime",
460 "modifiedCount", "priorSetTime", "rIDManagerReference",
461 "gPLink", "ipsecNFAReference", "fRSPrimaryMember",
462 "fSMORoleOwner", "masteredBy", "ipsecOwnersReference",
463 "wellKnownObjects", "otherWellKnownObjects", "badPwdCount",
464 "ipsecISAKMPReference", "ipsecFilterReference",
465 "msDs-masteredBy", "lastSetTime",
466 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath",
467 "accountExpires", "invocationId", "operatingSystemVersion",
469 # After Exchange preps
470 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
472 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
473 self.dn_attributes = [
474 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
475 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
476 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
477 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
478 # After Exchange preps
479 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
480 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
481 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
482 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",
483 # After 2012 R2 functional preparation
484 "msDS-MembersOfResourcePropertyListBL",
485 "msDS-ValueTypeReference",
486 "msDS-MembersOfResourcePropertyList",
487 "msDS-ValueTypeReferenceBL",
488 "msDS-ClaimTypeAppliesToClass",
490 self.dn_attributes = [x.upper() for x in self.dn_attributes]
492 # Attributes that contain the Domain name e.g. 'samba.org'
493 self.domain_attributes = [
494 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
495 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName", ]
496 self.domain_attributes = [x.upper() for x in self.domain_attributes]
498 # May contain DOMAIN_NETBIOS and SERVER_NAME
499 self.servername_attributes = ["distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
500 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
501 "msDS-IsDomainFor", "interSiteTopologyGenerator", ]
502 self.servername_attributes = [x.upper() for x in self.servername_attributes]
504 self.netbios_attributes = ["servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name", ]
505 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
507 self.other_attributes = ["name", "DC", ]
508 self.other_attributes = [x.upper() for x in self.other_attributes]
510 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
514 Log on the screen if there is no --quiet option set
517 self.outf.write(msg +"\n")
521 if not self.two_domains:
523 if res.upper().endswith(self.con.base_dn.upper()):
524 res = res[:len(res) - len(self.con.base_dn)] + "${DOMAIN_DN}"
527 def fix_domain_name(self, s):
529 if not self.two_domains:
531 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
532 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
535 def fix_domain_netbios(self, s):
537 if not self.two_domains:
539 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
540 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
543 def fix_server_name(self, s):
545 if not self.two_domains or len(self.con.server_names) > 1:
547 for x in self.con.server_names:
548 res = res.upper().replace(x, "${SERVER_NAME}")
551 def __eq__(self, other):
552 if self.con.descriptor:
553 return self.cmp_desc(other)
554 return self.cmp_attrs(other)
556 def cmp_desc(self, other):
557 d1 = Descriptor(self.con, self.dn, outf=self.outf, errf=self.errf)
558 d2 = Descriptor(other.con, other.dn, outf=self.outf, errf=self.errf)
559 if self.con.view == "section":
561 elif self.con.view == "collision":
564 raise Exception("Unknown --view option value.")
566 self.screen_output = res[1][:-1]
567 other.screen_output = res[1][:-1]
571 def cmp_attrs(self, other):
573 self.unique_attrs = []
574 self.df_value_attrs = []
575 other.unique_attrs = []
576 if self.attributes.keys() != other.attributes.keys():
578 title = 4 * " " + "Attributes found only in %s:" % self.con.host
579 for x in self.attributes.keys():
580 if x not in other.attributes.keys() and \
581 not x.upper() in [q.upper() for q in other.ignore_attributes]:
585 res += 8 * " " + x + "\n"
586 self.unique_attrs.append(x)
588 title = 4 * " " + "Attributes found only in %s:" % other.con.host
589 for x in other.attributes.keys():
590 if x not in self.attributes.keys() and \
591 not x.upper() in [q.upper() for q in self.ignore_attributes]:
595 res += 8 * " " + x + "\n"
596 other.unique_attrs.append(x)
598 missing_attrs = [x.upper() for x in self.unique_attrs]
599 missing_attrs += [x.upper() for x in other.unique_attrs]
600 title = 4 * " " + "Difference in attribute values:"
601 for x in self.attributes.keys():
602 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
604 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
605 self.attributes[x] = sorted(self.attributes[x])
606 other.attributes[x] = sorted(other.attributes[x])
607 if self.attributes[x] != other.attributes[x]:
612 # First check if the difference can be fixed but shunting the first part
613 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
614 if x.upper() in self.other_attributes:
615 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
616 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
619 # Attribute values that are list that contain DN based values that may differ
620 elif x.upper() in self.dn_attributes:
624 m = self.attributes[x]
625 n = other.attributes[x]
626 p = [self.fix_dn(j) for j in m]
627 q = [other.fix_dn(j) for j in n]
630 # Attributes that contain the Domain name in them
631 if x.upper() in self.domain_attributes:
635 m = self.attributes[x]
636 n = other.attributes[x]
637 p = [self.fix_domain_name(j) for j in m]
638 q = [other.fix_domain_name(j) for j in n]
642 if x.upper() in self.servername_attributes:
643 # Attributes with SERVER_NAME
647 m = self.attributes[x]
648 n = other.attributes[x]
649 p = [self.fix_server_name(j) for j in m]
650 q = [other.fix_server_name(j) for j in n]
654 if x.upper() in self.netbios_attributes:
655 # Attributes with NETBIOS Domain name
659 m = self.attributes[x]
660 n = other.attributes[x]
661 p = [self.fix_domain_netbios(j) for j in m]
662 q = [other.fix_domain_netbios(j) for j in n]
670 res += 8 * " " + x + " => \n%s\n%s" % (p, q) + "\n"
672 res += 8 * " " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
673 self.df_value_attrs.append(x)
675 if self.unique_attrs + other.unique_attrs != []:
676 assert self.unique_attrs != other.unique_attrs
677 self.summary["unique_attrs"] += self.unique_attrs
678 self.summary["df_value_attrs"] += self.df_value_attrs
679 other.summary["unique_attrs"] += other.unique_attrs
680 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
682 self.screen_output = res[:-1]
683 other.screen_output = res[:-1]
688 class LDAPBundle(object):
690 def __init__(self, connection, context, dn_list=None, filter_list=None,
691 outf=sys.stdout, errf=sys.stderr):
694 self.con = connection
695 self.two_domains = self.con.two_domains
696 self.quiet = self.con.quiet
697 self.verbose = self.con.verbose
698 self.search_base = self.con.search_base
699 self.search_scope = self.con.search_scope
700 self.skip_missing_dn = self.con.skip_missing_dn
702 self.summary["unique_attrs"] = []
703 self.summary["df_value_attrs"] = []
704 self.summary["known_ignored_dn"] = []
705 self.summary["abnormal_ignored_dn"] = []
706 self.filter_list = filter_list
708 self.dn_list = dn_list
709 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
710 self.context = context.upper()
711 self.dn_list = self.get_dn_list(context)
713 raise Exception("Unknown initialization data for LDAPBundle().")
715 while counter < len(self.dn_list) and self.two_domains:
716 # Use alias reference
717 tmp = self.dn_list[counter]
718 tmp = tmp[:len(tmp) - len(self.con.base_dn)] + "${DOMAIN_DN}"
719 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
720 if len(self.con.server_names) == 1:
721 for x in self.con.server_names:
722 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
723 self.dn_list[counter] = tmp
725 self.dn_list = list(set(self.dn_list))
726 self.dn_list = sorted(self.dn_list)
727 self.size = len(self.dn_list)
731 Log on the screen if there is no --quiet option set
734 self.outf.write(msg + "\n")
736 def update_size(self):
737 self.size = len(self.dn_list)
738 self.dn_list = sorted(self.dn_list)
740 def diff(self, other):
742 if self.size != other.size:
743 self.log("\n* DN lists have different size: %s != %s" % (self.size, other.size))
744 if not self.skip_missing_dn:
747 self_dns = set([q.upper() for q in self.dn_list])
748 other_dns = set([q.upper() for q in other.dn_list])
751 # This is the case where we want to explicitly compare two objects with different DNs.
752 # It does not matter if they are in the same DC, in two DC in one domain or in two
754 if self.search_scope != SCOPE_BASE and not self.skip_missing_dn:
756 self_only = self_dns - other_dns # missing in other
759 self.log("\n* DNs found only in %s:" % self.con.host)
761 self.log(4 * " " + x)
763 other_only = other_dns - self_dns # missing in self
766 self.log("\n* DNs found only in %s:" % other.con.host)
768 self.log(4 * " " + x)
770 common_dns = self_dns & other_dns
771 self.log("\n* Objects to be compared: %d" % len(common_dns))
773 for dn in common_dns:
776 object1 = LDAPObject(connection=self.con,
778 summary=self.summary,
779 filter_list=self.filter_list,
780 outf=self.outf, errf=self.errf)
781 except LdbError as e:
782 self.log("LdbError for dn %s: %s" % (dn, e))
786 object2 = LDAPObject(connection=other.con,
788 summary=other.summary,
789 filter_list=self.filter_list,
790 outf=self.outf, errf=self.errf)
791 except LdbError as e:
792 self.log("LdbError for dn %s: %s" % (dn, e))
795 if object1 == object2:
797 self.log("\nComparing:")
798 self.log("'%s' [%s]" % (object1.dn, object1.con.host))
799 self.log("'%s' [%s]" % (object2.dn, object2.con.host))
800 self.log(4 * " " + "OK")
802 self.log("\nComparing:")
803 self.log("'%s' [%s]" % (object1.dn, object1.con.host))
804 self.log("'%s' [%s]" % (object2.dn, object2.con.host))
805 self.log(object1.screen_output)
806 self.log(4 * " " + "FAILED")
808 self.summary = object1.summary
809 other.summary = object2.summary
813 def get_dn_list(self, context):
814 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
815 Parse all DNs and filter those that are 'strange' or abnormal.
817 if context.upper() == "DOMAIN":
818 search_base = self.con.base_dn
819 elif context.upper() == "CONFIGURATION":
820 search_base = self.con.config_dn
821 elif context.upper() == "SCHEMA":
822 search_base = self.con.schema_dn
823 elif context.upper() == "DNSDOMAIN":
824 search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
825 elif context.upper() == "DNSFOREST":
826 search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
829 if not self.search_base:
830 self.search_base = search_base
831 self.search_scope = self.search_scope.upper()
832 if self.search_scope == "SUB":
833 self.search_scope = SCOPE_SUBTREE
834 elif self.search_scope == "BASE":
835 self.search_scope = SCOPE_BASE
836 elif self.search_scope == "ONE":
837 self.search_scope = SCOPE_ONELEVEL
839 raise ValueError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
841 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
842 except LdbError as e3:
843 (enum, estr) = e3.args
844 self.outf.write("Failed search of base=%s\n" % self.search_base)
847 dn_list.append(x["dn"].get_linearized())
850 def print_summary(self):
851 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
852 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
854 if self.summary["unique_attrs"]:
855 self.log("\nAttributes found only in %s:" % self.con.host)
856 self.log("".join([str("\n" + 4 * " " + x) for x in self.summary["unique_attrs"]]))
858 if self.summary["df_value_attrs"]:
859 self.log("\nAttributes with different values:")
860 self.log("".join([str("\n" + 4 * " " + x) for x in self.summary["df_value_attrs"]]))
861 self.summary["df_value_attrs"] = []
864 class cmd_ldapcmp(Command):
865 """Compare two ldap databases."""
866 synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
868 takes_optiongroups = {
869 "sambaopts": options.SambaOptions,
870 "versionopts": options.VersionOptions,
871 "credopts": options.CredentialsOptionsDouble,
874 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?", "context4?", "context5?"]
877 Option("-w", "--two", dest="two", action="store_true", default=False,
878 help="Hosts are in two different domains"),
879 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
880 help="Do not print anything but relay on just exit code"),
881 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
882 help="Print all DN pairs that have been compared"),
883 Option("--sd", dest="descriptor", action="store_true", default=False,
884 help="Compare nTSecurityDescriptor attibutes only"),
885 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
886 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
887 Option("--view", dest="view", default="section", choices=["section", "collision"],
888 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
889 Option("--base", dest="base", default="",
890 help="Pass search base that will build DN list for the first DC."),
891 Option("--base2", dest="base2", default="",
892 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
893 Option("--scope", dest="scope", default="SUB", choices=["SUB", "ONE", "BASE"],
894 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
895 Option("--filter", dest="filter", default="",
896 help="List of comma separated attributes to ignore in the comparision"),
897 Option("--skip-missing-dn", dest="skip_missing_dn", action="store_true", default=False,
898 help="Skip report and failure due to missing DNs in one server or another"),
901 def run(self, URL1, URL2,
902 context1=None, context2=None, context3=None, context4=None, context5=None,
903 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
904 view="section", base="", base2="", scope="SUB", filter="",
905 credopts=None, sambaopts=None, versionopts=None, skip_missing_dn=False):
907 lp = sambaopts.get_loadparm()
909 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
912 creds = credopts.get_credentials(lp, fallback_machine=True)
915 creds2 = credopts.get_credentials2(lp, guess=False)
916 if creds2.is_anonymous():
919 creds2.set_domain("")
920 creds2.set_workstation("")
921 if using_ldap and not creds.authentication_requested():
922 raise CommandError("You must supply at least one username/password pair")
924 # make a list of contexts to compare in
928 # If search bases are specified context is defaulted to
929 # DOMAIN so the given search bases can be verified.
930 contexts = ["DOMAIN"]
932 # if no argument given, we compare all contexts
933 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
935 for c in [context1, context2, context3, context4, context5]:
938 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
939 raise CommandError("Incorrect argument: %s" % c)
940 contexts.append(c.upper())
942 if verbose and quiet:
943 raise CommandError("You cannot set --verbose and --quiet together")
944 if (not base and base2) or (base and not base2):
945 raise CommandError("You need to specify both --base and --base2 at the same time")
947 con1 = LDAPBase(URL1, creds, lp,
948 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
949 verbose=verbose, view=view, base=base, scope=scope,
950 outf=self.outf, errf=self.errf)
951 assert len(con1.base_dn) > 0
953 con2 = LDAPBase(URL2, creds2, lp,
954 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
955 verbose=verbose, view=view, base=base2, scope=scope,
956 outf=self.outf, errf=self.errf)
957 assert len(con2.base_dn) > 0
959 filter_list = filter.split(",")
962 for context in contexts:
964 self.outf.write("\n* Comparing [%s] context...\n" % context)
966 b1 = LDAPBundle(con1, context=context, filter_list=filter_list,
967 outf=self.outf, errf=self.errf)
968 b2 = LDAPBundle(con2, context=context, filter_list=filter_list,
969 outf=self.outf, errf=self.errf)
973 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
977 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
979 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
980 b2.summary["df_value_attrs"] = []
981 self.outf.write("\nSUMMARY\n")
982 self.outf.write("---------\n")
985 # mark exit status as FAILURE if a least one comparison failed
988 raise CommandError("Compare failed: %d" % status)