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