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
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/>.
32 import samba
.getopt
as options
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 (
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"):
55 if os
.path
.isfile(host
):
56 samdb_url
= "tdb://%s" % host
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
,
66 self
.search_base
= base
67 self
.search_scope
= scope
68 self
.two_domains
= two
70 self
.descriptor
= descriptor
71 self
.sort_aces
= sort_aces
73 self
.verbose
= verbose
75 self
.base_dn
= str(self
.ldb
.get_default_basedn())
76 self
.config_dn
= str(self
.ldb
.get_config_basedn())
77 self
.schema_dn
= str(self
.ldb
.get_schema_basedn())
78 self
.domain_netbios
= self
.find_netbios()
79 self
.server_names
= self
.find_servers()
80 self
.domain_name
= re
.sub("[Dd][Cc]=", "", self
.base_dn
).replace(",", ".")
81 self
.domain_sid
= self
.find_domain_sid()
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 print "\n* Place-holders for %s:" % self
.host
89 print 4*" " + "${DOMAIN_DN} => %s" % self
.base_dn
90 print 4*" " + "${DOMAIN_NETBIOS} => %s" % self
.domain_netbios
91 print 4*" " + "${SERVER_NAME} => %s" % self
.server_names
92 print 4*" " + "${DOMAIN_NAME} => %s" % self
.domain_name
94 def find_domain_sid(self
):
95 res
= self
.ldb
.search(base
=self
.base_dn
, expression
="(objectClass=*)", scope
=SCOPE_BASE
)
96 return ndr_unpack(security
.dom_sid
,res
[0]["objectSid"][0])
98 def find_servers(self
):
101 res
= self
.ldb
.search(base
="OU=Domain Controllers,%s" % self
.base_dn
, \
102 scope
=SCOPE_SUBTREE
, expression
="(objectClass=computer)", attrs
=["cn"])
106 srv
.append(x
["cn"][0])
109 def find_netbios(self
):
110 res
= self
.ldb
.search(base
="CN=Partitions,%s" % self
.config_dn
, \
111 scope
=SCOPE_SUBTREE
, attrs
=["nETBIOSName"])
114 if "nETBIOSName" in x
.keys():
115 return x
["nETBIOSName"][0]
117 def object_exists(self
, object_dn
):
120 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
)
121 except LdbError
, (enum
, estr
):
122 if enum
== ERR_NO_SUCH_OBJECT
:
127 def delete_force(self
, object_dn
):
129 self
.ldb
.delete(object_dn
)
130 except Ldb
.LdbError
, e
:
131 assert "No such object" in str(e
)
133 def get_attribute_name(self
, key
):
134 """ Returns the real attribute name
135 It resolved ranged results e.g. member;range=0-1499
138 r
= re
.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
146 def get_attribute_values(self
, object_dn
, key
, vals
):
147 """ Returns list with all attribute values
148 It resolved ranged results e.g. member;range=0-1499
151 r
= re
.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
155 # no range, just return the values
161 # get additional values in a loop
162 # until we get a response with '*' at the end
165 n
= "%s;range=%d-*" % (attr
, hi
+ 1)
166 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
, attrs
=[n
])
174 for key
in res
.keys():
180 if m
.group(1) != attr
:
184 fvals
= list(res
[key
])
191 if fm
.group(3) == "*":
192 # if we got "*" we're done
195 assert int(fm
.group(2)) == hi
+ 1
196 hi
= int(fm
.group(3))
200 def get_attributes(self
, object_dn
):
201 """ Returns dict with all default visible attributes
203 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
, attrs
=["*"])
206 # 'Dn' element is not iterable and we have it as 'distinguishedName'
208 for key
in res
.keys():
209 vals
= list(res
[key
])
211 name
= self
.get_attribute_name(key
)
212 res
[name
] = self
.get_attribute_values(object_dn
, key
, vals
)
216 def get_descriptor_sddl(self
, object_dn
):
217 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
, attrs
=["nTSecurityDescriptor"])
218 desc
= res
[0]["nTSecurityDescriptor"][0]
219 desc
= ndr_unpack(security
.descriptor
, desc
)
220 return desc
.as_sddl(self
.domain_sid
)
222 def guid_as_string(self
, guid_blob
):
223 """ Translate binary representation of schemaIDGUID to standard string representation.
224 @gid_blob: binary schemaIDGUID
226 blob
= "%s" % guid_blob
227 stops
= [4, 2, 2, 2, 6]
231 while x
< len(stops
):
235 c
= hex(ord(blob
[index
])).replace("0x", "")
236 c
= [None, "0" + c
, c
][len(c
)]
237 if 2 * index
< len(blob
):
245 assert index
== len(blob
)
246 return res
.strip().replace(" ", "-")
248 def get_guid_map(self
):
249 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
252 res
= self
.ldb
.search(base
=self
.schema_dn
,
253 expression
="(schemaIdGuid=*)", scope
=SCOPE_SUBTREE
, attrs
=["schemaIdGuid", "name"])
255 self
.guid_map
[self
.guid_as_string(item
["schemaIdGuid"]).lower()] = item
["name"][0]
257 res
= self
.ldb
.search(base
="cn=extended-rights,%s" % self
.config_dn
,
258 expression
="(rightsGuid=*)", scope
=SCOPE_SUBTREE
, attrs
=["rightsGuid", "name"])
260 self
.guid_map
[str(item
["rightsGuid"]).lower()] = item
["name"][0]
262 def get_sid_map(self
):
263 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
266 res
= self
.ldb
.search(base
=self
.base_dn
,
267 expression
="(objectSid=*)", scope
=SCOPE_SUBTREE
, attrs
=["objectSid", "sAMAccountName"])
270 self
.sid_map
["%s" % ndr_unpack(security
.dom_sid
, item
["objectSid"][0])] = item
["sAMAccountName"][0]
274 class Descriptor(object):
275 def __init__(self
, connection
, dn
):
276 self
.con
= connection
278 self
.sddl
= self
.con
.get_descriptor_sddl(self
.dn
)
279 self
.dacl_list
= self
.extract_dacl()
280 if self
.con
.sort_aces
:
281 self
.dacl_list
.sort()
283 def extract_dacl(self
):
284 """ Extracts the DACL as a list of ACE string (with the brakets).
287 if "S:" in self
.sddl
:
288 res
= re
.search("D:(.*?)(\(.*?\))S:", self
.sddl
).group(2)
290 res
= re
.search("D:(.*?)(\(.*\))", self
.sddl
).group(2)
291 except AttributeError:
293 return re
.findall("(\(.*?\))", res
)
295 def fix_guid(self
, ace
):
297 guids
= re
.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res
)
298 # If there are not GUIDs to replace return the same ACE
303 name
= self
.con
.guid_map
[guid
.lower()]
304 res
= res
.replace(guid
, name
)
306 # Do not bother if the GUID is not found in
307 # cn=Schema or cn=Extended-Rights
311 def fix_sid(self
, ace
):
313 sids
= re
.findall("S-[-0-9]+", res
)
314 # If there are not SIDs to replace return the same ACE
319 name
= self
.con
.sid_map
[sid
]
320 res
= res
.replace(sid
, name
)
322 # Do not bother if the SID is not found in baseDN
326 def fixit(self
, ace
):
327 """ Combine all replacement methods in one
330 res
= self
.fix_guid(res
)
331 res
= self
.fix_sid(res
)
334 def diff_1(self
, other
):
336 if len(self
.dacl_list
) != len(other
.dacl_list
):
337 res
+= 4*" " + "Difference in ACE count:\n"
338 res
+= 8*" " + "=> %s\n" % len(self
.dacl_list
)
339 res
+= 8*" " + "=> %s\n" % len(other
.dacl_list
)
347 self_ace
= "%s" % self
.dacl_list
[i
]
352 other_ace
= "%s" % other
.dacl_list
[i
]
355 if len(self_ace
) + len(other_ace
) == 0:
357 self_ace_fixed
= "%s" % self
.fixit(self_ace
)
358 other_ace_fixed
= "%s" % other
.fixit(other_ace
)
359 if self_ace_fixed
!= other_ace_fixed
:
360 res
+= "%60s * %s\n" % ( self_ace_fixed
, other_ace_fixed
)
363 res
+= "%60s | %s\n" % ( self_ace_fixed
, other_ace_fixed
)
367 def diff_2(self
, other
):
369 if len(self
.dacl_list
) != len(other
.dacl_list
):
370 res
+= 4*" " + "Difference in ACE count:\n"
371 res
+= 8*" " + "=> %s\n" % len(self
.dacl_list
)
372 res
+= 8*" " + "=> %s\n" % len(other
.dacl_list
)
377 self_dacl_list_fixed
= []
378 other_dacl_list_fixed
= []
379 [self_dacl_list_fixed
.append( self
.fixit(ace
) ) for ace
in self
.dacl_list
]
380 [other_dacl_list_fixed
.append( other
.fixit(ace
) ) for ace
in other
.dacl_list
]
381 for ace
in self_dacl_list_fixed
:
383 other_dacl_list_fixed
.index(ace
)
385 self_aces
.append(ace
)
387 common_aces
.append(ace
)
388 self_aces
= sorted(self_aces
)
389 if len(self_aces
) > 0:
390 res
+= 4*" " + "ACEs found only in %s:\n" % self
.con
.host
391 for ace
in self_aces
:
392 res
+= 8*" " + ace
+ "\n"
394 for ace
in other_dacl_list_fixed
:
396 self_dacl_list_fixed
.index(ace
)
398 other_aces
.append(ace
)
400 common_aces
.append(ace
)
401 other_aces
= sorted(other_aces
)
402 if len(other_aces
) > 0:
403 res
+= 4*" " + "ACEs found only in %s:\n" % other
.con
.host
404 for ace
in other_aces
:
405 res
+= 8*" " + ace
+ "\n"
407 common_aces
= sorted(list(set(common_aces
)))
409 res
+= 4*" " + "ACEs found in both:\n"
410 for ace
in common_aces
:
411 res
+= 8*" " + ace
+ "\n"
412 return (self_aces
== [] and other_aces
== [], res
)
414 class LDAPObject(object):
415 def __init__(self
, connection
, dn
, summary
, filter_list
):
416 self
.con
= connection
417 self
.two_domains
= self
.con
.two_domains
418 self
.quiet
= self
.con
.quiet
419 self
.verbose
= self
.con
.verbose
420 self
.summary
= summary
421 self
.dn
= dn
.replace("${DOMAIN_DN}", self
.con
.base_dn
)
422 self
.dn
= self
.dn
.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self
.con
.domain_netbios
)
423 for x
in self
.con
.server_names
:
424 self
.dn
= self
.dn
.replace("CN=${SERVER_NAME}", "CN=%s" % x
)
425 self
.attributes
= self
.con
.get_attributes(self
.dn
)
426 # Attributes that are considered always to be different e.g based on timestamp etc.
428 # One domain - two domain controllers
429 self
.ignore_attributes
= [
430 # Default Naming Context
431 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
432 "operatingSystemVersion","oEMInformation",
433 # Configuration Naming Context
434 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
435 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
436 # Schema Naming Context
439 self
.ignore_attributes
+= filter_list
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, filter_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"] = []
680 self
.filter_list
= filter_list
682 self
.dn_list
= dn_list
683 elif context
.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
684 self
.context
= context
.upper()
685 self
.dn_list
= self
.get_dn_list(context
)
687 raise Exception("Unknown initialization data for LDAPBundel().")
689 while counter
< len(self
.dn_list
) and self
.two_domains
:
690 # Use alias reference
691 tmp
= self
.dn_list
[counter
]
692 tmp
= tmp
[:len(tmp
)-len(self
.con
.base_dn
)] + "${DOMAIN_DN}"
693 tmp
= tmp
.replace("CN=%s" % self
.con
.domain_netbios
, "CN=${DOMAIN_NETBIOS}")
694 if len(self
.con
.server_names
) == 1:
695 for x
in self
.con
.server_names
:
696 tmp
= tmp
.replace("CN=%s" % x
, "CN=${SERVER_NAME}")
697 self
.dn_list
[counter
] = tmp
699 self
.dn_list
= list(set(self
.dn_list
))
700 self
.dn_list
= sorted(self
.dn_list
)
701 self
.size
= len(self
.dn_list
)
705 Log on the screen if there is no --quiet oprion set
710 def update_size(self
):
711 self
.size
= len(self
.dn_list
)
712 self
.dn_list
= sorted(self
.dn_list
)
714 def __eq__(self
, other
):
716 if self
.size
!= other
.size
:
717 self
.log( "\n* DN lists have different size: %s != %s" % (self
.size
, other
.size
) )
720 # This is the case where we want to explicitly compare two objects with different DNs.
721 # It does not matter if they are in the same DC, in two DC in one domain or in two
723 if self
.search_scope
!= SCOPE_BASE
:
724 title
= "\n* DNs found only in %s:" % self
.con
.host
725 for x
in self
.dn_list
:
726 if not x
.upper() in [q
.upper() for q
in other
.dn_list
]:
731 self
.log( 4*" " + x
)
732 self
.dn_list
[self
.dn_list
.index(x
)] = ""
733 self
.dn_list
= [x
for x
in self
.dn_list
if x
]
735 title
= "\n* DNs found only in %s:" % other
.con
.host
736 for x
in other
.dn_list
:
737 if not x
.upper() in [q
.upper() for q
in self
.dn_list
]:
742 self
.log( 4*" " + x
)
743 other
.dn_list
[other
.dn_list
.index(x
)] = ""
744 other
.dn_list
= [x
for x
in other
.dn_list
if x
]
748 assert self
.size
== other
.size
749 assert sorted([x
.upper() for x
in self
.dn_list
]) == sorted([x
.upper() for x
in other
.dn_list
])
750 self
.log( "\n* Objects to be compared: %s" % self
.size
)
753 while index
< self
.size
:
756 object1
= LDAPObject(connection
=self
.con
,
757 dn
=self
.dn_list
[index
],
758 summary
=self
.summary
,
759 filter_list
=self
.filter_list
)
760 except LdbError
, (enum
, estr
):
761 if enum
== ERR_NO_SUCH_OBJECT
:
762 self
.log( "\n!!! Object not found: %s" % self
.dn_list
[index
] )
766 object2
= LDAPObject(connection
=other
.con
,
767 dn
=other
.dn_list
[index
],
768 summary
=other
.summary
,
769 filter_list
=self
.filter_list
)
770 except LdbError
, (enum
, estr
):
771 if enum
== ERR_NO_SUCH_OBJECT
:
772 self
.log( "\n!!! Object not found: %s" % other
.dn_list
[index
] )
778 if object1
== object2
:
780 self
.log( "\nComparing:" )
781 self
.log( "'%s' [%s]" % (object1
.dn
, object1
.con
.host
) )
782 self
.log( "'%s' [%s]" % (object2
.dn
, object2
.con
.host
) )
783 self
.log( 4*" " + "OK" )
785 self
.log( "\nComparing:" )
786 self
.log( "'%s' [%s]" % (object1
.dn
, object1
.con
.host
) )
787 self
.log( "'%s' [%s]" % (object2
.dn
, object2
.con
.host
) )
788 self
.log( object1
.screen_output
)
789 self
.log( 4*" " + "FAILED" )
791 self
.summary
= object1
.summary
792 other
.summary
= object2
.summary
797 def get_dn_list(self
, context
):
798 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
799 Parse all DNs and filter those that are 'strange' or abnormal.
801 if context
.upper() == "DOMAIN":
802 search_base
= self
.con
.base_dn
803 elif context
.upper() == "CONFIGURATION":
804 search_base
= self
.con
.config_dn
805 elif context
.upper() == "SCHEMA":
806 search_base
= self
.con
.schema_dn
809 if not self
.search_base
:
810 self
.search_base
= search_base
811 self
.search_scope
= self
.search_scope
.upper()
812 if self
.search_scope
== "SUB":
813 self
.search_scope
= SCOPE_SUBTREE
814 elif self
.search_scope
== "BASE":
815 self
.search_scope
= SCOPE_BASE
816 elif self
.search_scope
== "ONE":
817 self
.search_scope
= SCOPE_ONELEVEL
819 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
820 if not self
.search_base
.upper().endswith(search_base
.upper()):
821 raise StandardError("Invalid search base specified: %s" % self
.search_base
)
822 res
= self
.con
.ldb
.search(base
=self
.search_base
, scope
=self
.search_scope
, attrs
=["dn"])
824 dn_list
.append(x
["dn"].get_linearized())
830 def print_summary(self
):
831 self
.summary
["unique_attrs"] = list(set(self
.summary
["unique_attrs"]))
832 self
.summary
["df_value_attrs"] = list(set(self
.summary
["df_value_attrs"]))
834 if self
.summary
["unique_attrs"]:
835 self
.log( "\nAttributes found only in %s:" % self
.con
.host
)
836 self
.log( "".join([str("\n" + 4*" " + x
) for x
in self
.summary
["unique_attrs"]]) )
838 if self
.summary
["df_value_attrs"]:
839 self
.log( "\nAttributes with different values:" )
840 self
.log( "".join([str("\n" + 4*" " + x
) for x
in self
.summary
["df_value_attrs"]]) )
841 self
.summary
["df_value_attrs"] = []
843 class cmd_ldapcmp(Command
):
844 """compare two ldap databases"""
845 synopsis
= "ldapcmp URL1 URL2 <domain|configuration|schema> [options]"
847 takes_optiongroups
= {
848 "sambaopts": options
.SambaOptions
,
849 "versionopts": options
.VersionOptions
,
850 "credopts": options
.CredentialsOptionsDouble
,
853 takes_args
= ["URL1", "URL2", "context1?", "context2?", "context3?"]
856 Option("-w", "--two", dest
="two", action
="store_true", default
=False,
857 help="Hosts are in two different domains"),
858 Option("-q", "--quiet", dest
="quiet", action
="store_true", default
=False,
859 help="Do not print anything but relay on just exit code"),
860 Option("-v", "--verbose", dest
="verbose", action
="store_true", default
=False,
861 help="Print all DN pairs that have been compared"),
862 Option("--sd", dest
="descriptor", action
="store_true", default
=False,
863 help="Compare nTSecurityDescriptor attibutes only"),
864 Option("--sort-aces", dest
="sort_aces", action
="store_true", default
=False,
865 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
866 Option("--view", dest
="view", default
="section",
867 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
868 Option("--base", dest
="base", default
="",
869 help="Pass search base that will build DN list for the first DC."),
870 Option("--base2", dest
="base2", default
="",
871 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
872 Option("--scope", dest
="scope", default
="SUB",
873 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
874 Option("--filter", dest
="filter", default
="",
875 help="List of comma separated attributes to ignore in the comparision"),
878 def run(self
, URL1
, URL2
,
879 context1
=None, context2
=None, context3
=None,
880 two
=False, quiet
=False, verbose
=False, descriptor
=False, sort_aces
=False,
881 view
="section", base
="", base2
="", scope
="SUB", filter="",
882 credopts
=None, sambaopts
=None, versionopts
=None):
884 lp
= sambaopts
.get_loadparm()
886 using_ldap
= URL1
.startswith("ldap") or URL2
.startswith("ldap")
889 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
892 creds2
= credopts
.get_credentials2(lp
, guess
=False)
893 if creds2
.is_anonymous():
896 creds2
.set_domain("")
897 creds2
.set_workstation("")
898 if using_ldap
and not creds
.authentication_requested():
899 raise CommandError("You must supply at least one username/password pair")
901 # make a list of contexts to compare in
905 # If search bases are specified context is defaulted to
906 # DOMAIN so the given search bases can be verified.
907 contexts
= ["DOMAIN"]
909 # if no argument given, we compare all contexts
910 contexts
= ["DOMAIN", "CONFIGURATION", "SCHEMA"]
912 for c
in [context1
, context2
, context3
]:
915 if not c
.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
916 raise CommandError("Incorrect argument: %s" % c
)
917 contexts
.append(c
.upper())
919 if verbose
and quiet
:
920 raise CommandError("You cannot set --verbose and --quiet together")
921 if (not base
and base2
) or (base
and not base2
):
922 raise CommandError("You need to specify both --base and --base2 at the same time")
923 if descriptor
and view
.upper() not in ["SECTION", "COLLISION"]:
924 raise CommandError("Invalid --view value. Choose from: section or collision")
925 if not scope
.upper() in ["SUB", "ONE", "BASE"]:
926 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
928 con1
= LDAPBase(URL1
, creds
, lp
,
929 two
=two
, quiet
=quiet
, descriptor
=descriptor
, sort_aces
=sort_aces
,
930 verbose
=verbose
,view
=view
, base
=base
, scope
=scope
)
931 assert len(con1
.base_dn
) > 0
933 con2
= LDAPBase(URL2
, creds2
, lp
,
934 two
=two
, quiet
=quiet
, descriptor
=descriptor
, sort_aces
=sort_aces
,
935 verbose
=verbose
, view
=view
, base
=base2
, scope
=scope
)
936 assert len(con2
.base_dn
) > 0
938 filter_list
= filter.split(",")
941 for context
in contexts
:
943 print "\n* Comparing [%s] context..." % context
945 b1
= LDAPBundel(con1
, context
=context
, filter_list
=filter_list
)
946 b2
= LDAPBundel(con2
, context
=context
, filter_list
=filter_list
)
950 print "\n* Result for [%s]: SUCCESS" % context
953 print "\n* Result for [%s]: FAILURE" % context
955 assert len(b1
.summary
["df_value_attrs"]) == len(b2
.summary
["df_value_attrs"])
956 b2
.summary
["df_value_attrs"] = []
961 # mark exit status as FAILURE if a least one comparison failed
964 raise CommandError("Compare failed: %d" % status
)