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_pack, 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):
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.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()
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, (enum, estr):
127 if enum == ERR_NO_SUCH_OBJECT:
132 def delete_force(self, object_dn):
134 self.ldb.delete(object_dn)
135 except Ldb.LdbError, 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_guid_map(self):
254 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
257 res = self.ldb.search(base=self.schema_dn,
258 expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
260 self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
262 res = self.ldb.search(base="cn=extended-rights,%s" % self.config_dn,
263 expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
265 self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
267 def get_sid_map(self):
268 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
271 res = self.ldb.search(base=self.base_dn,
272 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
275 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
279 class Descriptor(object):
280 def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr):
283 self.con = connection
285 self.sddl = self.con.get_descriptor_sddl(self.dn)
286 self.dacl_list = self.extract_dacl()
287 if self.con.sort_aces:
288 self.dacl_list.sort()
290 def extract_dacl(self):
291 """ Extracts the DACL as a list of ACE string (with the brakets).
294 if "S:" in self.sddl:
295 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
297 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
298 except AttributeError:
300 return re.findall("(\(.*?\))", res)
302 def fix_guid(self, ace):
304 guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
305 # If there are not GUIDs to replace return the same ACE
310 name = self.con.guid_map[guid.lower()]
311 res = res.replace(guid, name)
313 # Do not bother if the GUID is not found in
314 # cn=Schema or cn=Extended-Rights
318 def fix_sid(self, ace):
320 sids = re.findall("S-[-0-9]+", res)
321 # If there are not SIDs to replace return the same ACE
326 name = self.con.sid_map[sid]
327 res = res.replace(sid, name)
329 # Do not bother if the SID is not found in baseDN
333 def fixit(self, ace):
334 """ Combine all replacement methods in one
337 res = self.fix_guid(res)
338 res = self.fix_sid(res)
341 def diff_1(self, other):
343 if len(self.dacl_list) != len(other.dacl_list):
344 res += 4*" " + "Difference in ACE count:\n"
345 res += 8*" " + "=> %s\n" % len(self.dacl_list)
346 res += 8*" " + "=> %s\n" % len(other.dacl_list)
354 self_ace = "%s" % self.dacl_list[i]
359 other_ace = "%s" % other.dacl_list[i]
362 if len(self_ace) + len(other_ace) == 0:
364 self_ace_fixed = "%s" % self.fixit(self_ace)
365 other_ace_fixed = "%s" % other.fixit(other_ace)
366 if self_ace_fixed != other_ace_fixed:
367 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
370 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
374 def diff_2(self, other):
376 if len(self.dacl_list) != len(other.dacl_list):
377 res += 4*" " + "Difference in ACE count:\n"
378 res += 8*" " + "=> %s\n" % len(self.dacl_list)
379 res += 8*" " + "=> %s\n" % len(other.dacl_list)
384 self_dacl_list_fixed = []
385 other_dacl_list_fixed = []
386 [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
387 [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
388 for ace in self_dacl_list_fixed:
390 other_dacl_list_fixed.index(ace)
392 self_aces.append(ace)
394 common_aces.append(ace)
395 self_aces = sorted(self_aces)
396 if len(self_aces) > 0:
397 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
398 for ace in self_aces:
399 res += 8*" " + ace + "\n"
401 for ace in other_dacl_list_fixed:
403 self_dacl_list_fixed.index(ace)
405 other_aces.append(ace)
407 common_aces.append(ace)
408 other_aces = sorted(other_aces)
409 if len(other_aces) > 0:
410 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
411 for ace in other_aces:
412 res += 8*" " + ace + "\n"
414 common_aces = sorted(list(set(common_aces)))
416 res += 4*" " + "ACEs found in both:\n"
417 for ace in common_aces:
418 res += 8*" " + ace + "\n"
419 return (self_aces == [] and other_aces == [], res)
421 class LDAPObject(object):
422 def __init__(self, connection, dn, summary, filter_list,
423 outf=sys.stdout, errf=sys.stderr):
426 self.con = connection
427 self.two_domains = self.con.two_domains
428 self.quiet = self.con.quiet
429 self.verbose = self.con.verbose
430 self.summary = summary
431 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
432 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
433 for x in self.con.server_names:
434 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
435 self.attributes = self.con.get_attributes(self.dn)
436 # Attributes that are considered always to be different e.g based on timestamp etc.
438 # One domain - two domain controllers
439 self.ignore_attributes = [
440 # Default Naming Context
441 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
442 "operatingSystemVersion","oEMInformation",
443 "ridNextRID", "rIDPreviousAllocationPool",
444 # Configuration Naming Context
445 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
446 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
447 # Schema Naming Context
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", "pwdLastSet", "uSNCreated", "creationTime",
462 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
463 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
464 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
465 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
466 # After Exchange preps
467 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
469 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
470 self.dn_attributes = [
471 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
472 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
473 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
474 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
475 # After Exchange preps
476 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
477 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
478 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
479 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
480 self.dn_attributes = [x.upper() for x in self.dn_attributes]
482 # Attributes that contain the Domain name e.g. 'samba.org'
483 self.domain_attributes = [
484 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
485 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
486 self.domain_attributes = [x.upper() for x in self.domain_attributes]
488 # May contain DOMAIN_NETBIOS and SERVER_NAME
489 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
490 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
491 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
492 self.servername_attributes = [x.upper() for x in self.servername_attributes]
494 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
495 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
497 self.other_attributes = [ "name", "DC",]
498 self.other_attributes = [x.upper() for x in self.other_attributes]
500 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
504 Log on the screen if there is no --quiet oprion set
507 self.outf.write(msg+"\n")
511 if not self.two_domains:
513 if res.upper().endswith(self.con.base_dn.upper()):
514 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
517 def fix_domain_name(self, s):
519 if not self.two_domains:
521 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
522 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
525 def fix_domain_netbios(self, s):
527 if not self.two_domains:
529 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
530 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
533 def fix_server_name(self, s):
535 if not self.two_domains or len(self.con.server_names) > 1:
537 for x in self.con.server_names:
538 res = res.upper().replace(x, "${SERVER_NAME}")
541 def __eq__(self, other):
542 if self.con.descriptor:
543 return self.cmp_desc(other)
544 return self.cmp_attrs(other)
546 def cmp_desc(self, other):
547 d1 = Descriptor(self.con, self.dn, outf=self.outf, errf=self.errf)
548 d2 = Descriptor(other.con, other.dn, outf=self.outf, errf=self.errf)
549 if self.con.view == "section":
551 elif self.con.view == "collision":
554 raise Exception("Unknown --view option value.")
556 self.screen_output = res[1][:-1]
557 other.screen_output = res[1][:-1]
561 def cmp_attrs(self, other):
563 self.unique_attrs = []
564 self.df_value_attrs = []
565 other.unique_attrs = []
566 if self.attributes.keys() != other.attributes.keys():
568 title = 4*" " + "Attributes found only in %s:" % self.con.host
569 for x in self.attributes.keys():
570 if not x in other.attributes.keys() and \
571 not x.upper() in [q.upper() for q in other.ignore_attributes]:
575 res += 8*" " + x + "\n"
576 self.unique_attrs.append(x)
578 title = 4*" " + "Attributes found only in %s:" % other.con.host
579 for x in other.attributes.keys():
580 if not x in self.attributes.keys() and \
581 not x.upper() in [q.upper() for q in self.ignore_attributes]:
585 res += 8*" " + x + "\n"
586 other.unique_attrs.append(x)
588 missing_attrs = [x.upper() for x in self.unique_attrs]
589 missing_attrs += [x.upper() for x in other.unique_attrs]
590 title = 4*" " + "Difference in attribute values:"
591 for x in self.attributes.keys():
592 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
594 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
595 self.attributes[x] = sorted(self.attributes[x])
596 other.attributes[x] = sorted(other.attributes[x])
597 if self.attributes[x] != other.attributes[x]:
602 # First check if the difference can be fixed but shunting the first part
603 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
604 if x.upper() in self.other_attributes:
605 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
606 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
609 # Attribute values that are list that contain DN based values that may differ
610 elif x.upper() in self.dn_attributes:
614 m = self.attributes[x]
615 n = other.attributes[x]
616 p = [self.fix_dn(j) for j in m]
617 q = [other.fix_dn(j) for j in n]
620 # Attributes that contain the Domain name in them
621 if x.upper() in self.domain_attributes:
625 m = self.attributes[x]
626 n = other.attributes[x]
627 p = [self.fix_domain_name(j) for j in m]
628 q = [other.fix_domain_name(j) for j in n]
632 if x.upper() in self.servername_attributes:
633 # Attributes with SERVER_NAME
637 m = self.attributes[x]
638 n = other.attributes[x]
639 p = [self.fix_server_name(j) for j in m]
640 q = [other.fix_server_name(j) for j in n]
644 if x.upper() in self.netbios_attributes:
645 # Attributes with NETBIOS Domain name
649 m = self.attributes[x]
650 n = other.attributes[x]
651 p = [self.fix_domain_netbios(j) for j in m]
652 q = [other.fix_domain_netbios(j) for j in n]
660 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
662 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
663 self.df_value_attrs.append(x)
665 if self.unique_attrs + other.unique_attrs != []:
666 assert self.unique_attrs != other.unique_attrs
667 self.summary["unique_attrs"] += self.unique_attrs
668 self.summary["df_value_attrs"] += self.df_value_attrs
669 other.summary["unique_attrs"] += other.unique_attrs
670 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
672 self.screen_output = res[:-1]
673 other.screen_output = res[:-1]
678 class LDAPBundel(object):
680 def __init__(self, connection, context, dn_list=None, filter_list=None,
681 outf=sys.stdout, errf=sys.stderr):
684 self.con = connection
685 self.two_domains = self.con.two_domains
686 self.quiet = self.con.quiet
687 self.verbose = self.con.verbose
688 self.search_base = self.con.search_base
689 self.search_scope = self.con.search_scope
691 self.summary["unique_attrs"] = []
692 self.summary["df_value_attrs"] = []
693 self.summary["known_ignored_dn"] = []
694 self.summary["abnormal_ignored_dn"] = []
695 self.filter_list = filter_list
697 self.dn_list = dn_list
698 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
699 self.context = context.upper()
700 self.dn_list = self.get_dn_list(context)
702 raise Exception("Unknown initialization data for LDAPBundel().")
704 while counter < len(self.dn_list) and self.two_domains:
705 # Use alias reference
706 tmp = self.dn_list[counter]
707 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
708 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
709 if len(self.con.server_names) == 1:
710 for x in self.con.server_names:
711 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
712 self.dn_list[counter] = tmp
714 self.dn_list = list(set(self.dn_list))
715 self.dn_list = sorted(self.dn_list)
716 self.size = len(self.dn_list)
720 Log on the screen if there is no --quiet oprion set
723 self.outf.write(msg+"\n")
725 def update_size(self):
726 self.size = len(self.dn_list)
727 self.dn_list = sorted(self.dn_list)
729 def __eq__(self, other):
731 if self.size != other.size:
732 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
735 # This is the case where we want to explicitly compare two objects with different DNs.
736 # It does not matter if they are in the same DC, in two DC in one domain or in two
738 if self.search_scope != SCOPE_BASE:
739 title= "\n* DNs found only in %s:" % self.con.host
740 for x in self.dn_list:
741 if not x.upper() in [q.upper() for q in other.dn_list]:
746 self.log( 4*" " + x )
747 self.dn_list[self.dn_list.index(x)] = ""
748 self.dn_list = [x for x in self.dn_list if x]
750 title= "\n* DNs found only in %s:" % other.con.host
751 for x in other.dn_list:
752 if not x.upper() in [q.upper() for q in self.dn_list]:
757 self.log( 4*" " + x )
758 other.dn_list[other.dn_list.index(x)] = ""
759 other.dn_list = [x for x in other.dn_list if x]
763 assert self.size == other.size
764 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
765 self.log( "\n* Objects to be compared: %s" % self.size )
768 while index < self.size:
771 object1 = LDAPObject(connection=self.con,
772 dn=self.dn_list[index],
773 summary=self.summary,
774 filter_list=self.filter_list,
775 outf=self.outf, errf=self.errf)
776 except LdbError, (enum, estr):
777 if enum == ERR_NO_SUCH_OBJECT:
778 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
782 object2 = LDAPObject(connection=other.con,
783 dn=other.dn_list[index],
784 summary=other.summary,
785 filter_list=self.filter_list,
786 outf=self.outf, errf=self.errf)
787 except LdbError, (enum, estr):
788 if enum == ERR_NO_SUCH_OBJECT:
789 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
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
814 def get_dn_list(self, context):
815 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
816 Parse all DNs and filter those that are 'strange' or abnormal.
818 if context.upper() == "DOMAIN":
819 search_base = self.con.base_dn
820 elif context.upper() == "CONFIGURATION":
821 search_base = self.con.config_dn
822 elif context.upper() == "SCHEMA":
823 search_base = self.con.schema_dn
824 elif context.upper() == "DNSDOMAIN":
825 search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
826 elif context.upper() == "DNSFOREST":
827 search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
830 if not self.search_base:
831 self.search_base = search_base
832 self.search_scope = self.search_scope.upper()
833 if self.search_scope == "SUB":
834 self.search_scope = SCOPE_SUBTREE
835 elif self.search_scope == "BASE":
836 self.search_scope = SCOPE_BASE
837 elif self.search_scope == "ONE":
838 self.search_scope = SCOPE_ONELEVEL
840 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
842 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
843 except LdbError, (enum, estr):
844 self.outf.write("Failed search of base=%s\n" % self.search_base)
847 dn_list.append(x["dn"].get_linearized())
853 def print_summary(self):
854 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
855 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
857 if self.summary["unique_attrs"]:
858 self.log( "\nAttributes found only in %s:" % self.con.host )
859 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
861 if self.summary["df_value_attrs"]:
862 self.log( "\nAttributes with different values:" )
863 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
864 self.summary["df_value_attrs"] = []
867 class cmd_ldapcmp(Command):
868 """compare two ldap databases"""
869 synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
871 takes_optiongroups = {
872 "sambaopts": options.SambaOptions,
873 "versionopts": options.VersionOptions,
874 "credopts": options.CredentialsOptionsDouble,
877 takes_optiongroups = {
878 "sambaopts": options.SambaOptions,
879 "versionopts": options.VersionOptions,
880 "credopts": options.CredentialsOptionsDouble,
883 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
886 Option("-w", "--two", dest="two", action="store_true", default=False,
887 help="Hosts are in two different domains"),
888 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
889 help="Do not print anything but relay on just exit code"),
890 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
891 help="Print all DN pairs that have been compared"),
892 Option("--sd", dest="descriptor", action="store_true", default=False,
893 help="Compare nTSecurityDescriptor attibutes only"),
894 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
895 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
896 Option("--view", dest="view", default="section",
897 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
898 Option("--base", dest="base", default="",
899 help="Pass search base that will build DN list for the first DC."),
900 Option("--base2", dest="base2", default="",
901 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
902 Option("--scope", dest="scope", default="SUB",
903 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
904 Option("--filter", dest="filter", default="",
905 help="List of comma separated attributes to ignore in the comparision"),
908 def run(self, URL1, URL2,
909 context1=None, context2=None, context3=None,
910 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
911 view="section", base="", base2="", scope="SUB", filter="",
912 credopts=None, sambaopts=None, versionopts=None):
914 lp = sambaopts.get_loadparm()
916 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
919 creds = credopts.get_credentials(lp, fallback_machine=True)
922 creds2 = credopts.get_credentials2(lp, guess=False)
923 if creds2.is_anonymous():
926 creds2.set_domain("")
927 creds2.set_workstation("")
928 if using_ldap and not creds.authentication_requested():
929 raise CommandError("You must supply at least one username/password pair")
931 # make a list of contexts to compare in
935 # If search bases are specified context is defaulted to
936 # DOMAIN so the given search bases can be verified.
937 contexts = ["DOMAIN"]
939 # if no argument given, we compare all contexts
940 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
942 for c in [context1, context2, context3]:
945 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
946 raise CommandError("Incorrect argument: %s" % c)
947 contexts.append(c.upper())
949 if verbose and quiet:
950 raise CommandError("You cannot set --verbose and --quiet together")
951 if (not base and base2) or (base and not base2):
952 raise CommandError("You need to specify both --base and --base2 at the same time")
953 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
954 raise CommandError("Invalid --view value. Choose from: section or collision")
955 if not scope.upper() in ["SUB", "ONE", "BASE"]:
956 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
958 con1 = LDAPBase(URL1, creds, lp,
959 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
960 verbose=verbose,view=view, base=base, scope=scope,
961 outf=self.outf, errf=self.errf)
962 assert len(con1.base_dn) > 0
964 con2 = LDAPBase(URL2, creds2, lp,
965 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
966 verbose=verbose, view=view, base=base2, scope=scope,
967 outf=self.outf, errf=self.errf)
968 assert len(con2.base_dn) > 0
970 filter_list = filter.split(",")
973 for context in contexts:
975 self.outf.write("\n* Comparing [%s] context...\n" % context)
977 b1 = LDAPBundel(con1, context=context, filter_list=filter_list,
978 outf=self.outf, errf=self.errf)
979 b2 = LDAPBundel(con2, context=context, filter_list=filter_list,
980 outf=self.outf, errf=self.errf)
984 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
988 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
990 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
991 b2.summary["df_value_attrs"] = []
992 self.outf.write("\nSUMMARY\n")
993 self.outf.write("---------\n")
996 # mark exit status as FAILURE if a least one comparison failed
999 raise CommandError("Compare failed: %d" % status)