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 (
44 class LDAPBase(object):
46 def __init__(self, host, creds, lp,
47 two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
48 view="section", base="", scope="SUB",
49 outf=sys.stdout, errf=sys.stderr, skip_missing_dn=True):
53 if os.path.isfile(host):
54 samdb_url = "tdb://%s" % host
56 samdb_url = "ldap://%s" % host
57 # use 'paged_search' module when connecting remotely
58 if samdb_url.lower().startswith("ldap://"):
59 ldb_options = ["modules:paged_searches"]
62 self.ldb = Ldb(url=samdb_url,
66 self.search_base = base
67 self.search_scope = scope
68 self.two_domains = two
70 self.descriptor = descriptor
71 self.sort_aces = sort_aces
73 self.verbose = verbose
75 self.skip_missing_dn = skip_missing_dn
76 self.base_dn = str(self.ldb.get_default_basedn())
77 self.root_dn = str(self.ldb.get_root_basedn())
78 self.config_dn = str(self.ldb.get_config_basedn())
79 self.schema_dn = str(self.ldb.get_schema_basedn())
80 self.domain_netbios = self.find_netbios()
81 self.server_names = self.find_servers()
82 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
83 self.domain_sid = self.find_domain_sid()
86 # Log some domain controller specific place-holers that are being used
87 # when compare content of two DCs. Uncomment for DEBUG purposes.
88 if self.two_domains and not self.quiet:
89 self.outf.write("\n* Place-holders for %s:\n" % self.host)
90 self.outf.write(4 * " " + "${DOMAIN_DN} => %s\n" %
92 self.outf.write(4 * " " + "${DOMAIN_NETBIOS} => %s\n" %
94 self.outf.write(4 * " " + "${SERVER_NAME} => %s\n" %
96 self.outf.write(4 * " " + "${DOMAIN_NAME} => %s\n" %
99 def find_domain_sid(self):
100 res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
101 return ndr_unpack(security.dom_sid, res[0]["objectSid"][0])
103 def find_servers(self):
106 res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn,
107 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
111 srv.append(x["cn"][0])
114 def find_netbios(self):
115 res = self.ldb.search(base="CN=Partitions,%s" % self.config_dn,
116 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
119 if "nETBIOSName" in x.keys():
120 return x["nETBIOSName"][0]
122 def object_exists(self, object_dn):
125 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
126 except LdbError as e2:
127 (enum, estr) = e2.args
128 if enum == ERR_NO_SUCH_OBJECT:
133 def delete_force(self, object_dn):
135 self.ldb.delete(object_dn)
136 except Ldb.LdbError as e:
137 assert "No such object" in str(e)
139 def get_attribute_name(self, key):
140 """ Returns the real attribute name
141 It resolved ranged results e.g. member;range=0-1499
144 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
152 def get_attribute_values(self, object_dn, key, vals):
153 """ Returns list with all attribute values
154 It resolved ranged results e.g. member;range=0-1499
157 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
161 # no range, just return the values
167 # get additional values in a loop
168 # until we get a response with '*' at the end
171 n = "%s;range=%d-*" % (attr, hi + 1)
172 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
180 for key in res.keys():
186 if m.group(1) != attr:
190 fvals = list(res[key])
197 if fm.group(3) == "*":
198 # if we got "*" we're done
201 assert int(fm.group(2)) == hi + 1
202 hi = int(fm.group(3))
206 def get_attributes(self, object_dn):
207 """ Returns dict with all default visible attributes
209 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
212 # 'Dn' element is not iterable and we have it as 'distinguishedName'
214 for key in res.keys():
215 vals = list(res[key])
217 name = self.get_attribute_name(key)
218 res[name] = self.get_attribute_values(object_dn, key, vals)
222 def get_descriptor_sddl(self, object_dn):
223 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
224 desc = res[0]["nTSecurityDescriptor"][0]
225 desc = ndr_unpack(security.descriptor, desc)
226 return desc.as_sddl(self.domain_sid)
228 def guid_as_string(self, guid_blob):
229 """ Translate binary representation of schemaIDGUID to standard string representation.
230 @gid_blob: binary schemaIDGUID
232 blob = "%s" % guid_blob
233 stops = [4, 2, 2, 2, 6]
237 while x < len(stops):
241 c = hex(ord(blob[index])).replace("0x", "")
242 c = [None, "0" + c, c][len(c)]
243 if 2 * index < len(blob):
251 assert index == len(blob)
252 return res.strip().replace(" ", "-")
254 def get_sid_map(self):
255 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
258 res = self.ldb.search(base=self.base_dn,
259 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
262 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
267 class Descriptor(object):
268 def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr):
271 self.con = connection
273 self.sddl = self.con.get_descriptor_sddl(self.dn)
274 self.dacl_list = self.extract_dacl()
275 if self.con.sort_aces:
276 self.dacl_list.sort()
278 def extract_dacl(self):
279 """ Extracts the DACL as a list of ACE string (with the brakets).
282 if "S:" in self.sddl:
283 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
285 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
286 except AttributeError:
288 return re.findall("(\(.*?\))", res)
290 def fix_sid(self, ace):
292 sids = re.findall("S-[-0-9]+", res)
293 # If there are not SIDs to replace return the same ACE
298 name = self.con.sid_map[sid]
299 res = res.replace(sid, name)
301 # Do not bother if the SID is not found in baseDN
305 def diff_1(self, other):
307 if len(self.dacl_list) != len(other.dacl_list):
308 res += 4 * " " + "Difference in ACE count:\n"
309 res += 8 * " " + "=> %s\n" % len(self.dacl_list)
310 res += 8 * " " + "=> %s\n" % len(other.dacl_list)
318 self_ace = "%s" % self.dacl_list[i]
323 other_ace = "%s" % other.dacl_list[i]
326 if len(self_ace) + len(other_ace) == 0:
328 self_ace_fixed = "%s" % self.fix_sid(self_ace)
329 other_ace_fixed = "%s" % other.fix_sid(other_ace)
330 if self_ace_fixed != other_ace_fixed:
331 res += "%60s * %s\n" % (self_ace_fixed, other_ace_fixed)
334 res += "%60s | %s\n" % (self_ace_fixed, other_ace_fixed)
338 def diff_2(self, other):
340 if len(self.dacl_list) != len(other.dacl_list):
341 res += 4 * " " + "Difference in ACE count:\n"
342 res += 8 * " " + "=> %s\n" % len(self.dacl_list)
343 res += 8 * " " + "=> %s\n" % len(other.dacl_list)
348 self_dacl_list_fixed = []
349 other_dacl_list_fixed = []
350 [self_dacl_list_fixed.append(self.fix_sid(ace)) for ace in self.dacl_list]
351 [other_dacl_list_fixed.append(other.fix_sid(ace)) for ace in other.dacl_list]
352 for ace in self_dacl_list_fixed:
354 other_dacl_list_fixed.index(ace)
356 self_aces.append(ace)
358 common_aces.append(ace)
359 self_aces = sorted(self_aces)
360 if len(self_aces) > 0:
361 res += 4 * " " + "ACEs found only in %s:\n" % self.con.host
362 for ace in self_aces:
363 res += 8 * " " + ace + "\n"
365 for ace in other_dacl_list_fixed:
367 self_dacl_list_fixed.index(ace)
369 other_aces.append(ace)
371 common_aces.append(ace)
372 other_aces = sorted(other_aces)
373 if len(other_aces) > 0:
374 res += 4 * " " + "ACEs found only in %s:\n" % other.con.host
375 for ace in other_aces:
376 res += 8 * " " + ace + "\n"
378 common_aces = sorted(list(set(common_aces)))
380 res += 4 * " " + "ACEs found in both:\n"
381 for ace in common_aces:
382 res += 8 * " " + ace + "\n"
383 return (self_aces == [] and other_aces == [], res)
386 class LDAPObject(object):
387 def __init__(self, connection, dn, summary, filter_list,
388 outf=sys.stdout, errf=sys.stderr):
391 self.con = connection
392 self.two_domains = self.con.two_domains
393 self.quiet = self.con.quiet
394 self.verbose = self.con.verbose
395 self.summary = summary
396 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
397 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
398 for x in self.con.server_names:
399 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
400 self.attributes = self.con.get_attributes(self.dn)
401 # One domain - two domain controllers
403 # Some attributes are defined as FLAG_ATTR_NOT_REPLICATED
405 # The following list was generated by
406 # egrep '^systemFlags: |^ldapDisplayName: |^linkID: ' \
407 # source4/setup/ad-schema/MS-AD_Schema_2K8_R2_Attributes.txt | \
408 # grep -B1 FLAG_ATTR_NOT_REPLICATED | \
409 # grep ldapDisplayName | \
411 self.non_replicated_attributes = [
414 "dSCorePropagationData",
419 "msDS-Cached-Membership",
420 "msDS-Cached-Membership-Time-Stamp",
421 "msDS-EnabledFeatureBL",
422 "msDS-ExecuteScriptPassword",
424 "msDS-ReplicationEpoch",
425 "msDS-RetiredReplNCSignatures",
426 "msDS-USNLastSyncSuccess",
427 # "distinguishedName", # This is implicitly replicated
428 # "objectGUID", # This is implicitly replicated
429 "partialAttributeDeletionList",
430 "partialAttributeSet",
433 "replPropertyMetaData",
434 "replUpToDateVector",
438 "rIDPreviousAllocationPool",
445 "whenChanged", # This is implicitly replicated, but may diverge on updates of non-replicated attributes
447 self.ignore_attributes = self.non_replicated_attributes
448 self.ignore_attributes += ["msExchServer1HighestUSN"]
450 self.ignore_attributes += filter_list
452 self.dn_attributes = []
453 self.domain_attributes = []
454 self.servername_attributes = []
455 self.netbios_attributes = []
456 self.other_attributes = []
457 # Two domains - two domain controllers
460 self.ignore_attributes += [
461 "objectCategory", "objectGUID", "objectSid", "whenCreated",
462 "whenChanged", "pwdLastSet", "uSNCreated", "creationTime",
463 "modifiedCount", "priorSetTime", "rIDManagerReference",
464 "gPLink", "ipsecNFAReference", "fRSPrimaryMember",
465 "fSMORoleOwner", "masteredBy", "ipsecOwnersReference",
466 "wellKnownObjects", "otherWellKnownObjects", "badPwdCount",
467 "ipsecISAKMPReference", "ipsecFilterReference",
468 "msDs-masteredBy", "lastSetTime",
469 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath",
470 "accountExpires", "invocationId", "operatingSystemVersion",
472 # After Exchange preps
473 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
475 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
476 self.dn_attributes = [
477 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
478 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
479 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
480 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
481 # After Exchange preps
482 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
483 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
484 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
485 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",
486 # After 2012 R2 functional preparation
487 "msDS-MembersOfResourcePropertyListBL",
488 "msDS-ValueTypeReference",
489 "msDS-MembersOfResourcePropertyList",
490 "msDS-ValueTypeReferenceBL",
491 "msDS-ClaimTypeAppliesToClass",
493 self.dn_attributes = [x.upper() for x in self.dn_attributes]
495 # Attributes that contain the Domain name e.g. 'samba.org'
496 self.domain_attributes = [
497 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
498 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName", ]
499 self.domain_attributes = [x.upper() for x in self.domain_attributes]
501 # May contain DOMAIN_NETBIOS and SERVER_NAME
502 self.servername_attributes = ["distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
503 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
504 "msDS-IsDomainFor", "interSiteTopologyGenerator", ]
505 self.servername_attributes = [x.upper() for x in self.servername_attributes]
507 self.netbios_attributes = ["servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name", ]
508 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
510 self.other_attributes = ["name", "DC", ]
511 self.other_attributes = [x.upper() for x in self.other_attributes]
513 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
517 Log on the screen if there is no --quiet option set
520 self.outf.write(msg +"\n")
524 if not self.two_domains:
526 if res.upper().endswith(self.con.base_dn.upper()):
527 res = res[:len(res) - len(self.con.base_dn)] + "${DOMAIN_DN}"
530 def fix_domain_name(self, s):
532 if not self.two_domains:
534 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
535 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
538 def fix_domain_netbios(self, s):
540 if not self.two_domains:
542 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
543 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
546 def fix_server_name(self, s):
548 if not self.two_domains or len(self.con.server_names) > 1:
550 for x in self.con.server_names:
551 res = res.upper().replace(x, "${SERVER_NAME}")
554 def __eq__(self, other):
555 if self.con.descriptor:
556 return self.cmp_desc(other)
557 return self.cmp_attrs(other)
559 def cmp_desc(self, other):
560 d1 = Descriptor(self.con, self.dn, outf=self.outf, errf=self.errf)
561 d2 = Descriptor(other.con, other.dn, outf=self.outf, errf=self.errf)
562 if self.con.view == "section":
564 elif self.con.view == "collision":
567 raise Exception("Unknown --view option value.")
569 self.screen_output = res[1][:-1]
570 other.screen_output = res[1][:-1]
574 def cmp_attrs(self, other):
576 self.unique_attrs = []
577 self.df_value_attrs = []
578 other.unique_attrs = []
579 if self.attributes.keys() != other.attributes.keys():
581 title = 4 * " " + "Attributes found only in %s:" % self.con.host
582 for x in self.attributes.keys():
583 if x not in other.attributes.keys() and \
584 not x.upper() in [q.upper() for q in other.ignore_attributes]:
588 res += 8 * " " + x + "\n"
589 self.unique_attrs.append(x)
591 title = 4 * " " + "Attributes found only in %s:" % other.con.host
592 for x in other.attributes.keys():
593 if x not in self.attributes.keys() and \
594 not x.upper() in [q.upper() for q in self.ignore_attributes]:
598 res += 8 * " " + x + "\n"
599 other.unique_attrs.append(x)
601 missing_attrs = [x.upper() for x in self.unique_attrs]
602 missing_attrs += [x.upper() for x in other.unique_attrs]
603 title = 4 * " " + "Difference in attribute values:"
604 for x in self.attributes.keys():
605 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
607 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
608 self.attributes[x] = sorted(self.attributes[x])
609 other.attributes[x] = sorted(other.attributes[x])
610 if self.attributes[x] != other.attributes[x]:
615 # First check if the difference can be fixed but shunting the first part
616 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
617 if x.upper() in self.other_attributes:
618 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
619 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
622 # Attribute values that are list that contain DN based values that may differ
623 elif x.upper() in self.dn_attributes:
627 m = self.attributes[x]
628 n = other.attributes[x]
629 p = [self.fix_dn(j) for j in m]
630 q = [other.fix_dn(j) for j in n]
633 # Attributes that contain the Domain name in them
634 if x.upper() in self.domain_attributes:
638 m = self.attributes[x]
639 n = other.attributes[x]
640 p = [self.fix_domain_name(j) for j in m]
641 q = [other.fix_domain_name(j) for j in n]
645 if x.upper() in self.servername_attributes:
646 # Attributes with SERVER_NAME
650 m = self.attributes[x]
651 n = other.attributes[x]
652 p = [self.fix_server_name(j) for j in m]
653 q = [other.fix_server_name(j) for j in n]
657 if x.upper() in self.netbios_attributes:
658 # Attributes with NETBIOS Domain name
662 m = self.attributes[x]
663 n = other.attributes[x]
664 p = [self.fix_domain_netbios(j) for j in m]
665 q = [other.fix_domain_netbios(j) for j in n]
673 res += 8 * " " + x + " => \n%s\n%s" % (p, q) + "\n"
675 res += 8 * " " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
676 self.df_value_attrs.append(x)
678 if self.unique_attrs + other.unique_attrs != []:
679 assert self.unique_attrs != other.unique_attrs
680 self.summary["unique_attrs"] += self.unique_attrs
681 self.summary["df_value_attrs"] += self.df_value_attrs
682 other.summary["unique_attrs"] += other.unique_attrs
683 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
685 self.screen_output = res[:-1]
686 other.screen_output = res[:-1]
691 class LDAPBundel(object):
693 def __init__(self, connection, context, dn_list=None, filter_list=None,
694 outf=sys.stdout, errf=sys.stderr):
697 self.con = connection
698 self.two_domains = self.con.two_domains
699 self.quiet = self.con.quiet
700 self.verbose = self.con.verbose
701 self.search_base = self.con.search_base
702 self.search_scope = self.con.search_scope
703 self.skip_missing_dn = self.con.skip_missing_dn
705 self.summary["unique_attrs"] = []
706 self.summary["df_value_attrs"] = []
707 self.summary["known_ignored_dn"] = []
708 self.summary["abnormal_ignored_dn"] = []
709 self.filter_list = filter_list
711 self.dn_list = dn_list
712 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
713 self.context = context.upper()
714 self.dn_list = self.get_dn_list(context)
716 raise Exception("Unknown initialization data for LDAPBundel().")
718 while counter < len(self.dn_list) and self.two_domains:
719 # Use alias reference
720 tmp = self.dn_list[counter]
721 tmp = tmp[:len(tmp) - len(self.con.base_dn)] + "${DOMAIN_DN}"
722 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
723 if len(self.con.server_names) == 1:
724 for x in self.con.server_names:
725 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
726 self.dn_list[counter] = tmp
728 self.dn_list = list(set(self.dn_list))
729 self.dn_list = sorted(self.dn_list)
730 self.size = len(self.dn_list)
734 Log on the screen if there is no --quiet option set
737 self.outf.write(msg + "\n")
739 def update_size(self):
740 self.size = len(self.dn_list)
741 self.dn_list = sorted(self.dn_list)
743 def __eq__(self, other):
745 if self.size != other.size:
746 self.log("\n* DN lists have different size: %s != %s" % (self.size, other.size))
747 if not self.skip_missing_dn:
750 # This is the case where we want to explicitly compare two objects with different DNs.
751 # It does not matter if they are in the same DC, in two DC in one domain or in two
753 if self.search_scope != SCOPE_BASE:
754 title = "\n* DNs found only in %s:" % self.con.host
755 for x in self.dn_list:
756 if not x.upper() in [q.upper() for q in other.dn_list]:
757 if title and not self.skip_missing_dn:
761 self.log(4 * " " + x)
762 self.dn_list[self.dn_list.index(x)] = ""
763 self.dn_list = [x for x in self.dn_list if x]
765 title = "\n* DNs found only in %s:" % other.con.host
766 for x in other.dn_list:
767 if not x.upper() in [q.upper() for q in self.dn_list]:
768 if title and not self.skip_missing_dn:
772 self.log(4 * " " + x)
773 other.dn_list[other.dn_list.index(x)] = ""
774 other.dn_list = [x for x in other.dn_list if x]
778 assert self.size == other.size
779 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
780 self.log("\n* Objects to be compared: %s" % self.size)
783 while index < self.size:
786 object1 = LDAPObject(connection=self.con,
787 dn=self.dn_list[index],
788 summary=self.summary,
789 filter_list=self.filter_list,
790 outf=self.outf, errf=self.errf)
791 except LdbError as e:
792 (enum, estr) = e.args
793 if enum == ERR_NO_SUCH_OBJECT:
794 self.log("\n!!! Object not found: %s" % self.dn_list[index])
798 object2 = LDAPObject(connection=other.con,
799 dn=other.dn_list[index],
800 summary=other.summary,
801 filter_list=self.filter_list,
802 outf=self.outf, errf=self.errf)
803 except LdbError as e1:
804 (enum, estr) = e1.args
805 if enum == ERR_NO_SUCH_OBJECT:
806 self.log("\n!!! Object not found: %s" % other.dn_list[index])
812 if object1 == object2:
814 self.log("\nComparing:")
815 self.log("'%s' [%s]" % (object1.dn, object1.con.host))
816 self.log("'%s' [%s]" % (object2.dn, object2.con.host))
817 self.log(4 * " " + "OK")
819 self.log("\nComparing:")
820 self.log("'%s' [%s]" % (object1.dn, object1.con.host))
821 self.log("'%s' [%s]" % (object2.dn, object2.con.host))
822 self.log(object1.screen_output)
823 self.log(4 * " " + "FAILED")
825 self.summary = object1.summary
826 other.summary = object2.summary
831 def get_dn_list(self, context):
832 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
833 Parse all DNs and filter those that are 'strange' or abnormal.
835 if context.upper() == "DOMAIN":
836 search_base = self.con.base_dn
837 elif context.upper() == "CONFIGURATION":
838 search_base = self.con.config_dn
839 elif context.upper() == "SCHEMA":
840 search_base = self.con.schema_dn
841 elif context.upper() == "DNSDOMAIN":
842 search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
843 elif context.upper() == "DNSFOREST":
844 search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
847 if not self.search_base:
848 self.search_base = search_base
849 self.search_scope = self.search_scope.upper()
850 if self.search_scope == "SUB":
851 self.search_scope = SCOPE_SUBTREE
852 elif self.search_scope == "BASE":
853 self.search_scope = SCOPE_BASE
854 elif self.search_scope == "ONE":
855 self.search_scope = SCOPE_ONELEVEL
857 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
859 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
860 except LdbError as e3:
861 (enum, estr) = e3.args
862 self.outf.write("Failed search of base=%s\n" % self.search_base)
865 dn_list.append(x["dn"].get_linearized())
871 def print_summary(self):
872 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
873 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
875 if self.summary["unique_attrs"]:
876 self.log("\nAttributes found only in %s:" % self.con.host)
877 self.log("".join([str("\n" + 4 * " " + x) for x in self.summary["unique_attrs"]]))
879 if self.summary["df_value_attrs"]:
880 self.log("\nAttributes with different values:")
881 self.log("".join([str("\n" + 4 * " " + x) for x in self.summary["df_value_attrs"]]))
882 self.summary["df_value_attrs"] = []
885 class cmd_ldapcmp(Command):
886 """Compare two ldap databases."""
887 synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
889 takes_optiongroups = {
890 "sambaopts": options.SambaOptions,
891 "versionopts": options.VersionOptions,
892 "credopts": options.CredentialsOptionsDouble,
895 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?", "context4?", "context5?"]
898 Option("-w", "--two", dest="two", action="store_true", default=False,
899 help="Hosts are in two different domains"),
900 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
901 help="Do not print anything but relay on just exit code"),
902 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
903 help="Print all DN pairs that have been compared"),
904 Option("--sd", dest="descriptor", action="store_true", default=False,
905 help="Compare nTSecurityDescriptor attibutes only"),
906 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
907 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
908 Option("--view", dest="view", default="section",
909 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
910 Option("--base", dest="base", default="",
911 help="Pass search base that will build DN list for the first DC."),
912 Option("--base2", dest="base2", default="",
913 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
914 Option("--scope", dest="scope", default="SUB",
915 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
916 Option("--filter", dest="filter", default="",
917 help="List of comma separated attributes to ignore in the comparision"),
918 Option("--skip-missing-dn", dest="skip_missing_dn", action="store_true", default=False,
919 help="Skip report and failure due to missing DNs in one server or another"),
922 def run(self, URL1, URL2,
923 context1=None, context2=None, context3=None, context4=None, context5=None,
924 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
925 view="section", base="", base2="", scope="SUB", filter="",
926 credopts=None, sambaopts=None, versionopts=None, skip_missing_dn=False):
928 lp = sambaopts.get_loadparm()
930 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
933 creds = credopts.get_credentials(lp, fallback_machine=True)
936 creds2 = credopts.get_credentials2(lp, guess=False)
937 if creds2.is_anonymous():
940 creds2.set_domain("")
941 creds2.set_workstation("")
942 if using_ldap and not creds.authentication_requested():
943 raise CommandError("You must supply at least one username/password pair")
945 # make a list of contexts to compare in
949 # If search bases are specified context is defaulted to
950 # DOMAIN so the given search bases can be verified.
951 contexts = ["DOMAIN"]
953 # if no argument given, we compare all contexts
954 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
956 for c in [context1, context2, context3, context4, context5]:
959 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
960 raise CommandError("Incorrect argument: %s" % c)
961 contexts.append(c.upper())
963 if verbose and quiet:
964 raise CommandError("You cannot set --verbose and --quiet together")
965 if (not base and base2) or (base and not base2):
966 raise CommandError("You need to specify both --base and --base2 at the same time")
967 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
968 raise CommandError("Invalid --view value. Choose from: section or collision")
969 if not scope.upper() in ["SUB", "ONE", "BASE"]:
970 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
972 con1 = LDAPBase(URL1, creds, lp,
973 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
974 verbose=verbose, view=view, base=base, scope=scope,
975 outf=self.outf, errf=self.errf)
976 assert len(con1.base_dn) > 0
978 con2 = LDAPBase(URL2, creds2, lp,
979 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
980 verbose=verbose, view=view, base=base2, scope=scope,
981 outf=self.outf, errf=self.errf)
982 assert len(con2.base_dn) > 0
984 filter_list = filter.split(",")
987 for context in contexts:
989 self.outf.write("\n* Comparing [%s] context...\n" % context)
991 b1 = LDAPBundel(con1, context=context, filter_list=filter_list,
992 outf=self.outf, errf=self.errf)
993 b2 = LDAPBundel(con2, context=context, filter_list=filter_list,
994 outf=self.outf, errf=self.errf)
998 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
1002 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
1004 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
1005 b2.summary["df_value_attrs"] = []
1006 self.outf.write("\nSUMMARY\n")
1007 self.outf.write("---------\n")
1010 # mark exit status as FAILURE if a least one comparison failed
1013 raise CommandError("Compare failed: %d" % status)