3 # Unix SMB/CIFS implementation.
4 # A command to compare differences of objects and attributes between
5 # two LDAP servers both running at the same time. It generally compares
6 # one of the three pratitions DOMAIN, CONFIGURATION or SCHEMA. Users
7 # that have to be provided sheould be able to read objects in any of the
10 # Copyright (C) Zahari Zahariev <zahari.zahariev@postpath.com> 2009, 2010
11 # Copyright Giampaolo Lauria 2011 <lauria2@yahoo.com>
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 3 of the License, or
16 # (at your option) any later version.
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
32 import samba.getopt as options
34 from samba.ndr import ndr_pack, ndr_unpack
35 from samba.dcerpc import security
36 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, ERR_NO_SUCH_OBJECT, LdbError
37 from samba.netcmd import (
47 class LDAPBase(object):
49 def __init__(self, host, creds, lp,
50 two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
51 view="section", base="", scope="SUB"):
55 if os.path.isfile(host):
56 samdb_url = "tdb://%s" % host
58 samdb_url = "ldap://%s" % host
59 # use 'paged_search' module when connecting remotely
60 if samdb_url.lower().startswith("ldap://"):
61 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.config_dn = str(self.ldb.get_config_basedn())
77 self.schema_dn = str(self.ldb.get_schema_basedn())
78 self.domain_netbios = self.find_netbios()
79 self.server_names = self.find_servers()
80 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
81 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, (enum, estr):
126 if enum == ERR_NO_SUCH_OBJECT:
131 def delete_force(self, object_dn):
133 self.ldb.delete(object_dn)
134 except Ldb.LdbError, e:
135 assert "No such object" in str(e)
137 def get_attribute_name(self, key):
138 """ Returns the real attribute name
139 It resolved ranged results e.g. member;range=0-1499
142 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
150 def get_attribute_values(self, object_dn, key, vals):
151 """ Returns list with all attribute values
152 It resolved ranged results e.g. member;range=0-1499
155 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
159 # no range, just return the values
165 # get additional values in a loop
166 # until we get a response with '*' at the end
169 n = "%s;range=%d-*" % (attr, hi + 1)
170 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
178 for key in res.keys():
184 if m.group(1) != attr:
188 fvals = list(res[key])
195 if fm.group(3) == "*":
196 # if we got "*" we're done
199 assert int(fm.group(2)) == hi + 1
200 hi = int(fm.group(3))
204 def get_attributes(self, object_dn):
205 """ Returns dict with all default visible attributes
207 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
210 # 'Dn' element is not iterable and we have it as 'distinguishedName'
212 for key in res.keys():
213 vals = list(res[key])
215 name = self.get_attribute_name(key)
216 res[name] = self.get_attribute_values(object_dn, key, vals)
220 def get_descriptor_sddl(self, object_dn):
221 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
222 desc = res[0]["nTSecurityDescriptor"][0]
223 desc = ndr_unpack(security.descriptor, desc)
224 return desc.as_sddl(self.domain_sid)
226 def guid_as_string(self, guid_blob):
227 """ Translate binary representation of schemaIDGUID to standard string representation.
228 @gid_blob: binary schemaIDGUID
230 blob = "%s" % guid_blob
231 stops = [4, 2, 2, 2, 6]
235 while x < len(stops):
239 c = hex(ord(blob[index])).replace("0x", "")
240 c = [None, "0" + c, c][len(c)]
241 if 2 * index < len(blob):
249 assert index == len(blob)
250 return res.strip().replace(" ", "-")
252 def get_guid_map(self):
253 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
256 res = self.ldb.search(base=self.schema_dn,
257 expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
259 self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
261 res = self.ldb.search(base="cn=extended-rights,%s" % self.config_dn,
262 expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
264 self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
266 def get_sid_map(self):
267 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
270 res = self.ldb.search(base=self.base_dn,
271 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
274 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
278 class Descriptor(object):
279 def __init__(self, connection, dn):
280 self.con = connection
282 self.sddl = self.con.get_descriptor_sddl(self.dn)
283 self.dacl_list = self.extract_dacl()
284 if self.con.sort_aces:
285 self.dacl_list.sort()
287 def extract_dacl(self):
288 """ Extracts the DACL as a list of ACE string (with the brakets).
291 if "S:" in self.sddl:
292 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
294 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
295 except AttributeError:
297 return re.findall("(\(.*?\))", res)
299 def fix_guid(self, ace):
301 guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
302 # If there are not GUIDs to replace return the same ACE
307 name = self.con.guid_map[guid.lower()]
308 res = res.replace(guid, name)
310 # Do not bother if the GUID is not found in
311 # cn=Schema or cn=Extended-Rights
315 def fix_sid(self, ace):
317 sids = re.findall("S-[-0-9]+", res)
318 # If there are not SIDs to replace return the same ACE
323 name = self.con.sid_map[sid]
324 res = res.replace(sid, name)
326 # Do not bother if the SID is not found in baseDN
330 def fixit(self, ace):
331 """ Combine all replacement methods in one
334 res = self.fix_guid(res)
335 res = self.fix_sid(res)
338 def diff_1(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)
351 self_ace = "%s" % self.dacl_list[i]
356 other_ace = "%s" % other.dacl_list[i]
359 if len(self_ace) + len(other_ace) == 0:
361 self_ace_fixed = "%s" % self.fixit(self_ace)
362 other_ace_fixed = "%s" % other.fixit(other_ace)
363 if self_ace_fixed != other_ace_fixed:
364 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
367 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
371 def diff_2(self, other):
373 if len(self.dacl_list) != len(other.dacl_list):
374 res += 4*" " + "Difference in ACE count:\n"
375 res += 8*" " + "=> %s\n" % len(self.dacl_list)
376 res += 8*" " + "=> %s\n" % len(other.dacl_list)
381 self_dacl_list_fixed = []
382 other_dacl_list_fixed = []
383 [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
384 [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
385 for ace in self_dacl_list_fixed:
387 other_dacl_list_fixed.index(ace)
389 self_aces.append(ace)
391 common_aces.append(ace)
392 self_aces = sorted(self_aces)
393 if len(self_aces) > 0:
394 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
395 for ace in self_aces:
396 res += 8*" " + ace + "\n"
398 for ace in other_dacl_list_fixed:
400 self_dacl_list_fixed.index(ace)
402 other_aces.append(ace)
404 common_aces.append(ace)
405 other_aces = sorted(other_aces)
406 if len(other_aces) > 0:
407 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
408 for ace in other_aces:
409 res += 8*" " + ace + "\n"
411 common_aces = sorted(list(set(common_aces)))
413 res += 4*" " + "ACEs found in both:\n"
414 for ace in common_aces:
415 res += 8*" " + ace + "\n"
416 return (self_aces == [] and other_aces == [], res)
418 class LDAPObject(object):
419 def __init__(self, connection, dn, summary, filter_list):
420 self.con = connection
421 self.two_domains = self.con.two_domains
422 self.quiet = self.con.quiet
423 self.verbose = self.con.verbose
424 self.summary = summary
425 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
426 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
427 for x in self.con.server_names:
428 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
429 self.attributes = self.con.get_attributes(self.dn)
430 # Attributes that are considered always to be different e.g based on timestamp etc.
432 # One domain - two domain controllers
433 self.ignore_attributes = [
434 # Default Naming Context
435 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
436 "operatingSystemVersion","oEMInformation",
437 # Configuration Naming Context
438 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
439 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
440 # Schema Naming Context
443 self.ignore_attributes += filter_list
445 self.dn_attributes = []
446 self.domain_attributes = []
447 self.servername_attributes = []
448 self.netbios_attributes = []
449 self.other_attributes = []
450 # Two domains - two domain controllers
453 self.ignore_attributes += [
454 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
455 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
456 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
457 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
458 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
459 # After Exchange preps
460 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
462 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
463 self.dn_attributes = [
464 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
465 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
466 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
467 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
468 # After Exchange preps
469 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
470 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
471 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
472 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
473 self.dn_attributes = [x.upper() for x in self.dn_attributes]
475 # Attributes that contain the Domain name e.g. 'samba.org'
476 self.domain_attributes = [
477 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
478 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
479 self.domain_attributes = [x.upper() for x in self.domain_attributes]
481 # May contain DOMAIN_NETBIOS and SERVER_NAME
482 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
483 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
484 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
485 self.servername_attributes = [x.upper() for x in self.servername_attributes]
487 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
488 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
490 self.other_attributes = [ "name", "DC",]
491 self.other_attributes = [x.upper() for x in self.other_attributes]
493 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
497 Log on the screen if there is no --quiet oprion set
500 self.outf.write(msg+"\n")
504 if not self.two_domains:
506 if res.upper().endswith(self.con.base_dn.upper()):
507 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
510 def fix_domain_name(self, s):
512 if not self.two_domains:
514 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
515 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
518 def fix_domain_netbios(self, s):
520 if not self.two_domains:
522 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
523 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
526 def fix_server_name(self, s):
528 if not self.two_domains or len(self.con.server_names) > 1:
530 for x in self.con.server_names:
531 res = res.upper().replace(x, "${SERVER_NAME}")
534 def __eq__(self, other):
535 if self.con.descriptor:
536 return self.cmp_desc(other)
537 return self.cmp_attrs(other)
539 def cmp_desc(self, other):
540 d1 = Descriptor(self.con, self.dn)
541 d2 = Descriptor(other.con, other.dn)
542 if self.con.view == "section":
544 elif self.con.view == "collision":
547 raise Exception("Unknown --view option value.")
549 self.screen_output = res[1][:-1]
550 other.screen_output = res[1][:-1]
554 def cmp_attrs(self, other):
556 self.unique_attrs = []
557 self.df_value_attrs = []
558 other.unique_attrs = []
559 if self.attributes.keys() != other.attributes.keys():
561 title = 4*" " + "Attributes found only in %s:" % self.con.host
562 for x in self.attributes.keys():
563 if not x in other.attributes.keys() and \
564 not x.upper() in [q.upper() for q in other.ignore_attributes]:
568 res += 8*" " + x + "\n"
569 self.unique_attrs.append(x)
571 title = 4*" " + "Attributes found only in %s:" % other.con.host
572 for x in other.attributes.keys():
573 if not x in self.attributes.keys() and \
574 not x.upper() in [q.upper() for q in self.ignore_attributes]:
578 res += 8*" " + x + "\n"
579 other.unique_attrs.append(x)
581 missing_attrs = [x.upper() for x in self.unique_attrs]
582 missing_attrs += [x.upper() for x in other.unique_attrs]
583 title = 4*" " + "Difference in attribute values:"
584 for x in self.attributes.keys():
585 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
587 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
588 self.attributes[x] = sorted(self.attributes[x])
589 other.attributes[x] = sorted(other.attributes[x])
590 if self.attributes[x] != other.attributes[x]:
595 # First check if the difference can be fixed but shunting the first part
596 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
597 if x.upper() in self.other_attributes:
598 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
599 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
602 # Attribute values that are list that contain DN based values that may differ
603 elif x.upper() in self.dn_attributes:
607 m = self.attributes[x]
608 n = other.attributes[x]
609 p = [self.fix_dn(j) for j in m]
610 q = [other.fix_dn(j) for j in n]
613 # Attributes that contain the Domain name in them
614 if x.upper() in self.domain_attributes:
618 m = self.attributes[x]
619 n = other.attributes[x]
620 p = [self.fix_domain_name(j) for j in m]
621 q = [other.fix_domain_name(j) for j in n]
625 if x.upper() in self.servername_attributes:
626 # Attributes with SERVER_NAME
630 m = self.attributes[x]
631 n = other.attributes[x]
632 p = [self.fix_server_name(j) for j in m]
633 q = [other.fix_server_name(j) for j in n]
637 if x.upper() in self.netbios_attributes:
638 # Attributes with NETBIOS Domain name
642 m = self.attributes[x]
643 n = other.attributes[x]
644 p = [self.fix_domain_netbios(j) for j in m]
645 q = [other.fix_domain_netbios(j) for j in n]
653 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
655 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
656 self.df_value_attrs.append(x)
658 if self.unique_attrs + other.unique_attrs != []:
659 assert self.unique_attrs != other.unique_attrs
660 self.summary["unique_attrs"] += self.unique_attrs
661 self.summary["df_value_attrs"] += self.df_value_attrs
662 other.summary["unique_attrs"] += other.unique_attrs
663 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
665 self.screen_output = res[:-1]
666 other.screen_output = res[:-1]
671 class LDAPBundel(object):
673 def __init__(self, connection, context, dn_list=None, filter_list=None):
674 self.con = connection
675 self.two_domains = self.con.two_domains
676 self.quiet = self.con.quiet
677 self.verbose = self.con.verbose
678 self.search_base = self.con.search_base
679 self.search_scope = self.con.search_scope
681 self.summary["unique_attrs"] = []
682 self.summary["df_value_attrs"] = []
683 self.summary["known_ignored_dn"] = []
684 self.summary["abnormal_ignored_dn"] = []
685 self.filter_list = filter_list
687 self.dn_list = dn_list
688 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
689 self.context = context.upper()
690 self.dn_list = self.get_dn_list(context)
692 raise Exception("Unknown initialization data for LDAPBundel().")
694 while counter < len(self.dn_list) and self.two_domains:
695 # Use alias reference
696 tmp = self.dn_list[counter]
697 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
698 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
699 if len(self.con.server_names) == 1:
700 for x in self.con.server_names:
701 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
702 self.dn_list[counter] = tmp
704 self.dn_list = list(set(self.dn_list))
705 self.dn_list = sorted(self.dn_list)
706 self.size = len(self.dn_list)
710 Log on the screen if there is no --quiet oprion set
713 self.outf.write(msg+"\n")
715 def update_size(self):
716 self.size = len(self.dn_list)
717 self.dn_list = sorted(self.dn_list)
719 def __eq__(self, other):
721 if self.size != other.size:
722 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
725 # This is the case where we want to explicitly compare two objects with different DNs.
726 # It does not matter if they are in the same DC, in two DC in one domain or in two
728 if self.search_scope != SCOPE_BASE:
729 title= "\n* DNs found only in %s:" % self.con.host
730 for x in self.dn_list:
731 if not x.upper() in [q.upper() for q in other.dn_list]:
736 self.log( 4*" " + x )
737 self.dn_list[self.dn_list.index(x)] = ""
738 self.dn_list = [x for x in self.dn_list if x]
740 title= "\n* DNs found only in %s:" % other.con.host
741 for x in other.dn_list:
742 if not x.upper() in [q.upper() for q in self.dn_list]:
747 self.log( 4*" " + x )
748 other.dn_list[other.dn_list.index(x)] = ""
749 other.dn_list = [x for x in other.dn_list if x]
753 assert self.size == other.size
754 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
755 self.log( "\n* Objects to be compared: %s" % self.size )
758 while index < self.size:
761 object1 = LDAPObject(connection=self.con,
762 dn=self.dn_list[index],
763 summary=self.summary,
764 filter_list=self.filter_list)
765 except LdbError, (enum, estr):
766 if enum == ERR_NO_SUCH_OBJECT:
767 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
771 object2 = LDAPObject(connection=other.con,
772 dn=other.dn_list[index],
773 summary=other.summary,
774 filter_list=self.filter_list)
775 except LdbError, (enum, estr):
776 if enum == ERR_NO_SUCH_OBJECT:
777 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
783 if object1 == object2:
785 self.log( "\nComparing:" )
786 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
787 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
788 self.log( 4*" " + "OK" )
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( object1.screen_output )
794 self.log( 4*" " + "FAILED" )
796 self.summary = object1.summary
797 other.summary = object2.summary
802 def get_dn_list(self, context):
803 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
804 Parse all DNs and filter those that are 'strange' or abnormal.
806 if context.upper() == "DOMAIN":
807 search_base = self.con.base_dn
808 elif context.upper() == "CONFIGURATION":
809 search_base = self.con.config_dn
810 elif context.upper() == "SCHEMA":
811 search_base = self.con.schema_dn
814 if not self.search_base:
815 self.search_base = search_base
816 self.search_scope = self.search_scope.upper()
817 if self.search_scope == "SUB":
818 self.search_scope = SCOPE_SUBTREE
819 elif self.search_scope == "BASE":
820 self.search_scope = SCOPE_BASE
821 elif self.search_scope == "ONE":
822 self.search_scope = SCOPE_ONELEVEL
824 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
826 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
827 except LdbError, (enum, estr):
828 self.outf.write("Failed search of base=%s\n" % self.search_base)
831 dn_list.append(x["dn"].get_linearized())
837 def print_summary(self):
838 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
839 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
841 if self.summary["unique_attrs"]:
842 self.log( "\nAttributes found only in %s:" % self.con.host )
843 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
845 if self.summary["df_value_attrs"]:
846 self.log( "\nAttributes with different values:" )
847 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
848 self.summary["df_value_attrs"] = []
851 class cmd_ldapcmp(Command):
852 """compare two ldap databases"""
853 synopsis = "%prog ldapcmp <URL1> <URL2> (domain|configuration|schema) [options]"
855 takes_optiongroups = {
856 "sambaopts": options.SambaOptions,
857 "versionopts": options.VersionOptions,
858 "credopts": options.CredentialsOptionsDouble,
861 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
864 Option("-w", "--two", dest="two", action="store_true", default=False,
865 help="Hosts are in two different domains"),
866 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
867 help="Do not print anything but relay on just exit code"),
868 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
869 help="Print all DN pairs that have been compared"),
870 Option("--sd", dest="descriptor", action="store_true", default=False,
871 help="Compare nTSecurityDescriptor attibutes only"),
872 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
873 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
874 Option("--view", dest="view", default="section",
875 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
876 Option("--base", dest="base", default="",
877 help="Pass search base that will build DN list for the first DC."),
878 Option("--base2", dest="base2", default="",
879 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
880 Option("--scope", dest="scope", default="SUB",
881 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
882 Option("--filter", dest="filter", default="",
883 help="List of comma separated attributes to ignore in the comparision"),
886 def run(self, URL1, URL2,
887 context1=None, context2=None, context3=None,
888 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
889 view="section", base="", base2="", scope="SUB", filter="",
890 credopts=None, sambaopts=None, versionopts=None):
892 lp = sambaopts.get_loadparm()
894 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
897 creds = credopts.get_credentials(lp, fallback_machine=True)
900 creds2 = credopts.get_credentials2(lp, guess=False)
901 if creds2.is_anonymous():
904 creds2.set_domain("")
905 creds2.set_workstation("")
906 if using_ldap and not creds.authentication_requested():
907 raise CommandError("You must supply at least one username/password pair")
909 # make a list of contexts to compare in
913 # If search bases are specified context is defaulted to
914 # DOMAIN so the given search bases can be verified.
915 contexts = ["DOMAIN"]
917 # if no argument given, we compare all contexts
918 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
920 for c in [context1, context2, context3]:
923 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
924 raise CommandError("Incorrect argument: %s" % c)
925 contexts.append(c.upper())
927 if verbose and quiet:
928 raise CommandError("You cannot set --verbose and --quiet together")
929 if (not base and base2) or (base and not base2):
930 raise CommandError("You need to specify both --base and --base2 at the same time")
931 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
932 raise CommandError("Invalid --view value. Choose from: section or collision")
933 if not scope.upper() in ["SUB", "ONE", "BASE"]:
934 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
936 con1 = LDAPBase(URL1, creds, lp,
937 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
938 verbose=verbose,view=view, base=base, scope=scope)
939 assert len(con1.base_dn) > 0
941 con2 = LDAPBase(URL2, creds2, lp,
942 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
943 verbose=verbose, view=view, base=base2, scope=scope)
944 assert len(con2.base_dn) > 0
946 filter_list = filter.split(",")
949 for context in contexts:
951 self.outf.write("\n* Comparing [%s] context...\n" % context)
953 b1 = LDAPBundel(con1, context=context, filter_list=filter_list)
954 b2 = LDAPBundel(con2, context=context, filter_list=filter_list)
958 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
962 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
964 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
965 b2.summary["df_value_attrs"] = []
966 self.outf.write("\nSUMMARY\n")
967 self.outf.write("---------\n")
970 # mark exit status as FAILURE if a least one comparison failed
973 raise CommandError("Compare failed: %d" % status)