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