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
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 3 of the License, or
15 # (at your option) any later version.
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
22 # You should have received a copy of the GNU General Public License
23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
31 import samba.getopt as options
33 from samba.ndr import ndr_pack, ndr_unpack
34 from samba.dcerpc import security
35 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, ERR_NO_SUCH_OBJECT, LdbError
36 from samba.netcmd import (
46 class LDAPBase(object):
48 def __init__(self, host, creds, lp,
49 two=False, quiet=False, descriptor=False, verbose=False,
54 if os.path.isfile(host):
55 samdb_url = "tdb://%s" % host
57 samdb_url = "ldap://%s" % host
58 # use 'paged_search' module when connecting remotely
59 if samdb_url.lower().startswith("ldap://"):
60 ldb_options = ["modules:paged_searches"]
61 self.ldb = Ldb(url=samdb_url,
65 self.two_domains = two
67 self.descriptor = descriptor
69 self.verbose = verbose
71 self.base_dn = self.find_basedn()
72 self.domain_netbios = self.find_netbios()
73 self.server_names = self.find_servers()
74 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
75 self.domain_sid = self.find_domain_sid()
79 # Log some domain controller specific place-holers that are being used
80 # when compare content of two DCs. Uncomment for DEBUG purposes.
81 if self.two_domains and not self.quiet:
82 print "\n* Place-holders for %s:" % self.host
83 print 4*" " + "${DOMAIN_DN} => %s" % self.base_dn
84 print 4*" " + "${DOMAIN_NETBIOS} => %s" % self.domain_netbios
85 print 4*" " + "${SERVER_NAME} => %s" % self.server_names
86 print 4*" " + "${DOMAIN_NAME} => %s" % self.domain_name
88 def find_domain_sid(self):
89 res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
90 return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
92 def find_servers(self):
95 res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \
96 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
100 srv.append(x["cn"][0])
103 def find_netbios(self):
104 res = self.ldb.search(base="CN=Partitions,CN=Configuration,%s" % self.base_dn, \
105 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
108 if "nETBIOSName" in x.keys():
109 return x["nETBIOSName"][0]
111 def find_basedn(self):
112 res = self.ldb.search(base="", expression="(objectClass=*)", scope=SCOPE_BASE,
113 attrs=["defaultNamingContext"])
115 return res[0]["defaultNamingContext"][0]
117 def object_exists(self, object_dn):
120 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
121 except LdbError, (enum, estr):
122 if enum == ERR_NO_SUCH_OBJECT:
127 def delete_force(self, object_dn):
129 self.ldb.delete(object_dn)
130 except Ldb.LdbError, e:
131 assert "No such object" in str(e)
133 def get_attributes(self, object_dn):
134 """ Returns dict with all default visible attributes
136 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
139 # 'Dn' element is not iterable and we have it as 'distinguishedName'
141 for key in res.keys():
142 res[key] = list(res[key])
145 def get_descriptor_sddl(self, object_dn):
146 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
147 desc = res[0]["nTSecurityDescriptor"][0]
148 desc = ndr_unpack(security.descriptor, desc)
149 return desc.as_sddl(self.domain_sid)
151 def guid_as_string(self, guid_blob):
152 """ Translate binary representation of schemaIDGUID to standard string representation.
153 @gid_blob: binary schemaIDGUID
155 blob = "%s" % guid_blob
156 stops = [4, 2, 2, 2, 6]
160 while x < len(stops):
164 c = hex(ord(blob[index])).replace("0x", "")
165 c = [None, "0" + c, c][len(c)]
166 if 2 * index < len(blob):
174 assert index == len(blob)
175 return res.strip().replace(" ", "-")
177 def get_guid_map(self):
178 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
181 res = self.ldb.search(base="cn=schema,cn=configuration,%s" % self.base_dn, \
182 expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
184 self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
186 res = self.ldb.search(base="cn=extended-rights,cn=configuration,%s" % self.base_dn, \
187 expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
189 self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
191 def get_sid_map(self):
192 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
195 res = self.ldb.search(base="%s" % self.base_dn, \
196 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
199 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
203 class Descriptor(object):
204 def __init__(self, connection, dn):
205 self.con = connection
207 self.sddl = self.con.get_descriptor_sddl(self.dn)
208 self.dacl_list = self.extract_dacl()
210 def extract_dacl(self):
211 """ Extracts the DACL as a list of ACE string (with the brakets).
214 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
215 except AttributeError:
217 return re.findall("(\(.*?\))", res)
219 def fix_guid(self, ace):
221 guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
222 # If there are not GUIDs to replace return the same ACE
227 name = self.con.guid_map[guid.lower()]
228 res = res.replace(guid, name)
230 # Do not bother if the GUID is not found in
231 # cn=Schema or cn=Extended-Rights
235 def fix_sid(self, ace):
237 sids = re.findall("S-[-0-9]+", res)
238 # If there are not SIDs to replace return the same ACE
243 name = self.con.sid_map[sid]
244 res = res.replace(sid, name)
246 # Do not bother if the SID is not found in baseDN
250 def fixit(self, ace):
251 """ Combine all replacement methods in one
254 res = self.fix_guid(res)
255 res = self.fix_sid(res)
258 def diff_1(self, other):
260 if len(self.dacl_list) != len(other.dacl_list):
261 res += 4*" " + "Difference in ACE count:\n"
262 res += 8*" " + "=> %s\n" % len(self.dacl_list)
263 res += 8*" " + "=> %s\n" % len(other.dacl_list)
271 self_ace = "%s" % self.dacl_list[i]
276 other_ace = "%s" % other.dacl_list[i]
279 if len(self_ace) + len(other_ace) == 0:
281 self_ace_fixed = "%s" % self.fixit(self_ace)
282 other_ace_fixed = "%s" % other.fixit(other_ace)
283 if self_ace_fixed != other_ace_fixed:
284 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
287 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
291 def diff_2(self, other):
293 if len(self.dacl_list) != len(other.dacl_list):
294 res += 4*" " + "Difference in ACE count:\n"
295 res += 8*" " + "=> %s\n" % len(self.dacl_list)
296 res += 8*" " + "=> %s\n" % len(other.dacl_list)
301 self_dacl_list_fixed = []
302 other_dacl_list_fixed = []
303 [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
304 [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
305 for ace in self_dacl_list_fixed:
307 other_dacl_list_fixed.index(ace)
309 self_aces.append(ace)
311 common_aces.append(ace)
312 self_aces = sorted(self_aces)
313 if len(self_aces) > 0:
314 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
315 for ace in self_aces:
316 res += 8*" " + ace + "\n"
318 for ace in other_dacl_list_fixed:
320 self_dacl_list_fixed.index(ace)
322 other_aces.append(ace)
324 common_aces.append(ace)
325 other_aces = sorted(other_aces)
326 if len(other_aces) > 0:
327 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
328 for ace in other_aces:
329 res += 8*" " + ace + "\n"
331 common_aces = sorted(list(set(common_aces)))
333 res += 4*" " + "ACEs found in both:\n"
334 for ace in common_aces:
335 res += 8*" " + ace + "\n"
336 return (self_aces == [] and other_aces == [], res)
338 class LDAPObject(object):
339 def __init__(self, connection, dn, summary):
340 self.con = connection
341 self.two_domains = self.con.two_domains
342 self.quiet = self.con.quiet
343 self.verbose = self.con.verbose
344 self.summary = summary
345 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
346 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
347 for x in self.con.server_names:
348 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
349 self.attributes = self.con.get_attributes(self.dn)
350 # Attributes that are considered always to be different e.g based on timestamp etc.
352 # One domain - two domain controllers
353 self.ignore_attributes = [
354 # Default Naming Context
355 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
356 "operatingSystemVersion","oEMInformation",
357 # Configuration Naming Context
358 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
359 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
360 # Schema Naming Context
362 self.dn_attributes = []
363 self.domain_attributes = []
364 self.servername_attributes = []
365 self.netbios_attributes = []
366 self.other_attributes = []
367 # Two domains - two domain controllers
370 self.ignore_attributes += [
371 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
372 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
373 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
374 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
375 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
376 # After Exchange preps
377 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
379 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
380 self.dn_attributes = [
381 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
382 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
383 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
384 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
385 # After Exchange preps
386 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
387 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
388 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
389 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
390 self.dn_attributes = [x.upper() for x in self.dn_attributes]
392 # Attributes that contain the Domain name e.g. 'samba.org'
393 self.domain_attributes = [
394 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
395 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
396 self.domain_attributes = [x.upper() for x in self.domain_attributes]
398 # May contain DOMAIN_NETBIOS and SERVER_NAME
399 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
400 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
401 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
402 self.servername_attributes = [x.upper() for x in self.servername_attributes]
404 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
405 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
407 self.other_attributes = [ "name", "DC",]
408 self.other_attributes = [x.upper() for x in self.other_attributes]
410 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
414 Log on the screen if there is no --quiet oprion set
421 if not self.two_domains:
423 if res.upper().endswith(self.con.base_dn.upper()):
424 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
427 def fix_domain_name(self, s):
429 if not self.two_domains:
431 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
432 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
435 def fix_domain_netbios(self, s):
437 if not self.two_domains:
439 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
440 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
443 def fix_server_name(self, s):
445 if not self.two_domains or len(self.con.server_names) > 1:
447 for x in self.con.server_names:
448 res = res.upper().replace(x, "${SERVER_NAME}")
451 def __eq__(self, other):
452 if self.con.descriptor:
453 return self.cmp_desc(other)
454 return self.cmp_attrs(other)
456 def cmp_desc(self, other):
457 d1 = Descriptor(self.con, self.dn)
458 d2 = Descriptor(other.con, other.dn)
459 if self.con.view == "section":
461 elif self.con.view == "collision":
464 raise Exception("Unknown --view option value.")
466 self.screen_output = res[1][:-1]
467 other.screen_output = res[1][:-1]
471 def cmp_attrs(self, other):
473 self.unique_attrs = []
474 self.df_value_attrs = []
475 other.unique_attrs = []
476 if self.attributes.keys() != other.attributes.keys():
478 title = 4*" " + "Attributes found only in %s:" % self.con.host
479 for x in self.attributes.keys():
480 if not x in other.attributes.keys() and \
481 not x.upper() in [q.upper() for q in other.ignore_attributes]:
485 res += 8*" " + x + "\n"
486 self.unique_attrs.append(x)
488 title = 4*" " + "Attributes found only in %s:" % other.con.host
489 for x in other.attributes.keys():
490 if not x in self.attributes.keys() and \
491 not x.upper() in [q.upper() for q in self.ignore_attributes]:
495 res += 8*" " + x + "\n"
496 other.unique_attrs.append(x)
498 missing_attrs = [x.upper() for x in self.unique_attrs]
499 missing_attrs += [x.upper() for x in other.unique_attrs]
500 title = 4*" " + "Difference in attribute values:"
501 for x in self.attributes.keys():
502 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
504 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
505 self.attributes[x] = sorted(self.attributes[x])
506 other.attributes[x] = sorted(other.attributes[x])
507 if self.attributes[x] != other.attributes[x]:
512 # First check if the difference can be fixed but shunting the first part
513 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
514 if x.upper() in self.other_attributes:
515 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
516 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
519 # Attribute values that are list that contain DN based values that may differ
520 elif x.upper() in self.dn_attributes:
524 m = self.attributes[x]
525 n = other.attributes[x]
526 p = [self.fix_dn(j) for j in m]
527 q = [other.fix_dn(j) for j in n]
530 # Attributes that contain the Domain name in them
531 if x.upper() in self.domain_attributes:
535 m = self.attributes[x]
536 n = other.attributes[x]
537 p = [self.fix_domain_name(j) for j in m]
538 q = [other.fix_domain_name(j) for j in n]
542 if x.upper() in self.servername_attributes:
543 # Attributes with SERVER_NAME
547 m = self.attributes[x]
548 n = other.attributes[x]
549 p = [self.fix_server_name(j) for j in m]
550 q = [other.fix_server_name(j) for j in n]
554 if x.upper() in self.netbios_attributes:
555 # Attributes with NETBIOS Domain name
559 m = self.attributes[x]
560 n = other.attributes[x]
561 p = [self.fix_domain_netbios(j) for j in m]
562 q = [other.fix_domain_netbios(j) for j in n]
570 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
572 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
573 self.df_value_attrs.append(x)
575 if self.unique_attrs + other.unique_attrs != []:
576 assert self.unique_attrs != other.unique_attrs
577 self.summary["unique_attrs"] += self.unique_attrs
578 self.summary["df_value_attrs"] += self.df_value_attrs
579 other.summary["unique_attrs"] += other.unique_attrs
580 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
582 self.screen_output = res[:-1]
583 other.screen_output = res[:-1]
588 class LDAPBundel(object):
589 def __init__(self, connection, context, dn_list=None):
590 self.con = connection
591 self.two_domains = self.con.two_domains
592 self.quiet = self.con.quiet
593 self.verbose = self.con.verbose
595 self.summary["unique_attrs"] = []
596 self.summary["df_value_attrs"] = []
597 self.summary["known_ignored_dn"] = []
598 self.summary["abnormal_ignored_dn"] = []
600 self.dn_list = dn_list
601 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
602 self.context = context.upper()
603 self.dn_list = self.get_dn_list(context)
605 raise Exception("Unknown initialization data for LDAPBundel().")
607 while counter < len(self.dn_list) and self.two_domains:
608 # Use alias reference
609 tmp = self.dn_list[counter]
610 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
611 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
612 if len(self.con.server_names) == 1:
613 for x in self.con.server_names:
614 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
615 self.dn_list[counter] = tmp
617 self.dn_list = list(set(self.dn_list))
618 self.dn_list = sorted(self.dn_list)
619 self.size = len(self.dn_list)
623 Log on the screen if there is no --quiet oprion set
628 def update_size(self):
629 self.size = len(self.dn_list)
630 self.dn_list = sorted(self.dn_list)
632 def __eq__(self, other):
634 if self.size != other.size:
635 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
638 title= "\n* DNs found only in %s:" % self.con.host
639 for x in self.dn_list:
640 if not x.upper() in [q.upper() for q in other.dn_list]:
645 self.log( 4*" " + x )
646 self.dn_list[self.dn_list.index(x)] = ""
647 self.dn_list = [x for x in self.dn_list if x]
649 title= "\n* DNs found only in %s:" % other.con.host
650 for x in other.dn_list:
651 if not x.upper() in [q.upper() for q in self.dn_list]:
656 self.log( 4*" " + x )
657 other.dn_list[other.dn_list.index(x)] = ""
658 other.dn_list = [x for x in other.dn_list if x]
662 assert self.size == other.size
663 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
664 self.log( "\n* Objects to be compared: %s" % self.size )
667 while index < self.size:
670 object1 = LDAPObject(connection=self.con,
671 dn=self.dn_list[index],
672 summary=self.summary)
673 except LdbError, (enum, estr):
674 if enum == ERR_NO_SUCH_OBJECT:
675 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
679 object2 = LDAPObject(connection=other.con,
680 dn=other.dn_list[index],
681 summary=other.summary)
682 except LdbError, (enum, estr):
683 if enum == ERR_NO_SUCH_OBJECT:
684 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
690 if object1 == object2:
692 self.log( "\nComparing:" )
693 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
694 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
695 self.log( 4*" " + "OK" )
697 self.log( "\nComparing:" )
698 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
699 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
700 self.log( object1.screen_output )
701 self.log( 4*" " + "FAILED" )
703 self.summary = object1.summary
704 other.summary = object2.summary
709 def get_dn_list(self, context):
710 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
711 Parse all DNs and filter those that are 'strange' or abnormal.
713 if context.upper() == "DOMAIN":
714 search_base = "%s" % self.con.base_dn
715 elif context.upper() == "CONFIGURATION":
716 search_base = "CN=Configuration,%s" % self.con.base_dn
717 elif context.upper() == "SCHEMA":
718 search_base = "CN=Schema,CN=Configuration,%s" % self.con.base_dn
721 res = self.con.ldb.search(base=search_base, scope=SCOPE_SUBTREE, attrs=["dn"])
723 dn_list.append(x["dn"].get_linearized())
730 def print_summary(self):
731 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
732 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
734 if self.summary["unique_attrs"]:
735 self.log( "\nAttributes found only in %s:" % self.con.host )
736 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
738 if self.summary["df_value_attrs"]:
739 self.log( "\nAttributes with different values:" )
740 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
741 self.summary["df_value_attrs"] = []
743 class cmd_ldapcmp(Command):
744 """compare two ldap databases"""
745 synopsis = "ldapcmp URL1 URL2 <domain|configuration|schema> [options]"
747 takes_optiongroups = {
748 "sambaopts": options.SambaOptions,
749 "versionopts": options.VersionOptions,
750 "credopts": options.CredentialsOptionsDouble,
753 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
756 Option("-w", "--two", dest="two", action="store_true", default=False,
757 help="Hosts are in two different domains"),
758 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
759 help="Do not print anything but relay on just exit code"),
760 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
761 help="Print all DN pairs that have been compared"),
762 Option("--sd", dest="descriptor", action="store_true", default=False,
763 help="Compare nTSecurityDescriptor attibutes only"),
764 Option("--view", dest="view", default="section",
765 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision.")
768 def run(self, URL1, URL2,
769 context1=None, context2=None, context3=None,
770 two=False, quiet=False, verbose=False, descriptor=False, view="section",
771 credopts=None, sambaopts=None, versionopts=None):
772 lp = sambaopts.get_loadparm()
773 creds = credopts.get_credentials(lp, fallback_machine=True)
774 creds2 = credopts.get_credentials2(lp, False)
775 if creds2.is_anonymous():
777 if not creds.authentication_requested():
778 raise CommandError("You must supply at least one username/password pair")
780 # make a list of contexts to compare in
783 # if no argument given, we compare all contexts
784 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
786 for c in [context1, context2, context3]:
789 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
790 raise CommandError("Incorrect argument: %s" % c)
791 contexts.append(c.upper())
793 if verbose and quiet:
794 raise CommandError("You cannot set --verbose and --quiet together")
795 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
796 raise CommandError("Unknown --view option value. Choose from: section or collision.")
798 con1 = LDAPBase(URL1, creds, lp,
799 two=two, quiet=quiet, descriptor=descriptor, verbose=verbose, view=view)
800 assert len(con1.base_dn) > 0
802 con2 = LDAPBase(URL2, creds2, lp,
803 two=two, quiet=quiet, descriptor=descriptor, verbose=verbose, view=view)
804 assert len(con2.base_dn) > 0
807 for context in contexts:
809 print "\n* Comparing [%s] context..." % context
811 b1 = LDAPBundel(con1, context=context)
812 b2 = LDAPBundel(con2, context=context)
816 print "\n* Result for [%s]: SUCCESS" % context
819 print "\n* Result for [%s]: FAILURE" % context
821 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
822 b2.summary["df_value_attrs"] = []
827 # mark exit status as FAILURE if a least one comparison failed
830 raise CommandError("Compare failed: %d" % status)