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 print "\n* Place-holders for %s:" % self.host
89 print 4*" " + "${DOMAIN_DN} => %s" % self.base_dn
90 print 4*" " + "${DOMAIN_NETBIOS} => %s" % self.domain_netbios
91 print 4*" " + "${SERVER_NAME} => %s" % self.server_names
92 print 4*" " + "${DOMAIN_NAME} => %s" % self.domain_name
94 def find_domain_sid(self):
95 res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
96 return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
98 def find_servers(self):
101 res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \
102 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
106 srv.append(x["cn"][0])
109 def find_netbios(self):
110 res = self.ldb.search(base="CN=Partitions,%s" % self.config_dn, \
111 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
114 if "nETBIOSName" in x.keys():
115 return x["nETBIOSName"][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_attribute_name(self, key):
134 """ Returns the real attribute name
135 It resolved ranged results e.g. member;range=0-1499
138 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
146 def get_attribute_values(self, object_dn, key, vals):
147 """ Returns list with all attribute values
148 It resolved ranged results e.g. member;range=0-1499
151 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
155 # no range, just return the values
161 # get additional values in a loop
162 # until we get a response with '*' at the end
165 n = "%s;range=%d-*" % (attr, hi + 1)
166 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
174 for key in res.keys():
180 if m.group(1) != attr:
184 fvals = list(res[key])
191 if fm.group(3) == "*":
192 # if we got "*" we're done
195 assert int(fm.group(2)) == hi + 1
196 hi = int(fm.group(3))
200 def get_attributes(self, object_dn):
201 """ Returns dict with all default visible attributes
203 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
206 # 'Dn' element is not iterable and we have it as 'distinguishedName'
208 for key in res.keys():
209 vals = list(res[key])
211 name = self.get_attribute_name(key)
212 res[name] = self.get_attribute_values(object_dn, key, vals)
216 def get_descriptor_sddl(self, object_dn):
217 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
218 desc = res[0]["nTSecurityDescriptor"][0]
219 desc = ndr_unpack(security.descriptor, desc)
220 return desc.as_sddl(self.domain_sid)
222 def guid_as_string(self, guid_blob):
223 """ Translate binary representation of schemaIDGUID to standard string representation.
224 @gid_blob: binary schemaIDGUID
226 blob = "%s" % guid_blob
227 stops = [4, 2, 2, 2, 6]
231 while x < len(stops):
235 c = hex(ord(blob[index])).replace("0x", "")
236 c = [None, "0" + c, c][len(c)]
237 if 2 * index < len(blob):
245 assert index == len(blob)
246 return res.strip().replace(" ", "-")
248 def get_guid_map(self):
249 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
252 res = self.ldb.search(base=self.schema_dn,
253 expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
255 self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
257 res = self.ldb.search(base="cn=extended-rights,%s" % self.config_dn,
258 expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
260 self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
262 def get_sid_map(self):
263 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
266 res = self.ldb.search(base=self.base_dn,
267 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
270 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
274 class Descriptor(object):
275 def __init__(self, connection, dn):
276 self.con = connection
278 self.sddl = self.con.get_descriptor_sddl(self.dn)
279 self.dacl_list = self.extract_dacl()
280 if self.con.sort_aces:
281 self.dacl_list.sort()
283 def extract_dacl(self):
284 """ Extracts the DACL as a list of ACE string (with the brakets).
287 if "S:" in self.sddl:
288 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
290 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
291 except AttributeError:
293 return re.findall("(\(.*?\))", res)
295 def fix_guid(self, ace):
297 guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
298 # If there are not GUIDs to replace return the same ACE
303 name = self.con.guid_map[guid.lower()]
304 res = res.replace(guid, name)
306 # Do not bother if the GUID is not found in
307 # cn=Schema or cn=Extended-Rights
311 def fix_sid(self, ace):
313 sids = re.findall("S-[-0-9]+", res)
314 # If there are not SIDs to replace return the same ACE
319 name = self.con.sid_map[sid]
320 res = res.replace(sid, name)
322 # Do not bother if the SID is not found in baseDN
326 def fixit(self, ace):
327 """ Combine all replacement methods in one
330 res = self.fix_guid(res)
331 res = self.fix_sid(res)
334 def diff_1(self, other):
336 if len(self.dacl_list) != len(other.dacl_list):
337 res += 4*" " + "Difference in ACE count:\n"
338 res += 8*" " + "=> %s\n" % len(self.dacl_list)
339 res += 8*" " + "=> %s\n" % len(other.dacl_list)
347 self_ace = "%s" % self.dacl_list[i]
352 other_ace = "%s" % other.dacl_list[i]
355 if len(self_ace) + len(other_ace) == 0:
357 self_ace_fixed = "%s" % self.fixit(self_ace)
358 other_ace_fixed = "%s" % other.fixit(other_ace)
359 if self_ace_fixed != other_ace_fixed:
360 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
363 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
367 def diff_2(self, other):
369 if len(self.dacl_list) != len(other.dacl_list):
370 res += 4*" " + "Difference in ACE count:\n"
371 res += 8*" " + "=> %s\n" % len(self.dacl_list)
372 res += 8*" " + "=> %s\n" % len(other.dacl_list)
377 self_dacl_list_fixed = []
378 other_dacl_list_fixed = []
379 [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
380 [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
381 for ace in self_dacl_list_fixed:
383 other_dacl_list_fixed.index(ace)
385 self_aces.append(ace)
387 common_aces.append(ace)
388 self_aces = sorted(self_aces)
389 if len(self_aces) > 0:
390 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
391 for ace in self_aces:
392 res += 8*" " + ace + "\n"
394 for ace in other_dacl_list_fixed:
396 self_dacl_list_fixed.index(ace)
398 other_aces.append(ace)
400 common_aces.append(ace)
401 other_aces = sorted(other_aces)
402 if len(other_aces) > 0:
403 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
404 for ace in other_aces:
405 res += 8*" " + ace + "\n"
407 common_aces = sorted(list(set(common_aces)))
409 res += 4*" " + "ACEs found in both:\n"
410 for ace in common_aces:
411 res += 8*" " + ace + "\n"
412 return (self_aces == [] and other_aces == [], res)
414 class LDAPObject(object):
415 def __init__(self, connection, dn, summary):
416 self.con = connection
417 self.two_domains = self.con.two_domains
418 self.quiet = self.con.quiet
419 self.verbose = self.con.verbose
420 self.summary = summary
421 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
422 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
423 for x in self.con.server_names:
424 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
425 self.attributes = self.con.get_attributes(self.dn)
426 # Attributes that are considered always to be different e.g based on timestamp etc.
428 # One domain - two domain controllers
429 self.ignore_attributes = [
430 # Default Naming Context
431 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
432 "operatingSystemVersion","oEMInformation",
433 # Configuration Naming Context
434 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
435 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
436 # Schema Naming Context
438 self.dn_attributes = []
439 self.domain_attributes = []
440 self.servername_attributes = []
441 self.netbios_attributes = []
442 self.other_attributes = []
443 # Two domains - two domain controllers
446 self.ignore_attributes += [
447 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
448 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
449 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
450 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
451 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
452 # After Exchange preps
453 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
455 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
456 self.dn_attributes = [
457 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
458 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
459 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
460 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
461 # After Exchange preps
462 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
463 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
464 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
465 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
466 self.dn_attributes = [x.upper() for x in self.dn_attributes]
468 # Attributes that contain the Domain name e.g. 'samba.org'
469 self.domain_attributes = [
470 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
471 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
472 self.domain_attributes = [x.upper() for x in self.domain_attributes]
474 # May contain DOMAIN_NETBIOS and SERVER_NAME
475 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
476 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
477 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
478 self.servername_attributes = [x.upper() for x in self.servername_attributes]
480 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
481 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
483 self.other_attributes = [ "name", "DC",]
484 self.other_attributes = [x.upper() for x in self.other_attributes]
486 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
490 Log on the screen if there is no --quiet oprion set
497 if not self.two_domains:
499 if res.upper().endswith(self.con.base_dn.upper()):
500 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
503 def fix_domain_name(self, s):
505 if not self.two_domains:
507 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
508 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
511 def fix_domain_netbios(self, s):
513 if not self.two_domains:
515 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
516 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
519 def fix_server_name(self, s):
521 if not self.two_domains or len(self.con.server_names) > 1:
523 for x in self.con.server_names:
524 res = res.upper().replace(x, "${SERVER_NAME}")
527 def __eq__(self, other):
528 if self.con.descriptor:
529 return self.cmp_desc(other)
530 return self.cmp_attrs(other)
532 def cmp_desc(self, other):
533 d1 = Descriptor(self.con, self.dn)
534 d2 = Descriptor(other.con, other.dn)
535 if self.con.view == "section":
537 elif self.con.view == "collision":
540 raise Exception("Unknown --view option value.")
542 self.screen_output = res[1][:-1]
543 other.screen_output = res[1][:-1]
547 def cmp_attrs(self, other):
549 self.unique_attrs = []
550 self.df_value_attrs = []
551 other.unique_attrs = []
552 if self.attributes.keys() != other.attributes.keys():
554 title = 4*" " + "Attributes found only in %s:" % self.con.host
555 for x in self.attributes.keys():
556 if not x in other.attributes.keys() and \
557 not x.upper() in [q.upper() for q in other.ignore_attributes]:
561 res += 8*" " + x + "\n"
562 self.unique_attrs.append(x)
564 title = 4*" " + "Attributes found only in %s:" % other.con.host
565 for x in other.attributes.keys():
566 if not x in self.attributes.keys() and \
567 not x.upper() in [q.upper() for q in self.ignore_attributes]:
571 res += 8*" " + x + "\n"
572 other.unique_attrs.append(x)
574 missing_attrs = [x.upper() for x in self.unique_attrs]
575 missing_attrs += [x.upper() for x in other.unique_attrs]
576 title = 4*" " + "Difference in attribute values:"
577 for x in self.attributes.keys():
578 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
580 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
581 self.attributes[x] = sorted(self.attributes[x])
582 other.attributes[x] = sorted(other.attributes[x])
583 if self.attributes[x] != other.attributes[x]:
588 # First check if the difference can be fixed but shunting the first part
589 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
590 if x.upper() in self.other_attributes:
591 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
592 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
595 # Attribute values that are list that contain DN based values that may differ
596 elif x.upper() in self.dn_attributes:
600 m = self.attributes[x]
601 n = other.attributes[x]
602 p = [self.fix_dn(j) for j in m]
603 q = [other.fix_dn(j) for j in n]
606 # Attributes that contain the Domain name in them
607 if x.upper() in self.domain_attributes:
611 m = self.attributes[x]
612 n = other.attributes[x]
613 p = [self.fix_domain_name(j) for j in m]
614 q = [other.fix_domain_name(j) for j in n]
618 if x.upper() in self.servername_attributes:
619 # Attributes with SERVER_NAME
623 m = self.attributes[x]
624 n = other.attributes[x]
625 p = [self.fix_server_name(j) for j in m]
626 q = [other.fix_server_name(j) for j in n]
630 if x.upper() in self.netbios_attributes:
631 # Attributes with NETBIOS Domain name
635 m = self.attributes[x]
636 n = other.attributes[x]
637 p = [self.fix_domain_netbios(j) for j in m]
638 q = [other.fix_domain_netbios(j) for j in n]
646 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
648 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
649 self.df_value_attrs.append(x)
651 if self.unique_attrs + other.unique_attrs != []:
652 assert self.unique_attrs != other.unique_attrs
653 self.summary["unique_attrs"] += self.unique_attrs
654 self.summary["df_value_attrs"] += self.df_value_attrs
655 other.summary["unique_attrs"] += other.unique_attrs
656 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
658 self.screen_output = res[:-1]
659 other.screen_output = res[:-1]
664 class LDAPBundel(object):
665 def __init__(self, connection, context, dn_list=None):
666 self.con = connection
667 self.two_domains = self.con.two_domains
668 self.quiet = self.con.quiet
669 self.verbose = self.con.verbose
670 self.search_base = self.con.search_base
671 self.search_scope = self.con.search_scope
673 self.summary["unique_attrs"] = []
674 self.summary["df_value_attrs"] = []
675 self.summary["known_ignored_dn"] = []
676 self.summary["abnormal_ignored_dn"] = []
678 self.dn_list = dn_list
679 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
680 self.context = context.upper()
681 self.dn_list = self.get_dn_list(context)
683 raise Exception("Unknown initialization data for LDAPBundel().")
685 while counter < len(self.dn_list) and self.two_domains:
686 # Use alias reference
687 tmp = self.dn_list[counter]
688 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
689 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
690 if len(self.con.server_names) == 1:
691 for x in self.con.server_names:
692 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
693 self.dn_list[counter] = tmp
695 self.dn_list = list(set(self.dn_list))
696 self.dn_list = sorted(self.dn_list)
697 self.size = len(self.dn_list)
701 Log on the screen if there is no --quiet oprion set
706 def update_size(self):
707 self.size = len(self.dn_list)
708 self.dn_list = sorted(self.dn_list)
710 def __eq__(self, other):
712 if self.size != other.size:
713 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
716 # This is the case where we want to explicitly compare two objects with different DNs.
717 # It does not matter if they are in the same DC, in two DC in one domain or in two
719 if self.search_scope != SCOPE_BASE:
720 title= "\n* DNs found only in %s:" % self.con.host
721 for x in self.dn_list:
722 if not x.upper() in [q.upper() for q in other.dn_list]:
727 self.log( 4*" " + x )
728 self.dn_list[self.dn_list.index(x)] = ""
729 self.dn_list = [x for x in self.dn_list if x]
731 title= "\n* DNs found only in %s:" % other.con.host
732 for x in other.dn_list:
733 if not x.upper() in [q.upper() for q in self.dn_list]:
738 self.log( 4*" " + x )
739 other.dn_list[other.dn_list.index(x)] = ""
740 other.dn_list = [x for x in other.dn_list if x]
744 assert self.size == other.size
745 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
746 self.log( "\n* Objects to be compared: %s" % self.size )
749 while index < self.size:
752 object1 = LDAPObject(connection=self.con,
753 dn=self.dn_list[index],
754 summary=self.summary)
755 except LdbError, (enum, estr):
756 if enum == ERR_NO_SUCH_OBJECT:
757 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
761 object2 = LDAPObject(connection=other.con,
762 dn=other.dn_list[index],
763 summary=other.summary)
764 except LdbError, (enum, estr):
765 if enum == ERR_NO_SUCH_OBJECT:
766 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
772 if object1 == object2:
774 self.log( "\nComparing:" )
775 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
776 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
777 self.log( 4*" " + "OK" )
779 self.log( "\nComparing:" )
780 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
781 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
782 self.log( object1.screen_output )
783 self.log( 4*" " + "FAILED" )
785 self.summary = object1.summary
786 other.summary = object2.summary
791 def get_dn_list(self, context):
792 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
793 Parse all DNs and filter those that are 'strange' or abnormal.
795 if context.upper() == "DOMAIN":
796 search_base = self.con.base_dn
797 elif context.upper() == "CONFIGURATION":
798 search_base = self.con.config_dn
799 elif context.upper() == "SCHEMA":
800 search_base = self.con.schema_dn
803 if not self.search_base:
804 self.search_base = search_base
805 self.search_scope = self.search_scope.upper()
806 if self.search_scope == "SUB":
807 self.search_scope = SCOPE_SUBTREE
808 elif self.search_scope == "BASE":
809 self.search_scope = SCOPE_BASE
810 elif self.search_scope == "ONE":
811 self.search_scope = SCOPE_ONELEVEL
813 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
814 if not self.search_base.upper().endswith(search_base.upper()):
815 raise StandardError("Invalid search base specified: %s" % self.search_base)
816 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
818 dn_list.append(x["dn"].get_linearized())
824 def print_summary(self):
825 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
826 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
828 if self.summary["unique_attrs"]:
829 self.log( "\nAttributes found only in %s:" % self.con.host )
830 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
832 if self.summary["df_value_attrs"]:
833 self.log( "\nAttributes with different values:" )
834 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
835 self.summary["df_value_attrs"] = []
837 class cmd_ldapcmp(Command):
838 """compare two ldap databases"""
839 synopsis = "ldapcmp URL1 URL2 <domain|configuration|schema> [options]"
841 takes_optiongroups = {
842 "sambaopts": options.SambaOptions,
843 "versionopts": options.VersionOptions,
844 "credopts": options.CredentialsOptionsDouble,
847 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
850 Option("-w", "--two", dest="two", action="store_true", default=False,
851 help="Hosts are in two different domains"),
852 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
853 help="Do not print anything but relay on just exit code"),
854 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
855 help="Print all DN pairs that have been compared"),
856 Option("--sd", dest="descriptor", action="store_true", default=False,
857 help="Compare nTSecurityDescriptor attibutes only"),
858 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
859 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
860 Option("--view", dest="view", default="section",
861 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
862 Option("--base", dest="base", default="",
863 help="Pass search base that will build DN list for the first DC."),
864 Option("--base2", dest="base2", default="",
865 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
866 Option("--scope", dest="scope", default="SUB",
867 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
870 def run(self, URL1, URL2,
871 context1=None, context2=None, context3=None,
872 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False, view="section",
873 base="", base2="", scope="SUB", credopts=None, sambaopts=None, versionopts=None):
875 lp = sambaopts.get_loadparm()
877 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
880 creds = credopts.get_credentials(lp, fallback_machine=True)
883 creds2 = credopts.get_credentials2(lp, guess=False)
884 if creds2.is_anonymous():
887 creds2.set_domain("")
888 creds2.set_workstation("")
889 if using_ldap and not creds.authentication_requested():
890 raise CommandError("You must supply at least one username/password pair")
892 # make a list of contexts to compare in
896 # If search bases are specified context is defaulted to
897 # DOMAIN so the given search bases can be verified.
898 contexts = ["DOMAIN"]
900 # if no argument given, we compare all contexts
901 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
903 for c in [context1, context2, context3]:
906 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
907 raise CommandError("Incorrect argument: %s" % c)
908 contexts.append(c.upper())
910 if verbose and quiet:
911 raise CommandError("You cannot set --verbose and --quiet together")
912 if (not base and base2) or (base and not base2):
913 raise CommandError("You need to specify both --base and --base2 at the same time")
914 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
915 raise CommandError("Invalid --view value. Choose from: section or collision")
916 if not scope.upper() in ["SUB", "ONE", "BASE"]:
917 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
919 con1 = LDAPBase(URL1, creds, lp,
920 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
921 verbose=verbose,view=view, base=base, scope=scope)
922 assert len(con1.base_dn) > 0
924 con2 = LDAPBase(URL2, creds2, lp,
925 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
926 verbose=verbose, view=view, base=base2, scope=scope)
927 assert len(con2.base_dn) > 0
930 for context in contexts:
932 print "\n* Comparing [%s] context..." % context
934 b1 = LDAPBundel(con1, context=context)
935 b2 = LDAPBundel(con2, context=context)
939 print "\n* Result for [%s]: SUCCESS" % context
942 print "\n* Result for [%s]: FAILURE" % context
944 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
945 b2.summary["df_value_attrs"] = []
950 # mark exit status as FAILURE if a least one comparison failed
953 raise CommandError("Compare failed: %d" % status)