build: Change bin/default/python -> bin/python symlink to bin/default/python_modules
[Samba.git] / source4 / scripting / python / samba / netcmd / ldapcmp.py
blob8398205e4badefd096076f7169fd91f977f3d62d
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.
8 # Copyright (C) Zahari Zahariev <zahari.zahariev@postpath.com> 2009, 2010
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.
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.
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/>.
24 import os
25 import re
26 import sys
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,
40 global summary
41 summary = {}
43 class LDAPBase(object):
45 def __init__(self, host, creds, lp,
46 two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
47 view="section", base="", scope="SUB",
48 outf=sys.stdout, errf=sys.stderr):
49 ldb_options = []
50 samdb_url = host
51 if not "://" in host:
52 if os.path.isfile(host):
53 samdb_url = "tdb://%s" % host
54 else:
55 samdb_url = "ldap://%s" % host
56 # use 'paged_search' module when connecting remotely
57 if samdb_url.lower().startswith("ldap://"):
58 ldb_options = ["modules:paged_searches"]
59 self.outf = outf
60 self.errf = errf
61 self.ldb = Ldb(url=samdb_url,
62 credentials=creds,
63 lp=lp,
64 options=ldb_options)
65 self.search_base = base
66 self.search_scope = scope
67 self.two_domains = two
68 self.quiet = quiet
69 self.descriptor = descriptor
70 self.sort_aces = sort_aces
71 self.view = view
72 self.verbose = verbose
73 self.host = host
74 self.base_dn = str(self.ldb.get_default_basedn())
75 self.root_dn = str(self.ldb.get_root_basedn())
76 self.config_dn = str(self.ldb.get_config_basedn())
77 self.schema_dn = str(self.ldb.get_schema_basedn())
78 self.domain_netbios = self.find_netbios()
79 self.server_names = self.find_servers()
80 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
81 self.domain_sid = self.find_domain_sid()
82 self.get_guid_map()
83 self.get_sid_map()
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)
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])
102 def find_servers(self):
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
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]
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
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)
137 def get_attribute_name(self, key):
138 """ Returns the real attribute name
139 It resolved ranged results e.g. member;range=0-1499
142 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
144 m = r.match(key)
145 if m is None:
146 return key
148 return m.group(1)
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
155 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
157 m = r.match(key)
158 if m is None:
159 # no range, just return the values
160 return vals
162 attr = m.group(1)
163 hi = int(m.group(3))
165 # get additional values in a loop
166 # until we get a response with '*' at the end
167 while True:
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"]
175 fm = None
176 fvals = None
178 for key in res.keys():
179 m = r.match(key)
181 if m is None:
182 continue
184 if m.group(1) != attr:
185 continue
187 fm = m
188 fvals = list(res[key])
189 break
191 if fm is None:
192 break
194 vals.extend(fvals)
195 if fm.group(3) == "*":
196 # if we got "*" we're done
197 break
199 assert int(fm.group(2)) == hi + 1
200 hi = int(fm.group(3))
202 return vals
204 def get_attributes(self, object_dn):
205 """ Returns dict with all default visible attributes
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)
218 return res
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)
226 def guid_as_string(self, guid_blob):
227 """ Translate binary representation of schemaIDGUID to standard string representation.
228 @gid_blob: binary schemaIDGUID
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(" ", "-")
252 def get_guid_map(self):
253 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
255 self.guid_map = {}
256 res = self.ldb.search(base=self.schema_dn,
257 expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
258 for item in res:
259 self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
261 res = self.ldb.search(base="cn=extended-rights,%s" % self.config_dn,
262 expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
263 for item in res:
264 self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
266 def get_sid_map(self):
267 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
269 self.sid_map = {}
270 res = self.ldb.search(base=self.base_dn,
271 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
272 for item in res:
273 try:
274 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
275 except KeyError:
276 pass
278 class Descriptor(object):
279 def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr):
280 self.outf = outf
281 self.errf = errf
282 self.con = connection
283 self.dn = dn
284 self.sddl = self.con.get_descriptor_sddl(self.dn)
285 self.dacl_list = self.extract_dacl()
286 if self.con.sort_aces:
287 self.dacl_list.sort()
289 def extract_dacl(self):
290 """ Extracts the DACL as a list of ACE string (with the brakets).
292 try:
293 if "S:" in self.sddl:
294 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
295 else:
296 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
297 except AttributeError:
298 return []
299 return re.findall("(\(.*?\))", res)
301 def fix_guid(self, ace):
302 res = "%s" % ace
303 guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
304 # If there are not GUIDs to replace return the same ACE
305 if len(guids) == 0:
306 return res
307 for guid in guids:
308 try:
309 name = self.con.guid_map[guid.lower()]
310 res = res.replace(guid, name)
311 except KeyError:
312 # Do not bother if the GUID is not found in
313 # cn=Schema or cn=Extended-Rights
314 pass
315 return res
317 def fix_sid(self, ace):
318 res = "%s" % ace
319 sids = re.findall("S-[-0-9]+", res)
320 # If there are not SIDs to replace return the same ACE
321 if len(sids) == 0:
322 return res
323 for sid in sids:
324 try:
325 name = self.con.sid_map[sid]
326 res = res.replace(sid, name)
327 except KeyError:
328 # Do not bother if the SID is not found in baseDN
329 pass
330 return res
332 def fixit(self, ace):
333 """ Combine all replacement methods in one
335 res = "%s" % ace
336 res = self.fix_guid(res)
337 res = self.fix_sid(res)
338 return res
340 def diff_1(self, other):
341 res = ""
342 if len(self.dacl_list) != len(other.dacl_list):
343 res += 4*" " + "Difference in ACE count:\n"
344 res += 8*" " + "=> %s\n" % len(self.dacl_list)
345 res += 8*" " + "=> %s\n" % len(other.dacl_list)
347 i = 0
348 flag = True
349 while True:
350 self_ace = None
351 other_ace = None
352 try:
353 self_ace = "%s" % self.dacl_list[i]
354 except IndexError:
355 self_ace = ""
357 try:
358 other_ace = "%s" % other.dacl_list[i]
359 except IndexError:
360 other_ace = ""
361 if len(self_ace) + len(other_ace) == 0:
362 break
363 self_ace_fixed = "%s" % self.fixit(self_ace)
364 other_ace_fixed = "%s" % other.fixit(other_ace)
365 if self_ace_fixed != other_ace_fixed:
366 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
367 flag = False
368 else:
369 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
370 i += 1
371 return (flag, res)
373 def diff_2(self, other):
374 res = ""
375 if len(self.dacl_list) != len(other.dacl_list):
376 res += 4*" " + "Difference in ACE count:\n"
377 res += 8*" " + "=> %s\n" % len(self.dacl_list)
378 res += 8*" " + "=> %s\n" % len(other.dacl_list)
380 common_aces = []
381 self_aces = []
382 other_aces = []
383 self_dacl_list_fixed = []
384 other_dacl_list_fixed = []
385 [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
386 [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
387 for ace in self_dacl_list_fixed:
388 try:
389 other_dacl_list_fixed.index(ace)
390 except ValueError:
391 self_aces.append(ace)
392 else:
393 common_aces.append(ace)
394 self_aces = sorted(self_aces)
395 if len(self_aces) > 0:
396 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
397 for ace in self_aces:
398 res += 8*" " + ace + "\n"
400 for ace in other_dacl_list_fixed:
401 try:
402 self_dacl_list_fixed.index(ace)
403 except ValueError:
404 other_aces.append(ace)
405 else:
406 common_aces.append(ace)
407 other_aces = sorted(other_aces)
408 if len(other_aces) > 0:
409 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
410 for ace in other_aces:
411 res += 8*" " + ace + "\n"
413 common_aces = sorted(list(set(common_aces)))
414 if self.con.verbose:
415 res += 4*" " + "ACEs found in both:\n"
416 for ace in common_aces:
417 res += 8*" " + ace + "\n"
418 return (self_aces == [] and other_aces == [], res)
420 class LDAPObject(object):
421 def __init__(self, connection, dn, summary, filter_list,
422 outf=sys.stdout, errf=sys.stderr):
423 self.outf = outf
424 self.errf = errf
425 self.con = connection
426 self.two_domains = self.con.two_domains
427 self.quiet = self.con.quiet
428 self.verbose = self.con.verbose
429 self.summary = summary
430 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
431 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
432 for x in self.con.server_names:
433 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
434 self.attributes = self.con.get_attributes(self.dn)
435 # Attributes that are considered always to be different e.g based on timestamp etc.
437 # One domain - two domain controllers
438 self.ignore_attributes = [
439 # Default Naming Context
440 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
441 "operatingSystemVersion","oEMInformation",
442 "ridNextRID", "rIDPreviousAllocationPool",
443 # Configuration Naming Context
444 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
445 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
446 # Schema Naming Context
447 "prefixMap"]
448 if filter_list:
449 self.ignore_attributes += filter_list
451 self.dn_attributes = []
452 self.domain_attributes = []
453 self.servername_attributes = []
454 self.netbios_attributes = []
455 self.other_attributes = []
456 # Two domains - two domain controllers
458 if self.two_domains:
459 self.ignore_attributes += [
460 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
461 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
462 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
463 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
464 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
465 # After Exchange preps
466 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
468 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
469 self.dn_attributes = [
470 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
471 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
472 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
473 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
474 # After Exchange preps
475 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
476 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
477 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
478 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
479 self.dn_attributes = [x.upper() for x in self.dn_attributes]
481 # Attributes that contain the Domain name e.g. 'samba.org'
482 self.domain_attributes = [
483 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
484 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
485 self.domain_attributes = [x.upper() for x in self.domain_attributes]
487 # May contain DOMAIN_NETBIOS and SERVER_NAME
488 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
489 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
490 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
491 self.servername_attributes = [x.upper() for x in self.servername_attributes]
493 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
494 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
496 self.other_attributes = [ "name", "DC",]
497 self.other_attributes = [x.upper() for x in self.other_attributes]
499 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
501 def log(self, msg):
503 Log on the screen if there is no --quiet oprion set
505 if not self.quiet:
506 self.outf.write(msg+"\n")
508 def fix_dn(self, s):
509 res = "%s" % s
510 if not self.two_domains:
511 return res
512 if res.upper().endswith(self.con.base_dn.upper()):
513 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
514 return res
516 def fix_domain_name(self, s):
517 res = "%s" % s
518 if not self.two_domains:
519 return res
520 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
521 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
522 return res
524 def fix_domain_netbios(self, s):
525 res = "%s" % s
526 if not self.two_domains:
527 return res
528 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
529 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
530 return res
532 def fix_server_name(self, s):
533 res = "%s" % s
534 if not self.two_domains or len(self.con.server_names) > 1:
535 return res
536 for x in self.con.server_names:
537 res = res.upper().replace(x, "${SERVER_NAME}")
538 return res
540 def __eq__(self, other):
541 if self.con.descriptor:
542 return self.cmp_desc(other)
543 return self.cmp_attrs(other)
545 def cmp_desc(self, other):
546 d1 = Descriptor(self.con, self.dn, outf=self.outf, errf=self.errf)
547 d2 = Descriptor(other.con, other.dn, outf=self.outf, errf=self.errf)
548 if self.con.view == "section":
549 res = d1.diff_2(d2)
550 elif self.con.view == "collision":
551 res = d1.diff_1(d2)
552 else:
553 raise Exception("Unknown --view option value.")
555 self.screen_output = res[1][:-1]
556 other.screen_output = res[1][:-1]
558 return res[0]
560 def cmp_attrs(self, other):
561 res = ""
562 self.unique_attrs = []
563 self.df_value_attrs = []
564 other.unique_attrs = []
565 if self.attributes.keys() != other.attributes.keys():
567 title = 4*" " + "Attributes found only in %s:" % self.con.host
568 for x in self.attributes.keys():
569 if not x in other.attributes.keys() and \
570 not x.upper() in [q.upper() for q in other.ignore_attributes]:
571 if title:
572 res += title + "\n"
573 title = None
574 res += 8*" " + x + "\n"
575 self.unique_attrs.append(x)
577 title = 4*" " + "Attributes found only in %s:" % other.con.host
578 for x in other.attributes.keys():
579 if not x in self.attributes.keys() and \
580 not x.upper() in [q.upper() for q in self.ignore_attributes]:
581 if title:
582 res += title + "\n"
583 title = None
584 res += 8*" " + x + "\n"
585 other.unique_attrs.append(x)
587 missing_attrs = [x.upper() for x in self.unique_attrs]
588 missing_attrs += [x.upper() for x in other.unique_attrs]
589 title = 4*" " + "Difference in attribute values:"
590 for x in self.attributes.keys():
591 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
592 continue
593 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
594 self.attributes[x] = sorted(self.attributes[x])
595 other.attributes[x] = sorted(other.attributes[x])
596 if self.attributes[x] != other.attributes[x]:
597 p = None
598 q = None
599 m = None
600 n = None
601 # First check if the difference can be fixed but shunting the first part
602 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
603 if x.upper() in self.other_attributes:
604 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
605 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
606 if p == q:
607 continue
608 # Attribute values that are list that contain DN based values that may differ
609 elif x.upper() in self.dn_attributes:
610 m = p
611 n = q
612 if not p and not q:
613 m = self.attributes[x]
614 n = other.attributes[x]
615 p = [self.fix_dn(j) for j in m]
616 q = [other.fix_dn(j) for j in n]
617 if p == q:
618 continue
619 # Attributes that contain the Domain name in them
620 if x.upper() in self.domain_attributes:
621 m = p
622 n = q
623 if not p and not q:
624 m = self.attributes[x]
625 n = other.attributes[x]
626 p = [self.fix_domain_name(j) for j in m]
627 q = [other.fix_domain_name(j) for j in n]
628 if p == q:
629 continue
631 if x.upper() in self.servername_attributes:
632 # Attributes with SERVER_NAME
633 m = p
634 n = q
635 if not p and not q:
636 m = self.attributes[x]
637 n = other.attributes[x]
638 p = [self.fix_server_name(j) for j in m]
639 q = [other.fix_server_name(j) for j in n]
640 if p == q:
641 continue
643 if x.upper() in self.netbios_attributes:
644 # Attributes with NETBIOS Domain name
645 m = p
646 n = q
647 if not p and not q:
648 m = self.attributes[x]
649 n = other.attributes[x]
650 p = [self.fix_domain_netbios(j) for j in m]
651 q = [other.fix_domain_netbios(j) for j in n]
652 if p == q:
653 continue
655 if title:
656 res += title + "\n"
657 title = None
658 if p and q:
659 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
660 else:
661 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
662 self.df_value_attrs.append(x)
664 if self.unique_attrs + other.unique_attrs != []:
665 assert self.unique_attrs != other.unique_attrs
666 self.summary["unique_attrs"] += self.unique_attrs
667 self.summary["df_value_attrs"] += self.df_value_attrs
668 other.summary["unique_attrs"] += other.unique_attrs
669 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
671 self.screen_output = res[:-1]
672 other.screen_output = res[:-1]
674 return res == ""
677 class LDAPBundel(object):
679 def __init__(self, connection, context, dn_list=None, filter_list=None,
680 outf=sys.stdout, errf=sys.stderr):
681 self.outf = outf
682 self.errf = errf
683 self.con = connection
684 self.two_domains = self.con.two_domains
685 self.quiet = self.con.quiet
686 self.verbose = self.con.verbose
687 self.search_base = self.con.search_base
688 self.search_scope = self.con.search_scope
689 self.summary = {}
690 self.summary["unique_attrs"] = []
691 self.summary["df_value_attrs"] = []
692 self.summary["known_ignored_dn"] = []
693 self.summary["abnormal_ignored_dn"] = []
694 self.filter_list = filter_list
695 if dn_list:
696 self.dn_list = dn_list
697 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
698 self.context = context.upper()
699 self.dn_list = self.get_dn_list(context)
700 else:
701 raise Exception("Unknown initialization data for LDAPBundel().")
702 counter = 0
703 while counter < len(self.dn_list) and self.two_domains:
704 # Use alias reference
705 tmp = self.dn_list[counter]
706 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
707 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
708 if len(self.con.server_names) == 1:
709 for x in self.con.server_names:
710 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
711 self.dn_list[counter] = tmp
712 counter += 1
713 self.dn_list = list(set(self.dn_list))
714 self.dn_list = sorted(self.dn_list)
715 self.size = len(self.dn_list)
717 def log(self, msg):
719 Log on the screen if there is no --quiet oprion set
721 if not self.quiet:
722 self.outf.write(msg+"\n")
724 def update_size(self):
725 self.size = len(self.dn_list)
726 self.dn_list = sorted(self.dn_list)
728 def __eq__(self, other):
729 res = True
730 if self.size != other.size:
731 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
732 res = False
734 # This is the case where we want to explicitly compare two objects with different DNs.
735 # It does not matter if they are in the same DC, in two DC in one domain or in two
736 # different domains.
737 if self.search_scope != SCOPE_BASE:
738 title= "\n* DNs found only in %s:" % self.con.host
739 for x in self.dn_list:
740 if not x.upper() in [q.upper() for q in other.dn_list]:
741 if title:
742 self.log( title )
743 title = None
744 res = False
745 self.log( 4*" " + x )
746 self.dn_list[self.dn_list.index(x)] = ""
747 self.dn_list = [x for x in self.dn_list if x]
749 title= "\n* DNs found only in %s:" % other.con.host
750 for x in other.dn_list:
751 if not x.upper() in [q.upper() for q in self.dn_list]:
752 if title:
753 self.log( title )
754 title = None
755 res = False
756 self.log( 4*" " + x )
757 other.dn_list[other.dn_list.index(x)] = ""
758 other.dn_list = [x for x in other.dn_list if x]
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 )
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
811 return res
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.
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
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())
848 global summary
850 return dn_list
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"]))
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"]]) )
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"] = []
866 class cmd_ldapcmp(Command):
867 """Compare two ldap databases."""
868 synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
870 takes_optiongroups = {
871 "sambaopts": options.SambaOptions,
872 "versionopts": options.VersionOptions,
873 "credopts": options.CredentialsOptionsDouble,
876 takes_optiongroups = {
877 "sambaopts": options.SambaOptions,
878 "versionopts": options.VersionOptions,
879 "credopts": options.CredentialsOptionsDouble,
882 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
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"),
907 def run(self, URL1, URL2,
908 context1=None, context2=None, context3=None,
909 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
910 view="section", base="", base2="", scope="SUB", filter="",
911 credopts=None, sambaopts=None, versionopts=None):
913 lp = sambaopts.get_loadparm()
915 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
917 if using_ldap:
918 creds = credopts.get_credentials(lp, fallback_machine=True)
919 else:
920 creds = None
921 creds2 = credopts.get_credentials2(lp, guess=False)
922 if creds2.is_anonymous():
923 creds2 = creds
924 else:
925 creds2.set_domain("")
926 creds2.set_workstation("")
927 if using_ldap and not creds.authentication_requested():
928 raise CommandError("You must supply at least one username/password pair")
930 # make a list of contexts to compare in
931 contexts = []
932 if context1 is None:
933 if base and base2:
934 # If search bases are specified context is defaulted to
935 # DOMAIN so the given search bases can be verified.
936 contexts = ["DOMAIN"]
937 else:
938 # if no argument given, we compare all contexts
939 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
940 else:
941 for c in [context1, context2, context3]:
942 if c is None:
943 continue
944 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
945 raise CommandError("Incorrect argument: %s" % c)
946 contexts.append(c.upper())
948 if verbose and quiet:
949 raise CommandError("You cannot set --verbose and --quiet together")
950 if (not base and base2) or (base and not base2):
951 raise CommandError("You need to specify both --base and --base2 at the same time")
952 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
953 raise CommandError("Invalid --view value. Choose from: section or collision")
954 if not scope.upper() in ["SUB", "ONE", "BASE"]:
955 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
957 con1 = LDAPBase(URL1, creds, lp,
958 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
959 verbose=verbose,view=view, base=base, scope=scope,
960 outf=self.outf, errf=self.errf)
961 assert len(con1.base_dn) > 0
963 con2 = LDAPBase(URL2, creds2, lp,
964 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
965 verbose=verbose, view=view, base=base2, scope=scope,
966 outf=self.outf, errf=self.errf)
967 assert len(con2.base_dn) > 0
969 filter_list = filter.split(",")
971 status = 0
972 for context in contexts:
973 if not quiet:
974 self.outf.write("\n* Comparing [%s] context...\n" % context)
976 b1 = LDAPBundel(con1, context=context, filter_list=filter_list,
977 outf=self.outf, errf=self.errf)
978 b2 = LDAPBundel(con2, context=context, filter_list=filter_list,
979 outf=self.outf, errf=self.errf)
981 if b1 == b2:
982 if not quiet:
983 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
984 context)
985 else:
986 if not quiet:
987 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
988 if not descriptor:
989 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
990 b2.summary["df_value_attrs"] = []
991 self.outf.write("\nSUMMARY\n")
992 self.outf.write("---------\n")
993 b1.print_summary()
994 b2.print_summary()
995 # mark exit status as FAILURE if a least one comparison failed
996 status = -1
997 if status != 0:
998 raise CommandError("Compare failed: %d" % status)