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