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