96b94f2c16a2de575e0f9cc5eb0119f841a7fff6
[metze/samba/wip.git] / 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, skip_missing_dn=True):
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.skip_missing_dn = skip_missing_dn
75         self.base_dn = str(self.ldb.get_default_basedn())
76         self.root_dn = str(self.ldb.get_root_basedn())
77         self.config_dn = str(self.ldb.get_config_basedn())
78         self.schema_dn = str(self.ldb.get_schema_basedn())
79         self.domain_netbios = self.find_netbios()
80         self.server_names = self.find_servers()
81         self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
82         self.domain_sid = self.find_domain_sid()
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_sid_map(self):
253         """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
254         """
255         self.sid_map = {}
256         res = self.ldb.search(base=self.base_dn,
257                               expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
258         for item in res:
259             try:
260                 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
261             except KeyError:
262                 pass
263
264 class Descriptor(object):
265     def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr):
266         self.outf = outf
267         self.errf = errf
268         self.con = connection
269         self.dn = dn
270         self.sddl = self.con.get_descriptor_sddl(self.dn)
271         self.dacl_list = self.extract_dacl()
272         if self.con.sort_aces:
273             self.dacl_list.sort()
274
275     def extract_dacl(self):
276         """ Extracts the DACL as a list of ACE string (with the brakets).
277         """
278         try:
279             if "S:" in self.sddl:
280                 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
281             else:
282                 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
283         except AttributeError:
284             return []
285         return re.findall("(\(.*?\))", res)
286
287     def fix_sid(self, ace):
288         res = "%s" % ace
289         sids = re.findall("S-[-0-9]+", res)
290         # If there are not SIDs to replace return the same ACE
291         if len(sids) == 0:
292             return res
293         for sid in sids:
294             try:
295                 name = self.con.sid_map[sid]
296                 res = res.replace(sid, name)
297             except KeyError:
298                 # Do not bother if the SID is not found in baseDN
299                 pass
300         return res
301
302     def diff_1(self, other):
303         res = ""
304         if len(self.dacl_list) != len(other.dacl_list):
305             res += 4*" " + "Difference in ACE count:\n"
306             res += 8*" " + "=> %s\n" % len(self.dacl_list)
307             res += 8*" " + "=> %s\n" % len(other.dacl_list)
308         #
309         i = 0
310         flag = True
311         while True:
312             self_ace = None
313             other_ace = None
314             try:
315                 self_ace = "%s" % self.dacl_list[i]
316             except IndexError:
317                 self_ace = ""
318             #
319             try:
320                 other_ace = "%s" % other.dacl_list[i]
321             except IndexError:
322                 other_ace = ""
323             if len(self_ace) + len(other_ace) == 0:
324                 break
325             self_ace_fixed = "%s" % self.fix_sid(self_ace)
326             other_ace_fixed = "%s" % other.fix_sid(other_ace)
327             if self_ace_fixed != other_ace_fixed:
328                 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
329                 flag = False
330             else:
331                 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
332             i += 1
333         return (flag, res)
334
335     def diff_2(self, other):
336         res = ""
337         if len(self.dacl_list) != len(other.dacl_list):
338             res += 4*" " + "Difference in ACE count:\n"
339             res += 8*" " + "=> %s\n" % len(self.dacl_list)
340             res += 8*" " + "=> %s\n" % len(other.dacl_list)
341         #
342         common_aces = []
343         self_aces = []
344         other_aces = []
345         self_dacl_list_fixed = []
346         other_dacl_list_fixed = []
347         [self_dacl_list_fixed.append( self.fix_sid(ace) ) for ace in self.dacl_list]
348         [other_dacl_list_fixed.append( other.fix_sid(ace) ) for ace in other.dacl_list]
349         for ace in self_dacl_list_fixed:
350             try:
351                 other_dacl_list_fixed.index(ace)
352             except ValueError:
353                 self_aces.append(ace)
354             else:
355                 common_aces.append(ace)
356         self_aces = sorted(self_aces)
357         if len(self_aces) > 0:
358             res += 4*" " + "ACEs found only in %s:\n" % self.con.host
359             for ace in self_aces:
360                 res += 8*" " + ace + "\n"
361         #
362         for ace in other_dacl_list_fixed:
363             try:
364                 self_dacl_list_fixed.index(ace)
365             except ValueError:
366                 other_aces.append(ace)
367             else:
368                 common_aces.append(ace)
369         other_aces = sorted(other_aces)
370         if len(other_aces) > 0:
371             res += 4*" " + "ACEs found only in %s:\n" % other.con.host
372             for ace in other_aces:
373                 res += 8*" " + ace + "\n"
374         #
375         common_aces = sorted(list(set(common_aces)))
376         if self.con.verbose:
377             res += 4*" " + "ACEs found in both:\n"
378             for ace in common_aces:
379                 res += 8*" " + ace + "\n"
380         return (self_aces == [] and other_aces == [], res)
381
382 class LDAPObject(object):
383     def __init__(self, connection, dn, summary, filter_list,
384                  outf=sys.stdout, errf=sys.stderr):
385         self.outf = outf
386         self.errf = errf
387         self.con = connection
388         self.two_domains = self.con.two_domains
389         self.quiet = self.con.quiet
390         self.verbose = self.con.verbose
391         self.summary = summary
392         self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
393         self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
394         for x in self.con.server_names:
395             self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
396         self.attributes = self.con.get_attributes(self.dn)
397         # One domain - two domain controllers
398         #
399         # Some attributes are defined as FLAG_ATTR_NOT_REPLICATED
400         #
401         # The following list was generated by
402         # egrep '^systemFlags: |^ldapDisplayName: |^linkID: ' \
403         #       source4/setup/ad-schema/MS-AD_Schema_2K8_R2_Attributes.txt | \
404         #       grep -B1 FLAG_ATTR_NOT_REPLICATED | \
405         #       grep ldapDisplayName | \
406         #       cut -d ' ' -f2
407         self.non_replicated_attributes = [
408                 "badPasswordTime",
409                 "badPwdCount",
410                 "dSCorePropagationData",
411                 "lastLogoff",
412                 "lastLogon",
413                 "logonCount",
414                 "modifiedCount",
415                 "msDS-Cached-Membership",
416                 "msDS-Cached-Membership-Time-Stamp",
417                 "msDS-EnabledFeatureBL",
418                 "msDS-ExecuteScriptPassword",
419                 "msDS-NcType",
420                 "msDS-ReplicationEpoch",
421                 "msDS-RetiredReplNCSignatures",
422                 "msDS-USNLastSyncSuccess",
423                 # "distinguishedName", # This is implicitly replicated
424                 # "objectGUID", # This is implicitly replicated
425                 "partialAttributeDeletionList",
426                 "partialAttributeSet",
427                 "pekList",
428                 "prefixMap",
429                 "replPropertyMetaData",
430                 "replUpToDateVector",
431                 "repsFrom",
432                 "repsTo",
433                 "rIDNextRID",
434                 "rIDPreviousAllocationPool",
435                 "schemaUpdate",
436                 "serverState",
437                 "subRefs",
438                 "uSNChanged",
439                 "uSNCreated",
440                 "uSNLastObjRem",
441                 # "whenChanged", # This is implicitly replicated
442         ]
443         self.ignore_attributes = self.non_replicated_attributes
444         self.ignore_attributes += ["msExchServer1HighestUSN"]
445         if filter_list:
446             self.ignore_attributes += filter_list
447
448         self.dn_attributes = []
449         self.domain_attributes = []
450         self.servername_attributes = []
451         self.netbios_attributes = []
452         self.other_attributes = []
453         # Two domains - two domain controllers
454
455         if self.two_domains:
456             self.ignore_attributes +=  [
457                 "objectCategory", "objectGUID", "objectSid", "whenCreated", "whenChanged", "pwdLastSet", "uSNCreated", "creationTime",
458                 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
459                 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
460                 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
461                 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
462                 "operatingSystemVersion", "oEMInformation",
463                 # After Exchange preps
464                 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
465             #
466             # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
467             self.dn_attributes = [
468                 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
469                 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
470                 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
471                 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
472                 # After Exchange preps
473                 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
474                 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
475                 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
476                 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
477             self.dn_attributes = [x.upper() for x in self.dn_attributes]
478             #
479             # Attributes that contain the Domain name e.g. 'samba.org'
480             self.domain_attributes = [
481                 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
482                 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
483             self.domain_attributes = [x.upper() for x in self.domain_attributes]
484             #
485             # May contain DOMAIN_NETBIOS and SERVER_NAME
486             self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
487                 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
488                 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
489             self.servername_attributes = [x.upper() for x in self.servername_attributes]
490             #
491             self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
492             self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
493             #
494             self.other_attributes = [ "name", "DC",]
495             self.other_attributes = [x.upper() for x in self.other_attributes]
496         #
497         self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
498
499     def log(self, msg):
500         """
501         Log on the screen if there is no --quiet option set
502         """
503         if not self.quiet:
504             self.outf.write(msg+"\n")
505
506     def fix_dn(self, s):
507         res = "%s" % s
508         if not self.two_domains:
509             return res
510         if res.upper().endswith(self.con.base_dn.upper()):
511             res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
512         return res
513
514     def fix_domain_name(self, s):
515         res = "%s" % s
516         if not self.two_domains:
517             return res
518         res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
519         res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
520         return res
521
522     def fix_domain_netbios(self, s):
523         res = "%s" % s
524         if not self.two_domains:
525             return res
526         res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
527         res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
528         return res
529
530     def fix_server_name(self, s):
531         res = "%s" % s
532         if not self.two_domains or len(self.con.server_names) > 1:
533             return res
534         for x in self.con.server_names:
535             res = res.upper().replace(x, "${SERVER_NAME}")
536         return res
537
538     def __eq__(self, other):
539         if self.con.descriptor:
540             return self.cmp_desc(other)
541         return self.cmp_attrs(other)
542
543     def cmp_desc(self, other):
544         d1 = Descriptor(self.con, self.dn, outf=self.outf, errf=self.errf)
545         d2 = Descriptor(other.con, other.dn, outf=self.outf, errf=self.errf)
546         if self.con.view == "section":
547             res = d1.diff_2(d2)
548         elif self.con.view == "collision":
549             res = d1.diff_1(d2)
550         else:
551             raise Exception("Unknown --view option value.")
552         #
553         self.screen_output = res[1][:-1]
554         other.screen_output = res[1][:-1]
555         #
556         return res[0]
557
558     def cmp_attrs(self, other):
559         res = ""
560         self.unique_attrs = []
561         self.df_value_attrs = []
562         other.unique_attrs = []
563         if self.attributes.keys() != other.attributes.keys():
564             #
565             title = 4*" " + "Attributes found only in %s:" % self.con.host
566             for x in self.attributes.keys():
567                 if not x in other.attributes.keys() and \
568                 not x.upper() in [q.upper() for q in other.ignore_attributes]:
569                     if title:
570                         res += title + "\n"
571                         title = None
572                     res += 8*" " + x + "\n"
573                     self.unique_attrs.append(x)
574             #
575             title = 4*" " + "Attributes found only in %s:" % other.con.host
576             for x in other.attributes.keys():
577                 if not x in self.attributes.keys() and \
578                 not x.upper() in [q.upper() for q in self.ignore_attributes]:
579                     if title:
580                         res += title + "\n"
581                         title = None
582                     res += 8*" " + x + "\n"
583                     other.unique_attrs.append(x)
584         #
585         missing_attrs = [x.upper() for x in self.unique_attrs]
586         missing_attrs += [x.upper() for x in other.unique_attrs]
587         title = 4*" " + "Difference in attribute values:"
588         for x in self.attributes.keys():
589             if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
590                 continue
591             if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
592                 self.attributes[x] = sorted(self.attributes[x])
593                 other.attributes[x] = sorted(other.attributes[x])
594             if self.attributes[x] != other.attributes[x]:
595                 p = None
596                 q = None
597                 m = None
598                 n = None
599                 # First check if the difference can be fixed but shunting the first part
600                 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
601                 if x.upper() in self.other_attributes:
602                     p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
603                     q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
604                     if p == q:
605                         continue
606                 # Attribute values that are list that contain DN based values that may differ
607                 elif x.upper() in self.dn_attributes:
608                     m = p
609                     n = q
610                     if not p and not q:
611                         m = self.attributes[x]
612                         n = other.attributes[x]
613                     p = [self.fix_dn(j) for j in m]
614                     q = [other.fix_dn(j) for j in n]
615                     if p == q:
616                         continue
617                 # Attributes that contain the Domain name in them
618                 if x.upper() in self.domain_attributes:
619                     m = p
620                     n = q
621                     if not p and not q:
622                         m = self.attributes[x]
623                         n = other.attributes[x]
624                     p = [self.fix_domain_name(j) for j in m]
625                     q = [other.fix_domain_name(j) for j in n]
626                     if p == q:
627                         continue
628                 #
629                 if x.upper() in self.servername_attributes:
630                     # Attributes with SERVER_NAME
631                     m = p
632                     n = q
633                     if not p and not q:
634                         m = self.attributes[x]
635                         n = other.attributes[x]
636                     p = [self.fix_server_name(j) for j in m]
637                     q = [other.fix_server_name(j) for j in n]
638                     if p == q:
639                         continue
640                 #
641                 if x.upper() in self.netbios_attributes:
642                     # Attributes with NETBIOS Domain name
643                     m = p
644                     n = q
645                     if not p and not q:
646                         m = self.attributes[x]
647                         n = other.attributes[x]
648                     p = [self.fix_domain_netbios(j) for j in m]
649                     q = [other.fix_domain_netbios(j) for j in n]
650                     if p == q:
651                         continue
652                 #
653                 if title:
654                     res += title + "\n"
655                     title = None
656                 if p and q:
657                     res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
658                 else:
659                     res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
660                 self.df_value_attrs.append(x)
661         #
662         if self.unique_attrs + other.unique_attrs != []:
663             assert self.unique_attrs != other.unique_attrs
664         self.summary["unique_attrs"] += self.unique_attrs
665         self.summary["df_value_attrs"] += self.df_value_attrs
666         other.summary["unique_attrs"] += other.unique_attrs
667         other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
668         #
669         self.screen_output = res[:-1]
670         other.screen_output = res[:-1]
671         #
672         return res == ""
673
674
675 class LDAPBundel(object):
676
677     def __init__(self, connection, context, dn_list=None, filter_list=None,
678                  outf=sys.stdout, errf=sys.stderr):
679         self.outf = outf
680         self.errf = errf
681         self.con = connection
682         self.two_domains = self.con.two_domains
683         self.quiet = self.con.quiet
684         self.verbose = self.con.verbose
685         self.search_base = self.con.search_base
686         self.search_scope = self.con.search_scope
687         self.skip_missing_dn = self.con.skip_missing_dn
688         self.summary = {}
689         self.summary["unique_attrs"] = []
690         self.summary["df_value_attrs"] = []
691         self.summary["known_ignored_dn"] = []
692         self.summary["abnormal_ignored_dn"] = []
693         self.filter_list = filter_list
694         if dn_list:
695             self.dn_list = dn_list
696         elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
697             self.context = context.upper()
698             self.dn_list = self.get_dn_list(context)
699         else:
700             raise Exception("Unknown initialization data for LDAPBundel().")
701         counter = 0
702         while counter < len(self.dn_list) and self.two_domains:
703             # Use alias reference
704             tmp = self.dn_list[counter]
705             tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
706             tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
707             if len(self.con.server_names) == 1:
708                 for x in self.con.server_names:
709                     tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
710             self.dn_list[counter] = tmp
711             counter += 1
712         self.dn_list = list(set(self.dn_list))
713         self.dn_list = sorted(self.dn_list)
714         self.size = len(self.dn_list)
715
716     def log(self, msg):
717         """
718         Log on the screen if there is no --quiet option set
719         """
720         if not self.quiet:
721             self.outf.write(msg+"\n")
722
723     def update_size(self):
724         self.size = len(self.dn_list)
725         self.dn_list = sorted(self.dn_list)
726
727     def __eq__(self, other):
728         res = True
729         if self.size != other.size:
730             self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
731             if not self.skip_missing_dn:
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 and not self.skip_missing_dn:
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 and not self.skip_missing_dn:
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?", "context4?", "context5?"]
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         Option("--skip-missing-dn", dest="skip_missing_dn", action="store_true", default=False,
906             help="Skip report and failure due to missing DNs in one server or another"),
907         ]
908
909     def run(self, URL1, URL2,
910             context1=None, context2=None, context3=None, context4=None, context5=None,
911             two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
912             view="section", base="", base2="", scope="SUB", filter="",
913             credopts=None, sambaopts=None, versionopts=None, skip_missing_dn=False):
914
915         lp = sambaopts.get_loadparm()
916
917         using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
918
919         if using_ldap:
920             creds = credopts.get_credentials(lp, fallback_machine=True)
921         else:
922             creds = None
923         creds2 = credopts.get_credentials2(lp, guess=False)
924         if creds2.is_anonymous():
925             creds2 = creds
926         else:
927             creds2.set_domain("")
928             creds2.set_workstation("")
929         if using_ldap and not creds.authentication_requested():
930             raise CommandError("You must supply at least one username/password pair")
931
932         # make a list of contexts to compare in
933         contexts = []
934         if context1 is None:
935             if base and base2:
936                 # If search bases are specified context is defaulted to
937                 # DOMAIN so the given search bases can be verified.
938                 contexts = ["DOMAIN"]
939             else:
940                 # if no argument given, we compare all contexts
941                 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
942         else:
943             for c in [context1, context2, context3, context4, context5]:
944                 if c is None:
945                     continue
946                 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
947                     raise CommandError("Incorrect argument: %s" % c)
948                 contexts.append(c.upper())
949
950         if verbose and quiet:
951             raise CommandError("You cannot set --verbose and --quiet together")
952         if (not base and base2) or (base and not base2):
953             raise CommandError("You need to specify both --base and --base2 at the same time")
954         if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
955             raise CommandError("Invalid --view value. Choose from: section or collision")
956         if not scope.upper() in ["SUB", "ONE", "BASE"]:
957             raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
958
959         con1 = LDAPBase(URL1, creds, lp,
960                         two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
961                         verbose=verbose,view=view, base=base, scope=scope,
962                         outf=self.outf, errf=self.errf)
963         assert len(con1.base_dn) > 0
964
965         con2 = LDAPBase(URL2, creds2, lp,
966                         two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
967                         verbose=verbose, view=view, base=base2, scope=scope,
968                         outf=self.outf, errf=self.errf)
969         assert len(con2.base_dn) > 0
970
971         filter_list = filter.split(",")
972
973         status = 0
974         for context in contexts:
975             if not quiet:
976                 self.outf.write("\n* Comparing [%s] context...\n" % context)
977
978             b1 = LDAPBundel(con1, context=context, filter_list=filter_list,
979                             outf=self.outf, errf=self.errf)
980             b2 = LDAPBundel(con2, context=context, filter_list=filter_list,
981                             outf=self.outf, errf=self.errf)
982
983             if b1 == b2:
984                 if not quiet:
985                     self.outf.write("\n* Result for [%s]: SUCCESS\n" %
986                         context)
987             else:
988                 if not quiet:
989                     self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
990                     if not descriptor:
991                         assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
992                         b2.summary["df_value_attrs"] = []
993                         self.outf.write("\nSUMMARY\n")
994                         self.outf.write("---------\n")
995                         b1.print_summary()
996                         b2.print_summary()
997                 # mark exit status as FAILURE if a least one comparison failed
998                 status = -1
999         if status != 0:
1000             raise CommandError("Compare failed: %d" % status)