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
10 # Copyright (C) Zahari Zahariev <zahari.zahariev@postpath.com> 2009, 2010
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 3 of the License, or
15 # (at your option) any later version.
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
22 # You should have received a copy of the GNU General Public License
23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
31 import samba.getopt as options
33 from samba.ndr import ndr_pack, ndr_unpack
34 from samba.dcerpc import security
35 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, ERR_NO_SUCH_OBJECT, LdbError
36 from samba.netcmd import (
46 class LDAPBase(object):
48 def __init__(self, host, creds, lp,
49 two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
50 view="section", base="", scope="SUB"):
54 if os.path.isfile(host):
55 samdb_url = "tdb://%s" % host
57 samdb_url = "ldap://%s" % host
58 # use 'paged_search' module when connecting remotely
59 if samdb_url.lower().startswith("ldap://"):
60 ldb_options = ["modules:paged_searches"]
61 self.ldb = Ldb(url=samdb_url,
65 self.search_base = base
66 self.search_scope = scope
67 self.two_domains = two
69 self.descriptor = descriptor
70 self.sort_aces = sort_aces
72 self.verbose = verbose
74 self.base_dn = self.find_basedn()
75 self.domain_netbios = self.find_netbios()
76 self.server_names = self.find_servers()
77 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
78 self.domain_sid = self.find_domain_sid()
82 # Log some domain controller specific place-holers that are being used
83 # when compare content of two DCs. Uncomment for DEBUG purposes.
84 if self.two_domains and not self.quiet:
85 print "\n* Place-holders for %s:" % self.host
86 print 4*" " + "${DOMAIN_DN} => %s" % self.base_dn
87 print 4*" " + "${DOMAIN_NETBIOS} => %s" % self.domain_netbios
88 print 4*" " + "${SERVER_NAME} => %s" % self.server_names
89 print 4*" " + "${DOMAIN_NAME} => %s" % self.domain_name
91 def find_domain_sid(self):
92 res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
93 return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
95 def find_servers(self):
98 res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \
99 scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
103 srv.append(x["cn"][0])
106 def find_netbios(self):
107 res = self.ldb.search(base="CN=Partitions,CN=Configuration,%s" % self.base_dn, \
108 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
111 if "nETBIOSName" in x.keys():
112 return x["nETBIOSName"][0]
114 def find_basedn(self):
115 res = self.ldb.search(base="", expression="(objectClass=*)", scope=SCOPE_BASE,
116 attrs=["defaultNamingContext"])
118 return res[0]["defaultNamingContext"][0]
120 def object_exists(self, object_dn):
123 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
124 except LdbError, (enum, estr):
125 if enum == ERR_NO_SUCH_OBJECT:
130 def delete_force(self, object_dn):
132 self.ldb.delete(object_dn)
133 except Ldb.LdbError, e:
134 assert "No such object" in str(e)
136 def get_attribute_name(self, key):
137 """ Returns the real attribute name
138 It resolved ranged results e.g. member;range=0-1499
141 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
149 def get_attribute_values(self, object_dn, key, vals):
150 """ Returns list with all attribute values
151 It resolved ranged results e.g. member;range=0-1499
154 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
158 # no range, just return the values
164 # get additional values in a loop
165 # until we get a response with '*' at the end
168 n = "%s;range=%d-*" % (attr, hi + 1)
169 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
177 for key in res.keys():
183 if m.group(1) != attr:
187 fvals = list(res[key])
194 if fm.group(3) == "*":
195 # if we got "*" we're done
198 assert int(fm.group(2)) == hi + 1
199 hi = int(fm.group(3))
203 def get_attributes(self, object_dn):
204 """ Returns dict with all default visible attributes
206 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
209 # 'Dn' element is not iterable and we have it as 'distinguishedName'
211 for key in res.keys():
212 vals = list(res[key])
214 name = self.get_attribute_name(key)
215 res[name] = self.get_attribute_values(object_dn, key, vals)
219 def get_descriptor_sddl(self, object_dn):
220 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
221 desc = res[0]["nTSecurityDescriptor"][0]
222 desc = ndr_unpack(security.descriptor, desc)
223 return desc.as_sddl(self.domain_sid)
225 def guid_as_string(self, guid_blob):
226 """ Translate binary representation of schemaIDGUID to standard string representation.
227 @gid_blob: binary schemaIDGUID
229 blob = "%s" % guid_blob
230 stops = [4, 2, 2, 2, 6]
234 while x < len(stops):
238 c = hex(ord(blob[index])).replace("0x", "")
239 c = [None, "0" + c, c][len(c)]
240 if 2 * index < len(blob):
248 assert index == len(blob)
249 return res.strip().replace(" ", "-")
251 def get_guid_map(self):
252 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
255 res = self.ldb.search(base="cn=schema,cn=configuration,%s" % self.base_dn, \
256 expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
258 self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
260 res = self.ldb.search(base="cn=extended-rights,cn=configuration,%s" % self.base_dn, \
261 expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
263 self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
265 def get_sid_map(self):
266 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
269 res = self.ldb.search(base="%s" % self.base_dn, \
270 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
273 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
277 class Descriptor(object):
278 def __init__(self, connection, dn):
279 self.con = connection
281 self.sddl = self.con.get_descriptor_sddl(self.dn)
282 self.dacl_list = self.extract_dacl()
283 if self.con.sort_aces:
284 self.dacl_list.sort()
286 def extract_dacl(self):
287 """ Extracts the DACL as a list of ACE string (with the brakets).
290 if "S:" in self.sddl:
291 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
293 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
294 except AttributeError:
296 return re.findall("(\(.*?\))", res)
298 def fix_guid(self, ace):
300 guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
301 # If there are not GUIDs to replace return the same ACE
306 name = self.con.guid_map[guid.lower()]
307 res = res.replace(guid, name)
309 # Do not bother if the GUID is not found in
310 # cn=Schema or cn=Extended-Rights
314 def fix_sid(self, ace):
316 sids = re.findall("S-[-0-9]+", res)
317 # If there are not SIDs to replace return the same ACE
322 name = self.con.sid_map[sid]
323 res = res.replace(sid, name)
325 # Do not bother if the SID is not found in baseDN
329 def fixit(self, ace):
330 """ Combine all replacement methods in one
333 res = self.fix_guid(res)
334 res = self.fix_sid(res)
337 def diff_1(self, other):
339 if len(self.dacl_list) != len(other.dacl_list):
340 res += 4*" " + "Difference in ACE count:\n"
341 res += 8*" " + "=> %s\n" % len(self.dacl_list)
342 res += 8*" " + "=> %s\n" % len(other.dacl_list)
350 self_ace = "%s" % self.dacl_list[i]
355 other_ace = "%s" % other.dacl_list[i]
358 if len(self_ace) + len(other_ace) == 0:
360 self_ace_fixed = "%s" % self.fixit(self_ace)
361 other_ace_fixed = "%s" % other.fixit(other_ace)
362 if self_ace_fixed != other_ace_fixed:
363 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
366 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
370 def diff_2(self, other):
372 if len(self.dacl_list) != len(other.dacl_list):
373 res += 4*" " + "Difference in ACE count:\n"
374 res += 8*" " + "=> %s\n" % len(self.dacl_list)
375 res += 8*" " + "=> %s\n" % len(other.dacl_list)
380 self_dacl_list_fixed = []
381 other_dacl_list_fixed = []
382 [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
383 [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
384 for ace in self_dacl_list_fixed:
386 other_dacl_list_fixed.index(ace)
388 self_aces.append(ace)
390 common_aces.append(ace)
391 self_aces = sorted(self_aces)
392 if len(self_aces) > 0:
393 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
394 for ace in self_aces:
395 res += 8*" " + ace + "\n"
397 for ace in other_dacl_list_fixed:
399 self_dacl_list_fixed.index(ace)
401 other_aces.append(ace)
403 common_aces.append(ace)
404 other_aces = sorted(other_aces)
405 if len(other_aces) > 0:
406 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
407 for ace in other_aces:
408 res += 8*" " + ace + "\n"
410 common_aces = sorted(list(set(common_aces)))
412 res += 4*" " + "ACEs found in both:\n"
413 for ace in common_aces:
414 res += 8*" " + ace + "\n"
415 return (self_aces == [] and other_aces == [], res)
417 class LDAPObject(object):
418 def __init__(self, connection, dn, summary):
419 self.con = connection
420 self.two_domains = self.con.two_domains
421 self.quiet = self.con.quiet
422 self.verbose = self.con.verbose
423 self.summary = summary
424 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
425 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
426 for x in self.con.server_names:
427 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
428 self.attributes = self.con.get_attributes(self.dn)
429 # Attributes that are considered always to be different e.g based on timestamp etc.
431 # One domain - two domain controllers
432 self.ignore_attributes = [
433 # Default Naming Context
434 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
435 "operatingSystemVersion","oEMInformation",
436 # Configuration Naming Context
437 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
438 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
439 # Schema Naming Context
441 self.dn_attributes = []
442 self.domain_attributes = []
443 self.servername_attributes = []
444 self.netbios_attributes = []
445 self.other_attributes = []
446 # Two domains - two domain controllers
449 self.ignore_attributes += [
450 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
451 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
452 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
453 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
454 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
455 # After Exchange preps
456 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
458 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
459 self.dn_attributes = [
460 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
461 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
462 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
463 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
464 # After Exchange preps
465 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
466 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
467 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
468 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
469 self.dn_attributes = [x.upper() for x in self.dn_attributes]
471 # Attributes that contain the Domain name e.g. 'samba.org'
472 self.domain_attributes = [
473 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
474 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
475 self.domain_attributes = [x.upper() for x in self.domain_attributes]
477 # May contain DOMAIN_NETBIOS and SERVER_NAME
478 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
479 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
480 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
481 self.servername_attributes = [x.upper() for x in self.servername_attributes]
483 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
484 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
486 self.other_attributes = [ "name", "DC",]
487 self.other_attributes = [x.upper() for x in self.other_attributes]
489 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
493 Log on the screen if there is no --quiet oprion set
500 if not self.two_domains:
502 if res.upper().endswith(self.con.base_dn.upper()):
503 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
506 def fix_domain_name(self, s):
508 if not self.two_domains:
510 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
511 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
514 def fix_domain_netbios(self, s):
516 if not self.two_domains:
518 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
519 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
522 def fix_server_name(self, s):
524 if not self.two_domains or len(self.con.server_names) > 1:
526 for x in self.con.server_names:
527 res = res.upper().replace(x, "${SERVER_NAME}")
530 def __eq__(self, other):
531 if self.con.descriptor:
532 return self.cmp_desc(other)
533 return self.cmp_attrs(other)
535 def cmp_desc(self, other):
536 d1 = Descriptor(self.con, self.dn)
537 d2 = Descriptor(other.con, other.dn)
538 if self.con.view == "section":
540 elif self.con.view == "collision":
543 raise Exception("Unknown --view option value.")
545 self.screen_output = res[1][:-1]
546 other.screen_output = res[1][:-1]
550 def cmp_attrs(self, other):
552 self.unique_attrs = []
553 self.df_value_attrs = []
554 other.unique_attrs = []
555 if self.attributes.keys() != other.attributes.keys():
557 title = 4*" " + "Attributes found only in %s:" % self.con.host
558 for x in self.attributes.keys():
559 if not x in other.attributes.keys() and \
560 not x.upper() in [q.upper() for q in other.ignore_attributes]:
564 res += 8*" " + x + "\n"
565 self.unique_attrs.append(x)
567 title = 4*" " + "Attributes found only in %s:" % other.con.host
568 for x in other.attributes.keys():
569 if not x in self.attributes.keys() and \
570 not x.upper() in [q.upper() for q in self.ignore_attributes]:
574 res += 8*" " + x + "\n"
575 other.unique_attrs.append(x)
577 missing_attrs = [x.upper() for x in self.unique_attrs]
578 missing_attrs += [x.upper() for x in other.unique_attrs]
579 title = 4*" " + "Difference in attribute values:"
580 for x in self.attributes.keys():
581 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
583 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
584 self.attributes[x] = sorted(self.attributes[x])
585 other.attributes[x] = sorted(other.attributes[x])
586 if self.attributes[x] != other.attributes[x]:
591 # First check if the difference can be fixed but shunting the first part
592 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
593 if x.upper() in self.other_attributes:
594 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
595 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
598 # Attribute values that are list that contain DN based values that may differ
599 elif x.upper() in self.dn_attributes:
603 m = self.attributes[x]
604 n = other.attributes[x]
605 p = [self.fix_dn(j) for j in m]
606 q = [other.fix_dn(j) for j in n]
609 # Attributes that contain the Domain name in them
610 if x.upper() in self.domain_attributes:
614 m = self.attributes[x]
615 n = other.attributes[x]
616 p = [self.fix_domain_name(j) for j in m]
617 q = [other.fix_domain_name(j) for j in n]
621 if x.upper() in self.servername_attributes:
622 # Attributes with SERVER_NAME
626 m = self.attributes[x]
627 n = other.attributes[x]
628 p = [self.fix_server_name(j) for j in m]
629 q = [other.fix_server_name(j) for j in n]
633 if x.upper() in self.netbios_attributes:
634 # Attributes with NETBIOS Domain name
638 m = self.attributes[x]
639 n = other.attributes[x]
640 p = [self.fix_domain_netbios(j) for j in m]
641 q = [other.fix_domain_netbios(j) for j in n]
649 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
651 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
652 self.df_value_attrs.append(x)
654 if self.unique_attrs + other.unique_attrs != []:
655 assert self.unique_attrs != other.unique_attrs
656 self.summary["unique_attrs"] += self.unique_attrs
657 self.summary["df_value_attrs"] += self.df_value_attrs
658 other.summary["unique_attrs"] += other.unique_attrs
659 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
661 self.screen_output = res[:-1]
662 other.screen_output = res[:-1]
667 class LDAPBundel(object):
668 def __init__(self, connection, context, dn_list=None):
669 self.con = connection
670 self.two_domains = self.con.two_domains
671 self.quiet = self.con.quiet
672 self.verbose = self.con.verbose
673 self.search_base = self.con.search_base
674 self.search_scope = self.con.search_scope
676 self.summary["unique_attrs"] = []
677 self.summary["df_value_attrs"] = []
678 self.summary["known_ignored_dn"] = []
679 self.summary["abnormal_ignored_dn"] = []
681 self.dn_list = dn_list
682 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
683 self.context = context.upper()
684 self.dn_list = self.get_dn_list(context)
686 raise Exception("Unknown initialization data for LDAPBundel().")
688 while counter < len(self.dn_list) and self.two_domains:
689 # Use alias reference
690 tmp = self.dn_list[counter]
691 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
692 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
693 if len(self.con.server_names) == 1:
694 for x in self.con.server_names:
695 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
696 self.dn_list[counter] = tmp
698 self.dn_list = list(set(self.dn_list))
699 self.dn_list = sorted(self.dn_list)
700 self.size = len(self.dn_list)
704 Log on the screen if there is no --quiet oprion set
709 def update_size(self):
710 self.size = len(self.dn_list)
711 self.dn_list = sorted(self.dn_list)
713 def __eq__(self, other):
715 if self.size != other.size:
716 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
719 # This is the case where we want to explicitly compare two objects with different DNs.
720 # It does not matter if they are in the same DC, in two DC in one domain or in two
722 if self.search_scope != SCOPE_BASE:
723 title= "\n* DNs found only in %s:" % self.con.host
724 for x in self.dn_list:
725 if not x.upper() in [q.upper() for q in other.dn_list]:
730 self.log( 4*" " + x )
731 self.dn_list[self.dn_list.index(x)] = ""
732 self.dn_list = [x for x in self.dn_list if x]
734 title= "\n* DNs found only in %s:" % other.con.host
735 for x in other.dn_list:
736 if not x.upper() in [q.upper() for q in self.dn_list]:
741 self.log( 4*" " + x )
742 other.dn_list[other.dn_list.index(x)] = ""
743 other.dn_list = [x for x in other.dn_list if x]
747 assert self.size == other.size
748 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
749 self.log( "\n* Objects to be compared: %s" % self.size )
752 while index < self.size:
755 object1 = LDAPObject(connection=self.con,
756 dn=self.dn_list[index],
757 summary=self.summary)
758 except LdbError, (enum, estr):
759 if enum == ERR_NO_SUCH_OBJECT:
760 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
764 object2 = LDAPObject(connection=other.con,
765 dn=other.dn_list[index],
766 summary=other.summary)
767 except LdbError, (enum, estr):
768 if enum == ERR_NO_SUCH_OBJECT:
769 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
775 if object1 == object2:
777 self.log( "\nComparing:" )
778 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
779 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
780 self.log( 4*" " + "OK" )
782 self.log( "\nComparing:" )
783 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
784 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
785 self.log( object1.screen_output )
786 self.log( 4*" " + "FAILED" )
788 self.summary = object1.summary
789 other.summary = object2.summary
794 def get_dn_list(self, context):
795 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
796 Parse all DNs and filter those that are 'strange' or abnormal.
798 if context.upper() == "DOMAIN":
799 search_base = "%s" % self.con.base_dn
800 elif context.upper() == "CONFIGURATION":
801 search_base = "CN=Configuration,%s" % self.con.base_dn
802 elif context.upper() == "SCHEMA":
803 search_base = "CN=Schema,CN=Configuration,%s" % self.con.base_dn
806 if not self.search_base:
807 self.search_base = search_base
808 self.search_scope = self.search_scope.upper()
809 if self.search_scope == "SUB":
810 self.search_scope = SCOPE_SUBTREE
811 elif self.search_scope == "BASE":
812 self.search_scope = SCOPE_BASE
813 elif self.search_scope == "ONE":
814 self.search_scope = SCOPE_ONELEVEL
816 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
817 if not self.search_base.upper().endswith(search_base.upper()):
818 raise StandardError("Invalid search base specified: %s" % self.search_base)
819 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
821 dn_list.append(x["dn"].get_linearized())
827 def print_summary(self):
828 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
829 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
831 if self.summary["unique_attrs"]:
832 self.log( "\nAttributes found only in %s:" % self.con.host )
833 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
835 if self.summary["df_value_attrs"]:
836 self.log( "\nAttributes with different values:" )
837 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
838 self.summary["df_value_attrs"] = []
840 class cmd_ldapcmp(Command):
841 """compare two ldap databases"""
842 synopsis = "ldapcmp URL1 URL2 <domain|configuration|schema> [options]"
844 takes_optiongroups = {
845 "sambaopts": options.SambaOptions,
846 "versionopts": options.VersionOptions,
847 "credopts": options.CredentialsOptionsDouble,
850 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
853 Option("-w", "--two", dest="two", action="store_true", default=False,
854 help="Hosts are in two different domains"),
855 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
856 help="Do not print anything but relay on just exit code"),
857 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
858 help="Print all DN pairs that have been compared"),
859 Option("--sd", dest="descriptor", action="store_true", default=False,
860 help="Compare nTSecurityDescriptor attibutes only"),
861 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
862 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
863 Option("--view", dest="view", default="section",
864 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
865 Option("--base", dest="base", default="",
866 help="Pass search base that will build DN list for the first DC."),
867 Option("--base2", dest="base2", default="",
868 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
869 Option("--scope", dest="scope", default="SUB",
870 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
873 def run(self, URL1, URL2,
874 context1=None, context2=None, context3=None,
875 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False, view="section",
876 base="", base2="", scope="SUB", credopts=None, sambaopts=None, versionopts=None):
878 lp = sambaopts.get_loadparm()
880 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
883 creds = credopts.get_credentials(lp, fallback_machine=True)
886 creds2 = credopts.get_credentials2(lp, guess=False)
887 if creds2.is_anonymous():
890 creds2.set_domain("")
891 creds2.set_workstation("")
892 if using_ldap and not creds.authentication_requested():
893 raise CommandError("You must supply at least one username/password pair")
895 # make a list of contexts to compare in
899 # If search bases are specified context is defaulted to
900 # DOMAIN so the given search bases can be verified.
901 contexts = ["DOMAIN"]
903 # if no argument given, we compare all contexts
904 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
906 for c in [context1, context2, context3]:
909 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
910 raise CommandError("Incorrect argument: %s" % c)
911 contexts.append(c.upper())
913 if verbose and quiet:
914 raise CommandError("You cannot set --verbose and --quiet together")
915 if (not base and base2) or (base and not base2):
916 raise CommandError("You need to specify both --base and --base2 at the same time")
917 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
918 raise CommandError("Invalid --view value. Choose from: section or collision")
919 if not scope.upper() in ["SUB", "ONE", "BASE"]:
920 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
922 con1 = LDAPBase(URL1, creds, lp,
923 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
924 verbose=verbose,view=view, base=base, scope=scope)
925 assert len(con1.base_dn) > 0
927 con2 = LDAPBase(URL2, creds2, lp,
928 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
929 verbose=verbose, view=view, base=base2, scope=scope)
930 assert len(con2.base_dn) > 0
933 for context in contexts:
935 print "\n* Comparing [%s] context..." % context
937 b1 = LDAPBundel(con1, context=context)
938 b2 = LDAPBundel(con2, context=context)
942 print "\n* Result for [%s]: SUCCESS" % context
945 print "\n* Result for [%s]: FAILURE" % context
947 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
948 b2.summary["df_value_attrs"] = []
953 # mark exit status as FAILURE if a least one comparison failed
956 raise CommandError("Compare failed: %d" % status)