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_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 (
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):
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"]
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
.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()
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" %
91 self
.outf
.write(4*" " + "${DOMAIN_NETBIOS} => %s\n" %
93 self
.outf
.write(4*" " + "${SERVER_NAME} => %s\n" %
95 self
.outf
.write(4*" " + "${DOMAIN_NAME} => %s\n" %
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"])
110 srv
.append(x
["cn"][0])
113 def find_netbios(self
):
114 res
= self
.ldb
.search(base
="CN=Partitions,%s" % self
.config_dn
,
115 scope
=SCOPE_SUBTREE
, attrs
=["nETBIOSName"])
118 if "nETBIOSName" in x
.keys():
119 return x
["nETBIOSName"][0]
121 def object_exists(self
, object_dn
):
124 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
)
125 except LdbError
, (enum
, estr
):
126 if enum
== ERR_NO_SUCH_OBJECT
:
131 def delete_force(self
, object_dn
):
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+|\*)$")
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+|\*)$")
159 # no range, just return the values
165 # get additional values in a loop
166 # until we get a response with '*' at the end
169 n
= "%s;range=%d-*" % (attr
, hi
+ 1)
170 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
, attrs
=[n
])
178 for key
in res
.keys():
184 if m
.group(1) != attr
:
188 fvals
= list(res
[key
])
195 if fm
.group(3) == "*":
196 # if we got "*" we're done
199 assert int(fm
.group(2)) == hi
+ 1
200 hi
= int(fm
.group(3))
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
=["*"])
210 # 'Dn' element is not iterable and we have it as 'distinguishedName'
212 for key
in res
.keys():
213 vals
= list(res
[key
])
215 name
= self
.get_attribute_name(key
)
216 res
[name
] = self
.get_attribute_values(object_dn
, key
, vals
)
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]
235 while x
< len(stops
):
239 c
= hex(ord(blob
[index
])).replace("0x", "")
240 c
= [None, "0" + c
, c
][len(c
)]
241 if 2 * index
< len(blob
):
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.
256 res
= self
.ldb
.search(base
=self
.base_dn
,
257 expression
="(objectSid=*)", scope
=SCOPE_SUBTREE
, attrs
=["objectSid", "sAMAccountName"])
260 self
.sid_map
["%s" % ndr_unpack(security
.dom_sid
, item
["objectSid"][0])] = item
["sAMAccountName"][0]
264 class Descriptor(object):
265 def __init__(self
, connection
, dn
, outf
=sys
.stdout
, errf
=sys
.stderr
):
268 self
.con
= connection
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).
279 if "S:" in self
.sddl
:
280 res
= re
.search("D:(.*?)(\(.*?\))S:", self
.sddl
).group(2)
282 res
= re
.search("D:(.*?)(\(.*\))", self
.sddl
).group(2)
283 except AttributeError:
285 return re
.findall("(\(.*?\))", res
)
287 def fix_sid(self
, ace
):
289 sids
= re
.findall("S-[-0-9]+", res
)
290 # If there are not SIDs to replace return the same ACE
295 name
= self
.con
.sid_map
[sid
]
296 res
= res
.replace(sid
, name
)
298 # Do not bother if the SID is not found in baseDN
302 def diff_1(self
, other
):
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
)
315 self_ace
= "%s" % self
.dacl_list
[i
]
320 other_ace
= "%s" % other
.dacl_list
[i
]
323 if len(self_ace
) + len(other_ace
) == 0:
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
)
331 res
+= "%60s | %s\n" % ( self_ace_fixed
, other_ace_fixed
)
335 def diff_2(self
, other
):
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
)
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
:
351 other_dacl_list_fixed
.index(ace
)
353 self_aces
.append(ace
)
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
:
364 self_dacl_list_fixed
.index(ace
)
366 other_aces
.append(ace
)
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
)))
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
):
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 # Attributes that are considered always to be different e.g based on timestamp etc.
399 # One domain - two domain controllers
400 self
.ignore_attributes
= [
401 # Default Naming Context
402 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
403 "operatingSystemVersion","oEMInformation",
404 "ridNextRID", "rIDPreviousAllocationPool",
405 # Configuration Naming Context
406 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
407 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
408 # Schema Naming Context
411 self
.ignore_attributes
+= filter_list
413 self
.dn_attributes
= []
414 self
.domain_attributes
= []
415 self
.servername_attributes
= []
416 self
.netbios_attributes
= []
417 self
.other_attributes
= []
418 # Two domains - two domain controllers
421 self
.ignore_attributes
+= [
422 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
423 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
424 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
425 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
426 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
427 # After Exchange preps
428 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
430 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
431 self
.dn_attributes
= [
432 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
433 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
434 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
435 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
436 # After Exchange preps
437 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
438 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
439 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
440 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
441 self
.dn_attributes
= [x
.upper() for x
in self
.dn_attributes
]
443 # Attributes that contain the Domain name e.g. 'samba.org'
444 self
.domain_attributes
= [
445 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
446 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
447 self
.domain_attributes
= [x
.upper() for x
in self
.domain_attributes
]
449 # May contain DOMAIN_NETBIOS and SERVER_NAME
450 self
.servername_attributes
= [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
451 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
452 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
453 self
.servername_attributes
= [x
.upper() for x
in self
.servername_attributes
]
455 self
.netbios_attributes
= [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
456 self
.netbios_attributes
= [x
.upper() for x
in self
.netbios_attributes
]
458 self
.other_attributes
= [ "name", "DC",]
459 self
.other_attributes
= [x
.upper() for x
in self
.other_attributes
]
461 self
.ignore_attributes
= [x
.upper() for x
in self
.ignore_attributes
]
465 Log on the screen if there is no --quiet option set
468 self
.outf
.write(msg
+"\n")
472 if not self
.two_domains
:
474 if res
.upper().endswith(self
.con
.base_dn
.upper()):
475 res
= res
[:len(res
)-len(self
.con
.base_dn
)] + "${DOMAIN_DN}"
478 def fix_domain_name(self
, s
):
480 if not self
.two_domains
:
482 res
= res
.replace(self
.con
.domain_name
.lower(), self
.con
.domain_name
.upper())
483 res
= res
.replace(self
.con
.domain_name
.upper(), "${DOMAIN_NAME}")
486 def fix_domain_netbios(self
, s
):
488 if not self
.two_domains
:
490 res
= res
.replace(self
.con
.domain_netbios
.lower(), self
.con
.domain_netbios
.upper())
491 res
= res
.replace(self
.con
.domain_netbios
.upper(), "${DOMAIN_NETBIOS}")
494 def fix_server_name(self
, s
):
496 if not self
.two_domains
or len(self
.con
.server_names
) > 1:
498 for x
in self
.con
.server_names
:
499 res
= res
.upper().replace(x
, "${SERVER_NAME}")
502 def __eq__(self
, other
):
503 if self
.con
.descriptor
:
504 return self
.cmp_desc(other
)
505 return self
.cmp_attrs(other
)
507 def cmp_desc(self
, other
):
508 d1
= Descriptor(self
.con
, self
.dn
, outf
=self
.outf
, errf
=self
.errf
)
509 d2
= Descriptor(other
.con
, other
.dn
, outf
=self
.outf
, errf
=self
.errf
)
510 if self
.con
.view
== "section":
512 elif self
.con
.view
== "collision":
515 raise Exception("Unknown --view option value.")
517 self
.screen_output
= res
[1][:-1]
518 other
.screen_output
= res
[1][:-1]
522 def cmp_attrs(self
, other
):
524 self
.unique_attrs
= []
525 self
.df_value_attrs
= []
526 other
.unique_attrs
= []
527 if self
.attributes
.keys() != other
.attributes
.keys():
529 title
= 4*" " + "Attributes found only in %s:" % self
.con
.host
530 for x
in self
.attributes
.keys():
531 if not x
in other
.attributes
.keys() and \
532 not x
.upper() in [q
.upper() for q
in other
.ignore_attributes
]:
536 res
+= 8*" " + x
+ "\n"
537 self
.unique_attrs
.append(x
)
539 title
= 4*" " + "Attributes found only in %s:" % other
.con
.host
540 for x
in other
.attributes
.keys():
541 if not x
in self
.attributes
.keys() and \
542 not x
.upper() in [q
.upper() for q
in self
.ignore_attributes
]:
546 res
+= 8*" " + x
+ "\n"
547 other
.unique_attrs
.append(x
)
549 missing_attrs
= [x
.upper() for x
in self
.unique_attrs
]
550 missing_attrs
+= [x
.upper() for x
in other
.unique_attrs
]
551 title
= 4*" " + "Difference in attribute values:"
552 for x
in self
.attributes
.keys():
553 if x
.upper() in self
.ignore_attributes
or x
.upper() in missing_attrs
:
555 if isinstance(self
.attributes
[x
], list) and isinstance(other
.attributes
[x
], list):
556 self
.attributes
[x
] = sorted(self
.attributes
[x
])
557 other
.attributes
[x
] = sorted(other
.attributes
[x
])
558 if self
.attributes
[x
] != other
.attributes
[x
]:
563 # First check if the difference can be fixed but shunting the first part
564 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
565 if x
.upper() in self
.other_attributes
:
566 p
= [self
.con
.domain_name
.split(".")[0] == j
for j
in self
.attributes
[x
]]
567 q
= [other
.con
.domain_name
.split(".")[0] == j
for j
in other
.attributes
[x
]]
570 # Attribute values that are list that contain DN based values that may differ
571 elif x
.upper() in self
.dn_attributes
:
575 m
= self
.attributes
[x
]
576 n
= other
.attributes
[x
]
577 p
= [self
.fix_dn(j
) for j
in m
]
578 q
= [other
.fix_dn(j
) for j
in n
]
581 # Attributes that contain the Domain name in them
582 if x
.upper() in self
.domain_attributes
:
586 m
= self
.attributes
[x
]
587 n
= other
.attributes
[x
]
588 p
= [self
.fix_domain_name(j
) for j
in m
]
589 q
= [other
.fix_domain_name(j
) for j
in n
]
593 if x
.upper() in self
.servername_attributes
:
594 # Attributes with SERVER_NAME
598 m
= self
.attributes
[x
]
599 n
= other
.attributes
[x
]
600 p
= [self
.fix_server_name(j
) for j
in m
]
601 q
= [other
.fix_server_name(j
) for j
in n
]
605 if x
.upper() in self
.netbios_attributes
:
606 # Attributes with NETBIOS Domain name
610 m
= self
.attributes
[x
]
611 n
= other
.attributes
[x
]
612 p
= [self
.fix_domain_netbios(j
) for j
in m
]
613 q
= [other
.fix_domain_netbios(j
) for j
in n
]
621 res
+= 8*" " + x
+ " => \n%s\n%s" % (p
, q
) + "\n"
623 res
+= 8*" " + x
+ " => \n%s\n%s" % (self
.attributes
[x
], other
.attributes
[x
]) + "\n"
624 self
.df_value_attrs
.append(x
)
626 if self
.unique_attrs
+ other
.unique_attrs
!= []:
627 assert self
.unique_attrs
!= other
.unique_attrs
628 self
.summary
["unique_attrs"] += self
.unique_attrs
629 self
.summary
["df_value_attrs"] += self
.df_value_attrs
630 other
.summary
["unique_attrs"] += other
.unique_attrs
631 other
.summary
["df_value_attrs"] += self
.df_value_attrs
# they are the same
633 self
.screen_output
= res
[:-1]
634 other
.screen_output
= res
[:-1]
639 class LDAPBundel(object):
641 def __init__(self
, connection
, context
, dn_list
=None, filter_list
=None,
642 outf
=sys
.stdout
, errf
=sys
.stderr
):
645 self
.con
= connection
646 self
.two_domains
= self
.con
.two_domains
647 self
.quiet
= self
.con
.quiet
648 self
.verbose
= self
.con
.verbose
649 self
.search_base
= self
.con
.search_base
650 self
.search_scope
= self
.con
.search_scope
651 self
.skip_missing_dn
= self
.con
.skip_missing_dn
653 self
.summary
["unique_attrs"] = []
654 self
.summary
["df_value_attrs"] = []
655 self
.summary
["known_ignored_dn"] = []
656 self
.summary
["abnormal_ignored_dn"] = []
657 self
.filter_list
= filter_list
659 self
.dn_list
= dn_list
660 elif context
.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
661 self
.context
= context
.upper()
662 self
.dn_list
= self
.get_dn_list(context
)
664 raise Exception("Unknown initialization data for LDAPBundel().")
666 while counter
< len(self
.dn_list
) and self
.two_domains
:
667 # Use alias reference
668 tmp
= self
.dn_list
[counter
]
669 tmp
= tmp
[:len(tmp
)-len(self
.con
.base_dn
)] + "${DOMAIN_DN}"
670 tmp
= tmp
.replace("CN=%s" % self
.con
.domain_netbios
, "CN=${DOMAIN_NETBIOS}")
671 if len(self
.con
.server_names
) == 1:
672 for x
in self
.con
.server_names
:
673 tmp
= tmp
.replace("CN=%s" % x
, "CN=${SERVER_NAME}")
674 self
.dn_list
[counter
] = tmp
676 self
.dn_list
= list(set(self
.dn_list
))
677 self
.dn_list
= sorted(self
.dn_list
)
678 self
.size
= len(self
.dn_list
)
682 Log on the screen if there is no --quiet option set
685 self
.outf
.write(msg
+"\n")
687 def update_size(self
):
688 self
.size
= len(self
.dn_list
)
689 self
.dn_list
= sorted(self
.dn_list
)
691 def __eq__(self
, other
):
693 if self
.size
!= other
.size
:
694 self
.log( "\n* DN lists have different size: %s != %s" % (self
.size
, other
.size
) )
695 if not self
.skip_missing_dn
:
698 # This is the case where we want to explicitly compare two objects with different DNs.
699 # It does not matter if they are in the same DC, in two DC in one domain or in two
701 if self
.search_scope
!= SCOPE_BASE
:
702 title
= "\n* DNs found only in %s:" % self
.con
.host
703 for x
in self
.dn_list
:
704 if not x
.upper() in [q
.upper() for q
in other
.dn_list
]:
705 if title
and not self
.skip_missing_dn
:
709 self
.log( 4*" " + x
)
710 self
.dn_list
[self
.dn_list
.index(x
)] = ""
711 self
.dn_list
= [x
for x
in self
.dn_list
if x
]
713 title
= "\n* DNs found only in %s:" % other
.con
.host
714 for x
in other
.dn_list
:
715 if not x
.upper() in [q
.upper() for q
in self
.dn_list
]:
716 if title
and not self
.skip_missing_dn
:
720 self
.log( 4*" " + x
)
721 other
.dn_list
[other
.dn_list
.index(x
)] = ""
722 other
.dn_list
= [x
for x
in other
.dn_list
if x
]
726 assert self
.size
== other
.size
727 assert sorted([x
.upper() for x
in self
.dn_list
]) == sorted([x
.upper() for x
in other
.dn_list
])
728 self
.log( "\n* Objects to be compared: %s" % self
.size
)
731 while index
< self
.size
:
734 object1
= LDAPObject(connection
=self
.con
,
735 dn
=self
.dn_list
[index
],
736 summary
=self
.summary
,
737 filter_list
=self
.filter_list
,
738 outf
=self
.outf
, errf
=self
.errf
)
739 except LdbError
, (enum
, estr
):
740 if enum
== ERR_NO_SUCH_OBJECT
:
741 self
.log( "\n!!! Object not found: %s" % self
.dn_list
[index
] )
745 object2
= LDAPObject(connection
=other
.con
,
746 dn
=other
.dn_list
[index
],
747 summary
=other
.summary
,
748 filter_list
=self
.filter_list
,
749 outf
=self
.outf
, errf
=self
.errf
)
750 except LdbError
, (enum
, estr
):
751 if enum
== ERR_NO_SUCH_OBJECT
:
752 self
.log( "\n!!! Object not found: %s" % other
.dn_list
[index
] )
758 if object1
== object2
:
760 self
.log( "\nComparing:" )
761 self
.log( "'%s' [%s]" % (object1
.dn
, object1
.con
.host
) )
762 self
.log( "'%s' [%s]" % (object2
.dn
, object2
.con
.host
) )
763 self
.log( 4*" " + "OK" )
765 self
.log( "\nComparing:" )
766 self
.log( "'%s' [%s]" % (object1
.dn
, object1
.con
.host
) )
767 self
.log( "'%s' [%s]" % (object2
.dn
, object2
.con
.host
) )
768 self
.log( object1
.screen_output
)
769 self
.log( 4*" " + "FAILED" )
771 self
.summary
= object1
.summary
772 other
.summary
= object2
.summary
777 def get_dn_list(self
, context
):
778 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
779 Parse all DNs and filter those that are 'strange' or abnormal.
781 if context
.upper() == "DOMAIN":
782 search_base
= self
.con
.base_dn
783 elif context
.upper() == "CONFIGURATION":
784 search_base
= self
.con
.config_dn
785 elif context
.upper() == "SCHEMA":
786 search_base
= self
.con
.schema_dn
787 elif context
.upper() == "DNSDOMAIN":
788 search_base
= "DC=DomainDnsZones,%s" % self
.con
.base_dn
789 elif context
.upper() == "DNSFOREST":
790 search_base
= "DC=ForestDnsZones,%s" % self
.con
.root_dn
793 if not self
.search_base
:
794 self
.search_base
= search_base
795 self
.search_scope
= self
.search_scope
.upper()
796 if self
.search_scope
== "SUB":
797 self
.search_scope
= SCOPE_SUBTREE
798 elif self
.search_scope
== "BASE":
799 self
.search_scope
= SCOPE_BASE
800 elif self
.search_scope
== "ONE":
801 self
.search_scope
= SCOPE_ONELEVEL
803 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
805 res
= self
.con
.ldb
.search(base
=self
.search_base
, scope
=self
.search_scope
, attrs
=["dn"])
806 except LdbError
, (enum
, estr
):
807 self
.outf
.write("Failed search of base=%s\n" % self
.search_base
)
810 dn_list
.append(x
["dn"].get_linearized())
816 def print_summary(self
):
817 self
.summary
["unique_attrs"] = list(set(self
.summary
["unique_attrs"]))
818 self
.summary
["df_value_attrs"] = list(set(self
.summary
["df_value_attrs"]))
820 if self
.summary
["unique_attrs"]:
821 self
.log( "\nAttributes found only in %s:" % self
.con
.host
)
822 self
.log( "".join([str("\n" + 4*" " + x
) for x
in self
.summary
["unique_attrs"]]) )
824 if self
.summary
["df_value_attrs"]:
825 self
.log( "\nAttributes with different values:" )
826 self
.log( "".join([str("\n" + 4*" " + x
) for x
in self
.summary
["df_value_attrs"]]) )
827 self
.summary
["df_value_attrs"] = []
830 class cmd_ldapcmp(Command
):
831 """Compare two ldap databases."""
832 synopsis
= "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
834 takes_optiongroups
= {
835 "sambaopts": options
.SambaOptions
,
836 "versionopts": options
.VersionOptions
,
837 "credopts": options
.CredentialsOptionsDouble
,
840 takes_optiongroups
= {
841 "sambaopts": options
.SambaOptions
,
842 "versionopts": options
.VersionOptions
,
843 "credopts": options
.CredentialsOptionsDouble
,
846 takes_args
= ["URL1", "URL2", "context1?", "context2?", "context3?", "context4?", "context5?"]
849 Option("-w", "--two", dest
="two", action
="store_true", default
=False,
850 help="Hosts are in two different domains"),
851 Option("-q", "--quiet", dest
="quiet", action
="store_true", default
=False,
852 help="Do not print anything but relay on just exit code"),
853 Option("-v", "--verbose", dest
="verbose", action
="store_true", default
=False,
854 help="Print all DN pairs that have been compared"),
855 Option("--sd", dest
="descriptor", action
="store_true", default
=False,
856 help="Compare nTSecurityDescriptor attibutes only"),
857 Option("--sort-aces", dest
="sort_aces", action
="store_true", default
=False,
858 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
859 Option("--view", dest
="view", default
="section",
860 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
861 Option("--base", dest
="base", default
="",
862 help="Pass search base that will build DN list for the first DC."),
863 Option("--base2", dest
="base2", default
="",
864 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
865 Option("--scope", dest
="scope", default
="SUB",
866 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
867 Option("--filter", dest
="filter", default
="",
868 help="List of comma separated attributes to ignore in the comparision"),
869 Option("--skip-missing-dn", dest
="skip_missing_dn", action
="store_true", default
=False,
870 help="Skip report and failure due to missing DNs in one server or another"),
873 def run(self
, URL1
, URL2
,
874 context1
=None, context2
=None, context3
=None, context4
=None, context5
=None,
875 two
=False, quiet
=False, verbose
=False, descriptor
=False, sort_aces
=False,
876 view
="section", base
="", base2
="", scope
="SUB", filter="",
877 credopts
=None, sambaopts
=None, versionopts
=None, skip_missing_dn
=False):
879 lp
= sambaopts
.get_loadparm()
881 using_ldap
= URL1
.startswith("ldap") or URL2
.startswith("ldap")
884 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
887 creds2
= credopts
.get_credentials2(lp
, guess
=False)
888 if creds2
.is_anonymous():
891 creds2
.set_domain("")
892 creds2
.set_workstation("")
893 if using_ldap
and not creds
.authentication_requested():
894 raise CommandError("You must supply at least one username/password pair")
896 # make a list of contexts to compare in
900 # If search bases are specified context is defaulted to
901 # DOMAIN so the given search bases can be verified.
902 contexts
= ["DOMAIN"]
904 # if no argument given, we compare all contexts
905 contexts
= ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
907 for c
in [context1
, context2
, context3
, context4
, context5
]:
910 if not c
.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
911 raise CommandError("Incorrect argument: %s" % c
)
912 contexts
.append(c
.upper())
914 if verbose
and quiet
:
915 raise CommandError("You cannot set --verbose and --quiet together")
916 if (not base
and base2
) or (base
and not base2
):
917 raise CommandError("You need to specify both --base and --base2 at the same time")
918 if descriptor
and view
.upper() not in ["SECTION", "COLLISION"]:
919 raise CommandError("Invalid --view value. Choose from: section or collision")
920 if not scope
.upper() in ["SUB", "ONE", "BASE"]:
921 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
923 con1
= LDAPBase(URL1
, creds
, lp
,
924 two
=two
, quiet
=quiet
, descriptor
=descriptor
, sort_aces
=sort_aces
,
925 verbose
=verbose
,view
=view
, base
=base
, scope
=scope
,
926 outf
=self
.outf
, errf
=self
.errf
)
927 assert len(con1
.base_dn
) > 0
929 con2
= LDAPBase(URL2
, creds2
, lp
,
930 two
=two
, quiet
=quiet
, descriptor
=descriptor
, sort_aces
=sort_aces
,
931 verbose
=verbose
, view
=view
, base
=base2
, scope
=scope
,
932 outf
=self
.outf
, errf
=self
.errf
)
933 assert len(con2
.base_dn
) > 0
935 filter_list
= filter.split(",")
938 for context
in contexts
:
940 self
.outf
.write("\n* Comparing [%s] context...\n" % context
)
942 b1
= LDAPBundel(con1
, context
=context
, filter_list
=filter_list
,
943 outf
=self
.outf
, errf
=self
.errf
)
944 b2
= LDAPBundel(con2
, context
=context
, filter_list
=filter_list
,
945 outf
=self
.outf
, errf
=self
.errf
)
949 self
.outf
.write("\n* Result for [%s]: SUCCESS\n" %
953 self
.outf
.write("\n* Result for [%s]: FAILURE\n" % context
)
955 assert len(b1
.summary
["df_value_attrs"]) == len(b2
.summary
["df_value_attrs"])
956 b2
.summary
["df_value_attrs"] = []
957 self
.outf
.write("\nSUMMARY\n")
958 self
.outf
.write("---------\n")
961 # mark exit status as FAILURE if a least one comparison failed
964 raise CommandError("Compare failed: %d" % status
)