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 = set([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.df_value_attrs = []
577 self_attrs = set([attr.upper() for attr in self.attributes])
578 other_attrs = set([attr.upper() for attr in other.attributes])
580 self_unique_attrs = self_attrs - other_attrs - other.ignore_attributes
581 if self_unique_attrs:
582 res += 4 * " " + "Attributes found only in %s:" % self.con.host
583 for x in self_unique_attrs:
584 res += 8 * " " + x + "\n"
586 other_unique_attrs = other_attrs - self_attrs - self.ignore_attributes
587 if other_unique_attrs:
588 res += 4 * " " + "Attributes found only in %s:" % other.con.host
589 for x in other_unique_attrs:
590 res += 8 * " " + x + "\n"
592 missing_attrs = self_unique_attrs & other_unique_attrs
593 title = 4 * " " + "Difference in attribute values:"
594 for x in self.attributes.keys():
595 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
597 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
598 self.attributes[x] = sorted(self.attributes[x])
599 other.attributes[x] = sorted(other.attributes[x])
600 if self.attributes[x] != other.attributes[x]:
605 # First check if the difference can be fixed but shunting the first part
606 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
607 if x.upper() in self.other_attributes:
608 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
609 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
612 # Attribute values that are list that contain DN based values that may differ
613 elif x.upper() in self.dn_attributes:
617 m = self.attributes[x]
618 n = other.attributes[x]
619 p = [self.fix_dn(j) for j in m]
620 q = [other.fix_dn(j) for j in n]
623 # Attributes that contain the Domain name in them
624 if x.upper() in self.domain_attributes:
628 m = self.attributes[x]
629 n = other.attributes[x]
630 p = [self.fix_domain_name(j) for j in m]
631 q = [other.fix_domain_name(j) for j in n]
635 if x.upper() in self.servername_attributes:
636 # Attributes with SERVER_NAME
640 m = self.attributes[x]
641 n = other.attributes[x]
642 p = [self.fix_server_name(j) for j in m]
643 q = [other.fix_server_name(j) for j in n]
647 if x.upper() in self.netbios_attributes:
648 # Attributes with NETBIOS Domain name
652 m = self.attributes[x]
653 n = other.attributes[x]
654 p = [self.fix_domain_netbios(j) for j in m]
655 q = [other.fix_domain_netbios(j) for j in n]
663 res += 8 * " " + x + " => \n%s\n%s" % (p, q) + "\n"
665 res += 8 * " " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
666 self.df_value_attrs.append(x)
669 assert self_unique_attrs != other_unique_attrs
670 self.summary["unique_attrs"] += list(self_unique_attrs)
671 self.summary["df_value_attrs"] += self.df_value_attrs
672 other.summary["unique_attrs"] += list(other_unique_attrs)
673 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
675 self.screen_output = res
676 other.screen_output = res
681 class LDAPBundle(object):
683 def __init__(self, connection, context, dn_list=None, filter_list=None,
684 outf=sys.stdout, errf=sys.stderr):
687 self.con = connection
688 self.two_domains = self.con.two_domains
689 self.quiet = self.con.quiet
690 self.verbose = self.con.verbose
691 self.search_base = self.con.search_base
692 self.search_scope = self.con.search_scope
693 self.skip_missing_dn = self.con.skip_missing_dn
695 self.summary["unique_attrs"] = []
696 self.summary["df_value_attrs"] = []
697 self.summary["known_ignored_dn"] = []
698 self.summary["abnormal_ignored_dn"] = []
699 self.filter_list = filter_list
701 self.dn_list = dn_list
702 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
703 self.context = context.upper()
704 self.dn_list = self.get_dn_list(context)
706 raise Exception("Unknown initialization data for LDAPBundle().")
708 while counter < len(self.dn_list) and self.two_domains:
709 # Use alias reference
710 tmp = self.dn_list[counter]
711 tmp = tmp[:len(tmp) - len(self.con.base_dn)] + "${DOMAIN_DN}"
712 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
713 if len(self.con.server_names) == 1:
714 for x in self.con.server_names:
715 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
716 self.dn_list[counter] = tmp
718 self.dn_list = list(set(self.dn_list))
719 self.dn_list = sorted(self.dn_list)
720 self.size = len(self.dn_list)
724 Log on the screen if there is no --quiet option set
727 self.outf.write(msg + "\n")
729 def update_size(self):
730 self.size = len(self.dn_list)
731 self.dn_list = sorted(self.dn_list)
733 def diff(self, other):
735 if self.size != other.size:
736 self.log("\n* DN lists have different size: %s != %s" % (self.size, other.size))
737 if not self.skip_missing_dn:
740 self_dns = set([q.upper() for q in self.dn_list])
741 other_dns = set([q.upper() for q in other.dn_list])
744 # This is the case where we want to explicitly compare two objects with different DNs.
745 # It does not matter if they are in the same DC, in two DC in one domain or in two
747 if self.search_scope != SCOPE_BASE and not self.skip_missing_dn:
749 self_only = self_dns - other_dns # missing in other
752 self.log("\n* DNs found only in %s:" % self.con.host)
754 self.log(4 * " " + x)
756 other_only = other_dns - self_dns # missing in self
759 self.log("\n* DNs found only in %s:" % other.con.host)
761 self.log(4 * " " + x)
763 common_dns = self_dns & other_dns
764 self.log("\n* Objects to be compared: %d" % len(common_dns))
766 for dn in common_dns:
769 object1 = LDAPObject(connection=self.con,
771 summary=self.summary,
772 filter_list=self.filter_list,
773 outf=self.outf, errf=self.errf)
774 except LdbError as e:
775 self.log("LdbError for dn %s: %s" % (dn, e))
779 object2 = LDAPObject(connection=other.con,
781 summary=other.summary,
782 filter_list=self.filter_list,
783 outf=self.outf, errf=self.errf)
784 except LdbError as e:
785 self.log("LdbError for dn %s: %s" % (dn, e))
788 if object1 == object2:
790 self.log("\nComparing:")
791 self.log("'%s' [%s]" % (object1.dn, object1.con.host))
792 self.log("'%s' [%s]" % (object2.dn, object2.con.host))
793 self.log(4 * " " + "OK")
795 self.log("\nComparing:")
796 self.log("'%s' [%s]" % (object1.dn, object1.con.host))
797 self.log("'%s' [%s]" % (object2.dn, object2.con.host))
798 self.log(object1.screen_output)
799 self.log(4 * " " + "FAILED")
801 self.summary = object1.summary
802 other.summary = object2.summary
806 def get_dn_list(self, context):
807 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
808 Parse all DNs and filter those that are 'strange' or abnormal.
810 if context.upper() == "DOMAIN":
811 search_base = self.con.base_dn
812 elif context.upper() == "CONFIGURATION":
813 search_base = self.con.config_dn
814 elif context.upper() == "SCHEMA":
815 search_base = self.con.schema_dn
816 elif context.upper() == "DNSDOMAIN":
817 search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
818 elif context.upper() == "DNSFOREST":
819 search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
822 if not self.search_base:
823 self.search_base = search_base
824 self.search_scope = self.search_scope.upper()
825 if self.search_scope == "SUB":
826 self.search_scope = SCOPE_SUBTREE
827 elif self.search_scope == "BASE":
828 self.search_scope = SCOPE_BASE
829 elif self.search_scope == "ONE":
830 self.search_scope = SCOPE_ONELEVEL
832 raise ValueError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
834 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
835 except LdbError as e3:
836 (enum, estr) = e3.args
837 self.outf.write("Failed search of base=%s\n" % self.search_base)
840 dn_list.append(x["dn"].get_linearized())
843 def print_summary(self):
844 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
845 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
847 if self.summary["unique_attrs"]:
848 self.log("\nAttributes found only in %s:" % self.con.host)
849 self.log("".join([str("\n" + 4 * " " + x) for x in self.summary["unique_attrs"]]))
851 if self.summary["df_value_attrs"]:
852 self.log("\nAttributes with different values:")
853 self.log("".join([str("\n" + 4 * " " + x) for x in self.summary["df_value_attrs"]]))
854 self.summary["df_value_attrs"] = []
857 class cmd_ldapcmp(Command):
858 """Compare two ldap databases."""
859 synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
861 takes_optiongroups = {
862 "sambaopts": options.SambaOptions,
863 "versionopts": options.VersionOptions,
864 "credopts": options.CredentialsOptionsDouble,
867 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?", "context4?", "context5?"]
870 Option("-w", "--two", dest="two", action="store_true", default=False,
871 help="Hosts are in two different domains"),
872 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
873 help="Do not print anything but relay on just exit code"),
874 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
875 help="Print all DN pairs that have been compared"),
876 Option("--sd", dest="descriptor", action="store_true", default=False,
877 help="Compare nTSecurityDescriptor attibutes only"),
878 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
879 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
880 Option("--view", dest="view", default="section", choices=["section", "collision"],
881 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
882 Option("--base", dest="base", default="",
883 help="Pass search base that will build DN list for the first DC."),
884 Option("--base2", dest="base2", default="",
885 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
886 Option("--scope", dest="scope", default="SUB", choices=["SUB", "ONE", "BASE"],
887 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
888 Option("--filter", dest="filter", default="",
889 help="List of comma separated attributes to ignore in the comparision"),
890 Option("--skip-missing-dn", dest="skip_missing_dn", action="store_true", default=False,
891 help="Skip report and failure due to missing DNs in one server or another"),
894 def run(self, URL1, URL2,
895 context1=None, context2=None, context3=None, context4=None, context5=None,
896 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
897 view="section", base="", base2="", scope="SUB", filter="",
898 credopts=None, sambaopts=None, versionopts=None, skip_missing_dn=False):
900 lp = sambaopts.get_loadparm()
902 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
905 creds = credopts.get_credentials(lp, fallback_machine=True)
908 creds2 = credopts.get_credentials2(lp, guess=False)
909 if creds2.is_anonymous():
912 creds2.set_domain("")
913 creds2.set_workstation("")
914 if using_ldap and not creds.authentication_requested():
915 raise CommandError("You must supply at least one username/password pair")
917 # make a list of contexts to compare in
921 # If search bases are specified context is defaulted to
922 # DOMAIN so the given search bases can be verified.
923 contexts = ["DOMAIN"]
925 # if no argument given, we compare all contexts
926 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
928 for c in [context1, context2, context3, context4, context5]:
931 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
932 raise CommandError("Incorrect argument: %s" % c)
933 contexts.append(c.upper())
935 if verbose and quiet:
936 raise CommandError("You cannot set --verbose and --quiet together")
937 if (not base and base2) or (base and not base2):
938 raise CommandError("You need to specify both --base and --base2 at the same time")
940 con1 = LDAPBase(URL1, creds, lp,
941 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
942 verbose=verbose, view=view, base=base, scope=scope,
943 outf=self.outf, errf=self.errf, skip_missing_dn=skip_missing_dn)
944 assert len(con1.base_dn) > 0
946 con2 = LDAPBase(URL2, creds2, lp,
947 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
948 verbose=verbose, view=view, base=base2, scope=scope,
949 outf=self.outf, errf=self.errf, skip_missing_dn=skip_missing_dn)
950 assert len(con2.base_dn) > 0
952 filter_list = filter.split(",")
955 for context in contexts:
957 self.outf.write("\n* Comparing [%s] context...\n" % context)
959 b1 = LDAPBundle(con1, context=context, filter_list=filter_list,
960 outf=self.outf, errf=self.errf)
961 b2 = LDAPBundle(con2, context=context, filter_list=filter_list,
962 outf=self.outf, errf=self.errf)
966 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
970 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
972 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
973 b2.summary["df_value_attrs"] = []
974 self.outf.write("\nSUMMARY\n")
975 self.outf.write("---------\n")
978 # mark exit status as FAILURE if a least one comparison failed
981 raise CommandError("Compare failed: %d" % status)