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 (
43 class LDAPBase(object):
45 def __init__(self, host, creds, lp,
46 two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
47 view="section", base="", scope="SUB",
48 outf=sys.stdout, errf=sys.stderr, skip_missing_dn=True):
52 if os.path.isfile(host):
53 samdb_url = "tdb://%s" % host
55 samdb_url = "ldap://%s" % host
56 # use 'paged_search' module when connecting remotely
57 if samdb_url.lower().startswith("ldap://"):
58 ldb_options = ["modules:paged_searches"]
61 self.ldb = Ldb(url=samdb_url,
65 self.search_base = base
66 self.search_scope = scope
67 self.two_domains = two
69 self.descriptor = descriptor
70 self.sort_aces = sort_aces
72 self.verbose = verbose
74 self.skip_missing_dn = skip_missing_dn
75 self.base_dn = str(self.ldb.get_default_basedn())
76 self.root_dn = str(self.ldb.get_root_basedn())
77 self.config_dn = str(self.ldb.get_config_basedn())
78 self.schema_dn = str(self.ldb.get_schema_basedn())
79 self.domain_netbios = self.find_netbios()
80 self.server_names = self.find_servers()
81 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
82 self.domain_sid = self.find_domain_sid()
85 # Log some domain controller specific place-holers that are being used
86 # when compare content of two DCs. Uncomment for DEBUG purposes.
87 if self.two_domains and not self.quiet:
88 self.outf.write("\n* Place-holders for %s:\n" % self.host)
89 self.outf.write(4*" " + "${DOMAIN_DN} => %s\n" %
91 self.outf.write(4*" " + "${DOMAIN_NETBIOS} => %s\n" %
93 self.outf.write(4*" " + "${SERVER_NAME} => %s\n" %
95 self.outf.write(4*" " + "${DOMAIN_NAME} => %s\n" %
98 def find_domain_sid(self):
99 res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
100 return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
102 def find_servers(self):
105 res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn,
106 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
110 srv.append(x["cn"][0])
113 def find_netbios(self):
114 res = self.ldb.search(base="CN=Partitions,%s" % self.config_dn,
115 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
118 if "nETBIOSName" in x.keys():
119 return x["nETBIOSName"][0]
121 def object_exists(self, object_dn):
124 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
125 except LdbError as e2:
126 (enum, estr) = e2.args
127 if enum == ERR_NO_SUCH_OBJECT:
132 def delete_force(self, object_dn):
134 self.ldb.delete(object_dn)
135 except Ldb.LdbError as e:
136 assert "No such object" in str(e)
138 def get_attribute_name(self, key):
139 """ Returns the real attribute name
140 It resolved ranged results e.g. member;range=0-1499
143 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
151 def get_attribute_values(self, object_dn, key, vals):
152 """ Returns list with all attribute values
153 It resolved ranged results e.g. member;range=0-1499
156 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
160 # no range, just return the values
166 # get additional values in a loop
167 # until we get a response with '*' at the end
170 n = "%s;range=%d-*" % (attr, hi + 1)
171 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
179 for key in res.keys():
185 if m.group(1) != attr:
189 fvals = list(res[key])
196 if fm.group(3) == "*":
197 # if we got "*" we're done
200 assert int(fm.group(2)) == hi + 1
201 hi = int(fm.group(3))
205 def get_attributes(self, object_dn):
206 """ Returns dict with all default visible attributes
208 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
211 # 'Dn' element is not iterable and we have it as 'distinguishedName'
213 for key in res.keys():
214 vals = list(res[key])
216 name = self.get_attribute_name(key)
217 res[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])] = item["sAMAccountName"][0]
265 class Descriptor(object):
266 def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr):
269 self.con = connection
271 self.sddl = self.con.get_descriptor_sddl(self.dn)
272 self.dacl_list = self.extract_dacl()
273 if self.con.sort_aces:
274 self.dacl_list.sort()
276 def extract_dacl(self):
277 """ Extracts the DACL as a list of ACE string (with the brakets).
280 if "S:" in self.sddl:
281 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
283 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
284 except AttributeError:
286 return re.findall("(\(.*?\))", res)
288 def fix_sid(self, ace):
290 sids = re.findall("S-[-0-9]+", res)
291 # If there are not SIDs to replace return the same ACE
296 name = self.con.sid_map[sid]
297 res = res.replace(sid, name)
299 # Do not bother if the SID is not found in baseDN
303 def diff_1(self, other):
305 if len(self.dacl_list) != len(other.dacl_list):
306 res += 4*" " + "Difference in ACE count:\n"
307 res += 8*" " + "=> %s\n" % len(self.dacl_list)
308 res += 8*" " + "=> %s\n" % len(other.dacl_list)
316 self_ace = "%s" % self.dacl_list[i]
321 other_ace = "%s" % other.dacl_list[i]
324 if len(self_ace) + len(other_ace) == 0:
326 self_ace_fixed = "%s" % self.fix_sid(self_ace)
327 other_ace_fixed = "%s" % other.fix_sid(other_ace)
328 if self_ace_fixed != other_ace_fixed:
329 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
332 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
336 def diff_2(self, other):
338 if len(self.dacl_list) != len(other.dacl_list):
339 res += 4*" " + "Difference in ACE count:\n"
340 res += 8*" " + "=> %s\n" % len(self.dacl_list)
341 res += 8*" " + "=> %s\n" % len(other.dacl_list)
346 self_dacl_list_fixed = []
347 other_dacl_list_fixed = []
348 [self_dacl_list_fixed.append( self.fix_sid(ace) ) for ace in self.dacl_list]
349 [other_dacl_list_fixed.append( other.fix_sid(ace) ) for ace in other.dacl_list]
350 for ace in self_dacl_list_fixed:
352 other_dacl_list_fixed.index(ace)
354 self_aces.append(ace)
356 common_aces.append(ace)
357 self_aces = sorted(self_aces)
358 if len(self_aces) > 0:
359 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
360 for ace in self_aces:
361 res += 8*" " + ace + "\n"
363 for ace in other_dacl_list_fixed:
365 self_dacl_list_fixed.index(ace)
367 other_aces.append(ace)
369 common_aces.append(ace)
370 other_aces = sorted(other_aces)
371 if len(other_aces) > 0:
372 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
373 for ace in other_aces:
374 res += 8*" " + ace + "\n"
376 common_aces = sorted(list(set(common_aces)))
378 res += 4*" " + "ACEs found in both:\n"
379 for ace in common_aces:
380 res += 8*" " + ace + "\n"
381 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 not x 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 not x 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 LDAPBundel(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 LDAPBundel().")
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 __eq__(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 # This is the case where we want to explicitly compare two objects with different DNs.
748 # It does not matter if they are in the same DC, in two DC in one domain or in two
750 if self.search_scope != SCOPE_BASE:
751 title= "\n* DNs found only in %s:" % self.con.host
752 for x in self.dn_list:
753 if not x.upper() in [q.upper() for q in other.dn_list]:
754 if title and not self.skip_missing_dn:
758 self.log( 4*" " + x )
759 self.dn_list[self.dn_list.index(x)] = ""
760 self.dn_list = [x for x in self.dn_list if x]
762 title= "\n* DNs found only in %s:" % other.con.host
763 for x in other.dn_list:
764 if not x.upper() in [q.upper() for q in self.dn_list]:
765 if title and not self.skip_missing_dn:
769 self.log( 4*" " + x )
770 other.dn_list[other.dn_list.index(x)] = ""
771 other.dn_list = [x for x in other.dn_list if x]
775 assert self.size == other.size
776 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
777 self.log( "\n* Objects to be compared: %s" % self.size )
780 while index < self.size:
783 object1 = LDAPObject(connection=self.con,
784 dn=self.dn_list[index],
785 summary=self.summary,
786 filter_list=self.filter_list,
787 outf=self.outf, errf=self.errf)
788 except LdbError as e:
789 (enum, estr) = e.args
790 if enum == ERR_NO_SUCH_OBJECT:
791 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
795 object2 = LDAPObject(connection=other.con,
796 dn=other.dn_list[index],
797 summary=other.summary,
798 filter_list=self.filter_list,
799 outf=self.outf, errf=self.errf)
800 except LdbError as e1:
801 (enum, estr) = e1.args
802 if enum == ERR_NO_SUCH_OBJECT:
803 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
809 if object1 == object2:
811 self.log( "\nComparing:" )
812 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
813 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
814 self.log( 4*" " + "OK" )
816 self.log( "\nComparing:" )
817 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
818 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
819 self.log( object1.screen_output )
820 self.log( 4*" " + "FAILED" )
822 self.summary = object1.summary
823 other.summary = object2.summary
828 def get_dn_list(self, context):
829 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
830 Parse all DNs and filter those that are 'strange' or abnormal.
832 if context.upper() == "DOMAIN":
833 search_base = self.con.base_dn
834 elif context.upper() == "CONFIGURATION":
835 search_base = self.con.config_dn
836 elif context.upper() == "SCHEMA":
837 search_base = self.con.schema_dn
838 elif context.upper() == "DNSDOMAIN":
839 search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
840 elif context.upper() == "DNSFOREST":
841 search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
844 if not self.search_base:
845 self.search_base = search_base
846 self.search_scope = self.search_scope.upper()
847 if self.search_scope == "SUB":
848 self.search_scope = SCOPE_SUBTREE
849 elif self.search_scope == "BASE":
850 self.search_scope = SCOPE_BASE
851 elif self.search_scope == "ONE":
852 self.search_scope = SCOPE_ONELEVEL
854 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
856 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
857 except LdbError as e3:
858 (enum, estr) = e3.args
859 self.outf.write("Failed search of base=%s\n" % self.search_base)
862 dn_list.append(x["dn"].get_linearized())
868 def print_summary(self):
869 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
870 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
872 if self.summary["unique_attrs"]:
873 self.log( "\nAttributes found only in %s:" % self.con.host )
874 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
876 if self.summary["df_value_attrs"]:
877 self.log( "\nAttributes with different values:" )
878 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
879 self.summary["df_value_attrs"] = []
882 class cmd_ldapcmp(Command):
883 """Compare two ldap databases."""
884 synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
886 takes_optiongroups = {
887 "sambaopts": options.SambaOptions,
888 "versionopts": options.VersionOptions,
889 "credopts": options.CredentialsOptionsDouble,
892 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?", "context4?", "context5?"]
895 Option("-w", "--two", dest="two", action="store_true", default=False,
896 help="Hosts are in two different domains"),
897 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
898 help="Do not print anything but relay on just exit code"),
899 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
900 help="Print all DN pairs that have been compared"),
901 Option("--sd", dest="descriptor", action="store_true", default=False,
902 help="Compare nTSecurityDescriptor attibutes only"),
903 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
904 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
905 Option("--view", dest="view", default="section",
906 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
907 Option("--base", dest="base", default="",
908 help="Pass search base that will build DN list for the first DC."),
909 Option("--base2", dest="base2", default="",
910 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
911 Option("--scope", dest="scope", default="SUB",
912 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
913 Option("--filter", dest="filter", default="",
914 help="List of comma separated attributes to ignore in the comparision"),
915 Option("--skip-missing-dn", dest="skip_missing_dn", action="store_true", default=False,
916 help="Skip report and failure due to missing DNs in one server or another"),
919 def run(self, URL1, URL2,
920 context1=None, context2=None, context3=None, context4=None, context5=None,
921 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
922 view="section", base="", base2="", scope="SUB", filter="",
923 credopts=None, sambaopts=None, versionopts=None, skip_missing_dn=False):
925 lp = sambaopts.get_loadparm()
927 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
930 creds = credopts.get_credentials(lp, fallback_machine=True)
933 creds2 = credopts.get_credentials2(lp, guess=False)
934 if creds2.is_anonymous():
937 creds2.set_domain("")
938 creds2.set_workstation("")
939 if using_ldap and not creds.authentication_requested():
940 raise CommandError("You must supply at least one username/password pair")
942 # make a list of contexts to compare in
946 # If search bases are specified context is defaulted to
947 # DOMAIN so the given search bases can be verified.
948 contexts = ["DOMAIN"]
950 # if no argument given, we compare all contexts
951 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
953 for c in [context1, context2, context3, context4, context5]:
956 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
957 raise CommandError("Incorrect argument: %s" % c)
958 contexts.append(c.upper())
960 if verbose and quiet:
961 raise CommandError("You cannot set --verbose and --quiet together")
962 if (not base and base2) or (base and not base2):
963 raise CommandError("You need to specify both --base and --base2 at the same time")
964 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
965 raise CommandError("Invalid --view value. Choose from: section or collision")
966 if not scope.upper() in ["SUB", "ONE", "BASE"]:
967 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
969 con1 = LDAPBase(URL1, creds, lp,
970 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
971 verbose=verbose,view=view, base=base, scope=scope,
972 outf=self.outf, errf=self.errf)
973 assert len(con1.base_dn) > 0
975 con2 = LDAPBase(URL2, creds2, lp,
976 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
977 verbose=verbose, view=view, base=base2, scope=scope,
978 outf=self.outf, errf=self.errf)
979 assert len(con2.base_dn) > 0
981 filter_list = filter.split(",")
984 for context in contexts:
986 self.outf.write("\n* Comparing [%s] context...\n" % context)
988 b1 = LDAPBundel(con1, context=context, filter_list=filter_list,
989 outf=self.outf, errf=self.errf)
990 b2 = LDAPBundel(con2, context=context, filter_list=filter_list,
991 outf=self.outf, errf=self.errf)
995 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
999 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
1001 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
1002 b2.summary["df_value_attrs"] = []
1003 self.outf.write("\nSUMMARY\n")
1004 self.outf.write("---------\n")
1007 # mark exit status as FAILURE if a least one comparison failed
1010 raise CommandError("Compare failed: %d" % status)