s4 dns: Check more of the returned values for the A query
[Samba/gebeck_regimport.git] / source4 / scripting / python / samba / netcmd / ldapcmp.py
bloba6db4b11b0272f4ab7f23adc6b28d0bd40f12ee3
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.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):
281 self.con = connection
282 self.dn = dn
283 self.sddl = self.con.get_descriptor_sddl(self.dn)
284 self.dacl_list = self.extract_dacl()
285 if self.con.sort_aces:
286 self.dacl_list.sort()
288 def extract_dacl(self):
289 """ Extracts the DACL as a list of ACE string (with the brakets).
291 try:
292 if "S:" in self.sddl:
293 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
294 else:
295 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
296 except AttributeError:
297 return []
298 return re.findall("(\(.*?\))", res)
300 def fix_guid(self, ace):
301 res = "%s" % ace
302 guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
303 # If there are not GUIDs to replace return the same ACE
304 if len(guids) == 0:
305 return res
306 for guid in guids:
307 try:
308 name = self.con.guid_map[guid.lower()]
309 res = res.replace(guid, name)
310 except KeyError:
311 # Do not bother if the GUID is not found in
312 # cn=Schema or cn=Extended-Rights
313 pass
314 return res
316 def fix_sid(self, ace):
317 res = "%s" % ace
318 sids = re.findall("S-[-0-9]+", res)
319 # If there are not SIDs to replace return the same ACE
320 if len(sids) == 0:
321 return res
322 for sid in sids:
323 try:
324 name = self.con.sid_map[sid]
325 res = res.replace(sid, name)
326 except KeyError:
327 # Do not bother if the SID is not found in baseDN
328 pass
329 return res
331 def fixit(self, ace):
332 """ Combine all replacement methods in one
334 res = "%s" % ace
335 res = self.fix_guid(res)
336 res = self.fix_sid(res)
337 return res
339 def diff_1(self, other):
340 res = ""
341 if len(self.dacl_list) != len(other.dacl_list):
342 res += 4*" " + "Difference in ACE count:\n"
343 res += 8*" " + "=> %s\n" % len(self.dacl_list)
344 res += 8*" " + "=> %s\n" % len(other.dacl_list)
346 i = 0
347 flag = True
348 while True:
349 self_ace = None
350 other_ace = None
351 try:
352 self_ace = "%s" % self.dacl_list[i]
353 except IndexError:
354 self_ace = ""
356 try:
357 other_ace = "%s" % other.dacl_list[i]
358 except IndexError:
359 other_ace = ""
360 if len(self_ace) + len(other_ace) == 0:
361 break
362 self_ace_fixed = "%s" % self.fixit(self_ace)
363 other_ace_fixed = "%s" % other.fixit(other_ace)
364 if self_ace_fixed != other_ace_fixed:
365 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
366 flag = False
367 else:
368 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
369 i += 1
370 return (flag, res)
372 def diff_2(self, other):
373 res = ""
374 if len(self.dacl_list) != len(other.dacl_list):
375 res += 4*" " + "Difference in ACE count:\n"
376 res += 8*" " + "=> %s\n" % len(self.dacl_list)
377 res += 8*" " + "=> %s\n" % len(other.dacl_list)
379 common_aces = []
380 self_aces = []
381 other_aces = []
382 self_dacl_list_fixed = []
383 other_dacl_list_fixed = []
384 [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
385 [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
386 for ace in self_dacl_list_fixed:
387 try:
388 other_dacl_list_fixed.index(ace)
389 except ValueError:
390 self_aces.append(ace)
391 else:
392 common_aces.append(ace)
393 self_aces = sorted(self_aces)
394 if len(self_aces) > 0:
395 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
396 for ace in self_aces:
397 res += 8*" " + ace + "\n"
399 for ace in other_dacl_list_fixed:
400 try:
401 self_dacl_list_fixed.index(ace)
402 except ValueError:
403 other_aces.append(ace)
404 else:
405 common_aces.append(ace)
406 other_aces = sorted(other_aces)
407 if len(other_aces) > 0:
408 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
409 for ace in other_aces:
410 res += 8*" " + ace + "\n"
412 common_aces = sorted(list(set(common_aces)))
413 if self.con.verbose:
414 res += 4*" " + "ACEs found in both:\n"
415 for ace in common_aces:
416 res += 8*" " + ace + "\n"
417 return (self_aces == [] and other_aces == [], res)
419 class LDAPObject(object):
420 def __init__(self, connection, dn, summary, filter_list):
421 self.con = connection
422 self.two_domains = self.con.two_domains
423 self.quiet = self.con.quiet
424 self.verbose = self.con.verbose
425 self.summary = summary
426 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
427 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
428 for x in self.con.server_names:
429 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
430 self.attributes = self.con.get_attributes(self.dn)
431 # Attributes that are considered always to be different e.g based on timestamp etc.
433 # One domain - two domain controllers
434 self.ignore_attributes = [
435 # Default Naming Context
436 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
437 "operatingSystemVersion","oEMInformation",
438 # Configuration Naming Context
439 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
440 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
441 # Schema Naming Context
442 "prefixMap"]
443 if filter_list:
444 self.ignore_attributes += filter_list
446 self.dn_attributes = []
447 self.domain_attributes = []
448 self.servername_attributes = []
449 self.netbios_attributes = []
450 self.other_attributes = []
451 # Two domains - two domain controllers
453 if self.two_domains:
454 self.ignore_attributes += [
455 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
456 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
457 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
458 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
459 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
460 # After Exchange preps
461 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
463 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
464 self.dn_attributes = [
465 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
466 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
467 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
468 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
469 # After Exchange preps
470 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
471 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
472 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
473 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
474 self.dn_attributes = [x.upper() for x in self.dn_attributes]
476 # Attributes that contain the Domain name e.g. 'samba.org'
477 self.domain_attributes = [
478 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
479 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
480 self.domain_attributes = [x.upper() for x in self.domain_attributes]
482 # May contain DOMAIN_NETBIOS and SERVER_NAME
483 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
484 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
485 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
486 self.servername_attributes = [x.upper() for x in self.servername_attributes]
488 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
489 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
491 self.other_attributes = [ "name", "DC",]
492 self.other_attributes = [x.upper() for x in self.other_attributes]
494 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
496 def log(self, msg):
498 Log on the screen if there is no --quiet oprion set
500 if not self.quiet:
501 self.outf.write(msg+"\n")
503 def fix_dn(self, s):
504 res = "%s" % s
505 if not self.two_domains:
506 return res
507 if res.upper().endswith(self.con.base_dn.upper()):
508 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
509 return res
511 def fix_domain_name(self, s):
512 res = "%s" % s
513 if not self.two_domains:
514 return res
515 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
516 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
517 return res
519 def fix_domain_netbios(self, s):
520 res = "%s" % s
521 if not self.two_domains:
522 return res
523 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
524 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
525 return res
527 def fix_server_name(self, s):
528 res = "%s" % s
529 if not self.two_domains or len(self.con.server_names) > 1:
530 return res
531 for x in self.con.server_names:
532 res = res.upper().replace(x, "${SERVER_NAME}")
533 return res
535 def __eq__(self, other):
536 if self.con.descriptor:
537 return self.cmp_desc(other)
538 return self.cmp_attrs(other)
540 def cmp_desc(self, other):
541 d1 = Descriptor(self.con, self.dn)
542 d2 = Descriptor(other.con, other.dn)
543 if self.con.view == "section":
544 res = d1.diff_2(d2)
545 elif self.con.view == "collision":
546 res = d1.diff_1(d2)
547 else:
548 raise Exception("Unknown --view option value.")
550 self.screen_output = res[1][:-1]
551 other.screen_output = res[1][:-1]
553 return res[0]
555 def cmp_attrs(self, other):
556 res = ""
557 self.unique_attrs = []
558 self.df_value_attrs = []
559 other.unique_attrs = []
560 if self.attributes.keys() != other.attributes.keys():
562 title = 4*" " + "Attributes found only in %s:" % self.con.host
563 for x in self.attributes.keys():
564 if not x in other.attributes.keys() and \
565 not x.upper() in [q.upper() for q in other.ignore_attributes]:
566 if title:
567 res += title + "\n"
568 title = None
569 res += 8*" " + x + "\n"
570 self.unique_attrs.append(x)
572 title = 4*" " + "Attributes found only in %s:" % other.con.host
573 for x in other.attributes.keys():
574 if not x in self.attributes.keys() and \
575 not x.upper() in [q.upper() for q in self.ignore_attributes]:
576 if title:
577 res += title + "\n"
578 title = None
579 res += 8*" " + x + "\n"
580 other.unique_attrs.append(x)
582 missing_attrs = [x.upper() for x in self.unique_attrs]
583 missing_attrs += [x.upper() for x in other.unique_attrs]
584 title = 4*" " + "Difference in attribute values:"
585 for x in self.attributes.keys():
586 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
587 continue
588 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
589 self.attributes[x] = sorted(self.attributes[x])
590 other.attributes[x] = sorted(other.attributes[x])
591 if self.attributes[x] != other.attributes[x]:
592 p = None
593 q = None
594 m = None
595 n = None
596 # First check if the difference can be fixed but shunting the first part
597 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
598 if x.upper() in self.other_attributes:
599 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
600 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
601 if p == q:
602 continue
603 # Attribute values that are list that contain DN based values that may differ
604 elif x.upper() in self.dn_attributes:
605 m = p
606 n = q
607 if not p and not q:
608 m = self.attributes[x]
609 n = other.attributes[x]
610 p = [self.fix_dn(j) for j in m]
611 q = [other.fix_dn(j) for j in n]
612 if p == q:
613 continue
614 # Attributes that contain the Domain name in them
615 if x.upper() in self.domain_attributes:
616 m = p
617 n = q
618 if not p and not q:
619 m = self.attributes[x]
620 n = other.attributes[x]
621 p = [self.fix_domain_name(j) for j in m]
622 q = [other.fix_domain_name(j) for j in n]
623 if p == q:
624 continue
626 if x.upper() in self.servername_attributes:
627 # Attributes with SERVER_NAME
628 m = p
629 n = q
630 if not p and not q:
631 m = self.attributes[x]
632 n = other.attributes[x]
633 p = [self.fix_server_name(j) for j in m]
634 q = [other.fix_server_name(j) for j in n]
635 if p == q:
636 continue
638 if x.upper() in self.netbios_attributes:
639 # Attributes with NETBIOS Domain name
640 m = p
641 n = q
642 if not p and not q:
643 m = self.attributes[x]
644 n = other.attributes[x]
645 p = [self.fix_domain_netbios(j) for j in m]
646 q = [other.fix_domain_netbios(j) for j in n]
647 if p == q:
648 continue
650 if title:
651 res += title + "\n"
652 title = None
653 if p and q:
654 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
655 else:
656 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
657 self.df_value_attrs.append(x)
659 if self.unique_attrs + other.unique_attrs != []:
660 assert self.unique_attrs != other.unique_attrs
661 self.summary["unique_attrs"] += self.unique_attrs
662 self.summary["df_value_attrs"] += self.df_value_attrs
663 other.summary["unique_attrs"] += other.unique_attrs
664 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
666 self.screen_output = res[:-1]
667 other.screen_output = res[:-1]
669 return res == ""
672 class LDAPBundel(object):
674 def __init__(self, connection, context, dn_list=None, filter_list=None):
675 self.con = connection
676 self.two_domains = self.con.two_domains
677 self.quiet = self.con.quiet
678 self.verbose = self.con.verbose
679 self.search_base = self.con.search_base
680 self.search_scope = self.con.search_scope
681 self.summary = {}
682 self.summary["unique_attrs"] = []
683 self.summary["df_value_attrs"] = []
684 self.summary["known_ignored_dn"] = []
685 self.summary["abnormal_ignored_dn"] = []
686 self.filter_list = filter_list
687 if dn_list:
688 self.dn_list = dn_list
689 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
690 self.context = context.upper()
691 self.dn_list = self.get_dn_list(context)
692 else:
693 raise Exception("Unknown initialization data for LDAPBundel().")
694 counter = 0
695 while counter < len(self.dn_list) and self.two_domains:
696 # Use alias reference
697 tmp = self.dn_list[counter]
698 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
699 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
700 if len(self.con.server_names) == 1:
701 for x in self.con.server_names:
702 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
703 self.dn_list[counter] = tmp
704 counter += 1
705 self.dn_list = list(set(self.dn_list))
706 self.dn_list = sorted(self.dn_list)
707 self.size = len(self.dn_list)
709 def log(self, msg):
711 Log on the screen if there is no --quiet oprion set
713 if not self.quiet:
714 self.outf.write(msg+"\n")
716 def update_size(self):
717 self.size = len(self.dn_list)
718 self.dn_list = sorted(self.dn_list)
720 def __eq__(self, other):
721 res = True
722 if self.size != other.size:
723 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
724 res = False
726 # This is the case where we want to explicitly compare two objects with different DNs.
727 # It does not matter if they are in the same DC, in two DC in one domain or in two
728 # different domains.
729 if self.search_scope != SCOPE_BASE:
730 title= "\n* DNs found only in %s:" % self.con.host
731 for x in self.dn_list:
732 if not x.upper() in [q.upper() for q in other.dn_list]:
733 if title:
734 self.log( title )
735 title = None
736 res = False
737 self.log( 4*" " + x )
738 self.dn_list[self.dn_list.index(x)] = ""
739 self.dn_list = [x for x in self.dn_list if x]
741 title= "\n* DNs found only in %s:" % other.con.host
742 for x in other.dn_list:
743 if not x.upper() in [q.upper() for q in self.dn_list]:
744 if title:
745 self.log( title )
746 title = None
747 res = False
748 self.log( 4*" " + x )
749 other.dn_list[other.dn_list.index(x)] = ""
750 other.dn_list = [x for x in other.dn_list if x]
752 self.update_size()
753 other.update_size()
754 assert self.size == other.size
755 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
756 self.log( "\n* Objects to be compared: %s" % self.size )
758 index = 0
759 while index < self.size:
760 skip = False
761 try:
762 object1 = LDAPObject(connection=self.con,
763 dn=self.dn_list[index],
764 summary=self.summary,
765 filter_list=self.filter_list)
766 except LdbError, (enum, estr):
767 if enum == ERR_NO_SUCH_OBJECT:
768 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
769 skip = True
770 raise
771 try:
772 object2 = LDAPObject(connection=other.con,
773 dn=other.dn_list[index],
774 summary=other.summary,
775 filter_list=self.filter_list)
776 except LdbError, (enum, estr):
777 if enum == ERR_NO_SUCH_OBJECT:
778 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
779 skip = True
780 raise
781 if skip:
782 index += 1
783 continue
784 if object1 == object2:
785 if self.con.verbose:
786 self.log( "\nComparing:" )
787 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
788 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
789 self.log( 4*" " + "OK" )
790 else:
791 self.log( "\nComparing:" )
792 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
793 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
794 self.log( object1.screen_output )
795 self.log( 4*" " + "FAILED" )
796 res = False
797 self.summary = object1.summary
798 other.summary = object2.summary
799 index += 1
801 return res
803 def get_dn_list(self, context):
804 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
805 Parse all DNs and filter those that are 'strange' or abnormal.
807 if context.upper() == "DOMAIN":
808 search_base = self.con.base_dn
809 elif context.upper() == "CONFIGURATION":
810 search_base = self.con.config_dn
811 elif context.upper() == "SCHEMA":
812 search_base = self.con.schema_dn
813 elif context.upper() == "DNSDOMAIN":
814 search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
815 elif context.upper() == "DNSFOREST":
816 search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
818 dn_list = []
819 if not self.search_base:
820 self.search_base = search_base
821 self.search_scope = self.search_scope.upper()
822 if self.search_scope == "SUB":
823 self.search_scope = SCOPE_SUBTREE
824 elif self.search_scope == "BASE":
825 self.search_scope = SCOPE_BASE
826 elif self.search_scope == "ONE":
827 self.search_scope = SCOPE_ONELEVEL
828 else:
829 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
830 try:
831 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
832 except LdbError, (enum, estr):
833 self.outf.write("Failed search of base=%s\n" % self.search_base)
834 raise
835 for x in res:
836 dn_list.append(x["dn"].get_linearized())
838 global summary
840 return dn_list
842 def print_summary(self):
843 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
844 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
846 if self.summary["unique_attrs"]:
847 self.log( "\nAttributes found only in %s:" % self.con.host )
848 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
850 if self.summary["df_value_attrs"]:
851 self.log( "\nAttributes with different values:" )
852 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
853 self.summary["df_value_attrs"] = []
856 class cmd_ldapcmp(Command):
857 """compare two ldap databases"""
858 synopsis = "%prog ldapcmp <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
860 takes_optiongroups = {
861 "sambaopts": options.SambaOptions,
862 "versionopts": options.VersionOptions,
863 "credopts": options.CredentialsOptionsDouble,
866 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
868 takes_options = [
869 Option("-w", "--two", dest="two", action="store_true", default=False,
870 help="Hosts are in two different domains"),
871 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
872 help="Do not print anything but relay on just exit code"),
873 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
874 help="Print all DN pairs that have been compared"),
875 Option("--sd", dest="descriptor", action="store_true", default=False,
876 help="Compare nTSecurityDescriptor attibutes only"),
877 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
878 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
879 Option("--view", dest="view", default="section",
880 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
881 Option("--base", dest="base", default="",
882 help="Pass search base that will build DN list for the first DC."),
883 Option("--base2", dest="base2", default="",
884 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
885 Option("--scope", dest="scope", default="SUB",
886 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
887 Option("--filter", dest="filter", default="",
888 help="List of comma separated attributes to ignore in the comparision"),
891 def run(self, URL1, URL2,
892 context1=None, context2=None, context3=None,
893 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
894 view="section", base="", base2="", scope="SUB", filter="",
895 credopts=None, sambaopts=None, versionopts=None):
897 lp = sambaopts.get_loadparm()
899 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
901 if using_ldap:
902 creds = credopts.get_credentials(lp, fallback_machine=True)
903 else:
904 creds = None
905 creds2 = credopts.get_credentials2(lp, guess=False)
906 if creds2.is_anonymous():
907 creds2 = creds
908 else:
909 creds2.set_domain("")
910 creds2.set_workstation("")
911 if using_ldap and not creds.authentication_requested():
912 raise CommandError("You must supply at least one username/password pair")
914 # make a list of contexts to compare in
915 contexts = []
916 if context1 is None:
917 if base and base2:
918 # If search bases are specified context is defaulted to
919 # DOMAIN so the given search bases can be verified.
920 contexts = ["DOMAIN"]
921 else:
922 # if no argument given, we compare all contexts
923 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
924 else:
925 for c in [context1, context2, context3]:
926 if c is None:
927 continue
928 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
929 raise CommandError("Incorrect argument: %s" % c)
930 contexts.append(c.upper())
932 if verbose and quiet:
933 raise CommandError("You cannot set --verbose and --quiet together")
934 if (not base and base2) or (base and not base2):
935 raise CommandError("You need to specify both --base and --base2 at the same time")
936 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
937 raise CommandError("Invalid --view value. Choose from: section or collision")
938 if not scope.upper() in ["SUB", "ONE", "BASE"]:
939 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
941 con1 = LDAPBase(URL1, creds, lp,
942 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
943 verbose=verbose,view=view, base=base, scope=scope)
944 assert len(con1.base_dn) > 0
946 con2 = LDAPBase(URL2, creds2, lp,
947 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
948 verbose=verbose, view=view, base=base2, scope=scope)
949 assert len(con2.base_dn) > 0
951 filter_list = filter.split(",")
953 status = 0
954 for context in contexts:
955 if not quiet:
956 self.outf.write("\n* Comparing [%s] context...\n" % context)
958 b1 = LDAPBundel(con1, context=context, filter_list=filter_list)
959 b2 = LDAPBundel(con2, context=context, filter_list=filter_list)
961 if b1 == b2:
962 if not quiet:
963 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
964 context)
965 else:
966 if not quiet:
967 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
968 if not descriptor:
969 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
970 b2.summary["df_value_attrs"] = []
971 self.outf.write("\nSUMMARY\n")
972 self.outf.write("---------\n")
973 b1.print_summary()
974 b2.print_summary()
975 # mark exit status as FAILURE if a least one comparison failed
976 status = -1
977 if status != 0:
978 raise CommandError("Compare failed: %d" % status)