dbcheck: Add explict tests for unknown and unsorted attributeID values
[Samba.git] / python / samba / netcmd / ldapcmp.py
blob96b94f2c16a2de575e0f9cc5eb0119f841a7fff6
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_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_sid_map(self):
253 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
255 self.sid_map = {}
256 res = self.ldb.search(base=self.base_dn,
257 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
258 for item in res:
259 try:
260 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
261 except KeyError:
262 pass
264 class Descriptor(object):
265 def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr):
266 self.outf = outf
267 self.errf = errf
268 self.con = connection
269 self.dn = dn
270 self.sddl = self.con.get_descriptor_sddl(self.dn)
271 self.dacl_list = self.extract_dacl()
272 if self.con.sort_aces:
273 self.dacl_list.sort()
275 def extract_dacl(self):
276 """ Extracts the DACL as a list of ACE string (with the brakets).
278 try:
279 if "S:" in self.sddl:
280 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
281 else:
282 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
283 except AttributeError:
284 return []
285 return re.findall("(\(.*?\))", res)
287 def fix_sid(self, ace):
288 res = "%s" % ace
289 sids = re.findall("S-[-0-9]+", res)
290 # If there are not SIDs to replace return the same ACE
291 if len(sids) == 0:
292 return res
293 for sid in sids:
294 try:
295 name = self.con.sid_map[sid]
296 res = res.replace(sid, name)
297 except KeyError:
298 # Do not bother if the SID is not found in baseDN
299 pass
300 return res
302 def diff_1(self, other):
303 res = ""
304 if len(self.dacl_list) != len(other.dacl_list):
305 res += 4*" " + "Difference in ACE count:\n"
306 res += 8*" " + "=> %s\n" % len(self.dacl_list)
307 res += 8*" " + "=> %s\n" % len(other.dacl_list)
309 i = 0
310 flag = True
311 while True:
312 self_ace = None
313 other_ace = None
314 try:
315 self_ace = "%s" % self.dacl_list[i]
316 except IndexError:
317 self_ace = ""
319 try:
320 other_ace = "%s" % other.dacl_list[i]
321 except IndexError:
322 other_ace = ""
323 if len(self_ace) + len(other_ace) == 0:
324 break
325 self_ace_fixed = "%s" % self.fix_sid(self_ace)
326 other_ace_fixed = "%s" % other.fix_sid(other_ace)
327 if self_ace_fixed != other_ace_fixed:
328 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
329 flag = False
330 else:
331 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
332 i += 1
333 return (flag, res)
335 def diff_2(self, other):
336 res = ""
337 if len(self.dacl_list) != len(other.dacl_list):
338 res += 4*" " + "Difference in ACE count:\n"
339 res += 8*" " + "=> %s\n" % len(self.dacl_list)
340 res += 8*" " + "=> %s\n" % len(other.dacl_list)
342 common_aces = []
343 self_aces = []
344 other_aces = []
345 self_dacl_list_fixed = []
346 other_dacl_list_fixed = []
347 [self_dacl_list_fixed.append( self.fix_sid(ace) ) for ace in self.dacl_list]
348 [other_dacl_list_fixed.append( other.fix_sid(ace) ) for ace in other.dacl_list]
349 for ace in self_dacl_list_fixed:
350 try:
351 other_dacl_list_fixed.index(ace)
352 except ValueError:
353 self_aces.append(ace)
354 else:
355 common_aces.append(ace)
356 self_aces = sorted(self_aces)
357 if len(self_aces) > 0:
358 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
359 for ace in self_aces:
360 res += 8*" " + ace + "\n"
362 for ace in other_dacl_list_fixed:
363 try:
364 self_dacl_list_fixed.index(ace)
365 except ValueError:
366 other_aces.append(ace)
367 else:
368 common_aces.append(ace)
369 other_aces = sorted(other_aces)
370 if len(other_aces) > 0:
371 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
372 for ace in other_aces:
373 res += 8*" " + ace + "\n"
375 common_aces = sorted(list(set(common_aces)))
376 if self.con.verbose:
377 res += 4*" " + "ACEs found in both:\n"
378 for ace in common_aces:
379 res += 8*" " + ace + "\n"
380 return (self_aces == [] and other_aces == [], res)
382 class LDAPObject(object):
383 def __init__(self, connection, dn, summary, filter_list,
384 outf=sys.stdout, errf=sys.stderr):
385 self.outf = outf
386 self.errf = errf
387 self.con = connection
388 self.two_domains = self.con.two_domains
389 self.quiet = self.con.quiet
390 self.verbose = self.con.verbose
391 self.summary = summary
392 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
393 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
394 for x in self.con.server_names:
395 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
396 self.attributes = self.con.get_attributes(self.dn)
397 # One domain - two domain controllers
399 # Some attributes are defined as FLAG_ATTR_NOT_REPLICATED
401 # The following list was generated by
402 # egrep '^systemFlags: |^ldapDisplayName: |^linkID: ' \
403 # source4/setup/ad-schema/MS-AD_Schema_2K8_R2_Attributes.txt | \
404 # grep -B1 FLAG_ATTR_NOT_REPLICATED | \
405 # grep ldapDisplayName | \
406 # cut -d ' ' -f2
407 self.non_replicated_attributes = [
408 "badPasswordTime",
409 "badPwdCount",
410 "dSCorePropagationData",
411 "lastLogoff",
412 "lastLogon",
413 "logonCount",
414 "modifiedCount",
415 "msDS-Cached-Membership",
416 "msDS-Cached-Membership-Time-Stamp",
417 "msDS-EnabledFeatureBL",
418 "msDS-ExecuteScriptPassword",
419 "msDS-NcType",
420 "msDS-ReplicationEpoch",
421 "msDS-RetiredReplNCSignatures",
422 "msDS-USNLastSyncSuccess",
423 # "distinguishedName", # This is implicitly replicated
424 # "objectGUID", # This is implicitly replicated
425 "partialAttributeDeletionList",
426 "partialAttributeSet",
427 "pekList",
428 "prefixMap",
429 "replPropertyMetaData",
430 "replUpToDateVector",
431 "repsFrom",
432 "repsTo",
433 "rIDNextRID",
434 "rIDPreviousAllocationPool",
435 "schemaUpdate",
436 "serverState",
437 "subRefs",
438 "uSNChanged",
439 "uSNCreated",
440 "uSNLastObjRem",
441 # "whenChanged", # This is implicitly replicated
443 self.ignore_attributes = self.non_replicated_attributes
444 self.ignore_attributes += ["msExchServer1HighestUSN"]
445 if filter_list:
446 self.ignore_attributes += filter_list
448 self.dn_attributes = []
449 self.domain_attributes = []
450 self.servername_attributes = []
451 self.netbios_attributes = []
452 self.other_attributes = []
453 # Two domains - two domain controllers
455 if self.two_domains:
456 self.ignore_attributes += [
457 "objectCategory", "objectGUID", "objectSid", "whenCreated", "whenChanged", "pwdLastSet", "uSNCreated", "creationTime",
458 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
459 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
460 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
461 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
462 "operatingSystemVersion", "oEMInformation",
463 # After Exchange preps
464 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
466 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
467 self.dn_attributes = [
468 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
469 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
470 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
471 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
472 # After Exchange preps
473 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
474 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
475 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
476 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
477 self.dn_attributes = [x.upper() for x in self.dn_attributes]
479 # Attributes that contain the Domain name e.g. 'samba.org'
480 self.domain_attributes = [
481 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
482 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
483 self.domain_attributes = [x.upper() for x in self.domain_attributes]
485 # May contain DOMAIN_NETBIOS and SERVER_NAME
486 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
487 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
488 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
489 self.servername_attributes = [x.upper() for x in self.servername_attributes]
491 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
492 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
494 self.other_attributes = [ "name", "DC",]
495 self.other_attributes = [x.upper() for x in self.other_attributes]
497 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
499 def log(self, msg):
501 Log on the screen if there is no --quiet option set
503 if not self.quiet:
504 self.outf.write(msg+"\n")
506 def fix_dn(self, s):
507 res = "%s" % s
508 if not self.two_domains:
509 return res
510 if res.upper().endswith(self.con.base_dn.upper()):
511 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
512 return res
514 def fix_domain_name(self, s):
515 res = "%s" % s
516 if not self.two_domains:
517 return res
518 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
519 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
520 return res
522 def fix_domain_netbios(self, s):
523 res = "%s" % s
524 if not self.two_domains:
525 return res
526 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
527 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
528 return res
530 def fix_server_name(self, s):
531 res = "%s" % s
532 if not self.two_domains or len(self.con.server_names) > 1:
533 return res
534 for x in self.con.server_names:
535 res = res.upper().replace(x, "${SERVER_NAME}")
536 return res
538 def __eq__(self, other):
539 if self.con.descriptor:
540 return self.cmp_desc(other)
541 return self.cmp_attrs(other)
543 def cmp_desc(self, other):
544 d1 = Descriptor(self.con, self.dn, outf=self.outf, errf=self.errf)
545 d2 = Descriptor(other.con, other.dn, outf=self.outf, errf=self.errf)
546 if self.con.view == "section":
547 res = d1.diff_2(d2)
548 elif self.con.view == "collision":
549 res = d1.diff_1(d2)
550 else:
551 raise Exception("Unknown --view option value.")
553 self.screen_output = res[1][:-1]
554 other.screen_output = res[1][:-1]
556 return res[0]
558 def cmp_attrs(self, other):
559 res = ""
560 self.unique_attrs = []
561 self.df_value_attrs = []
562 other.unique_attrs = []
563 if self.attributes.keys() != other.attributes.keys():
565 title = 4*" " + "Attributes found only in %s:" % self.con.host
566 for x in self.attributes.keys():
567 if not x in other.attributes.keys() and \
568 not x.upper() in [q.upper() for q in other.ignore_attributes]:
569 if title:
570 res += title + "\n"
571 title = None
572 res += 8*" " + x + "\n"
573 self.unique_attrs.append(x)
575 title = 4*" " + "Attributes found only in %s:" % other.con.host
576 for x in other.attributes.keys():
577 if not x in self.attributes.keys() and \
578 not x.upper() in [q.upper() for q in self.ignore_attributes]:
579 if title:
580 res += title + "\n"
581 title = None
582 res += 8*" " + x + "\n"
583 other.unique_attrs.append(x)
585 missing_attrs = [x.upper() for x in self.unique_attrs]
586 missing_attrs += [x.upper() for x in other.unique_attrs]
587 title = 4*" " + "Difference in attribute values:"
588 for x in self.attributes.keys():
589 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
590 continue
591 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
592 self.attributes[x] = sorted(self.attributes[x])
593 other.attributes[x] = sorted(other.attributes[x])
594 if self.attributes[x] != other.attributes[x]:
595 p = None
596 q = None
597 m = None
598 n = None
599 # First check if the difference can be fixed but shunting the first part
600 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
601 if x.upper() in self.other_attributes:
602 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
603 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
604 if p == q:
605 continue
606 # Attribute values that are list that contain DN based values that may differ
607 elif x.upper() in self.dn_attributes:
608 m = p
609 n = q
610 if not p and not q:
611 m = self.attributes[x]
612 n = other.attributes[x]
613 p = [self.fix_dn(j) for j in m]
614 q = [other.fix_dn(j) for j in n]
615 if p == q:
616 continue
617 # Attributes that contain the Domain name in them
618 if x.upper() in self.domain_attributes:
619 m = p
620 n = q
621 if not p and not q:
622 m = self.attributes[x]
623 n = other.attributes[x]
624 p = [self.fix_domain_name(j) for j in m]
625 q = [other.fix_domain_name(j) for j in n]
626 if p == q:
627 continue
629 if x.upper() in self.servername_attributes:
630 # Attributes with SERVER_NAME
631 m = p
632 n = q
633 if not p and not q:
634 m = self.attributes[x]
635 n = other.attributes[x]
636 p = [self.fix_server_name(j) for j in m]
637 q = [other.fix_server_name(j) for j in n]
638 if p == q:
639 continue
641 if x.upper() in self.netbios_attributes:
642 # Attributes with NETBIOS Domain name
643 m = p
644 n = q
645 if not p and not q:
646 m = self.attributes[x]
647 n = other.attributes[x]
648 p = [self.fix_domain_netbios(j) for j in m]
649 q = [other.fix_domain_netbios(j) for j in n]
650 if p == q:
651 continue
653 if title:
654 res += title + "\n"
655 title = None
656 if p and q:
657 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
658 else:
659 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
660 self.df_value_attrs.append(x)
662 if self.unique_attrs + other.unique_attrs != []:
663 assert self.unique_attrs != other.unique_attrs
664 self.summary["unique_attrs"] += self.unique_attrs
665 self.summary["df_value_attrs"] += self.df_value_attrs
666 other.summary["unique_attrs"] += other.unique_attrs
667 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
669 self.screen_output = res[:-1]
670 other.screen_output = res[:-1]
672 return res == ""
675 class LDAPBundel(object):
677 def __init__(self, connection, context, dn_list=None, filter_list=None,
678 outf=sys.stdout, errf=sys.stderr):
679 self.outf = outf
680 self.errf = errf
681 self.con = connection
682 self.two_domains = self.con.two_domains
683 self.quiet = self.con.quiet
684 self.verbose = self.con.verbose
685 self.search_base = self.con.search_base
686 self.search_scope = self.con.search_scope
687 self.skip_missing_dn = self.con.skip_missing_dn
688 self.summary = {}
689 self.summary["unique_attrs"] = []
690 self.summary["df_value_attrs"] = []
691 self.summary["known_ignored_dn"] = []
692 self.summary["abnormal_ignored_dn"] = []
693 self.filter_list = filter_list
694 if dn_list:
695 self.dn_list = dn_list
696 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
697 self.context = context.upper()
698 self.dn_list = self.get_dn_list(context)
699 else:
700 raise Exception("Unknown initialization data for LDAPBundel().")
701 counter = 0
702 while counter < len(self.dn_list) and self.two_domains:
703 # Use alias reference
704 tmp = self.dn_list[counter]
705 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
706 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
707 if len(self.con.server_names) == 1:
708 for x in self.con.server_names:
709 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
710 self.dn_list[counter] = tmp
711 counter += 1
712 self.dn_list = list(set(self.dn_list))
713 self.dn_list = sorted(self.dn_list)
714 self.size = len(self.dn_list)
716 def log(self, msg):
718 Log on the screen if there is no --quiet option set
720 if not self.quiet:
721 self.outf.write(msg+"\n")
723 def update_size(self):
724 self.size = len(self.dn_list)
725 self.dn_list = sorted(self.dn_list)
727 def __eq__(self, other):
728 res = True
729 if self.size != other.size:
730 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
731 if not self.skip_missing_dn:
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 and not self.skip_missing_dn:
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 and not self.skip_missing_dn:
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?", "context4?", "context5?"]
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"),
905 Option("--skip-missing-dn", dest="skip_missing_dn", action="store_true", default=False,
906 help="Skip report and failure due to missing DNs in one server or another"),
909 def run(self, URL1, URL2,
910 context1=None, context2=None, context3=None, context4=None, context5=None,
911 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
912 view="section", base="", base2="", scope="SUB", filter="",
913 credopts=None, sambaopts=None, versionopts=None, skip_missing_dn=False):
915 lp = sambaopts.get_loadparm()
917 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
919 if using_ldap:
920 creds = credopts.get_credentials(lp, fallback_machine=True)
921 else:
922 creds = None
923 creds2 = credopts.get_credentials2(lp, guess=False)
924 if creds2.is_anonymous():
925 creds2 = creds
926 else:
927 creds2.set_domain("")
928 creds2.set_workstation("")
929 if using_ldap and not creds.authentication_requested():
930 raise CommandError("You must supply at least one username/password pair")
932 # make a list of contexts to compare in
933 contexts = []
934 if context1 is None:
935 if base and base2:
936 # If search bases are specified context is defaulted to
937 # DOMAIN so the given search bases can be verified.
938 contexts = ["DOMAIN"]
939 else:
940 # if no argument given, we compare all contexts
941 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
942 else:
943 for c in [context1, context2, context3, context4, context5]:
944 if c is None:
945 continue
946 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
947 raise CommandError("Incorrect argument: %s" % c)
948 contexts.append(c.upper())
950 if verbose and quiet:
951 raise CommandError("You cannot set --verbose and --quiet together")
952 if (not base and base2) or (base and not base2):
953 raise CommandError("You need to specify both --base and --base2 at the same time")
954 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
955 raise CommandError("Invalid --view value. Choose from: section or collision")
956 if not scope.upper() in ["SUB", "ONE", "BASE"]:
957 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
959 con1 = LDAPBase(URL1, creds, lp,
960 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
961 verbose=verbose,view=view, base=base, scope=scope,
962 outf=self.outf, errf=self.errf)
963 assert len(con1.base_dn) > 0
965 con2 = LDAPBase(URL2, creds2, lp,
966 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
967 verbose=verbose, view=view, base=base2, scope=scope,
968 outf=self.outf, errf=self.errf)
969 assert len(con2.base_dn) > 0
971 filter_list = filter.split(",")
973 status = 0
974 for context in contexts:
975 if not quiet:
976 self.outf.write("\n* Comparing [%s] context...\n" % context)
978 b1 = LDAPBundel(con1, context=context, filter_list=filter_list,
979 outf=self.outf, errf=self.errf)
980 b2 = LDAPBundel(con2, context=context, filter_list=filter_list,
981 outf=self.outf, errf=self.errf)
983 if b1 == b2:
984 if not quiet:
985 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
986 context)
987 else:
988 if not quiet:
989 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
990 if not descriptor:
991 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
992 b2.summary["df_value_attrs"] = []
993 self.outf.write("\nSUMMARY\n")
994 self.outf.write("---------\n")
995 b1.print_summary()
996 b2.print_summary()
997 # mark exit status as FAILURE if a least one comparison failed
998 status = -1
999 if status != 0:
1000 raise CommandError("Compare failed: %d" % status)