samba-tool: Add --filter option to ldapcmp to ignore specified attributes
[samba.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 # Copyright Giampaolo Lauria 2011 <lauria2@yahoo.com>
12 #
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.
17 #
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.
22 #
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/>.
25 #
26
27 import os
28 import re
29 import sys
30
31 import samba
32 import samba.getopt as options
33 from samba import Ldb
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 (
38     Command,
39     CommandError,
40     Option,
41     SuperCommand,
42     )
43
44 global summary
45 summary = {}
46
47 class LDAPBase(object):
48
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"):
52         ldb_options = []
53         samdb_url = host
54         if not "://" in host:
55             if os.path.isfile(host):
56                 samdb_url = "tdb://%s" % host
57             else:
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,
63                        credentials=creds,
64                        lp=lp,
65                        options=ldb_options)
66         self.search_base = base
67         self.search_scope = scope
68         self.two_domains = two
69         self.quiet = quiet
70         self.descriptor = descriptor
71         self.sort_aces = sort_aces
72         self.view = view
73         self.verbose = verbose
74         self.host = host
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()
82         self.get_guid_map()
83         self.get_sid_map()
84         #
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
93
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])
97
98     def find_servers(self):
99         """
100         """
101         res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \
102                 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
103         assert len(res) > 0
104         srv = []
105         for x in res:
106             srv.append(x["cn"][0])
107         return srv
108
109     def find_netbios(self):
110         res = self.ldb.search(base="CN=Partitions,%s" % self.config_dn, \
111                 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
112         assert len(res) > 0
113         for x in res:
114             if "nETBIOSName" in x.keys():
115                 return x["nETBIOSName"][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_attribute_name(self, key):
134         """ Returns the real attribute name
135             It resolved ranged results e.g. member;range=0-1499
136         """
137
138         r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
139
140         m = r.match(key)
141         if m is None:
142             return key
143
144         return m.group(1)
145
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
149         """
150
151         r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
152
153         m = r.match(key)
154         if m is None:
155             # no range, just return the values
156             return vals
157
158         attr = m.group(1)
159         hi = int(m.group(3))
160
161         # get additional values in a loop
162         # until we get a response with '*' at the end
163         while True:
164
165             n = "%s;range=%d-*" % (attr, hi + 1)
166             res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
167             assert len(res) == 1
168             res = dict(res[0])
169             del res["dn"]
170
171             fm = None
172             fvals = None
173
174             for key in res.keys():
175                 m = r.match(key)
176
177                 if m is None:
178                     continue
179
180                 if m.group(1) != attr:
181                     continue
182
183                 fm = m
184                 fvals = list(res[key])
185                 break
186
187             if fm is None:
188                 break
189
190             vals.extend(fvals)
191             if fm.group(3) == "*":
192                 # if we got "*" we're done
193                 break
194
195             assert int(fm.group(2)) == hi + 1
196             hi = int(fm.group(3))
197
198         return vals
199
200     def get_attributes(self, object_dn):
201         """ Returns dict with all default visible attributes
202         """
203         res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
204         assert len(res) == 1
205         res = dict(res[0])
206         # 'Dn' element is not iterable and we have it as 'distinguishedName'
207         del res["dn"]
208         for key in res.keys():
209             vals = list(res[key])
210             del res[key]
211             name = self.get_attribute_name(key)
212             res[name] = self.get_attribute_values(object_dn, key, vals)
213
214         return res
215
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)
221
222     def guid_as_string(self, guid_blob):
223         """ Translate binary representation of schemaIDGUID to standard string representation.
224             @gid_blob: binary schemaIDGUID
225         """
226         blob = "%s" % guid_blob
227         stops = [4, 2, 2, 2, 6]
228         index = 0
229         res = ""
230         x = 0
231         while x < len(stops):
232             tmp = ""
233             y = 0
234             while y < stops[x]:
235                 c = hex(ord(blob[index])).replace("0x", "")
236                 c = [None, "0" + c, c][len(c)]
237                 if 2 * index < len(blob):
238                     tmp = c + tmp
239                 else:
240                     tmp += c
241                 index += 1
242                 y += 1
243             res += tmp + " "
244             x += 1
245         assert index == len(blob)
246         return res.strip().replace(" ", "-")
247
248     def get_guid_map(self):
249         """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
250         """
251         self.guid_map = {}
252         res = self.ldb.search(base=self.schema_dn,
253                               expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
254         for item in res:
255             self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
256         #
257         res = self.ldb.search(base="cn=extended-rights,%s" % self.config_dn,
258                               expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
259         for item in res:
260             self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
261
262     def get_sid_map(self):
263         """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
264         """
265         self.sid_map = {}
266         res = self.ldb.search(base=self.base_dn,
267                               expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
268         for item in res:
269             try:
270                 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
271             except KeyError:
272                 pass
273
274 class Descriptor(object):
275     def __init__(self, connection, dn):
276         self.con = connection
277         self.dn = dn
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()
282
283     def extract_dacl(self):
284         """ Extracts the DACL as a list of ACE string (with the brakets).
285         """
286         try:
287             if "S:" in self.sddl:
288                 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
289             else:
290                 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
291         except AttributeError:
292             return []
293         return re.findall("(\(.*?\))", res)
294
295     def fix_guid(self, ace):
296         res = "%s" % 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
299         if len(guids) == 0:
300             return res
301         for guid in guids:
302             try:
303                 name = self.con.guid_map[guid.lower()]
304                 res = res.replace(guid, name)
305             except KeyError:
306                 # Do not bother if the GUID is not found in
307                 # cn=Schema or cn=Extended-Rights
308                 pass
309         return res
310
311     def fix_sid(self, ace):
312         res = "%s" % ace
313         sids = re.findall("S-[-0-9]+", res)
314         # If there are not SIDs to replace return the same ACE
315         if len(sids) == 0:
316             return res
317         for sid in sids:
318             try:
319                 name = self.con.sid_map[sid]
320                 res = res.replace(sid, name)
321             except KeyError:
322                 # Do not bother if the SID is not found in baseDN
323                 pass
324         return res
325
326     def fixit(self, ace):
327         """ Combine all replacement methods in one
328         """
329         res = "%s" % ace
330         res = self.fix_guid(res)
331         res = self.fix_sid(res)
332         return res
333
334     def diff_1(self, other):
335         res = ""
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)
340         #
341         i = 0
342         flag = True
343         while True:
344             self_ace = None
345             other_ace = None
346             try:
347                 self_ace = "%s" % self.dacl_list[i]
348             except IndexError:
349                 self_ace = ""
350             #
351             try:
352                 other_ace = "%s" % other.dacl_list[i]
353             except IndexError:
354                 other_ace = ""
355             if len(self_ace) + len(other_ace) == 0:
356                 break
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 )
361                 flag = False
362             else:
363                 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
364             i += 1
365         return (flag, res)
366
367     def diff_2(self, other):
368         res = ""
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)
373         #
374         common_aces = []
375         self_aces = []
376         other_aces = []
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:
382             try:
383                 other_dacl_list_fixed.index(ace)
384             except ValueError:
385                 self_aces.append(ace)
386             else:
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"
393         #
394         for ace in other_dacl_list_fixed:
395             try:
396                 self_dacl_list_fixed.index(ace)
397             except ValueError:
398                 other_aces.append(ace)
399             else:
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"
406         #
407         common_aces = sorted(list(set(common_aces)))
408         if self.con.verbose:
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)
413
414 class LDAPObject(object):
415     def __init__(self, connection, dn, summary, filter_list):
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.
427         #
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
437                 "prefixMap"]
438         if filter_list:
439             self.ignore_attributes += filter_list
440
441         self.dn_attributes = []
442         self.domain_attributes = []
443         self.servername_attributes = []
444         self.netbios_attributes = []
445         self.other_attributes = []
446         # Two domains - two domain controllers
447
448         if self.two_domains:
449             self.ignore_attributes +=  [
450                 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
451                 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
452                 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
453                 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
454                 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
455                 # After Exchange preps
456                 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
457             #
458             # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
459             self.dn_attributes = [
460                 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
461                 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
462                 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
463                 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
464                 # After Exchange preps
465                 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
466                 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
467                 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
468                 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
469             self.dn_attributes = [x.upper() for x in self.dn_attributes]
470             #
471             # Attributes that contain the Domain name e.g. 'samba.org'
472             self.domain_attributes = [
473                 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
474                 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
475             self.domain_attributes = [x.upper() for x in self.domain_attributes]
476             #
477             # May contain DOMAIN_NETBIOS and SERVER_NAME
478             self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
479                 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
480                 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
481             self.servername_attributes = [x.upper() for x in self.servername_attributes]
482             #
483             self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
484             self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
485             #
486             self.other_attributes = [ "name", "DC",]
487             self.other_attributes = [x.upper() for x in self.other_attributes]
488         #
489         self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
490
491     def log(self, msg):
492         """
493         Log on the screen if there is no --quiet oprion set
494         """
495         if not self.quiet:
496             print msg
497
498     def fix_dn(self, s):
499         res = "%s" % s
500         if not self.two_domains:
501             return res
502         if res.upper().endswith(self.con.base_dn.upper()):
503             res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
504         return res
505
506     def fix_domain_name(self, s):
507         res = "%s" % s
508         if not self.two_domains:
509             return res
510         res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
511         res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
512         return res
513
514     def fix_domain_netbios(self, s):
515         res = "%s" % s
516         if not self.two_domains:
517             return res
518         res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
519         res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
520         return res
521
522     def fix_server_name(self, s):
523         res = "%s" % s
524         if not self.two_domains or len(self.con.server_names) > 1:
525             return res
526         for x in self.con.server_names:
527             res = res.upper().replace(x, "${SERVER_NAME}")
528         return res
529
530     def __eq__(self, other):
531         if self.con.descriptor:
532             return self.cmp_desc(other)
533         return self.cmp_attrs(other)
534
535     def cmp_desc(self, other):
536         d1 = Descriptor(self.con, self.dn)
537         d2 = Descriptor(other.con, other.dn)
538         if self.con.view == "section":
539             res = d1.diff_2(d2)
540         elif self.con.view == "collision":
541             res = d1.diff_1(d2)
542         else:
543             raise Exception("Unknown --view option value.")
544         #
545         self.screen_output = res[1][:-1]
546         other.screen_output = res[1][:-1]
547         #
548         return res[0]
549
550     def cmp_attrs(self, other):
551         res = ""
552         self.unique_attrs = []
553         self.df_value_attrs = []
554         other.unique_attrs = []
555         if self.attributes.keys() != other.attributes.keys():
556             #
557             title = 4*" " + "Attributes found only in %s:" % self.con.host
558             for x in self.attributes.keys():
559                 if not x in other.attributes.keys() and \
560                 not x.upper() in [q.upper() for q in other.ignore_attributes]:
561                     if title:
562                         res += title + "\n"
563                         title = None
564                     res += 8*" " + x + "\n"
565                     self.unique_attrs.append(x)
566             #
567             title = 4*" " + "Attributes found only in %s:" % other.con.host
568             for x in other.attributes.keys():
569                 if not x in self.attributes.keys() and \
570                 not x.upper() in [q.upper() for q in self.ignore_attributes]:
571                     if title:
572                         res += title + "\n"
573                         title = None
574                     res += 8*" " + x + "\n"
575                     other.unique_attrs.append(x)
576         #
577         missing_attrs = [x.upper() for x in self.unique_attrs]
578         missing_attrs += [x.upper() for x in other.unique_attrs]
579         title = 4*" " + "Difference in attribute values:"
580         for x in self.attributes.keys():
581             if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
582                 continue
583             if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
584                 self.attributes[x] = sorted(self.attributes[x])
585                 other.attributes[x] = sorted(other.attributes[x])
586             if self.attributes[x] != other.attributes[x]:
587                 p = None
588                 q = None
589                 m = None
590                 n = None
591                 # First check if the difference can be fixed but shunting the first part
592                 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
593                 if x.upper() in self.other_attributes:
594                     p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
595                     q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
596                     if p == q:
597                         continue
598                 # Attribute values that are list that contain DN based values that may differ
599                 elif x.upper() in self.dn_attributes:
600                     m = p
601                     n = q
602                     if not p and not q:
603                         m = self.attributes[x]
604                         n = other.attributes[x]
605                     p = [self.fix_dn(j) for j in m]
606                     q = [other.fix_dn(j) for j in n]
607                     if p == q:
608                         continue
609                 # Attributes that contain the Domain name in them
610                 if x.upper() in self.domain_attributes:
611                     m = p
612                     n = q
613                     if not p and not q:
614                         m = self.attributes[x]
615                         n = other.attributes[x]
616                     p = [self.fix_domain_name(j) for j in m]
617                     q = [other.fix_domain_name(j) for j in n]
618                     if p == q:
619                         continue
620                 #
621                 if x.upper() in self.servername_attributes:
622                     # Attributes with SERVER_NAME
623                     m = p
624                     n = q
625                     if not p and not q:
626                         m = self.attributes[x]
627                         n = other.attributes[x]
628                     p = [self.fix_server_name(j) for j in m]
629                     q = [other.fix_server_name(j) for j in n]
630                     if p == q:
631                         continue
632                 #
633                 if x.upper() in self.netbios_attributes:
634                     # Attributes with NETBIOS Domain name
635                     m = p
636                     n = q
637                     if not p and not q:
638                         m = self.attributes[x]
639                         n = other.attributes[x]
640                     p = [self.fix_domain_netbios(j) for j in m]
641                     q = [other.fix_domain_netbios(j) for j in n]
642                     if p == q:
643                         continue
644                 #
645                 if title:
646                     res += title + "\n"
647                     title = None
648                 if p and q:
649                     res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
650                 else:
651                     res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
652                 self.df_value_attrs.append(x)
653         #
654         if self.unique_attrs + other.unique_attrs != []:
655             assert self.unique_attrs != other.unique_attrs
656         self.summary["unique_attrs"] += self.unique_attrs
657         self.summary["df_value_attrs"] += self.df_value_attrs
658         other.summary["unique_attrs"] += other.unique_attrs
659         other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
660         #
661         self.screen_output = res[:-1]
662         other.screen_output = res[:-1]
663         #
664         return res == ""
665
666
667 class LDAPBundel(object):
668     def __init__(self, connection, context, dn_list=None, filter_list=None):
669         self.con = connection
670         self.two_domains = self.con.two_domains
671         self.quiet = self.con.quiet
672         self.verbose = self.con.verbose
673         self.search_base = self.con.search_base
674         self.search_scope = self.con.search_scope
675         self.summary = {}
676         self.summary["unique_attrs"] = []
677         self.summary["df_value_attrs"] = []
678         self.summary["known_ignored_dn"] = []
679         self.summary["abnormal_ignored_dn"] = []
680         self.filter_list = filter_list
681         if dn_list:
682             self.dn_list = dn_list
683         elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
684             self.context = context.upper()
685             self.dn_list = self.get_dn_list(context)
686         else:
687             raise Exception("Unknown initialization data for LDAPBundel().")
688         counter = 0
689         while counter < len(self.dn_list) and self.two_domains:
690             # Use alias reference
691             tmp = self.dn_list[counter]
692             tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
693             tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
694             if len(self.con.server_names) == 1:
695                 for x in self.con.server_names:
696                     tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
697             self.dn_list[counter] = tmp
698             counter += 1
699         self.dn_list = list(set(self.dn_list))
700         self.dn_list = sorted(self.dn_list)
701         self.size = len(self.dn_list)
702
703     def log(self, msg):
704         """
705         Log on the screen if there is no --quiet oprion set
706         """
707         if not self.quiet:
708             print msg
709
710     def update_size(self):
711         self.size = len(self.dn_list)
712         self.dn_list = sorted(self.dn_list)
713
714     def __eq__(self, other):
715         res = True
716         if self.size != other.size:
717             self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
718             res = False
719         #
720         # This is the case where we want to explicitly compare two objects with different DNs.
721         # It does not matter if they are in the same DC, in two DC in one domain or in two
722         # different domains.
723         if self.search_scope != SCOPE_BASE:
724             title= "\n* DNs found only in %s:" % self.con.host
725             for x in self.dn_list:
726                 if not x.upper() in [q.upper() for q in other.dn_list]:
727                     if title:
728                         self.log( title )
729                         title = None
730                         res = False
731                     self.log( 4*" " + x )
732                     self.dn_list[self.dn_list.index(x)] = ""
733             self.dn_list = [x for x in self.dn_list if x]
734             #
735             title= "\n* DNs found only in %s:" % other.con.host
736             for x in other.dn_list:
737                 if not x.upper() in [q.upper() for q in self.dn_list]:
738                     if title:
739                         self.log( title )
740                         title = None
741                         res = False
742                     self.log( 4*" " + x )
743                     other.dn_list[other.dn_list.index(x)] = ""
744             other.dn_list = [x for x in other.dn_list if x]
745             #
746             self.update_size()
747             other.update_size()
748             assert self.size == other.size
749             assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
750         self.log( "\n* Objects to be compared: %s" % self.size )
751
752         index = 0
753         while index < self.size:
754             skip = False
755             try:
756                 object1 = LDAPObject(connection=self.con,
757                                      dn=self.dn_list[index],
758                                      summary=self.summary,
759                                      filter_list=self.filter_list)
760             except LdbError, (enum, estr):
761                 if enum == ERR_NO_SUCH_OBJECT:
762                     self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
763                     skip = True
764                 raise
765             try:
766                 object2 = LDAPObject(connection=other.con,
767                         dn=other.dn_list[index],
768                         summary=other.summary,
769                         filter_list=self.filter_list)
770             except LdbError, (enum, estr):
771                 if enum == ERR_NO_SUCH_OBJECT:
772                     self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
773                     skip = True
774                 raise
775             if skip:
776                 index += 1
777                 continue
778             if object1 == object2:
779                 if self.con.verbose:
780                     self.log( "\nComparing:" )
781                     self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
782                     self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
783                     self.log( 4*" " + "OK" )
784             else:
785                 self.log( "\nComparing:" )
786                 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
787                 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
788                 self.log( object1.screen_output )
789                 self.log( 4*" " + "FAILED" )
790                 res = False
791             self.summary = object1.summary
792             other.summary = object2.summary
793             index += 1
794         #
795         return res
796
797     def get_dn_list(self, context):
798         """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
799             Parse all DNs and filter those that are 'strange' or abnormal.
800         """
801         if context.upper() == "DOMAIN":
802             search_base = self.con.base_dn
803         elif context.upper() == "CONFIGURATION":
804             search_base = self.con.config_dn
805         elif context.upper() == "SCHEMA":
806             search_base = self.con.schema_dn
807
808         dn_list = []
809         if not self.search_base:
810             self.search_base = search_base
811         self.search_scope = self.search_scope.upper()
812         if self.search_scope == "SUB":
813             self.search_scope = SCOPE_SUBTREE
814         elif self.search_scope == "BASE":
815             self.search_scope = SCOPE_BASE
816         elif self.search_scope == "ONE":
817             self.search_scope = SCOPE_ONELEVEL
818         else:
819             raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
820         if not self.search_base.upper().endswith(search_base.upper()):
821             raise StandardError("Invalid search base specified: %s" % self.search_base)
822         res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
823         for x in res:
824            dn_list.append(x["dn"].get_linearized())
825         #
826         global summary
827         #
828         return dn_list
829
830     def print_summary(self):
831         self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
832         self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
833         #
834         if self.summary["unique_attrs"]:
835             self.log( "\nAttributes found only in %s:" % self.con.host )
836             self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
837         #
838         if self.summary["df_value_attrs"]:
839             self.log( "\nAttributes with different values:" )
840             self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
841             self.summary["df_value_attrs"] = []
842
843 class cmd_ldapcmp(Command):
844     """compare two ldap databases"""
845     synopsis = "ldapcmp URL1 URL2 <domain|configuration|schema> [options]"
846
847     takes_optiongroups = {
848         "sambaopts": options.SambaOptions,
849         "versionopts": options.VersionOptions,
850         "credopts": options.CredentialsOptionsDouble,
851     }
852
853     takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
854
855     takes_options = [
856         Option("-w", "--two", dest="two", action="store_true", default=False,
857             help="Hosts are in two different domains"),
858         Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
859             help="Do not print anything but relay on just exit code"),
860         Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
861             help="Print all DN pairs that have been compared"),
862         Option("--sd", dest="descriptor", action="store_true", default=False,
863             help="Compare nTSecurityDescriptor attibutes only"),
864         Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
865             help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
866         Option("--view", dest="view", default="section",
867             help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
868         Option("--base", dest="base", default="",
869             help="Pass search base that will build DN list for the first DC."),
870         Option("--base2", dest="base2", default="",
871             help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
872         Option("--scope", dest="scope", default="SUB",
873             help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
874         Option("--filter", dest="filter", default="",
875             help="List of comma separated attributes to ignore in the comparision"),
876         ]
877
878     def run(self, URL1, URL2,
879             context1=None, context2=None, context3=None,
880             two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
881             view="section", base="", base2="", scope="SUB", filter="",
882             credopts=None, sambaopts=None, versionopts=None):
883
884         lp = sambaopts.get_loadparm()
885
886         using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
887
888         if using_ldap:
889             creds = credopts.get_credentials(lp, fallback_machine=True)
890         else:
891             creds = None
892         creds2 = credopts.get_credentials2(lp, guess=False)
893         if creds2.is_anonymous():
894             creds2 = creds
895         else:
896             creds2.set_domain("")
897             creds2.set_workstation("")
898         if using_ldap and not creds.authentication_requested():
899             raise CommandError("You must supply at least one username/password pair")
900
901         # make a list of contexts to compare in
902         contexts = []
903         if context1 is None:
904             if base and base2:
905                 # If search bases are specified context is defaulted to
906                 # DOMAIN so the given search bases can be verified.
907                 contexts = ["DOMAIN"]
908             else:
909                 # if no argument given, we compare all contexts
910                 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
911         else:
912             for c in [context1, context2, context3]:
913                 if c is None:
914                     continue
915                 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
916                     raise CommandError("Incorrect argument: %s" % c)
917                 contexts.append(c.upper())
918
919         if verbose and quiet:
920             raise CommandError("You cannot set --verbose and --quiet together")
921         if (not base and base2) or (base and not base2):
922             raise CommandError("You need to specify both --base and --base2 at the same time")
923         if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
924             raise CommandError("Invalid --view value. Choose from: section or collision")
925         if not scope.upper() in ["SUB", "ONE", "BASE"]:
926             raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
927
928         con1 = LDAPBase(URL1, creds, lp,
929                         two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
930                         verbose=verbose,view=view, base=base, scope=scope)
931         assert len(con1.base_dn) > 0
932
933         con2 = LDAPBase(URL2, creds2, lp,
934                         two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
935                         verbose=verbose, view=view, base=base2, scope=scope)
936         assert len(con2.base_dn) > 0
937
938         filter_list = filter.split(",")
939
940         status = 0
941         for context in contexts:
942             if not quiet:
943                 print "\n* Comparing [%s] context..." % context
944
945             b1 = LDAPBundel(con1, context=context, filter_list=filter_list)
946             b2 = LDAPBundel(con2, context=context, filter_list=filter_list)
947
948             if b1 == b2:
949                 if not quiet:
950                     print "\n* Result for [%s]: SUCCESS" % context
951             else:
952                 if not quiet:
953                     print "\n* Result for [%s]: FAILURE" % context
954                     if not descriptor:
955                         assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
956                         b2.summary["df_value_attrs"] = []
957                         print "\nSUMMARY"
958                         print "---------"
959                         b1.print_summary()
960                         b2.print_summary()
961                 # mark exit status as FAILURE if a least one comparison failed
962                 status = -1
963         if status != 0:
964             raise CommandError("Compare failed: %d" % status)