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
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/>.
29 import samba.getopt as options
31 from samba.ndr import ndr_pack, 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 (
44 class LDAPBase(object):
46 def __init__(self, host, creds, lp,
47 two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
48 view="section", base="", scope="SUB"):
52 if os.path.isfile(host):
53 samdb_url = "tdb://%s" % host
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.ldb = Ldb(url=samdb_url,
63 self.search_base = base
64 self.search_scope = scope
65 self.two_domains = two
67 self.descriptor = descriptor
68 self.sort_aces = sort_aces
70 self.verbose = verbose
72 self.base_dn = str(self.ldb.get_default_basedn())
73 self.root_dn = str(self.ldb.get_root_basedn())
74 self.config_dn = str(self.ldb.get_config_basedn())
75 self.schema_dn = str(self.ldb.get_schema_basedn())
76 self.domain_netbios = self.find_netbios()
77 self.server_names = self.find_servers()
78 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
79 self.domain_sid = self.find_domain_sid()
83 # Log some domain controller specific place-holers that are being used
84 # when compare content of two DCs. Uncomment for DEBUG purposes.
85 if self.two_domains and not self.quiet:
86 self.outf.write("\n* Place-holders for %s:\n" % self.host)
87 self.outf.write(4*" " + "${DOMAIN_DN} => %s\n" %
89 self.outf.write(4*" " + "${DOMAIN_NETBIOS} => %s\n" %
91 self.outf.write(4*" " + "${SERVER_NAME} => %s\n" %
93 self.outf.write(4*" " + "${DOMAIN_NAME} => %s\n" %
96 def find_domain_sid(self):
97 res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
98 return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
100 def find_servers(self):
103 res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \
104 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
108 srv.append(x["cn"][0])
111 def find_netbios(self):
112 res = self.ldb.search(base="CN=Partitions,%s" % self.config_dn, \
113 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
116 if "nETBIOSName" in x.keys():
117 return x["nETBIOSName"][0]
119 def object_exists(self, object_dn):
122 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
123 except LdbError, (enum, estr):
124 if enum == ERR_NO_SUCH_OBJECT:
129 def delete_force(self, object_dn):
131 self.ldb.delete(object_dn)
132 except Ldb.LdbError, e:
133 assert "No such object" in str(e)
135 def get_attribute_name(self, key):
136 """ Returns the real attribute name
137 It resolved ranged results e.g. member;range=0-1499
140 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
148 def get_attribute_values(self, object_dn, key, vals):
149 """ Returns list with all attribute values
150 It resolved ranged results e.g. member;range=0-1499
153 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
157 # no range, just return the values
163 # get additional values in a loop
164 # until we get a response with '*' at the end
167 n = "%s;range=%d-*" % (attr, hi + 1)
168 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
176 for key in res.keys():
182 if m.group(1) != attr:
186 fvals = list(res[key])
193 if fm.group(3) == "*":
194 # if we got "*" we're done
197 assert int(fm.group(2)) == hi + 1
198 hi = int(fm.group(3))
202 def get_attributes(self, object_dn):
203 """ Returns dict with all default visible attributes
205 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
208 # 'Dn' element is not iterable and we have it as 'distinguishedName'
210 for key in res.keys():
211 vals = list(res[key])
213 name = self.get_attribute_name(key)
214 res[name] = self.get_attribute_values(object_dn, key, vals)
218 def get_descriptor_sddl(self, object_dn):
219 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
220 desc = res[0]["nTSecurityDescriptor"][0]
221 desc = ndr_unpack(security.descriptor, desc)
222 return desc.as_sddl(self.domain_sid)
224 def guid_as_string(self, guid_blob):
225 """ Translate binary representation of schemaIDGUID to standard string representation.
226 @gid_blob: binary schemaIDGUID
228 blob = "%s" % guid_blob
229 stops = [4, 2, 2, 2, 6]
233 while x < len(stops):
237 c = hex(ord(blob[index])).replace("0x", "")
238 c = [None, "0" + c, c][len(c)]
239 if 2 * index < len(blob):
247 assert index == len(blob)
248 return res.strip().replace(" ", "-")
250 def get_guid_map(self):
251 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
254 res = self.ldb.search(base=self.schema_dn,
255 expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
257 self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
259 res = self.ldb.search(base="cn=extended-rights,%s" % self.config_dn,
260 expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
262 self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
264 def get_sid_map(self):
265 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
268 res = self.ldb.search(base=self.base_dn,
269 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
272 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
276 class Descriptor(object):
277 def __init__(self, connection, dn):
278 self.con = connection
280 self.sddl = self.con.get_descriptor_sddl(self.dn)
281 self.dacl_list = self.extract_dacl()
282 if self.con.sort_aces:
283 self.dacl_list.sort()
285 def extract_dacl(self):
286 """ Extracts the DACL as a list of ACE string (with the brakets).
289 if "S:" in self.sddl:
290 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
292 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
293 except AttributeError:
295 return re.findall("(\(.*?\))", res)
297 def fix_guid(self, ace):
299 guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
300 # If there are not GUIDs to replace return the same ACE
305 name = self.con.guid_map[guid.lower()]
306 res = res.replace(guid, name)
308 # Do not bother if the GUID is not found in
309 # cn=Schema or cn=Extended-Rights
313 def fix_sid(self, ace):
315 sids = re.findall("S-[-0-9]+", res)
316 # If there are not SIDs to replace return the same ACE
321 name = self.con.sid_map[sid]
322 res = res.replace(sid, name)
324 # Do not bother if the SID is not found in baseDN
328 def fixit(self, ace):
329 """ Combine all replacement methods in one
332 res = self.fix_guid(res)
333 res = self.fix_sid(res)
336 def diff_1(self, other):
338 if len(self.dacl_list) != len(other.dacl_list):
339 res += 4*" " + "Difference in ACE count:\n"
340 res += 8*" " + "=> %s\n" % len(self.dacl_list)
341 res += 8*" " + "=> %s\n" % len(other.dacl_list)
349 self_ace = "%s" % self.dacl_list[i]
354 other_ace = "%s" % other.dacl_list[i]
357 if len(self_ace) + len(other_ace) == 0:
359 self_ace_fixed = "%s" % self.fixit(self_ace)
360 other_ace_fixed = "%s" % other.fixit(other_ace)
361 if self_ace_fixed != other_ace_fixed:
362 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
365 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
369 def diff_2(self, other):
371 if len(self.dacl_list) != len(other.dacl_list):
372 res += 4*" " + "Difference in ACE count:\n"
373 res += 8*" " + "=> %s\n" % len(self.dacl_list)
374 res += 8*" " + "=> %s\n" % len(other.dacl_list)
379 self_dacl_list_fixed = []
380 other_dacl_list_fixed = []
381 [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
382 [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
383 for ace in self_dacl_list_fixed:
385 other_dacl_list_fixed.index(ace)
387 self_aces.append(ace)
389 common_aces.append(ace)
390 self_aces = sorted(self_aces)
391 if len(self_aces) > 0:
392 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
393 for ace in self_aces:
394 res += 8*" " + ace + "\n"
396 for ace in other_dacl_list_fixed:
398 self_dacl_list_fixed.index(ace)
400 other_aces.append(ace)
402 common_aces.append(ace)
403 other_aces = sorted(other_aces)
404 if len(other_aces) > 0:
405 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
406 for ace in other_aces:
407 res += 8*" " + ace + "\n"
409 common_aces = sorted(list(set(common_aces)))
411 res += 4*" " + "ACEs found in both:\n"
412 for ace in common_aces:
413 res += 8*" " + ace + "\n"
414 return (self_aces == [] and other_aces == [], res)
416 class LDAPObject(object):
417 def __init__(self, connection, dn, summary, filter_list):
418 self.con = connection
419 self.two_domains = self.con.two_domains
420 self.quiet = self.con.quiet
421 self.verbose = self.con.verbose
422 self.summary = summary
423 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
424 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
425 for x in self.con.server_names:
426 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
427 self.attributes = self.con.get_attributes(self.dn)
428 # Attributes that are considered always to be different e.g based on timestamp etc.
430 # One domain - two domain controllers
431 self.ignore_attributes = [
432 # Default Naming Context
433 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
434 "operatingSystemVersion","oEMInformation",
435 # Configuration Naming Context
436 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
437 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
438 # Schema Naming Context
441 self.ignore_attributes += filter_list
443 self.dn_attributes = []
444 self.domain_attributes = []
445 self.servername_attributes = []
446 self.netbios_attributes = []
447 self.other_attributes = []
448 # Two domains - two domain controllers
451 self.ignore_attributes += [
452 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
453 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
454 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
455 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
456 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
457 # After Exchange preps
458 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
460 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
461 self.dn_attributes = [
462 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
463 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
464 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
465 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
466 # After Exchange preps
467 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
468 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
469 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
470 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
471 self.dn_attributes = [x.upper() for x in self.dn_attributes]
473 # Attributes that contain the Domain name e.g. 'samba.org'
474 self.domain_attributes = [
475 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
476 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
477 self.domain_attributes = [x.upper() for x in self.domain_attributes]
479 # May contain DOMAIN_NETBIOS and SERVER_NAME
480 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
481 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
482 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
483 self.servername_attributes = [x.upper() for x in self.servername_attributes]
485 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
486 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
488 self.other_attributes = [ "name", "DC",]
489 self.other_attributes = [x.upper() for x in self.other_attributes]
491 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
495 Log on the screen if there is no --quiet oprion set
498 self.outf.write(msg+"\n")
502 if not self.two_domains:
504 if res.upper().endswith(self.con.base_dn.upper()):
505 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
508 def fix_domain_name(self, s):
510 if not self.two_domains:
512 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
513 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
516 def fix_domain_netbios(self, s):
518 if not self.two_domains:
520 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
521 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
524 def fix_server_name(self, s):
526 if not self.two_domains or len(self.con.server_names) > 1:
528 for x in self.con.server_names:
529 res = res.upper().replace(x, "${SERVER_NAME}")
532 def __eq__(self, other):
533 if self.con.descriptor:
534 return self.cmp_desc(other)
535 return self.cmp_attrs(other)
537 def cmp_desc(self, other):
538 d1 = Descriptor(self.con, self.dn)
539 d2 = Descriptor(other.con, other.dn)
540 if self.con.view == "section":
542 elif self.con.view == "collision":
545 raise Exception("Unknown --view option value.")
547 self.screen_output = res[1][:-1]
548 other.screen_output = res[1][:-1]
552 def cmp_attrs(self, other):
554 self.unique_attrs = []
555 self.df_value_attrs = []
556 other.unique_attrs = []
557 if self.attributes.keys() != other.attributes.keys():
559 title = 4*" " + "Attributes found only in %s:" % self.con.host
560 for x in self.attributes.keys():
561 if not x in other.attributes.keys() and \
562 not x.upper() in [q.upper() for q in other.ignore_attributes]:
566 res += 8*" " + x + "\n"
567 self.unique_attrs.append(x)
569 title = 4*" " + "Attributes found only in %s:" % other.con.host
570 for x in other.attributes.keys():
571 if not x in self.attributes.keys() and \
572 not x.upper() in [q.upper() for q in self.ignore_attributes]:
576 res += 8*" " + x + "\n"
577 other.unique_attrs.append(x)
579 missing_attrs = [x.upper() for x in self.unique_attrs]
580 missing_attrs += [x.upper() for x in other.unique_attrs]
581 title = 4*" " + "Difference in attribute values:"
582 for x in self.attributes.keys():
583 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
585 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
586 self.attributes[x] = sorted(self.attributes[x])
587 other.attributes[x] = sorted(other.attributes[x])
588 if self.attributes[x] != other.attributes[x]:
593 # First check if the difference can be fixed but shunting the first part
594 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
595 if x.upper() in self.other_attributes:
596 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
597 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
600 # Attribute values that are list that contain DN based values that may differ
601 elif x.upper() in self.dn_attributes:
605 m = self.attributes[x]
606 n = other.attributes[x]
607 p = [self.fix_dn(j) for j in m]
608 q = [other.fix_dn(j) for j in n]
611 # Attributes that contain the Domain name in them
612 if x.upper() in self.domain_attributes:
616 m = self.attributes[x]
617 n = other.attributes[x]
618 p = [self.fix_domain_name(j) for j in m]
619 q = [other.fix_domain_name(j) for j in n]
623 if x.upper() in self.servername_attributes:
624 # Attributes with SERVER_NAME
628 m = self.attributes[x]
629 n = other.attributes[x]
630 p = [self.fix_server_name(j) for j in m]
631 q = [other.fix_server_name(j) for j in n]
635 if x.upper() in self.netbios_attributes:
636 # Attributes with NETBIOS Domain name
640 m = self.attributes[x]
641 n = other.attributes[x]
642 p = [self.fix_domain_netbios(j) for j in m]
643 q = [other.fix_domain_netbios(j) for j in n]
651 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
653 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
654 self.df_value_attrs.append(x)
656 if self.unique_attrs + other.unique_attrs != []:
657 assert self.unique_attrs != other.unique_attrs
658 self.summary["unique_attrs"] += self.unique_attrs
659 self.summary["df_value_attrs"] += self.df_value_attrs
660 other.summary["unique_attrs"] += other.unique_attrs
661 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
663 self.screen_output = res[:-1]
664 other.screen_output = res[:-1]
669 class LDAPBundel(object):
671 def __init__(self, connection, context, dn_list=None, filter_list=None):
672 self.con = connection
673 self.two_domains = self.con.two_domains
674 self.quiet = self.con.quiet
675 self.verbose = self.con.verbose
676 self.search_base = self.con.search_base
677 self.search_scope = self.con.search_scope
679 self.summary["unique_attrs"] = []
680 self.summary["df_value_attrs"] = []
681 self.summary["known_ignored_dn"] = []
682 self.summary["abnormal_ignored_dn"] = []
683 self.filter_list = filter_list
685 self.dn_list = dn_list
686 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
687 self.context = context.upper()
688 self.dn_list = self.get_dn_list(context)
690 raise Exception("Unknown initialization data for LDAPBundel().")
692 while counter < len(self.dn_list) and self.two_domains:
693 # Use alias reference
694 tmp = self.dn_list[counter]
695 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
696 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
697 if len(self.con.server_names) == 1:
698 for x in self.con.server_names:
699 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
700 self.dn_list[counter] = tmp
702 self.dn_list = list(set(self.dn_list))
703 self.dn_list = sorted(self.dn_list)
704 self.size = len(self.dn_list)
708 Log on the screen if there is no --quiet oprion set
711 self.outf.write(msg+"\n")
713 def update_size(self):
714 self.size = len(self.dn_list)
715 self.dn_list = sorted(self.dn_list)
717 def __eq__(self, other):
719 if self.size != other.size:
720 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
723 # This is the case where we want to explicitly compare two objects with different DNs.
724 # It does not matter if they are in the same DC, in two DC in one domain or in two
726 if self.search_scope != SCOPE_BASE:
727 title= "\n* DNs found only in %s:" % self.con.host
728 for x in self.dn_list:
729 if not x.upper() in [q.upper() for q in other.dn_list]:
734 self.log( 4*" " + x )
735 self.dn_list[self.dn_list.index(x)] = ""
736 self.dn_list = [x for x in self.dn_list if x]
738 title= "\n* DNs found only in %s:" % other.con.host
739 for x in other.dn_list:
740 if not x.upper() in [q.upper() for q in self.dn_list]:
745 self.log( 4*" " + x )
746 other.dn_list[other.dn_list.index(x)] = ""
747 other.dn_list = [x for x in other.dn_list if x]
751 assert self.size == other.size
752 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
753 self.log( "\n* Objects to be compared: %s" % self.size )
756 while index < self.size:
759 object1 = LDAPObject(connection=self.con,
760 dn=self.dn_list[index],
761 summary=self.summary,
762 filter_list=self.filter_list)
763 except LdbError, (enum, estr):
764 if enum == ERR_NO_SUCH_OBJECT:
765 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
769 object2 = LDAPObject(connection=other.con,
770 dn=other.dn_list[index],
771 summary=other.summary,
772 filter_list=self.filter_list)
773 except LdbError, (enum, estr):
774 if enum == ERR_NO_SUCH_OBJECT:
775 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
781 if object1 == object2:
783 self.log( "\nComparing:" )
784 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
785 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
786 self.log( 4*" " + "OK" )
788 self.log( "\nComparing:" )
789 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
790 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
791 self.log( object1.screen_output )
792 self.log( 4*" " + "FAILED" )
794 self.summary = object1.summary
795 other.summary = object2.summary
800 def get_dn_list(self, context):
801 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
802 Parse all DNs and filter those that are 'strange' or abnormal.
804 if context.upper() == "DOMAIN":
805 search_base = self.con.base_dn
806 elif context.upper() == "CONFIGURATION":
807 search_base = self.con.config_dn
808 elif context.upper() == "SCHEMA":
809 search_base = self.con.schema_dn
810 elif context.upper() == "DNSDOMAIN":
811 search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
812 elif context.upper() == "DNSFOREST":
813 search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
816 if not self.search_base:
817 self.search_base = search_base
818 self.search_scope = self.search_scope.upper()
819 if self.search_scope == "SUB":
820 self.search_scope = SCOPE_SUBTREE
821 elif self.search_scope == "BASE":
822 self.search_scope = SCOPE_BASE
823 elif self.search_scope == "ONE":
824 self.search_scope = SCOPE_ONELEVEL
826 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
828 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
829 except LdbError, (enum, estr):
830 self.outf.write("Failed search of base=%s\n" % self.search_base)
833 dn_list.append(x["dn"].get_linearized())
839 def print_summary(self):
840 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
841 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
843 if self.summary["unique_attrs"]:
844 self.log( "\nAttributes found only in %s:" % self.con.host )
845 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
847 if self.summary["df_value_attrs"]:
848 self.log( "\nAttributes with different values:" )
849 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
850 self.summary["df_value_attrs"] = []
853 class cmd_ldapcmp(Command):
854 """compare two ldap databases"""
855 synopsis = "%prog ldapcmp <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
857 takes_optiongroups = {
858 "sambaopts": options.SambaOptions,
859 "versionopts": options.VersionOptions,
860 "credopts": options.CredentialsOptionsDouble,
863 takes_optiongroups = {
864 "sambaopts": options.SambaOptions,
865 "versionopts": options.VersionOptions,
866 "credopts": options.CredentialsOptionsDouble,
869 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
872 Option("-w", "--two", dest="two", action="store_true", default=False,
873 help="Hosts are in two different domains"),
874 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
875 help="Do not print anything but relay on just exit code"),
876 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
877 help="Print all DN pairs that have been compared"),
878 Option("--sd", dest="descriptor", action="store_true", default=False,
879 help="Compare nTSecurityDescriptor attibutes only"),
880 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
881 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
882 Option("--view", dest="view", default="section",
883 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
884 Option("--base", dest="base", default="",
885 help="Pass search base that will build DN list for the first DC."),
886 Option("--base2", dest="base2", default="",
887 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
888 Option("--scope", dest="scope", default="SUB",
889 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
890 Option("--filter", dest="filter", default="",
891 help="List of comma separated attributes to ignore in the comparision"),
894 def run(self, URL1, URL2,
895 context1=None, context2=None, context3=None,
896 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
897 view="section", base="", base2="", scope="SUB", filter="",
898 credopts=None, sambaopts=None, versionopts=None):
900 lp = sambaopts.get_loadparm()
902 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
905 creds = credopts.get_credentials(lp, fallback_machine=True)
908 creds2 = credopts.get_credentials2(lp, guess=False)
909 if creds2.is_anonymous():
912 creds2.set_domain("")
913 creds2.set_workstation("")
914 if using_ldap and not creds.authentication_requested():
915 raise CommandError("You must supply at least one username/password pair")
917 # make a list of contexts to compare in
921 # If search bases are specified context is defaulted to
922 # DOMAIN so the given search bases can be verified.
923 contexts = ["DOMAIN"]
925 # if no argument given, we compare all contexts
926 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
928 for c in [context1, context2, context3]:
931 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
932 raise CommandError("Incorrect argument: %s" % c)
933 contexts.append(c.upper())
935 if verbose and quiet:
936 raise CommandError("You cannot set --verbose and --quiet together")
937 if (not base and base2) or (base and not base2):
938 raise CommandError("You need to specify both --base and --base2 at the same time")
939 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
940 raise CommandError("Invalid --view value. Choose from: section or collision")
941 if not scope.upper() in ["SUB", "ONE", "BASE"]:
942 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
944 con1 = LDAPBase(URL1, creds, lp,
945 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
946 verbose=verbose,view=view, base=base, scope=scope)
947 assert len(con1.base_dn) > 0
949 con2 = LDAPBase(URL2, creds2, lp,
950 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
951 verbose=verbose, view=view, base=base2, scope=scope)
952 assert len(con2.base_dn) > 0
954 filter_list = filter.split(",")
957 for context in contexts:
959 self.outf.write("\n* Comparing [%s] context...\n" % context)
961 b1 = LDAPBundel(con1, context=context, filter_list=filter_list)
962 b2 = LDAPBundel(con2, context=context, filter_list=filter_list)
966 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
970 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
972 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
973 b2.summary["df_value_attrs"] = []
974 self.outf.write("\nSUMMARY\n")
975 self.outf.write("---------\n")
978 # mark exit status as FAILURE if a least one comparison failed
981 raise CommandError("Compare failed: %d" % status)