Revert "s4/ldapcmp: Fix the parsing of the second set of credentials"
[metze/samba/wip.git] / source4 / scripting / python / samba / netcmd / ldapcmp.py
1 #!/usr/bin/env python
2 #
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
8 # above partitions.
9
10 # Copyright (C) Zahari Zahariev <zahari.zahariev@postpath.com> 2009, 2010
11 #
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.
16 #
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.
21 #
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/>.
24 #
25
26 import os
27 import re
28 import sys
29
30 import samba
31 import samba.getopt as options
32 from samba import Ldb
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 (
37     Command,
38     CommandError,
39     Option,
40     SuperCommand,
41     )
42
43 global summary
44 summary = {}
45
46 class LDAPBase(object):
47
48     def __init__(self, host, creds, lp,
49                  two=False, quiet=False, descriptor=False, verbose=False,
50                  view="section"):
51         ldb_options = []
52         samdb_url = host
53         if not "://" in host:
54             if os.path.isfile(host):
55                 samdb_url = "tdb://%s" % host
56             else:
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,
62                        credentials=creds,
63                        lp=lp,
64                        options=ldb_options)
65         self.two_domains = two
66         self.quiet = quiet
67         self.descriptor = descriptor
68         self.view = view
69         self.verbose = verbose
70         self.host = host
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()
76         self.get_guid_map()
77         self.get_sid_map()
78         #
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
87
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])
91
92     def find_servers(self):
93         """
94         """
95         res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \
96                 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
97         assert len(res) > 0
98         srv = []
99         for x in res:
100             srv.append(x["cn"][0])
101         return srv
102
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"])
106         assert len(res) > 0
107         for x in res:
108             if "nETBIOSName" in x.keys():
109                 return x["nETBIOSName"][0]
110
111     def find_basedn(self):
112         res = self.ldb.search(base="", expression="(objectClass=*)", scope=SCOPE_BASE,
113                 attrs=["defaultNamingContext"])
114         assert len(res) == 1
115         return res[0]["defaultNamingContext"][0]
116
117     def object_exists(self, object_dn):
118         res = None
119         try:
120             res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
121         except LdbError, (enum, estr):
122             if enum == ERR_NO_SUCH_OBJECT:
123                 return False
124             raise
125         return len(res) == 1
126
127     def delete_force(self, object_dn):
128         try:
129             self.ldb.delete(object_dn)
130         except Ldb.LdbError, e:
131             assert "No such object" in str(e)
132
133     def get_attributes(self, object_dn):
134         """ Returns dict with all default visible attributes
135         """
136         res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
137         assert len(res) == 1
138         res = dict(res[0])
139         # 'Dn' element is not iterable and we have it as 'distinguishedName'
140         del res["dn"]
141         for key in res.keys():
142             res[key] = list(res[key])
143         return res
144
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)
150
151     def guid_as_string(self, guid_blob):
152         """ Translate binary representation of schemaIDGUID to standard string representation.
153             @gid_blob: binary schemaIDGUID
154         """
155         blob = "%s" % guid_blob
156         stops = [4, 2, 2, 2, 6]
157         index = 0
158         res = ""
159         x = 0
160         while x < len(stops):
161             tmp = ""
162             y = 0
163             while y < stops[x]:
164                 c = hex(ord(blob[index])).replace("0x", "")
165                 c = [None, "0" + c, c][len(c)]
166                 if 2 * index < len(blob):
167                     tmp = c + tmp
168                 else:
169                     tmp += c
170                 index += 1
171                 y += 1
172             res += tmp + " "
173             x += 1
174         assert index == len(blob)
175         return res.strip().replace(" ", "-")
176
177     def get_guid_map(self):
178         """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
179         """
180         self.guid_map = {}
181         res = self.ldb.search(base="cn=schema,cn=configuration,%s" % self.base_dn, \
182                 expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
183         for item in res:
184             self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
185         #
186         res = self.ldb.search(base="cn=extended-rights,cn=configuration,%s" % self.base_dn, \
187                 expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
188         for item in res:
189             self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
190
191     def get_sid_map(self):
192         """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
193         """
194         self.sid_map = {}
195         res = self.ldb.search(base="%s" % self.base_dn, \
196                 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
197         for item in res:
198             try:
199                 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
200             except KeyError:
201                 pass
202
203 class Descriptor(object):
204     def __init__(self, connection, dn):
205         self.con = connection
206         self.dn = dn
207         self.sddl = self.con.get_descriptor_sddl(self.dn)
208         self.dacl_list = self.extract_dacl()
209
210     def extract_dacl(self):
211         """ Extracts the DACL as a list of ACE string (with the brakets).
212         """
213         try:
214             res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
215         except AttributeError:
216             return []
217         return re.findall("(\(.*?\))", res)
218
219     def fix_guid(self, ace):
220         res = "%s" % 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
223         if len(guids) == 0:
224             return res
225         for guid in guids:
226             try:
227                 name = self.con.guid_map[guid.lower()]
228                 res = res.replace(guid, name)
229             except KeyError:
230                 # Do not bother if the GUID is not found in
231                 # cn=Schema or cn=Extended-Rights
232                 pass
233         return res
234
235     def fix_sid(self, ace):
236         res = "%s" % ace
237         sids = re.findall("S-[-0-9]+", res)
238         # If there are not SIDs to replace return the same ACE
239         if len(sids) == 0:
240             return res
241         for sid in sids:
242             try:
243                 name = self.con.sid_map[sid]
244                 res = res.replace(sid, name)
245             except KeyError:
246                 # Do not bother if the SID is not found in baseDN
247                 pass
248         return res
249
250     def fixit(self, ace):
251         """ Combine all replacement methods in one
252         """
253         res = "%s" % ace
254         res = self.fix_guid(res)
255         res = self.fix_sid(res)
256         return res
257
258     def diff_1(self, other):
259         res = ""
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)
264         #
265         i = 0
266         flag = True
267         while True:
268             self_ace = None
269             other_ace = None
270             try:
271                 self_ace = "%s" % self.dacl_list[i]
272             except IndexError:
273                 self_ace = ""
274             #
275             try:
276                 other_ace = "%s" % other.dacl_list[i]
277             except IndexError:
278                 other_ace = ""
279             if len(self_ace) + len(other_ace) == 0:
280                 break
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 )
285                 flag = False
286             else:
287                 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
288             i += 1
289         return (flag, res)
290
291     def diff_2(self, other):
292         res = ""
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)
297         #
298         common_aces = []
299         self_aces = []
300         other_aces = []
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:
306             try:
307                 other_dacl_list_fixed.index(ace)
308             except ValueError:
309                 self_aces.append(ace)
310             else:
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"
317         #
318         for ace in other_dacl_list_fixed:
319             try:
320                 self_dacl_list_fixed.index(ace)
321             except ValueError:
322                 other_aces.append(ace)
323             else:
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"
330         #
331         common_aces = sorted(list(set(common_aces)))
332         if self.con.verbose:
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)
337
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.
351         #
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
361                 "prefixMap",]
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
368
369         if self.two_domains:
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"]
378             #
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]
391             #
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]
397             #
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]
403             #
404             self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
405             self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
406             #
407             self.other_attributes = [ "name", "DC",]
408             self.other_attributes = [x.upper() for x in self.other_attributes]
409         #
410         self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
411
412     def log(self, msg):
413         """
414         Log on the screen if there is no --quiet oprion set
415         """
416         if not self.quiet:
417             print msg
418
419     def fix_dn(self, s):
420         res = "%s" % s
421         if not self.two_domains:
422             return res
423         if res.upper().endswith(self.con.base_dn.upper()):
424             res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
425         return res
426
427     def fix_domain_name(self, s):
428         res = "%s" % s
429         if not self.two_domains:
430             return res
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}")
433         return res
434
435     def fix_domain_netbios(self, s):
436         res = "%s" % s
437         if not self.two_domains:
438             return res
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}")
441         return res
442
443     def fix_server_name(self, s):
444         res = "%s" % s
445         if not self.two_domains or len(self.con.server_names) > 1:
446             return res
447         for x in self.con.server_names:
448             res = res.upper().replace(x, "${SERVER_NAME}")
449         return res
450
451     def __eq__(self, other):
452         if self.con.descriptor:
453             return self.cmp_desc(other)
454         return self.cmp_attrs(other)
455
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":
460             res = d1.diff_2(d2)
461         elif self.con.view == "collision":
462             res = d1.diff_1(d2)
463         else:
464             raise Exception("Unknown --view option value.")
465         #
466         self.screen_output = res[1][:-1]
467         other.screen_output = res[1][:-1]
468         #
469         return res[0]
470
471     def cmp_attrs(self, other):
472         res = ""
473         self.unique_attrs = []
474         self.df_value_attrs = []
475         other.unique_attrs = []
476         if self.attributes.keys() != other.attributes.keys():
477             #
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]:
482                     if title:
483                         res += title + "\n"
484                         title = None
485                     res += 8*" " + x + "\n"
486                     self.unique_attrs.append(x)
487             #
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]:
492                     if title:
493                         res += title + "\n"
494                         title = None
495                     res += 8*" " + x + "\n"
496                     other.unique_attrs.append(x)
497         #
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:
503                 continue
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]:
508                 p = None
509                 q = None
510                 m = None
511                 n = None
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]]
517                     if p == q:
518                         continue
519                 # Attribute values that are list that contain DN based values that may differ
520                 elif x.upper() in self.dn_attributes:
521                     m = p
522                     n = q
523                     if not p and not q:
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]
528                     if p == q:
529                         continue
530                 # Attributes that contain the Domain name in them
531                 if x.upper() in self.domain_attributes:
532                     m = p
533                     n = q
534                     if not p and not q:
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]
539                     if p == q:
540                         continue
541                 #
542                 if x.upper() in self.servername_attributes:
543                     # Attributes with SERVER_NAME
544                     m = p
545                     n = q
546                     if not p and not q:
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]
551                     if p == q:
552                         continue
553                 #
554                 if x.upper() in self.netbios_attributes:
555                     # Attributes with NETBIOS Domain name
556                     m = p
557                     n = q
558                     if not p and not q:
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]
563                     if p == q:
564                         continue
565                 #
566                 if title:
567                     res += title + "\n"
568                     title = None
569                 if p and q:
570                     res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
571                 else:
572                     res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
573                 self.df_value_attrs.append(x)
574         #
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
581         #
582         self.screen_output = res[:-1]
583         other.screen_output = res[:-1]
584         #
585         return res == ""
586
587
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
594         self.summary = {}
595         self.summary["unique_attrs"] = []
596         self.summary["df_value_attrs"] = []
597         self.summary["known_ignored_dn"] = []
598         self.summary["abnormal_ignored_dn"] = []
599         if dn_list:
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)
604         else:
605             raise Exception("Unknown initialization data for LDAPBundel().")
606         counter = 0
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
616             counter += 1
617         self.dn_list = list(set(self.dn_list))
618         self.dn_list = sorted(self.dn_list)
619         self.size = len(self.dn_list)
620
621     def log(self, msg):
622         """
623         Log on the screen if there is no --quiet oprion set
624         """
625         if not self.quiet:
626             print msg
627
628     def update_size(self):
629         self.size = len(self.dn_list)
630         self.dn_list = sorted(self.dn_list)
631
632     def __eq__(self, other):
633         res = True
634         if self.size != other.size:
635             self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
636             res = False
637         #
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]:
641                 if title:
642                     self.log( title )
643                     title = None
644                     res = False
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]
648         #
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]:
652                 if title:
653                     self.log( title )
654                     title = None
655                     res = False
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]
659         #
660         self.update_size()
661         other.update_size()
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 )
665
666         index = 0
667         while index < self.size:
668             skip = False
669             try:
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] )
676                     skip = True
677                 raise
678             try:
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] )
685                     skip = True
686                 raise
687             if skip:
688                 index += 1
689                 continue
690             if object1 == object2:
691                 if self.con.verbose:
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" )
696             else:
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" )
702                 res = False
703             self.summary = object1.summary
704             other.summary = object2.summary
705             index += 1
706         #
707         return res
708
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.
712         """
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
719
720         dn_list = []
721         res = self.con.ldb.search(base=search_base, scope=SCOPE_SUBTREE, attrs=["dn"])
722         for x in res:
723            dn_list.append(x["dn"].get_linearized())
724
725         #
726         global summary
727         #
728         return dn_list
729
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"]))
733         #
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"]]) )
737         #
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"] = []
742
743 class cmd_ldapcmp(Command):
744     """compare two ldap databases"""
745     synopsis = "ldapcmp URL1 URL2 <domain|configuration|schema> [options]"
746
747     takes_optiongroups = {
748         "sambaopts": options.SambaOptions,
749         "versionopts": options.VersionOptions,
750         "credopts": options.CredentialsOptionsDouble,
751     }
752
753     takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
754
755     takes_options = [
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.")
766         ]
767
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():
776             creds2 = creds
777         if not creds.authentication_requested():
778             raise CommandError("You must supply at least one username/password pair")
779
780         # make a list of contexts to compare in
781         contexts = []
782         if context1 is None:
783             # if no argument given, we compare all contexts
784             contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
785         else:
786             for c in [context1, context2, context3]:
787                 if c is None:
788                     continue
789                 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
790                     raise CommandError("Incorrect argument: %s" % c)
791                 contexts.append(c.upper())
792
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.")
797
798         con1 = LDAPBase(URL1, creds, lp,
799                         two=two, quiet=quiet, descriptor=descriptor, verbose=verbose, view=view)
800         assert len(con1.base_dn) > 0
801
802         con2 = LDAPBase(URL2, creds2, lp,
803                         two=two, quiet=quiet, descriptor=descriptor, verbose=verbose, view=view)
804         assert len(con2.base_dn) > 0
805
806         status = 0
807         for context in contexts:
808             if not quiet:
809                 print "\n* Comparing [%s] context..." % context
810
811             b1 = LDAPBundel(con1, context=context)
812             b2 = LDAPBundel(con2, context=context)
813
814             if b1 == b2:
815                 if not quiet:
816                     print "\n* Result for [%s]: SUCCESS" % context
817             else:
818                 if not quiet:
819                     print "\n* Result for [%s]: FAILURE" % context
820                     if not descriptor:
821                         assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
822                         b2.summary["df_value_attrs"] = []
823                         print "\nSUMMARY"
824                         print "---------"
825                         b1.print_summary()
826                         b2.print_summary()
827                 # mark exit status as FAILURE if a least one comparison failed
828                 status = -1
829         if status != 0:
830             raise CommandError("Compare failed: %d" % status)