allow_remaining
[metze/samba/wip.git] / source4 / scripting / python / samba / netcmd / ldapcmp.py
1 # Unix SMB/CIFS implementation.
2 # A command to compare differences of objects and attributes between
3 # two LDAP servers both running at the same time. It generally compares
4 # one of the three pratitions DOMAIN, CONFIGURATION or SCHEMA. Users
5 # that have to be provided sheould be able to read objects in any of the
6 # above partitions.
7
8 # Copyright (C) Zahari Zahariev <zahari.zahariev@postpath.com> 2009, 2010
9 #
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
22 #
23
24 import os
25 import re
26 import sys
27
28 import samba
29 import samba.getopt as options
30 from samba import Ldb
31 from samba.ndr import ndr_unpack
32 from samba.dcerpc import security
33 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, ERR_NO_SUCH_OBJECT, LdbError
34 from samba.netcmd import (
35     Command,
36     CommandError,
37     Option,
38     )
39
40 global summary
41 summary = {}
42
43 class LDAPBase(object):
44
45     def __init__(self, host, creds, lp,
46                  two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
47                  view="section", base="", scope="SUB",
48                  outf=sys.stdout, errf=sys.stderr):
49         ldb_options = []
50         samdb_url = host
51         if not "://" in host:
52             if os.path.isfile(host):
53                 samdb_url = "tdb://%s" % host
54             else:
55                 samdb_url = "ldap://%s" % host
56         # use 'paged_search' module when connecting remotely
57         if samdb_url.lower().startswith("ldap://"):
58             ldb_options = ["modules:paged_searches"]
59         self.outf = outf
60         self.errf = errf
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,allow_remaining=True)
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, outf=sys.stdout, errf=sys.stderr):
280         self.outf = outf
281         self.errf = errf
282         self.con = connection
283         self.dn = dn
284         self.sddl = self.con.get_descriptor_sddl(self.dn)
285         self.dacl_list = self.extract_dacl()
286         if self.con.sort_aces:
287             self.dacl_list.sort()
288
289     def extract_dacl(self):
290         """ Extracts the DACL as a list of ACE string (with the brakets).
291         """
292         try:
293             if "S:" in self.sddl:
294                 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
295             else:
296                 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
297         except AttributeError:
298             return []
299         return re.findall("(\(.*?\))", res)
300
301     def fix_guid(self, ace):
302         res = "%s" % ace
303         guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
304         # If there are not GUIDs to replace return the same ACE
305         if len(guids) == 0:
306             return res
307         for guid in guids:
308             try:
309                 name = self.con.guid_map[guid.lower()]
310                 res = res.replace(guid, name)
311             except KeyError:
312                 # Do not bother if the GUID is not found in
313                 # cn=Schema or cn=Extended-Rights
314                 pass
315         return res
316
317     def fix_sid(self, ace):
318         res = "%s" % ace
319         sids = re.findall("S-[-0-9]+", res)
320         # If there are not SIDs to replace return the same ACE
321         if len(sids) == 0:
322             return res
323         for sid in sids:
324             try:
325                 name = self.con.sid_map[sid]
326                 res = res.replace(sid, name)
327             except KeyError:
328                 # Do not bother if the SID is not found in baseDN
329                 pass
330         return res
331
332     def fixit(self, ace):
333         """ Combine all replacement methods in one
334         """
335         res = "%s" % ace
336         res = self.fix_guid(res)
337         res = self.fix_sid(res)
338         return res
339
340     def diff_1(self, other):
341         res = ""
342         if len(self.dacl_list) != len(other.dacl_list):
343             res += 4*" " + "Difference in ACE count:\n"
344             res += 8*" " + "=> %s\n" % len(self.dacl_list)
345             res += 8*" " + "=> %s\n" % len(other.dacl_list)
346         #
347         i = 0
348         flag = True
349         while True:
350             self_ace = None
351             other_ace = None
352             try:
353                 self_ace = "%s" % self.dacl_list[i]
354             except IndexError:
355                 self_ace = ""
356             #
357             try:
358                 other_ace = "%s" % other.dacl_list[i]
359             except IndexError:
360                 other_ace = ""
361             if len(self_ace) + len(other_ace) == 0:
362                 break
363             self_ace_fixed = "%s" % self.fixit(self_ace)
364             other_ace_fixed = "%s" % other.fixit(other_ace)
365             if self_ace_fixed != other_ace_fixed:
366                 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
367                 flag = False
368             else:
369                 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
370             i += 1
371         return (flag, res)
372
373     def diff_2(self, other):
374         res = ""
375         if len(self.dacl_list) != len(other.dacl_list):
376             res += 4*" " + "Difference in ACE count:\n"
377             res += 8*" " + "=> %s\n" % len(self.dacl_list)
378             res += 8*" " + "=> %s\n" % len(other.dacl_list)
379         #
380         common_aces = []
381         self_aces = []
382         other_aces = []
383         self_dacl_list_fixed = []
384         other_dacl_list_fixed = []
385         [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
386         [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
387         for ace in self_dacl_list_fixed:
388             try:
389                 other_dacl_list_fixed.index(ace)
390             except ValueError:
391                 self_aces.append(ace)
392             else:
393                 common_aces.append(ace)
394         self_aces = sorted(self_aces)
395         if len(self_aces) > 0:
396             res += 4*" " + "ACEs found only in %s:\n" % self.con.host
397             for ace in self_aces:
398                 res += 8*" " + ace + "\n"
399         #
400         for ace in other_dacl_list_fixed:
401             try:
402                 self_dacl_list_fixed.index(ace)
403             except ValueError:
404                 other_aces.append(ace)
405             else:
406                 common_aces.append(ace)
407         other_aces = sorted(other_aces)
408         if len(other_aces) > 0:
409             res += 4*" " + "ACEs found only in %s:\n" % other.con.host
410             for ace in other_aces:
411                 res += 8*" " + ace + "\n"
412         #
413         common_aces = sorted(list(set(common_aces)))
414         if self.con.verbose:
415             res += 4*" " + "ACEs found in both:\n"
416             for ace in common_aces:
417                 res += 8*" " + ace + "\n"
418         return (self_aces == [] and other_aces == [], res)
419
420 class LDAPObject(object):
421     def __init__(self, connection, dn, summary, filter_list,
422                  outf=sys.stdout, errf=sys.stderr):
423         self.outf = outf
424         self.errf = errf
425         self.con = connection
426         self.two_domains = self.con.two_domains
427         self.quiet = self.con.quiet
428         self.verbose = self.con.verbose
429         self.summary = summary
430         self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
431         self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
432         for x in self.con.server_names:
433             self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
434         self.attributes = self.con.get_attributes(self.dn)
435         # Attributes that are considered always to be different e.g based on timestamp etc.
436         #
437         # One domain - two domain controllers
438         self.ignore_attributes =  [
439                 # Default Naming Context
440                 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
441                 "operatingSystemVersion","oEMInformation",
442                 "ridNextRID", "rIDPreviousAllocationPool",
443                 # Configuration Naming Context
444                 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
445                 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
446                 # Schema Naming Context
447                 "prefixMap"]
448         if filter_list:
449             self.ignore_attributes += filter_list
450
451         self.dn_attributes = []
452         self.domain_attributes = []
453         self.servername_attributes = []
454         self.netbios_attributes = []
455         self.other_attributes = []
456         # Two domains - two domain controllers
457
458         if self.two_domains:
459             self.ignore_attributes +=  [
460                 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
461                 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
462                 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
463                 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
464                 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
465                 # After Exchange preps
466                 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
467             #
468             # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
469             self.dn_attributes = [
470                 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
471                 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
472                 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
473                 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
474                 # After Exchange preps
475                 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
476                 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
477                 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
478                 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
479             self.dn_attributes = [x.upper() for x in self.dn_attributes]
480             #
481             # Attributes that contain the Domain name e.g. 'samba.org'
482             self.domain_attributes = [
483                 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
484                 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
485             self.domain_attributes = [x.upper() for x in self.domain_attributes]
486             #
487             # May contain DOMAIN_NETBIOS and SERVER_NAME
488             self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
489                 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
490                 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
491             self.servername_attributes = [x.upper() for x in self.servername_attributes]
492             #
493             self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
494             self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
495             #
496             self.other_attributes = [ "name", "DC",]
497             self.other_attributes = [x.upper() for x in self.other_attributes]
498         #
499         self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
500
501     def log(self, msg):
502         """
503         Log on the screen if there is no --quiet oprion set
504         """
505         if not self.quiet:
506             self.outf.write(msg+"\n")
507
508     def fix_dn(self, s):
509         res = "%s" % s
510         if not self.two_domains:
511             return res
512         if res.upper().endswith(self.con.base_dn.upper()):
513             res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
514         return res
515
516     def fix_domain_name(self, s):
517         res = "%s" % s
518         if not self.two_domains:
519             return res
520         res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
521         res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
522         return res
523
524     def fix_domain_netbios(self, s):
525         res = "%s" % s
526         if not self.two_domains:
527             return res
528         res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
529         res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
530         return res
531
532     def fix_server_name(self, s):
533         res = "%s" % s
534         if not self.two_domains or len(self.con.server_names) > 1:
535             return res
536         for x in self.con.server_names:
537             res = res.upper().replace(x, "${SERVER_NAME}")
538         return res
539
540     def __eq__(self, other):
541         if self.con.descriptor:
542             return self.cmp_desc(other)
543         return self.cmp_attrs(other)
544
545     def cmp_desc(self, other):
546         d1 = Descriptor(self.con, self.dn, outf=self.outf, errf=self.errf)
547         d2 = Descriptor(other.con, other.dn, outf=self.outf, errf=self.errf)
548         if self.con.view == "section":
549             res = d1.diff_2(d2)
550         elif self.con.view == "collision":
551             res = d1.diff_1(d2)
552         else:
553             raise Exception("Unknown --view option value.")
554         #
555         self.screen_output = res[1][:-1]
556         other.screen_output = res[1][:-1]
557         #
558         return res[0]
559
560     def cmp_attrs(self, other):
561         res = ""
562         self.unique_attrs = []
563         self.df_value_attrs = []
564         other.unique_attrs = []
565         if self.attributes.keys() != other.attributes.keys():
566             #
567             title = 4*" " + "Attributes found only in %s:" % self.con.host
568             for x in self.attributes.keys():
569                 if not x in other.attributes.keys() and \
570                 not x.upper() in [q.upper() for q in other.ignore_attributes]:
571                     if title:
572                         res += title + "\n"
573                         title = None
574                     res += 8*" " + x + "\n"
575                     self.unique_attrs.append(x)
576             #
577             title = 4*" " + "Attributes found only in %s:" % other.con.host
578             for x in other.attributes.keys():
579                 if not x in self.attributes.keys() and \
580                 not x.upper() in [q.upper() for q in self.ignore_attributes]:
581                     if title:
582                         res += title + "\n"
583                         title = None
584                     res += 8*" " + x + "\n"
585                     other.unique_attrs.append(x)
586         #
587         missing_attrs = [x.upper() for x in self.unique_attrs]
588         missing_attrs += [x.upper() for x in other.unique_attrs]
589         title = 4*" " + "Difference in attribute values:"
590         for x in self.attributes.keys():
591             if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
592                 continue
593             if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
594                 self.attributes[x] = sorted(self.attributes[x])
595                 other.attributes[x] = sorted(other.attributes[x])
596             if self.attributes[x] != other.attributes[x]:
597                 p = None
598                 q = None
599                 m = None
600                 n = None
601                 # First check if the difference can be fixed but shunting the first part
602                 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
603                 if x.upper() in self.other_attributes:
604                     p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
605                     q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
606                     if p == q:
607                         continue
608                 # Attribute values that are list that contain DN based values that may differ
609                 elif x.upper() in self.dn_attributes:
610                     m = p
611                     n = q
612                     if not p and not q:
613                         m = self.attributes[x]
614                         n = other.attributes[x]
615                     p = [self.fix_dn(j) for j in m]
616                     q = [other.fix_dn(j) for j in n]
617                     if p == q:
618                         continue
619                 # Attributes that contain the Domain name in them
620                 if x.upper() in self.domain_attributes:
621                     m = p
622                     n = q
623                     if not p and not q:
624                         m = self.attributes[x]
625                         n = other.attributes[x]
626                     p = [self.fix_domain_name(j) for j in m]
627                     q = [other.fix_domain_name(j) for j in n]
628                     if p == q:
629                         continue
630                 #
631                 if x.upper() in self.servername_attributes:
632                     # Attributes with SERVER_NAME
633                     m = p
634                     n = q
635                     if not p and not q:
636                         m = self.attributes[x]
637                         n = other.attributes[x]
638                     p = [self.fix_server_name(j) for j in m]
639                     q = [other.fix_server_name(j) for j in n]
640                     if p == q:
641                         continue
642                 #
643                 if x.upper() in self.netbios_attributes:
644                     # Attributes with NETBIOS Domain name
645                     m = p
646                     n = q
647                     if not p and not q:
648                         m = self.attributes[x]
649                         n = other.attributes[x]
650                     p = [self.fix_domain_netbios(j) for j in m]
651                     q = [other.fix_domain_netbios(j) for j in n]
652                     if p == q:
653                         continue
654                 #
655                 if title:
656                     res += title + "\n"
657                     title = None
658                 if p and q:
659                     res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
660                 else:
661                     res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
662                 self.df_value_attrs.append(x)
663         #
664         if self.unique_attrs + other.unique_attrs != []:
665             assert self.unique_attrs != other.unique_attrs
666         self.summary["unique_attrs"] += self.unique_attrs
667         self.summary["df_value_attrs"] += self.df_value_attrs
668         other.summary["unique_attrs"] += other.unique_attrs
669         other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
670         #
671         self.screen_output = res[:-1]
672         other.screen_output = res[:-1]
673         #
674         return res == ""
675
676
677 class LDAPBundel(object):
678
679     def __init__(self, connection, context, dn_list=None, filter_list=None,
680                  outf=sys.stdout, errf=sys.stderr):
681         self.outf = outf
682         self.errf = errf
683         self.con = connection
684         self.two_domains = self.con.two_domains
685         self.quiet = self.con.quiet
686         self.verbose = self.con.verbose
687         self.search_base = self.con.search_base
688         self.search_scope = self.con.search_scope
689         self.summary = {}
690         self.summary["unique_attrs"] = []
691         self.summary["df_value_attrs"] = []
692         self.summary["known_ignored_dn"] = []
693         self.summary["abnormal_ignored_dn"] = []
694         self.filter_list = filter_list
695         if dn_list:
696             self.dn_list = dn_list
697         elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
698             self.context = context.upper()
699             self.dn_list = self.get_dn_list(context)
700         else:
701             raise Exception("Unknown initialization data for LDAPBundel().")
702         counter = 0
703         while counter < len(self.dn_list) and self.two_domains:
704             # Use alias reference
705             tmp = self.dn_list[counter]
706             tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
707             tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
708             if len(self.con.server_names) == 1:
709                 for x in self.con.server_names:
710                     tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
711             self.dn_list[counter] = tmp
712             counter += 1
713         self.dn_list = list(set(self.dn_list))
714         self.dn_list = sorted(self.dn_list)
715         self.size = len(self.dn_list)
716
717     def log(self, msg):
718         """
719         Log on the screen if there is no --quiet oprion set
720         """
721         if not self.quiet:
722             self.outf.write(msg+"\n")
723
724     def update_size(self):
725         self.size = len(self.dn_list)
726         self.dn_list = sorted(self.dn_list)
727
728     def __eq__(self, other):
729         res = True
730         if self.size != other.size:
731             self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
732             res = False
733         #
734         # This is the case where we want to explicitly compare two objects with different DNs.
735         # It does not matter if they are in the same DC, in two DC in one domain or in two
736         # different domains.
737         if self.search_scope != SCOPE_BASE:
738             title= "\n* DNs found only in %s:" % self.con.host
739             for x in self.dn_list:
740                 if not x.upper() in [q.upper() for q in other.dn_list]:
741                     if title:
742                         self.log( title )
743                         title = None
744                         res = False
745                     self.log( 4*" " + x )
746                     self.dn_list[self.dn_list.index(x)] = ""
747             self.dn_list = [x for x in self.dn_list if x]
748             #
749             title= "\n* DNs found only in %s:" % other.con.host
750             for x in other.dn_list:
751                 if not x.upper() in [q.upper() for q in self.dn_list]:
752                     if title:
753                         self.log( title )
754                         title = None
755                         res = False
756                     self.log( 4*" " + x )
757                     other.dn_list[other.dn_list.index(x)] = ""
758             other.dn_list = [x for x in other.dn_list if x]
759             #
760             self.update_size()
761             other.update_size()
762             assert self.size == other.size
763             assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
764         self.log( "\n* Objects to be compared: %s" % self.size )
765
766         index = 0
767         while index < self.size:
768             skip = False
769             try:
770                 object1 = LDAPObject(connection=self.con,
771                                      dn=self.dn_list[index],
772                                      summary=self.summary,
773                                      filter_list=self.filter_list,
774                                      outf=self.outf, errf=self.errf)
775             except LdbError, (enum, estr):
776                 if enum == ERR_NO_SUCH_OBJECT:
777                     self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
778                     skip = True
779                 raise
780             try:
781                 object2 = LDAPObject(connection=other.con,
782                         dn=other.dn_list[index],
783                         summary=other.summary,
784                         filter_list=self.filter_list,
785                         outf=self.outf, errf=self.errf)
786             except LdbError, (enum, estr):
787                 if enum == ERR_NO_SUCH_OBJECT:
788                     self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
789                     skip = True
790                 raise
791             if skip:
792                 index += 1
793                 continue
794             if object1 == object2:
795                 if self.con.verbose:
796                     self.log( "\nComparing:" )
797                     self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
798                     self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
799                     self.log( 4*" " + "OK" )
800             else:
801                 self.log( "\nComparing:" )
802                 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
803                 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
804                 self.log( object1.screen_output )
805                 self.log( 4*" " + "FAILED" )
806                 res = False
807             self.summary = object1.summary
808             other.summary = object2.summary
809             index += 1
810         #
811         return res
812
813     def get_dn_list(self, context):
814         """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
815             Parse all DNs and filter those that are 'strange' or abnormal.
816         """
817         if context.upper() == "DOMAIN":
818             search_base = self.con.base_dn
819         elif context.upper() == "CONFIGURATION":
820             search_base = self.con.config_dn
821         elif context.upper() == "SCHEMA":
822             search_base = self.con.schema_dn
823         elif context.upper() == "DNSDOMAIN":
824             search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
825         elif context.upper() == "DNSFOREST":
826             search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
827
828         dn_list = []
829         if not self.search_base:
830             self.search_base = search_base
831         self.search_scope = self.search_scope.upper()
832         if self.search_scope == "SUB":
833             self.search_scope = SCOPE_SUBTREE
834         elif self.search_scope == "BASE":
835             self.search_scope = SCOPE_BASE
836         elif self.search_scope == "ONE":
837             self.search_scope = SCOPE_ONELEVEL
838         else:
839             raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
840         try:
841             res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
842         except LdbError, (enum, estr):
843             self.outf.write("Failed search of base=%s\n" % self.search_base)
844             raise
845         for x in res:
846            dn_list.append(x["dn"].get_linearized())
847         #
848         global summary
849         #
850         return dn_list
851
852     def print_summary(self):
853         self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
854         self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
855         #
856         if self.summary["unique_attrs"]:
857             self.log( "\nAttributes found only in %s:" % self.con.host )
858             self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
859         #
860         if self.summary["df_value_attrs"]:
861             self.log( "\nAttributes with different values:" )
862             self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
863             self.summary["df_value_attrs"] = []
864
865
866 class cmd_ldapcmp(Command):
867     """Compare two ldap databases."""
868     synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
869
870     takes_optiongroups = {
871         "sambaopts": options.SambaOptions,
872         "versionopts": options.VersionOptions,
873         "credopts": options.CredentialsOptionsDouble,
874     }
875
876     takes_optiongroups = {
877         "sambaopts": options.SambaOptions,
878         "versionopts": options.VersionOptions,
879         "credopts": options.CredentialsOptionsDouble,
880     }
881
882     takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
883
884     takes_options = [
885         Option("-w", "--two", dest="two", action="store_true", default=False,
886             help="Hosts are in two different domains"),
887         Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
888             help="Do not print anything but relay on just exit code"),
889         Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
890             help="Print all DN pairs that have been compared"),
891         Option("--sd", dest="descriptor", action="store_true", default=False,
892             help="Compare nTSecurityDescriptor attibutes only"),
893         Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
894             help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
895         Option("--view", dest="view", default="section",
896             help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
897         Option("--base", dest="base", default="",
898             help="Pass search base that will build DN list for the first DC."),
899         Option("--base2", dest="base2", default="",
900             help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
901         Option("--scope", dest="scope", default="SUB",
902             help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
903         Option("--filter", dest="filter", default="",
904             help="List of comma separated attributes to ignore in the comparision"),
905         ]
906
907     def run(self, URL1, URL2,
908             context1=None, context2=None, context3=None,
909             two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
910             view="section", base="", base2="", scope="SUB", filter="",
911             credopts=None, sambaopts=None, versionopts=None):
912
913         lp = sambaopts.get_loadparm()
914
915         using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
916
917         if using_ldap:
918             creds = credopts.get_credentials(lp, fallback_machine=True)
919         else:
920             creds = None
921         creds2 = credopts.get_credentials2(lp, guess=False)
922         if creds2.is_anonymous():
923             creds2 = creds
924         else:
925             creds2.set_domain("")
926             creds2.set_workstation("")
927         if using_ldap and not creds.authentication_requested():
928             raise CommandError("You must supply at least one username/password pair")
929
930         # make a list of contexts to compare in
931         contexts = []
932         if context1 is None:
933             if base and base2:
934                 # If search bases are specified context is defaulted to
935                 # DOMAIN so the given search bases can be verified.
936                 contexts = ["DOMAIN"]
937             else:
938                 # if no argument given, we compare all contexts
939                 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
940         else:
941             for c in [context1, context2, context3]:
942                 if c is None:
943                     continue
944                 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
945                     raise CommandError("Incorrect argument: %s" % c)
946                 contexts.append(c.upper())
947
948         if verbose and quiet:
949             raise CommandError("You cannot set --verbose and --quiet together")
950         if (not base and base2) or (base and not base2):
951             raise CommandError("You need to specify both --base and --base2 at the same time")
952         if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
953             raise CommandError("Invalid --view value. Choose from: section or collision")
954         if not scope.upper() in ["SUB", "ONE", "BASE"]:
955             raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
956
957         con1 = LDAPBase(URL1, creds, lp,
958                         two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
959                         verbose=verbose,view=view, base=base, scope=scope,
960                         outf=self.outf, errf=self.errf)
961         assert len(con1.base_dn) > 0
962
963         con2 = LDAPBase(URL2, creds2, lp,
964                         two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
965                         verbose=verbose, view=view, base=base2, scope=scope,
966                         outf=self.outf, errf=self.errf)
967         assert len(con2.base_dn) > 0
968
969         filter_list = filter.split(",")
970
971         status = 0
972         for context in contexts:
973             if not quiet:
974                 self.outf.write("\n* Comparing [%s] context...\n" % context)
975
976             b1 = LDAPBundel(con1, context=context, filter_list=filter_list,
977                             outf=self.outf, errf=self.errf)
978             b2 = LDAPBundel(con2, context=context, filter_list=filter_list,
979                             outf=self.outf, errf=self.errf)
980
981             if b1 == b2:
982                 if not quiet:
983                     self.outf.write("\n* Result for [%s]: SUCCESS\n" %
984                         context)
985             else:
986                 if not quiet:
987                     self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
988                     if not descriptor:
989                         assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
990                         b2.summary["df_value_attrs"] = []
991                         self.outf.write("\nSUMMARY\n")
992                         self.outf.write("---------\n")
993                         b1.print_summary()
994                         b2.print_summary()
995                 # mark exit status as FAILURE if a least one comparison failed
996                 status = -1
997         if status != 0:
998             raise CommandError("Compare failed: %d" % status)