summaryrefslogtreecommitdiff
path: root/source4/scripting/python
diff options
context:
space:
mode:
authorAndrew Tridgell <tridge@samba.org>2010-11-29 14:10:26 +1100
committerAndrew Tridgell <tridge@samba.org>2010-11-29 18:04:42 +1100
commitabe9ac53f0d240a867d499f184866603143756cf (patch)
treefcb5c963ff38f285ad68ba1e78170749d28f8899 /source4/scripting/python
parentf8d73e466b454a63f256021ad2f353e9ad93e8f7 (diff)
downloadsamba-abe9ac53f0d240a867d499f184866603143756cf.tar.gz
samba-abe9ac53f0d240a867d499f184866603143756cf.tar.bz2
samba-abe9ac53f0d240a867d499f184866603143756cf.zip
s4-ldapcmp: make ldapcmp a samba-tool command
The ldapcmp tool is very useful, and should be available to Samba admins, not just developers. This makes it a samba-tool command, which also gives it the nicer command line handling that samba-tool has
Diffstat (limited to 'source4/scripting/python')
-rw-r--r--source4/scripting/python/samba/netcmd/__init__.py2
-rwxr-xr-xsource4/scripting/python/samba/netcmd/ldapcmp.py830
2 files changed, 832 insertions, 0 deletions
diff --git a/source4/scripting/python/samba/netcmd/__init__.py b/source4/scripting/python/samba/netcmd/__init__.py
index d47b39dced..5a6a68cd29 100644
--- a/source4/scripting/python/samba/netcmd/__init__.py
+++ b/source4/scripting/python/samba/netcmd/__init__.py
@@ -172,3 +172,5 @@ from samba.netcmd.drs import cmd_drs
commands["drs"] = cmd_drs()
from samba.netcmd.gpo import cmd_gpo
commands["gpo2"] = cmd_gpo()
+from samba.netcmd.ldapcmp import cmd_ldapcmp
+commands["ldapcmp"] = cmd_ldapcmp()
diff --git a/source4/scripting/python/samba/netcmd/ldapcmp.py b/source4/scripting/python/samba/netcmd/ldapcmp.py
new file mode 100755
index 0000000000..8bb1c8a8c7
--- /dev/null
+++ b/source4/scripting/python/samba/netcmd/ldapcmp.py
@@ -0,0 +1,830 @@
+#!/usr/bin/env python
+#
+# Unix SMB/CIFS implementation.
+# A command to compare differences of objects and attributes between
+# two LDAP servers both running at the same time. It generally compares
+# one of the three pratitions DOMAIN, CONFIGURATION or SCHEMA. Users
+# that have to be provided sheould be able to read objects in any of the
+# above partitions.
+
+# Copyright (C) Zahari Zahariev <zahari.zahariev@postpath.com> 2009, 2010
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import re
+import sys
+
+import samba
+import samba.getopt as options
+from samba import Ldb
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.dcerpc import security
+from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, ERR_NO_SUCH_OBJECT, LdbError
+from samba.netcmd import (
+ Command,
+ CommandError,
+ Option,
+ SuperCommand,
+ )
+
+global summary
+summary = {}
+
+class LDAPBase(object):
+
+ def __init__(self, host, creds, lp,
+ two=False, quiet=False, descriptor=False, verbose=False,
+ view="section"):
+ ldb_options = []
+ samdb_url = host
+ if not "://" in host:
+ if os.path.isfile(host):
+ samdb_url = "tdb://%s" % host
+ else:
+ samdb_url = "ldap://%s" % host
+ # use 'paged_search' module when connecting remotely
+ if samdb_url.lower().startswith("ldap://"):
+ ldb_options = ["modules:paged_searches"]
+ self.ldb = Ldb(url=samdb_url,
+ credentials=creds,
+ lp=lp,
+ options=ldb_options)
+ self.two_domains = two
+ self.quiet = quiet
+ self.descriptor = descriptor
+ self.view = view
+ self.verbose = verbose
+ self.host = host
+ self.base_dn = self.find_basedn()
+ self.domain_netbios = self.find_netbios()
+ self.server_names = self.find_servers()
+ self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
+ self.domain_sid = self.find_domain_sid()
+ self.get_guid_map()
+ self.get_sid_map()
+ #
+ # Log some domain controller specific place-holers that are being used
+ # when compare content of two DCs. Uncomment for DEBUG purposes.
+ if self.two_domains and not self.quiet:
+ print "\n* Place-holders for %s:" % self.host
+ print 4*" " + "${DOMAIN_DN} => %s" % self.base_dn
+ print 4*" " + "${DOMAIN_NETBIOS} => %s" % self.domain_netbios
+ print 4*" " + "${SERVER_NAME} => %s" % self.server_names
+ print 4*" " + "${DOMAIN_NAME} => %s" % self.domain_name
+
+ def find_domain_sid(self):
+ res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
+ return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
+
+ def find_servers(self):
+ """
+ """
+ res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \
+ scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
+ assert len(res) > 0
+ srv = []
+ for x in res:
+ srv.append(x["cn"][0])
+ return srv
+
+ def find_netbios(self):
+ res = self.ldb.search(base="CN=Partitions,CN=Configuration,%s" % self.base_dn, \
+ scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
+ assert len(res) > 0
+ for x in res:
+ if "nETBIOSName" in x.keys():
+ return x["nETBIOSName"][0]
+
+ def find_basedn(self):
+ res = self.ldb.search(base="", expression="(objectClass=*)", scope=SCOPE_BASE,
+ attrs=["defaultNamingContext"])
+ assert len(res) == 1
+ return res[0]["defaultNamingContext"][0]
+
+ def object_exists(self, object_dn):
+ res = None
+ try:
+ res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
+ except LdbError, (enum, estr):
+ if enum == ERR_NO_SUCH_OBJECT:
+ return False
+ raise
+ return len(res) == 1
+
+ def delete_force(self, object_dn):
+ try:
+ self.ldb.delete(object_dn)
+ except Ldb.LdbError, e:
+ assert "No such object" in str(e)
+
+ def get_attributes(self, object_dn):
+ """ Returns dict with all default visible attributes
+ """
+ res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
+ assert len(res) == 1
+ res = dict(res[0])
+ # 'Dn' element is not iterable and we have it as 'distinguishedName'
+ del res["dn"]
+ for key in res.keys():
+ res[key] = list(res[key])
+ return res
+
+ def get_descriptor_sddl(self, object_dn):
+ res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ return desc.as_sddl(self.domain_sid)
+
+ def guid_as_string(self, guid_blob):
+ """ Translate binary representation of schemaIDGUID to standard string representation.
+ @gid_blob: binary schemaIDGUID
+ """
+ blob = "%s" % guid_blob
+ stops = [4, 2, 2, 2, 6]
+ index = 0
+ res = ""
+ x = 0
+ while x < len(stops):
+ tmp = ""
+ y = 0
+ while y < stops[x]:
+ c = hex(ord(blob[index])).replace("0x", "")
+ c = [None, "0" + c, c][len(c)]
+ if 2 * index < len(blob):
+ tmp = c + tmp
+ else:
+ tmp += c
+ index += 1
+ y += 1
+ res += tmp + " "
+ x += 1
+ assert index == len(blob)
+ return res.strip().replace(" ", "-")
+
+ def get_guid_map(self):
+ """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
+ """
+ self.guid_map = {}
+ res = self.ldb.search(base="cn=schema,cn=configuration,%s" % self.base_dn, \
+ expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
+ for item in res:
+ self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
+ #
+ res = self.ldb.search(base="cn=extended-rights,cn=configuration,%s" % self.base_dn, \
+ expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
+ for item in res:
+ self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
+
+ def get_sid_map(self):
+ """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
+ """
+ self.sid_map = {}
+ res = self.ldb.search(base="%s" % self.base_dn, \
+ expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
+ for item in res:
+ try:
+ self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
+ except KeyError:
+ pass
+
+class Descriptor(object):
+ def __init__(self, connection, dn):
+ self.con = connection
+ self.dn = dn
+ self.sddl = self.con.get_descriptor_sddl(self.dn)
+ self.dacl_list = self.extract_dacl()
+
+ def extract_dacl(self):
+ """ Extracts the DACL as a list of ACE string (with the brakets).
+ """
+ try:
+ res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
+ except AttributeError:
+ return []
+ return re.findall("(\(.*?\))", res)
+
+ def fix_guid(self, ace):
+ res = "%s" % ace
+ guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
+ # If there are not GUIDs to replace return the same ACE
+ if len(guids) == 0:
+ return res
+ for guid in guids:
+ try:
+ name = self.con.guid_map[guid.lower()]
+ res = res.replace(guid, name)
+ except KeyError:
+ # Do not bother if the GUID is not found in
+ # cn=Schema or cn=Extended-Rights
+ pass
+ return res
+
+ def fix_sid(self, ace):
+ res = "%s" % ace
+ sids = re.findall("S-[-0-9]+", res)
+ # If there are not SIDs to replace return the same ACE
+ if len(sids) == 0:
+ return res
+ for sid in sids:
+ try:
+ name = self.con.sid_map[sid]
+ res = res.replace(sid, name)
+ except KeyError:
+ # Do not bother if the SID is not found in baseDN
+ pass
+ return res
+
+ def fixit(self, ace):
+ """ Combine all replacement methods in one
+ """
+ res = "%s" % ace
+ res = self.fix_guid(res)
+ res = self.fix_sid(res)
+ return res
+
+ def diff_1(self, other):
+ res = ""
+ if len(self.dacl_list) != len(other.dacl_list):
+ res += 4*" " + "Difference in ACE count:\n"
+ res += 8*" " + "=> %s\n" % len(self.dacl_list)
+ res += 8*" " + "=> %s\n" % len(other.dacl_list)
+ #
+ i = 0
+ flag = True
+ while True:
+ self_ace = None
+ other_ace = None
+ try:
+ self_ace = "%s" % self.dacl_list[i]
+ except IndexError:
+ self_ace = ""
+ #
+ try:
+ other_ace = "%s" % other.dacl_list[i]
+ except IndexError:
+ other_ace = ""
+ if len(self_ace) + len(other_ace) == 0:
+ break
+ self_ace_fixed = "%s" % self.fixit(self_ace)
+ other_ace_fixed = "%s" % other.fixit(other_ace)
+ if self_ace_fixed != other_ace_fixed:
+ res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
+ flag = False
+ else:
+ res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
+ i += 1
+ return (flag, res)
+
+ def diff_2(self, other):
+ res = ""
+ if len(self.dacl_list) != len(other.dacl_list):
+ res += 4*" " + "Difference in ACE count:\n"
+ res += 8*" " + "=> %s\n" % len(self.dacl_list)
+ res += 8*" " + "=> %s\n" % len(other.dacl_list)
+ #
+ common_aces = []
+ self_aces = []
+ other_aces = []
+ self_dacl_list_fixed = []
+ other_dacl_list_fixed = []
+ [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
+ [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
+ for ace in self_dacl_list_fixed:
+ try:
+ other_dacl_list_fixed.index(ace)
+ except ValueError:
+ self_aces.append(ace)
+ else:
+ common_aces.append(ace)
+ self_aces = sorted(self_aces)
+ if len(self_aces) > 0:
+ res += 4*" " + "ACEs found only in %s:\n" % self.con.host
+ for ace in self_aces:
+ res += 8*" " + ace + "\n"
+ #
+ for ace in other_dacl_list_fixed:
+ try:
+ self_dacl_list_fixed.index(ace)
+ except ValueError:
+ other_aces.append(ace)
+ else:
+ common_aces.append(ace)
+ other_aces = sorted(other_aces)
+ if len(other_aces) > 0:
+ res += 4*" " + "ACEs found only in %s:\n" % other.con.host
+ for ace in other_aces:
+ res += 8*" " + ace + "\n"
+ #
+ common_aces = sorted(list(set(common_aces)))
+ if self.con.verbose:
+ res += 4*" " + "ACEs found in both:\n"
+ for ace in common_aces:
+ res += 8*" " + ace + "\n"
+ return (self_aces == [] and other_aces == [], res)
+
+class LDAPObject(object):
+ def __init__(self, connection, dn, summary):
+ self.con = connection
+ self.two_domains = self.con.two_domains
+ self.quiet = self.con.quiet
+ self.verbose = self.con.verbose
+ self.summary = summary
+ self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
+ self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
+ for x in self.con.server_names:
+ self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
+ self.attributes = self.con.get_attributes(self.dn)
+ # Attributes that are considered always to be different e.g based on timestamp etc.
+ #
+ # One domain - two domain controllers
+ self.ignore_attributes = [
+ # Default Naming Context
+ "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
+ "operatingSystemVersion","oEMInformation",
+ # Configuration Naming Context
+ "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
+ "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
+ # Schema Naming Context
+ "prefixMap",]
+ self.dn_attributes = []
+ self.domain_attributes = []
+ self.servername_attributes = []
+ self.netbios_attributes = []
+ self.other_attributes = []
+ # Two domains - two domain controllers
+
+ if self.two_domains:
+ self.ignore_attributes += [
+ "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
+ "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
+ "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
+ "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
+ "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
+ # After Exchange preps
+ "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
+ #
+ # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
+ self.dn_attributes = [
+ "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
+ "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
+ "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
+ "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
+ # After Exchange preps
+ "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
+ "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
+ "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
+ "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
+ self.dn_attributes = [x.upper() for x in self.dn_attributes]
+ #
+ # Attributes that contain the Domain name e.g. 'samba.org'
+ self.domain_attributes = [
+ "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
+ "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
+ self.domain_attributes = [x.upper() for x in self.domain_attributes]
+ #
+ # May contain DOMAIN_NETBIOS and SERVER_NAME
+ self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
+ "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
+ "msDS-IsDomainFor", "interSiteTopologyGenerator",]
+ self.servername_attributes = [x.upper() for x in self.servername_attributes]
+ #
+ self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
+ self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
+ #
+ self.other_attributes = [ "name", "DC",]
+ self.other_attributes = [x.upper() for x in self.other_attributes]
+ #
+ self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
+
+ def log(self, msg):
+ """
+ Log on the screen if there is no --quiet oprion set
+ """
+ if not self.quiet:
+ print msg
+
+ def fix_dn(self, s):
+ res = "%s" % s
+ if not self.two_domains:
+ return res
+ if res.upper().endswith(self.con.base_dn.upper()):
+ res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
+ return res
+
+ def fix_domain_name(self, s):
+ res = "%s" % s
+ if not self.two_domains:
+ return res
+ res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
+ res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
+ return res
+
+ def fix_domain_netbios(self, s):
+ res = "%s" % s
+ if not self.two_domains:
+ return res
+ res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
+ res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
+ return res
+
+ def fix_server_name(self, s):
+ res = "%s" % s
+ if not self.two_domains or len(self.con.server_names) > 1:
+ return res
+ for x in self.con.server_names:
+ res = res.upper().replace(x, "${SERVER_NAME}")
+ return res
+
+ def __eq__(self, other):
+ if self.con.descriptor:
+ return self.cmp_desc(other)
+ return self.cmp_attrs(other)
+
+ def cmp_desc(self, other):
+ d1 = Descriptor(self.con, self.dn)
+ d2 = Descriptor(other.con, other.dn)
+ if self.con.view == "section":
+ res = d1.diff_2(d2)
+ elif self.con.view == "collision":
+ res = d1.diff_1(d2)
+ else:
+ raise Exception("Unknown --view option value.")
+ #
+ self.screen_output = res[1][:-1]
+ other.screen_output = res[1][:-1]
+ #
+ return res[0]
+
+ def cmp_attrs(self, other):
+ res = ""
+ self.unique_attrs = []
+ self.df_value_attrs = []
+ other.unique_attrs = []
+ if self.attributes.keys() != other.attributes.keys():
+ #
+ title = 4*" " + "Attributes found only in %s:" % self.con.host
+ for x in self.attributes.keys():
+ if not x in other.attributes.keys() and \
+ not x.upper() in [q.upper() for q in other.ignore_attributes]:
+ if title:
+ res += title + "\n"
+ title = None
+ res += 8*" " + x + "\n"
+ self.unique_attrs.append(x)
+ #
+ title = 4*" " + "Attributes found only in %s:" % other.con.host
+ for x in other.attributes.keys():
+ if not x in self.attributes.keys() and \
+ not x.upper() in [q.upper() for q in self.ignore_attributes]:
+ if title:
+ res += title + "\n"
+ title = None
+ res += 8*" " + x + "\n"
+ other.unique_attrs.append(x)
+ #
+ missing_attrs = [x.upper() for x in self.unique_attrs]
+ missing_attrs += [x.upper() for x in other.unique_attrs]
+ title = 4*" " + "Difference in attribute values:"
+ for x in self.attributes.keys():
+ if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
+ continue
+ if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
+ self.attributes[x] = sorted(self.attributes[x])
+ other.attributes[x] = sorted(other.attributes[x])
+ if self.attributes[x] != other.attributes[x]:
+ p = None
+ q = None
+ m = None
+ n = None
+ # First check if the difference can be fixed but shunting the first part
+ # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
+ if x.upper() in self.other_attributes:
+ p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
+ q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
+ if p == q:
+ continue
+ # Attribute values that are list that contain DN based values that may differ
+ elif x.upper() in self.dn_attributes:
+ m = p
+ n = q
+ if not p and not q:
+ m = self.attributes[x]
+ n = other.attributes[x]
+ p = [self.fix_dn(j) for j in m]
+ q = [other.fix_dn(j) for j in n]
+ if p == q:
+ continue
+ # Attributes that contain the Domain name in them
+ if x.upper() in self.domain_attributes:
+ m = p
+ n = q
+ if not p and not q:
+ m = self.attributes[x]
+ n = other.attributes[x]
+ p = [self.fix_domain_name(j) for j in m]
+ q = [other.fix_domain_name(j) for j in n]
+ if p == q:
+ continue
+ #
+ if x.upper() in self.servername_attributes:
+ # Attributes with SERVER_NAME
+ m = p
+ n = q
+ if not p and not q:
+ m = self.attributes[x]
+ n = other.attributes[x]
+ p = [self.fix_server_name(j) for j in m]
+ q = [other.fix_server_name(j) for j in n]
+ if p == q:
+ continue
+ #
+ if x.upper() in self.netbios_attributes:
+ # Attributes with NETBIOS Domain name
+ m = p
+ n = q
+ if not p and not q:
+ m = self.attributes[x]
+ n = other.attributes[x]
+ p = [self.fix_domain_netbios(j) for j in m]
+ q = [other.fix_domain_netbios(j) for j in n]
+ if p == q:
+ continue
+ #
+ if title:
+ res += title + "\n"
+ title = None
+ if p and q:
+ res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
+ else:
+ res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
+ self.df_value_attrs.append(x)
+ #
+ if self.unique_attrs + other.unique_attrs != []:
+ assert self.unique_attrs != other.unique_attrs
+ self.summary["unique_attrs"] += self.unique_attrs
+ self.summary["df_value_attrs"] += self.df_value_attrs
+ other.summary["unique_attrs"] += other.unique_attrs
+ other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
+ #
+ self.screen_output = res[:-1]
+ other.screen_output = res[:-1]
+ #
+ return res == ""
+
+
+class LDAPBundel(object):
+ def __init__(self, connection, context, dn_list=None):
+ self.con = connection
+ self.two_domains = self.con.two_domains
+ self.quiet = self.con.quiet
+ self.verbose = self.con.verbose
+ self.summary = {}
+ self.summary["unique_attrs"] = []
+ self.summary["df_value_attrs"] = []
+ self.summary["known_ignored_dn"] = []
+ self.summary["abnormal_ignored_dn"] = []
+ if dn_list:
+ self.dn_list = dn_list
+ elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
+ self.context = context.upper()
+ self.dn_list = self.get_dn_list(context)
+ else:
+ raise Exception("Unknown initialization data for LDAPBundel().")
+ counter = 0
+ while counter < len(self.dn_list) and self.two_domains:
+ # Use alias reference
+ tmp = self.dn_list[counter]
+ tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
+ tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
+ if len(self.con.server_names) == 1:
+ for x in self.con.server_names:
+ tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
+ self.dn_list[counter] = tmp
+ counter += 1
+ self.dn_list = list(set(self.dn_list))
+ self.dn_list = sorted(self.dn_list)
+ self.size = len(self.dn_list)
+
+ def log(self, msg):
+ """
+ Log on the screen if there is no --quiet oprion set
+ """
+ if not self.quiet:
+ print msg
+
+ def update_size(self):
+ self.size = len(self.dn_list)
+ self.dn_list = sorted(self.dn_list)
+
+ def __eq__(self, other):
+ res = True
+ if self.size != other.size:
+ self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
+ res = False
+ #
+ title= "\n* DNs found only in %s:" % self.con.host
+ for x in self.dn_list:
+ if not x.upper() in [q.upper() for q in other.dn_list]:
+ if title:
+ self.log( title )
+ title = None
+ res = False
+ self.log( 4*" " + x )
+ self.dn_list[self.dn_list.index(x)] = ""
+ self.dn_list = [x for x in self.dn_list if x]
+ #
+ title= "\n* DNs found only in %s:" % other.con.host
+ for x in other.dn_list:
+ if not x.upper() in [q.upper() for q in self.dn_list]:
+ if title:
+ self.log( title )
+ title = None
+ res = False
+ self.log( 4*" " + x )
+ other.dn_list[other.dn_list.index(x)] = ""
+ other.dn_list = [x for x in other.dn_list if x]
+ #
+ self.update_size()
+ other.update_size()
+ assert self.size == other.size
+ assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
+ self.log( "\n* Objects to be compared: %s" % self.size )
+
+ index = 0
+ while index < self.size:
+ skip = False
+ try:
+ object1 = LDAPObject(connection=self.con,
+ dn=self.dn_list[index],
+ summary=self.summary)
+ except LdbError, (enum, estr):
+ if enum == ERR_NO_SUCH_OBJECT:
+ self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
+ skip = True
+ raise
+ try:
+ object2 = LDAPObject(connection=other.con,
+ dn=other.dn_list[index],
+ summary=other.summary)
+ except LdbError, (enum, estr):
+ if enum == ERR_NO_SUCH_OBJECT:
+ self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
+ skip = True
+ raise
+ if skip:
+ index += 1
+ continue
+ if object1 == object2:
+ if self.con.verbose:
+ self.log( "\nComparing:" )
+ self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
+ self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
+ self.log( 4*" " + "OK" )
+ else:
+ self.log( "\nComparing:" )
+ self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
+ self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
+ self.log( object1.screen_output )
+ self.log( 4*" " + "FAILED" )
+ res = False
+ self.summary = object1.summary
+ other.summary = object2.summary
+ index += 1
+ #
+ return res
+
+ def get_dn_list(self, context):
+ """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
+ Parse all DNs and filter those that are 'strange' or abnormal.
+ """
+ if context.upper() == "DOMAIN":
+ search_base = "%s" % self.con.base_dn
+ elif context.upper() == "CONFIGURATION":
+ search_base = "CN=Configuration,%s" % self.con.base_dn
+ elif context.upper() == "SCHEMA":
+ search_base = "CN=Schema,CN=Configuration,%s" % self.con.base_dn
+
+ dn_list = []
+ res = self.con.ldb.search(base=search_base, scope=SCOPE_SUBTREE, attrs=["dn"])
+ for x in res:
+ dn_list.append(x["dn"].get_linearized())
+
+ #
+ global summary
+ #
+ return dn_list
+
+ def print_summary(self):
+ self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
+ self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
+ #
+ if self.summary["unique_attrs"]:
+ self.log( "\nAttributes found only in %s:" % self.con.host )
+ self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
+ #
+ if self.summary["df_value_attrs"]:
+ self.log( "\nAttributes with different values:" )
+ self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
+ self.summary["df_value_attrs"] = []
+
+class cmd_ldapcmp(Command):
+ """compare two ldap databases"""
+ synopsis = "ldapcmp URL1 URL2 <domain|configuration|schema> [options]"
+
+ takes_optiongroups = {
+ "sambaopts": options.SambaOptions,
+ "versionopts": options.VersionOptions,
+ "credopts": options.CredentialsOptionsDouble,
+ }
+
+ takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
+
+ takes_options = [
+ Option("-w", "--two", dest="two", action="store_true", default=False,
+ help="Hosts are in two different domains"),
+ Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
+ help="Do not print anything but relay on just exit code"),
+ Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
+ help="Print all DN pairs that have been compared"),
+ Option("--sd", dest="descriptor", action="store_true", default=False,
+ help="Compare nTSecurityDescriptor attibutes only"),
+ Option("--view", dest="view", default="section",
+ help="Display mode for nTSecurityDescriptor results. Possible values: section or collision.")
+ ]
+
+ def run(self, URL1, URL2,
+ context1=None, context2=None, context3=None,
+ two=False, quiet=False, verbose=False, descriptor=False, view="section",
+ credopts=None, sambaopts=None, versionopts=None):
+ lp = sambaopts.get_loadparm()
+ creds = credopts.get_credentials(lp)
+ creds2 = credopts.get_credentials2(lp, False)
+ if creds2.is_anonymous():
+ creds2 = creds
+ if not creds.authentication_requested():
+ raise CommandError("You must supply at least one username/password pair")
+
+ # make a list of contexts to compare in
+ contexts = []
+ if context1 is None:
+ # if no argument given, we compare all contexts
+ contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
+ else:
+ for c in [context1, context2, context3]:
+ if c is None:
+ continue
+ if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
+ raise CommandError("Incorrect argument: %s" % c)
+ contexts.append(c.upper())
+
+ if verbose and quiet:
+ raise CommandError("You cannot set --verbose and --quiet together")
+ if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
+ raise CommandError("Unknown --view option value. Choose from: section or collision.")
+
+ con1 = LDAPBase(URL1, creds, lp,
+ two=two, quiet=quiet, descriptor=descriptor, verbose=verbose, view=view)
+ assert len(con1.base_dn) > 0
+
+ con2 = LDAPBase(URL2, creds2, lp,
+ two=two, quiet=quiet, descriptor=descriptor, verbose=verbose, view=view)
+ assert len(con2.base_dn) > 0
+
+ status = 0
+ for context in contexts:
+ if not quiet:
+ print "\n* Comparing [%s] context..." % context
+
+ b1 = LDAPBundel(con1, context=context)
+ b2 = LDAPBundel(con2, context=context)
+
+ if b1 == b2:
+ if not quiet:
+ print "\n* Result for [%s]: SUCCESS" % context
+ else:
+ if not quiet:
+ print "\n* Result for [%s]: FAILURE" % context
+ if not descriptor:
+ assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
+ b2.summary["df_value_attrs"] = []
+ print "\nSUMMARY"
+ print "---------"
+ b1.print_summary()
+ b2.print_summary()
+ # mark exit status as FAILURE if a least one comparison failed
+ status = -1
+ if status != 0:
+ raise CommandError("Compare failed: %d" % status)