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 = self.find_basedn()
76 self.domain_netbios = self.find_netbios()
77 self.server_names = self.find_servers()
78 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
79 self.domain_sid = self.find_domain_sid()
83 # Log some domain controller specific place-holers that are being used
84 # when compare content of two DCs. Uncomment for DEBUG purposes.
85 if self.two_domains and not self.quiet:
86 print "\n* Place-holders for %s:" % self.host
87 print 4*" " + "${DOMAIN_DN} => %s" % self.base_dn
88 print 4*" " + "${DOMAIN_NETBIOS} => %s" % self.domain_netbios
89 print 4*" " + "${SERVER_NAME} => %s" % self.server_names
90 print 4*" " + "${DOMAIN_NAME} => %s" % self.domain_name
92 def find_domain_sid(self):
93 res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
94 return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
96 def find_servers(self):
99 res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \
100 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
104 srv.append(x["cn"][0])
107 def find_netbios(self):
108 res = self.ldb.search(base="CN=Partitions,CN=Configuration,%s" % self.base_dn, \
109 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
112 if "nETBIOSName" in x.keys():
113 return x["nETBIOSName"][0]
115 def find_basedn(self):
116 res = self.ldb.search(base="", expression="(objectClass=*)", scope=SCOPE_BASE,
117 attrs=["defaultNamingContext"])
119 return res[0]["defaultNamingContext"][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="cn=schema,cn=configuration,%s" % self.base_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,cn=configuration,%s" % self.base_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="%s" % 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):
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
442 self.dn_attributes = []
443 self.domain_attributes = []
444 self.servername_attributes = []
445 self.netbios_attributes = []
446 self.other_attributes = []
447 # Two domains - two domain controllers
450 self.ignore_attributes += [
451 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
452 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
453 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
454 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
455 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
456 # After Exchange preps
457 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
459 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
460 self.dn_attributes = [
461 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
462 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
463 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
464 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
465 # After Exchange preps
466 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
467 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
468 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
469 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
470 self.dn_attributes = [x.upper() for x in self.dn_attributes]
472 # Attributes that contain the Domain name e.g. 'samba.org'
473 self.domain_attributes = [
474 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
475 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
476 self.domain_attributes = [x.upper() for x in self.domain_attributes]
478 # May contain DOMAIN_NETBIOS and SERVER_NAME
479 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
480 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
481 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
482 self.servername_attributes = [x.upper() for x in self.servername_attributes]
484 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
485 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
487 self.other_attributes = [ "name", "DC",]
488 self.other_attributes = [x.upper() for x in self.other_attributes]
490 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
494 Log on the screen if there is no --quiet oprion set
501 if not self.two_domains:
503 if res.upper().endswith(self.con.base_dn.upper()):
504 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
507 def fix_domain_name(self, s):
509 if not self.two_domains:
511 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
512 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
515 def fix_domain_netbios(self, s):
517 if not self.two_domains:
519 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
520 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
523 def fix_server_name(self, s):
525 if not self.two_domains or len(self.con.server_names) > 1:
527 for x in self.con.server_names:
528 res = res.upper().replace(x, "${SERVER_NAME}")
531 def __eq__(self, other):
532 if self.con.descriptor:
533 return self.cmp_desc(other)
534 return self.cmp_attrs(other)
536 def cmp_desc(self, other):
537 d1 = Descriptor(self.con, self.dn)
538 d2 = Descriptor(other.con, other.dn)
539 if self.con.view == "section":
541 elif self.con.view == "collision":
544 raise Exception("Unknown --view option value.")
546 self.screen_output = res[1][:-1]
547 other.screen_output = res[1][:-1]
551 def cmp_attrs(self, other):
553 self.unique_attrs = []
554 self.df_value_attrs = []
555 other.unique_attrs = []
556 if self.attributes.keys() != other.attributes.keys():
558 title = 4*" " + "Attributes found only in %s:" % self.con.host
559 for x in self.attributes.keys():
560 if not x in other.attributes.keys() and \
561 not x.upper() in [q.upper() for q in other.ignore_attributes]:
565 res += 8*" " + x + "\n"
566 self.unique_attrs.append(x)
568 title = 4*" " + "Attributes found only in %s:" % other.con.host
569 for x in other.attributes.keys():
570 if not x in self.attributes.keys() and \
571 not x.upper() in [q.upper() for q in self.ignore_attributes]:
575 res += 8*" " + x + "\n"
576 other.unique_attrs.append(x)
578 missing_attrs = [x.upper() for x in self.unique_attrs]
579 missing_attrs += [x.upper() for x in other.unique_attrs]
580 title = 4*" " + "Difference in attribute values:"
581 for x in self.attributes.keys():
582 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
584 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
585 self.attributes[x] = sorted(self.attributes[x])
586 other.attributes[x] = sorted(other.attributes[x])
587 if self.attributes[x] != other.attributes[x]:
592 # First check if the difference can be fixed but shunting the first part
593 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
594 if x.upper() in self.other_attributes:
595 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
596 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
599 # Attribute values that are list that contain DN based values that may differ
600 elif x.upper() in self.dn_attributes:
604 m = self.attributes[x]
605 n = other.attributes[x]
606 p = [self.fix_dn(j) for j in m]
607 q = [other.fix_dn(j) for j in n]
610 # Attributes that contain the Domain name in them
611 if x.upper() in self.domain_attributes:
615 m = self.attributes[x]
616 n = other.attributes[x]
617 p = [self.fix_domain_name(j) for j in m]
618 q = [other.fix_domain_name(j) for j in n]
622 if x.upper() in self.servername_attributes:
623 # Attributes with SERVER_NAME
627 m = self.attributes[x]
628 n = other.attributes[x]
629 p = [self.fix_server_name(j) for j in m]
630 q = [other.fix_server_name(j) for j in n]
634 if x.upper() in self.netbios_attributes:
635 # Attributes with NETBIOS Domain name
639 m = self.attributes[x]
640 n = other.attributes[x]
641 p = [self.fix_domain_netbios(j) for j in m]
642 q = [other.fix_domain_netbios(j) for j in n]
650 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
652 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
653 self.df_value_attrs.append(x)
655 if self.unique_attrs + other.unique_attrs != []:
656 assert self.unique_attrs != other.unique_attrs
657 self.summary["unique_attrs"] += self.unique_attrs
658 self.summary["df_value_attrs"] += self.df_value_attrs
659 other.summary["unique_attrs"] += other.unique_attrs
660 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
662 self.screen_output = res[:-1]
663 other.screen_output = res[:-1]
668 class LDAPBundel(object):
669 def __init__(self, connection, context, dn_list=None):
670 self.con = connection
671 self.two_domains = self.con.two_domains
672 self.quiet = self.con.quiet
673 self.verbose = self.con.verbose
674 self.search_base = self.con.search_base
675 self.search_scope = self.con.search_scope
677 self.summary["unique_attrs"] = []
678 self.summary["df_value_attrs"] = []
679 self.summary["known_ignored_dn"] = []
680 self.summary["abnormal_ignored_dn"] = []
682 self.dn_list = dn_list
683 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
684 self.context = context.upper()
685 self.dn_list = self.get_dn_list(context)
687 raise Exception("Unknown initialization data for LDAPBundel().")
689 while counter < len(self.dn_list) and self.two_domains:
690 # Use alias reference
691 tmp = self.dn_list[counter]
692 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
693 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
694 if len(self.con.server_names) == 1:
695 for x in self.con.server_names:
696 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
697 self.dn_list[counter] = tmp
699 self.dn_list = list(set(self.dn_list))
700 self.dn_list = sorted(self.dn_list)
701 self.size = len(self.dn_list)
705 Log on the screen if there is no --quiet oprion set
710 def update_size(self):
711 self.size = len(self.dn_list)
712 self.dn_list = sorted(self.dn_list)
714 def __eq__(self, other):
716 if self.size != other.size:
717 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
720 # This is the case where we want to explicitly compare two objects with different DNs.
721 # It does not matter if they are in the same DC, in two DC in one domain or in two
723 if self.search_scope != SCOPE_BASE:
724 title= "\n* DNs found only in %s:" % self.con.host
725 for x in self.dn_list:
726 if not x.upper() in [q.upper() for q in other.dn_list]:
731 self.log( 4*" " + x )
732 self.dn_list[self.dn_list.index(x)] = ""
733 self.dn_list = [x for x in self.dn_list if x]
735 title= "\n* DNs found only in %s:" % other.con.host
736 for x in other.dn_list:
737 if not x.upper() in [q.upper() for q in self.dn_list]:
742 self.log( 4*" " + x )
743 other.dn_list[other.dn_list.index(x)] = ""
744 other.dn_list = [x for x in other.dn_list if x]
748 assert self.size == other.size
749 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
750 self.log( "\n* Objects to be compared: %s" % self.size )
753 while index < self.size:
756 object1 = LDAPObject(connection=self.con,
757 dn=self.dn_list[index],
758 summary=self.summary)
759 except LdbError, (enum, estr):
760 if enum == ERR_NO_SUCH_OBJECT:
761 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
765 object2 = LDAPObject(connection=other.con,
766 dn=other.dn_list[index],
767 summary=other.summary)
768 except LdbError, (enum, estr):
769 if enum == ERR_NO_SUCH_OBJECT:
770 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
776 if object1 == object2:
778 self.log( "\nComparing:" )
779 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
780 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
781 self.log( 4*" " + "OK" )
783 self.log( "\nComparing:" )
784 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
785 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
786 self.log( object1.screen_output )
787 self.log( 4*" " + "FAILED" )
789 self.summary = object1.summary
790 other.summary = object2.summary
795 def get_dn_list(self, context):
796 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
797 Parse all DNs and filter those that are 'strange' or abnormal.
799 if context.upper() == "DOMAIN":
800 search_base = "%s" % self.con.base_dn
801 elif context.upper() == "CONFIGURATION":
802 search_base = "CN=Configuration,%s" % self.con.base_dn
803 elif context.upper() == "SCHEMA":
804 search_base = "CN=Schema,CN=Configuration,%s" % self.con.base_dn
807 if not self.search_base:
808 self.search_base = search_base
809 self.search_scope = self.search_scope.upper()
810 if self.search_scope == "SUB":
811 self.search_scope = SCOPE_SUBTREE
812 elif self.search_scope == "BASE":
813 self.search_scope = SCOPE_BASE
814 elif self.search_scope == "ONE":
815 self.search_scope = SCOPE_ONELEVEL
817 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
818 if not self.search_base.upper().endswith(search_base.upper()):
819 raise StandardError("Invalid search base specified: %s" % self.search_base)
820 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
822 dn_list.append(x["dn"].get_linearized())
828 def print_summary(self):
829 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
830 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
832 if self.summary["unique_attrs"]:
833 self.log( "\nAttributes found only in %s:" % self.con.host )
834 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
836 if self.summary["df_value_attrs"]:
837 self.log( "\nAttributes with different values:" )
838 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
839 self.summary["df_value_attrs"] = []
841 class cmd_ldapcmp(Command):
842 """compare two ldap databases"""
843 synopsis = "ldapcmp URL1 URL2 <domain|configuration|schema> [options]"
845 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
848 Option("-w", "--two", dest="two", action="store_true", default=False,
849 help="Hosts are in two different domains"),
850 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
851 help="Do not print anything but relay on just exit code"),
852 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
853 help="Print all DN pairs that have been compared"),
854 Option("--sd", dest="descriptor", action="store_true", default=False,
855 help="Compare nTSecurityDescriptor attibutes only"),
856 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
857 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
858 Option("--view", dest="view", default="section",
859 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
860 Option("--base", dest="base", default="",
861 help="Pass search base that will build DN list for the first DC."),
862 Option("--base2", dest="base2", default="",
863 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
864 Option("--scope", dest="scope", default="SUB",
865 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
868 def run(self, URL1, URL2,
869 context1=None, context2=None, context3=None,
870 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False, view="section",
871 base="", base2="", scope="SUB", credopts=None, sambaopts=None, versionopts=None):
873 lp = sambaopts.get_loadparm()
875 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
878 creds = credopts.get_credentials(lp, fallback_machine=True)
881 creds2 = credopts.get_credentials2(lp, guess=False)
882 if creds2.is_anonymous():
885 creds2.set_domain("")
886 creds2.set_workstation("")
887 if using_ldap and not creds.authentication_requested():
888 raise CommandError("You must supply at least one username/password pair")
890 # make a list of contexts to compare in
894 # If search bases are specified context is defaulted to
895 # DOMAIN so the given search bases can be verified.
896 contexts = ["DOMAIN"]
898 # if no argument given, we compare all contexts
899 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
901 for c in [context1, context2, context3]:
904 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
905 raise CommandError("Incorrect argument: %s" % c)
906 contexts.append(c.upper())
908 if verbose and quiet:
909 raise CommandError("You cannot set --verbose and --quiet together")
910 if (not base and base2) or (base and not base2):
911 raise CommandError("You need to specify both --base and --base2 at the same time")
912 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
913 raise CommandError("Invalid --view value. Choose from: section or collision")
914 if not scope.upper() in ["SUB", "ONE", "BASE"]:
915 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
917 con1 = LDAPBase(URL1, creds, lp,
918 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
919 verbose=verbose,view=view, base=base, scope=scope)
920 assert len(con1.base_dn) > 0
922 con2 = LDAPBase(URL2, creds2, lp,
923 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
924 verbose=verbose, view=view, base=base2, scope=scope)
925 assert len(con2.base_dn) > 0
928 for context in contexts:
930 print "\n* Comparing [%s] context..." % context
932 b1 = LDAPBundel(con1, context=context)
933 b2 = LDAPBundel(con2, context=context)
937 print "\n* Result for [%s]: SUCCESS" % context
940 print "\n* Result for [%s]: FAILURE" % context
942 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
943 b2.summary["df_value_attrs"] = []
948 # mark exit status as FAILURE if a least one comparison failed
951 raise CommandError("Compare failed: %d" % status)