samba-tool: Add --filter option to ldapcmp to ignore specified attributes
[Samba/gebeck_regimport.git] / source4 / scripting / python / samba / netcmd / ldapcmp.py
blob1db1372eef947607cb9349b698e82b065cec7781
1 #!/usr/bin/env python
3 # Unix SMB/CIFS implementation.
4 # A command to compare differences of objects and attributes between
5 # two LDAP servers both running at the same time. It generally compares
6 # one of the three pratitions DOMAIN, CONFIGURATION or SCHEMA. Users
7 # that have to be provided sheould be able to read objects in any of the
8 # above partitions.
10 # Copyright (C) Zahari Zahariev <zahari.zahariev@postpath.com> 2009, 2010
11 # Copyright Giampaolo Lauria 2011 <lauria2@yahoo.com>
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 3 of the License, or
16 # (at your option) any later version.
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 import os
28 import re
29 import sys
31 import samba
32 import samba.getopt as options
33 from samba import Ldb
34 from samba.ndr import ndr_pack, ndr_unpack
35 from samba.dcerpc import security
36 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, ERR_NO_SUCH_OBJECT, LdbError
37 from samba.netcmd import (
38 Command,
39 CommandError,
40 Option,
41 SuperCommand,
44 global summary
45 summary = {}
47 class LDAPBase(object):
49 def __init__(self, host, creds, lp,
50 two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
51 view="section", base="", scope="SUB"):
52 ldb_options = []
53 samdb_url = host
54 if not "://" in host:
55 if os.path.isfile(host):
56 samdb_url = "tdb://%s" % host
57 else:
58 samdb_url = "ldap://%s" % host
59 # use 'paged_search' module when connecting remotely
60 if samdb_url.lower().startswith("ldap://"):
61 ldb_options = ["modules:paged_searches"]
62 self.ldb = Ldb(url=samdb_url,
63 credentials=creds,
64 lp=lp,
65 options=ldb_options)
66 self.search_base = base
67 self.search_scope = scope
68 self.two_domains = two
69 self.quiet = quiet
70 self.descriptor = descriptor
71 self.sort_aces = sort_aces
72 self.view = view
73 self.verbose = verbose
74 self.host = host
75 self.base_dn = str(self.ldb.get_default_basedn())
76 self.config_dn = str(self.ldb.get_config_basedn())
77 self.schema_dn = str(self.ldb.get_schema_basedn())
78 self.domain_netbios = self.find_netbios()
79 self.server_names = self.find_servers()
80 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
81 self.domain_sid = self.find_domain_sid()
82 self.get_guid_map()
83 self.get_sid_map()
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 print "\n* Place-holders for %s:" % self.host
89 print 4*" " + "${DOMAIN_DN} => %s" % self.base_dn
90 print 4*" " + "${DOMAIN_NETBIOS} => %s" % self.domain_netbios
91 print 4*" " + "${SERVER_NAME} => %s" % self.server_names
92 print 4*" " + "${DOMAIN_NAME} => %s" % self.domain_name
94 def find_domain_sid(self):
95 res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
96 return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
98 def find_servers(self):
99 """
101 res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \
102 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
103 assert len(res) > 0
104 srv = []
105 for x in res:
106 srv.append(x["cn"][0])
107 return srv
109 def find_netbios(self):
110 res = self.ldb.search(base="CN=Partitions,%s" % self.config_dn, \
111 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
112 assert len(res) > 0
113 for x in res:
114 if "nETBIOSName" in x.keys():
115 return x["nETBIOSName"][0]
117 def object_exists(self, object_dn):
118 res = None
119 try:
120 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
121 except LdbError, (enum, estr):
122 if enum == ERR_NO_SUCH_OBJECT:
123 return False
124 raise
125 return len(res) == 1
127 def delete_force(self, object_dn):
128 try:
129 self.ldb.delete(object_dn)
130 except Ldb.LdbError, e:
131 assert "No such object" in str(e)
133 def get_attribute_name(self, key):
134 """ Returns the real attribute name
135 It resolved ranged results e.g. member;range=0-1499
138 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
140 m = r.match(key)
141 if m is None:
142 return key
144 return m.group(1)
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
151 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
153 m = r.match(key)
154 if m is None:
155 # no range, just return the values
156 return vals
158 attr = m.group(1)
159 hi = int(m.group(3))
161 # get additional values in a loop
162 # until we get a response with '*' at the end
163 while True:
165 n = "%s;range=%d-*" % (attr, hi + 1)
166 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
167 assert len(res) == 1
168 res = dict(res[0])
169 del res["dn"]
171 fm = None
172 fvals = None
174 for key in res.keys():
175 m = r.match(key)
177 if m is None:
178 continue
180 if m.group(1) != attr:
181 continue
183 fm = m
184 fvals = list(res[key])
185 break
187 if fm is None:
188 break
190 vals.extend(fvals)
191 if fm.group(3) == "*":
192 # if we got "*" we're done
193 break
195 assert int(fm.group(2)) == hi + 1
196 hi = int(fm.group(3))
198 return vals
200 def get_attributes(self, object_dn):
201 """ Returns dict with all default visible attributes
203 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
204 assert len(res) == 1
205 res = dict(res[0])
206 # 'Dn' element is not iterable and we have it as 'distinguishedName'
207 del res["dn"]
208 for key in res.keys():
209 vals = list(res[key])
210 del res[key]
211 name = self.get_attribute_name(key)
212 res[name] = self.get_attribute_values(object_dn, key, vals)
214 return res
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)
220 return desc.as_sddl(self.domain_sid)
222 def guid_as_string(self, guid_blob):
223 """ Translate binary representation of schemaIDGUID to standard string representation.
224 @gid_blob: binary schemaIDGUID
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(" ", "-")
248 def get_guid_map(self):
249 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
251 self.guid_map = {}
252 res = self.ldb.search(base=self.schema_dn,
253 expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
254 for item in res:
255 self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
257 res = self.ldb.search(base="cn=extended-rights,%s" % self.config_dn,
258 expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
259 for item in res:
260 self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
262 def get_sid_map(self):
263 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
265 self.sid_map = {}
266 res = self.ldb.search(base=self.base_dn,
267 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
268 for item in res:
269 try:
270 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
271 except KeyError:
272 pass
274 class Descriptor(object):
275 def __init__(self, connection, dn):
276 self.con = connection
277 self.dn = dn
278 self.sddl = self.con.get_descriptor_sddl(self.dn)
279 self.dacl_list = self.extract_dacl()
280 if self.con.sort_aces:
281 self.dacl_list.sort()
283 def extract_dacl(self):
284 """ Extracts the DACL as a list of ACE string (with the brakets).
286 try:
287 if "S:" in self.sddl:
288 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
289 else:
290 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
291 except AttributeError:
292 return []
293 return re.findall("(\(.*?\))", res)
295 def fix_guid(self, ace):
296 res = "%s" % ace
297 guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
298 # If there are not GUIDs to replace return the same ACE
299 if len(guids) == 0:
300 return res
301 for guid in guids:
302 try:
303 name = self.con.guid_map[guid.lower()]
304 res = res.replace(guid, name)
305 except KeyError:
306 # Do not bother if the GUID is not found in
307 # cn=Schema or cn=Extended-Rights
308 pass
309 return res
311 def fix_sid(self, ace):
312 res = "%s" % ace
313 sids = re.findall("S-[-0-9]+", res)
314 # If there are not SIDs to replace return the same ACE
315 if len(sids) == 0:
316 return res
317 for sid in sids:
318 try:
319 name = self.con.sid_map[sid]
320 res = res.replace(sid, name)
321 except KeyError:
322 # Do not bother if the SID is not found in baseDN
323 pass
324 return res
326 def fixit(self, ace):
327 """ Combine all replacement methods in one
329 res = "%s" % ace
330 res = self.fix_guid(res)
331 res = self.fix_sid(res)
332 return res
334 def diff_1(self, other):
335 res = ""
336 if len(self.dacl_list) != len(other.dacl_list):
337 res += 4*" " + "Difference in ACE count:\n"
338 res += 8*" " + "=> %s\n" % len(self.dacl_list)
339 res += 8*" " + "=> %s\n" % len(other.dacl_list)
341 i = 0
342 flag = True
343 while True:
344 self_ace = None
345 other_ace = None
346 try:
347 self_ace = "%s" % self.dacl_list[i]
348 except IndexError:
349 self_ace = ""
351 try:
352 other_ace = "%s" % other.dacl_list[i]
353 except IndexError:
354 other_ace = ""
355 if len(self_ace) + len(other_ace) == 0:
356 break
357 self_ace_fixed = "%s" % self.fixit(self_ace)
358 other_ace_fixed = "%s" % other.fixit(other_ace)
359 if self_ace_fixed != other_ace_fixed:
360 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
361 flag = False
362 else:
363 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
364 i += 1
365 return (flag, res)
367 def diff_2(self, other):
368 res = ""
369 if len(self.dacl_list) != len(other.dacl_list):
370 res += 4*" " + "Difference in ACE count:\n"
371 res += 8*" " + "=> %s\n" % len(self.dacl_list)
372 res += 8*" " + "=> %s\n" % len(other.dacl_list)
374 common_aces = []
375 self_aces = []
376 other_aces = []
377 self_dacl_list_fixed = []
378 other_dacl_list_fixed = []
379 [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
380 [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
381 for ace in self_dacl_list_fixed:
382 try:
383 other_dacl_list_fixed.index(ace)
384 except ValueError:
385 self_aces.append(ace)
386 else:
387 common_aces.append(ace)
388 self_aces = sorted(self_aces)
389 if len(self_aces) > 0:
390 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
391 for ace in self_aces:
392 res += 8*" " + ace + "\n"
394 for ace in other_dacl_list_fixed:
395 try:
396 self_dacl_list_fixed.index(ace)
397 except ValueError:
398 other_aces.append(ace)
399 else:
400 common_aces.append(ace)
401 other_aces = sorted(other_aces)
402 if len(other_aces) > 0:
403 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
404 for ace in other_aces:
405 res += 8*" " + ace + "\n"
407 common_aces = sorted(list(set(common_aces)))
408 if self.con.verbose:
409 res += 4*" " + "ACEs found in both:\n"
410 for ace in common_aces:
411 res += 8*" " + ace + "\n"
412 return (self_aces == [] and other_aces == [], res)
414 class LDAPObject(object):
415 def __init__(self, connection, dn, summary, filter_list):
416 self.con = connection
417 self.two_domains = self.con.two_domains
418 self.quiet = self.con.quiet
419 self.verbose = self.con.verbose
420 self.summary = summary
421 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
422 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
423 for x in self.con.server_names:
424 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
425 self.attributes = self.con.get_attributes(self.dn)
426 # Attributes that are considered always to be different e.g based on timestamp etc.
428 # One domain - two domain controllers
429 self.ignore_attributes = [
430 # Default Naming Context
431 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
432 "operatingSystemVersion","oEMInformation",
433 # Configuration Naming Context
434 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
435 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
436 # Schema Naming Context
437 "prefixMap"]
438 if filter_list:
439 self.ignore_attributes += filter_list
441 self.dn_attributes = []
442 self.domain_attributes = []
443 self.servername_attributes = []
444 self.netbios_attributes = []
445 self.other_attributes = []
446 # Two domains - two domain controllers
448 if self.two_domains:
449 self.ignore_attributes += [
450 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
451 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
452 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
453 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
454 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
455 # After Exchange preps
456 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
458 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
459 self.dn_attributes = [
460 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
461 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
462 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
463 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
464 # After Exchange preps
465 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
466 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
467 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
468 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
469 self.dn_attributes = [x.upper() for x in self.dn_attributes]
471 # Attributes that contain the Domain name e.g. 'samba.org'
472 self.domain_attributes = [
473 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
474 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
475 self.domain_attributes = [x.upper() for x in self.domain_attributes]
477 # May contain DOMAIN_NETBIOS and SERVER_NAME
478 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
479 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
480 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
481 self.servername_attributes = [x.upper() for x in self.servername_attributes]
483 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
484 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
486 self.other_attributes = [ "name", "DC",]
487 self.other_attributes = [x.upper() for x in self.other_attributes]
489 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
491 def log(self, msg):
493 Log on the screen if there is no --quiet oprion set
495 if not self.quiet:
496 print msg
498 def fix_dn(self, s):
499 res = "%s" % s
500 if not self.two_domains:
501 return res
502 if res.upper().endswith(self.con.base_dn.upper()):
503 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
504 return res
506 def fix_domain_name(self, s):
507 res = "%s" % s
508 if not self.two_domains:
509 return res
510 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
511 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
512 return res
514 def fix_domain_netbios(self, s):
515 res = "%s" % s
516 if not self.two_domains:
517 return res
518 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
519 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
520 return res
522 def fix_server_name(self, s):
523 res = "%s" % s
524 if not self.two_domains or len(self.con.server_names) > 1:
525 return res
526 for x in self.con.server_names:
527 res = res.upper().replace(x, "${SERVER_NAME}")
528 return res
530 def __eq__(self, other):
531 if self.con.descriptor:
532 return self.cmp_desc(other)
533 return self.cmp_attrs(other)
535 def cmp_desc(self, other):
536 d1 = Descriptor(self.con, self.dn)
537 d2 = Descriptor(other.con, other.dn)
538 if self.con.view == "section":
539 res = d1.diff_2(d2)
540 elif self.con.view == "collision":
541 res = d1.diff_1(d2)
542 else:
543 raise Exception("Unknown --view option value.")
545 self.screen_output = res[1][:-1]
546 other.screen_output = res[1][:-1]
548 return res[0]
550 def cmp_attrs(self, other):
551 res = ""
552 self.unique_attrs = []
553 self.df_value_attrs = []
554 other.unique_attrs = []
555 if self.attributes.keys() != other.attributes.keys():
557 title = 4*" " + "Attributes found only in %s:" % self.con.host
558 for x in self.attributes.keys():
559 if not x in other.attributes.keys() and \
560 not x.upper() in [q.upper() for q in other.ignore_attributes]:
561 if title:
562 res += title + "\n"
563 title = None
564 res += 8*" " + x + "\n"
565 self.unique_attrs.append(x)
567 title = 4*" " + "Attributes found only in %s:" % other.con.host
568 for x in other.attributes.keys():
569 if not x in self.attributes.keys() and \
570 not x.upper() in [q.upper() for q in self.ignore_attributes]:
571 if title:
572 res += title + "\n"
573 title = None
574 res += 8*" " + x + "\n"
575 other.unique_attrs.append(x)
577 missing_attrs = [x.upper() for x in self.unique_attrs]
578 missing_attrs += [x.upper() for x in other.unique_attrs]
579 title = 4*" " + "Difference in attribute values:"
580 for x in self.attributes.keys():
581 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
582 continue
583 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
584 self.attributes[x] = sorted(self.attributes[x])
585 other.attributes[x] = sorted(other.attributes[x])
586 if self.attributes[x] != other.attributes[x]:
587 p = None
588 q = None
589 m = None
590 n = None
591 # First check if the difference can be fixed but shunting the first part
592 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
593 if x.upper() in self.other_attributes:
594 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
595 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
596 if p == q:
597 continue
598 # Attribute values that are list that contain DN based values that may differ
599 elif x.upper() in self.dn_attributes:
600 m = p
601 n = q
602 if not p and not q:
603 m = self.attributes[x]
604 n = other.attributes[x]
605 p = [self.fix_dn(j) for j in m]
606 q = [other.fix_dn(j) for j in n]
607 if p == q:
608 continue
609 # Attributes that contain the Domain name in them
610 if x.upper() in self.domain_attributes:
611 m = p
612 n = q
613 if not p and not q:
614 m = self.attributes[x]
615 n = other.attributes[x]
616 p = [self.fix_domain_name(j) for j in m]
617 q = [other.fix_domain_name(j) for j in n]
618 if p == q:
619 continue
621 if x.upper() in self.servername_attributes:
622 # Attributes with SERVER_NAME
623 m = p
624 n = q
625 if not p and not q:
626 m = self.attributes[x]
627 n = other.attributes[x]
628 p = [self.fix_server_name(j) for j in m]
629 q = [other.fix_server_name(j) for j in n]
630 if p == q:
631 continue
633 if x.upper() in self.netbios_attributes:
634 # Attributes with NETBIOS Domain name
635 m = p
636 n = q
637 if not p and not q:
638 m = self.attributes[x]
639 n = other.attributes[x]
640 p = [self.fix_domain_netbios(j) for j in m]
641 q = [other.fix_domain_netbios(j) for j in n]
642 if p == q:
643 continue
645 if title:
646 res += title + "\n"
647 title = None
648 if p and q:
649 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
650 else:
651 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
652 self.df_value_attrs.append(x)
654 if self.unique_attrs + other.unique_attrs != []:
655 assert self.unique_attrs != other.unique_attrs
656 self.summary["unique_attrs"] += self.unique_attrs
657 self.summary["df_value_attrs"] += self.df_value_attrs
658 other.summary["unique_attrs"] += other.unique_attrs
659 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
661 self.screen_output = res[:-1]
662 other.screen_output = res[:-1]
664 return res == ""
667 class LDAPBundel(object):
668 def __init__(self, connection, context, dn_list=None, filter_list=None):
669 self.con = connection
670 self.two_domains = self.con.two_domains
671 self.quiet = self.con.quiet
672 self.verbose = self.con.verbose
673 self.search_base = self.con.search_base
674 self.search_scope = self.con.search_scope
675 self.summary = {}
676 self.summary["unique_attrs"] = []
677 self.summary["df_value_attrs"] = []
678 self.summary["known_ignored_dn"] = []
679 self.summary["abnormal_ignored_dn"] = []
680 self.filter_list = filter_list
681 if dn_list:
682 self.dn_list = dn_list
683 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
684 self.context = context.upper()
685 self.dn_list = self.get_dn_list(context)
686 else:
687 raise Exception("Unknown initialization data for LDAPBundel().")
688 counter = 0
689 while counter < len(self.dn_list) and self.two_domains:
690 # Use alias reference
691 tmp = self.dn_list[counter]
692 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
693 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
694 if len(self.con.server_names) == 1:
695 for x in self.con.server_names:
696 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
697 self.dn_list[counter] = tmp
698 counter += 1
699 self.dn_list = list(set(self.dn_list))
700 self.dn_list = sorted(self.dn_list)
701 self.size = len(self.dn_list)
703 def log(self, msg):
705 Log on the screen if there is no --quiet oprion set
707 if not self.quiet:
708 print msg
710 def update_size(self):
711 self.size = len(self.dn_list)
712 self.dn_list = sorted(self.dn_list)
714 def __eq__(self, other):
715 res = True
716 if self.size != other.size:
717 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
718 res = False
720 # This is the case where we want to explicitly compare two objects with different DNs.
721 # It does not matter if they are in the same DC, in two DC in one domain or in two
722 # different domains.
723 if self.search_scope != SCOPE_BASE:
724 title= "\n* DNs found only in %s:" % self.con.host
725 for x in self.dn_list:
726 if not x.upper() in [q.upper() for q in other.dn_list]:
727 if title:
728 self.log( title )
729 title = None
730 res = False
731 self.log( 4*" " + x )
732 self.dn_list[self.dn_list.index(x)] = ""
733 self.dn_list = [x for x in self.dn_list if x]
735 title= "\n* DNs found only in %s:" % other.con.host
736 for x in other.dn_list:
737 if not x.upper() in [q.upper() for q in self.dn_list]:
738 if title:
739 self.log( title )
740 title = None
741 res = False
742 self.log( 4*" " + x )
743 other.dn_list[other.dn_list.index(x)] = ""
744 other.dn_list = [x for x in other.dn_list if x]
746 self.update_size()
747 other.update_size()
748 assert self.size == other.size
749 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
750 self.log( "\n* Objects to be compared: %s" % self.size )
752 index = 0
753 while index < self.size:
754 skip = False
755 try:
756 object1 = LDAPObject(connection=self.con,
757 dn=self.dn_list[index],
758 summary=self.summary,
759 filter_list=self.filter_list)
760 except LdbError, (enum, estr):
761 if enum == ERR_NO_SUCH_OBJECT:
762 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
763 skip = True
764 raise
765 try:
766 object2 = LDAPObject(connection=other.con,
767 dn=other.dn_list[index],
768 summary=other.summary,
769 filter_list=self.filter_list)
770 except LdbError, (enum, estr):
771 if enum == ERR_NO_SUCH_OBJECT:
772 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
773 skip = True
774 raise
775 if skip:
776 index += 1
777 continue
778 if object1 == object2:
779 if self.con.verbose:
780 self.log( "\nComparing:" )
781 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
782 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
783 self.log( 4*" " + "OK" )
784 else:
785 self.log( "\nComparing:" )
786 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
787 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
788 self.log( object1.screen_output )
789 self.log( 4*" " + "FAILED" )
790 res = False
791 self.summary = object1.summary
792 other.summary = object2.summary
793 index += 1
795 return res
797 def get_dn_list(self, context):
798 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
799 Parse all DNs and filter those that are 'strange' or abnormal.
801 if context.upper() == "DOMAIN":
802 search_base = self.con.base_dn
803 elif context.upper() == "CONFIGURATION":
804 search_base = self.con.config_dn
805 elif context.upper() == "SCHEMA":
806 search_base = self.con.schema_dn
808 dn_list = []
809 if not self.search_base:
810 self.search_base = search_base
811 self.search_scope = self.search_scope.upper()
812 if self.search_scope == "SUB":
813 self.search_scope = SCOPE_SUBTREE
814 elif self.search_scope == "BASE":
815 self.search_scope = SCOPE_BASE
816 elif self.search_scope == "ONE":
817 self.search_scope = SCOPE_ONELEVEL
818 else:
819 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
820 if not self.search_base.upper().endswith(search_base.upper()):
821 raise StandardError("Invalid search base specified: %s" % self.search_base)
822 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
823 for x in res:
824 dn_list.append(x["dn"].get_linearized())
826 global summary
828 return dn_list
830 def print_summary(self):
831 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
832 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
834 if self.summary["unique_attrs"]:
835 self.log( "\nAttributes found only in %s:" % self.con.host )
836 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
838 if self.summary["df_value_attrs"]:
839 self.log( "\nAttributes with different values:" )
840 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
841 self.summary["df_value_attrs"] = []
843 class cmd_ldapcmp(Command):
844 """compare two ldap databases"""
845 synopsis = "ldapcmp URL1 URL2 <domain|configuration|schema> [options]"
847 takes_optiongroups = {
848 "sambaopts": options.SambaOptions,
849 "versionopts": options.VersionOptions,
850 "credopts": options.CredentialsOptionsDouble,
853 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
855 takes_options = [
856 Option("-w", "--two", dest="two", action="store_true", default=False,
857 help="Hosts are in two different domains"),
858 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
859 help="Do not print anything but relay on just exit code"),
860 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
861 help="Print all DN pairs that have been compared"),
862 Option("--sd", dest="descriptor", action="store_true", default=False,
863 help="Compare nTSecurityDescriptor attibutes only"),
864 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
865 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
866 Option("--view", dest="view", default="section",
867 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
868 Option("--base", dest="base", default="",
869 help="Pass search base that will build DN list for the first DC."),
870 Option("--base2", dest="base2", default="",
871 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
872 Option("--scope", dest="scope", default="SUB",
873 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
874 Option("--filter", dest="filter", default="",
875 help="List of comma separated attributes to ignore in the comparision"),
878 def run(self, URL1, URL2,
879 context1=None, context2=None, context3=None,
880 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
881 view="section", base="", base2="", scope="SUB", filter="",
882 credopts=None, sambaopts=None, versionopts=None):
884 lp = sambaopts.get_loadparm()
886 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
888 if using_ldap:
889 creds = credopts.get_credentials(lp, fallback_machine=True)
890 else:
891 creds = None
892 creds2 = credopts.get_credentials2(lp, guess=False)
893 if creds2.is_anonymous():
894 creds2 = creds
895 else:
896 creds2.set_domain("")
897 creds2.set_workstation("")
898 if using_ldap and not creds.authentication_requested():
899 raise CommandError("You must supply at least one username/password pair")
901 # make a list of contexts to compare in
902 contexts = []
903 if context1 is None:
904 if base and base2:
905 # If search bases are specified context is defaulted to
906 # DOMAIN so the given search bases can be verified.
907 contexts = ["DOMAIN"]
908 else:
909 # if no argument given, we compare all contexts
910 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
911 else:
912 for c in [context1, context2, context3]:
913 if c is None:
914 continue
915 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
916 raise CommandError("Incorrect argument: %s" % c)
917 contexts.append(c.upper())
919 if verbose and quiet:
920 raise CommandError("You cannot set --verbose and --quiet together")
921 if (not base and base2) or (base and not base2):
922 raise CommandError("You need to specify both --base and --base2 at the same time")
923 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
924 raise CommandError("Invalid --view value. Choose from: section or collision")
925 if not scope.upper() in ["SUB", "ONE", "BASE"]:
926 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
928 con1 = LDAPBase(URL1, creds, lp,
929 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
930 verbose=verbose,view=view, base=base, scope=scope)
931 assert len(con1.base_dn) > 0
933 con2 = LDAPBase(URL2, creds2, lp,
934 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
935 verbose=verbose, view=view, base=base2, scope=scope)
936 assert len(con2.base_dn) > 0
938 filter_list = filter.split(",")
940 status = 0
941 for context in contexts:
942 if not quiet:
943 print "\n* Comparing [%s] context..." % context
945 b1 = LDAPBundel(con1, context=context, filter_list=filter_list)
946 b2 = LDAPBundel(con2, context=context, filter_list=filter_list)
948 if b1 == b2:
949 if not quiet:
950 print "\n* Result for [%s]: SUCCESS" % context
951 else:
952 if not quiet:
953 print "\n* Result for [%s]: FAILURE" % context
954 if not descriptor:
955 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
956 b2.summary["df_value_attrs"] = []
957 print "\nSUMMARY"
958 print "---------"
959 b1.print_summary()
960 b2.print_summary()
961 # mark exit status as FAILURE if a least one comparison failed
962 status = -1
963 if status != 0:
964 raise CommandError("Compare failed: %d" % status)