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",
49 outf
=sys
.stdout
, errf
=sys
.stderr
):
53 if os
.path
.isfile(host
):
54 samdb_url
= "tdb://%s" % host
56 samdb_url
= "ldap://%s" % host
57 # use 'paged_search' module when connecting remotely
58 if samdb_url
.lower().startswith("ldap://"):
59 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
.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()
86 # Log some domain controller specific place-holers that are being used
87 # when compare content of two DCs. Uncomment for DEBUG purposes.
88 if self
.two_domains
and not self
.quiet
:
89 self
.outf
.write("\n* Place-holders for %s:\n" % self
.host
)
90 self
.outf
.write(4*" " + "${DOMAIN_DN} => %s\n" %
92 self
.outf
.write(4*" " + "${DOMAIN_NETBIOS} => %s\n" %
94 self
.outf
.write(4*" " + "${SERVER_NAME} => %s\n" %
96 self
.outf
.write(4*" " + "${DOMAIN_NAME} => %s\n" %
99 def find_domain_sid(self
):
100 res
= self
.ldb
.search(base
=self
.base_dn
, expression
="(objectClass=*)", scope
=SCOPE_BASE
)
101 return ndr_unpack(security
.dom_sid
,res
[0]["objectSid"][0])
103 def find_servers(self
):
106 res
= self
.ldb
.search(base
="OU=Domain Controllers,%s" % self
.base_dn
, \
107 scope
=SCOPE_SUBTREE
, expression
="(objectClass=computer)", attrs
=["cn"])
111 srv
.append(x
["cn"][0])
114 def find_netbios(self
):
115 res
= self
.ldb
.search(base
="CN=Partitions,%s" % self
.config_dn
, \
116 scope
=SCOPE_SUBTREE
, attrs
=["nETBIOSName"])
119 if "nETBIOSName" in x
.keys():
120 return x
["nETBIOSName"][0]
122 def object_exists(self
, object_dn
):
125 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
)
126 except LdbError
, (enum
, estr
):
127 if enum
== ERR_NO_SUCH_OBJECT
:
132 def delete_force(self
, object_dn
):
134 self
.ldb
.delete(object_dn
)
135 except Ldb
.LdbError
, e
:
136 assert "No such object" in str(e
)
138 def get_attribute_name(self
, key
):
139 """ Returns the real attribute name
140 It resolved ranged results e.g. member;range=0-1499
143 r
= re
.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
151 def get_attribute_values(self
, object_dn
, key
, vals
):
152 """ Returns list with all attribute values
153 It resolved ranged results e.g. member;range=0-1499
156 r
= re
.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
160 # no range, just return the values
166 # get additional values in a loop
167 # until we get a response with '*' at the end
170 n
= "%s;range=%d-*" % (attr
, hi
+ 1)
171 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
, attrs
=[n
])
179 for key
in res
.keys():
185 if m
.group(1) != attr
:
189 fvals
= list(res
[key
])
196 if fm
.group(3) == "*":
197 # if we got "*" we're done
200 assert int(fm
.group(2)) == hi
+ 1
201 hi
= int(fm
.group(3))
205 def get_attributes(self
, object_dn
):
206 """ Returns dict with all default visible attributes
208 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
, attrs
=["*"])
211 # 'Dn' element is not iterable and we have it as 'distinguishedName'
213 for key
in res
.keys():
214 vals
= list(res
[key
])
216 name
= self
.get_attribute_name(key
)
217 res
[name
] = self
.get_attribute_values(object_dn
, key
, vals
)
221 def get_descriptor_sddl(self
, object_dn
):
222 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
, attrs
=["nTSecurityDescriptor"])
223 desc
= res
[0]["nTSecurityDescriptor"][0]
224 desc
= ndr_unpack(security
.descriptor
, desc
)
225 return desc
.as_sddl(self
.domain_sid
)
227 def guid_as_string(self
, guid_blob
):
228 """ Translate binary representation of schemaIDGUID to standard string representation.
229 @gid_blob: binary schemaIDGUID
231 blob
= "%s" % guid_blob
232 stops
= [4, 2, 2, 2, 6]
236 while x
< len(stops
):
240 c
= hex(ord(blob
[index
])).replace("0x", "")
241 c
= [None, "0" + c
, c
][len(c
)]
242 if 2 * index
< len(blob
):
250 assert index
== len(blob
)
251 return res
.strip().replace(" ", "-")
253 def get_guid_map(self
):
254 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
257 res
= self
.ldb
.search(base
=self
.schema_dn
,
258 expression
="(schemaIdGuid=*)", scope
=SCOPE_SUBTREE
, attrs
=["schemaIdGuid", "name"])
260 self
.guid_map
[self
.guid_as_string(item
["schemaIdGuid"]).lower()] = item
["name"][0]
262 res
= self
.ldb
.search(base
="cn=extended-rights,%s" % self
.config_dn
,
263 expression
="(rightsGuid=*)", scope
=SCOPE_SUBTREE
, attrs
=["rightsGuid", "name"])
265 self
.guid_map
[str(item
["rightsGuid"]).lower()] = item
["name"][0]
267 def get_sid_map(self
):
268 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
271 res
= self
.ldb
.search(base
=self
.base_dn
,
272 expression
="(objectSid=*)", scope
=SCOPE_SUBTREE
, attrs
=["objectSid", "sAMAccountName"])
275 self
.sid_map
["%s" % ndr_unpack(security
.dom_sid
, item
["objectSid"][0])] = item
["sAMAccountName"][0]
279 class Descriptor(object):
280 def __init__(self
, connection
, dn
, outf
=sys
.stdout
, errf
=sys
.stderr
):
283 self
.con
= connection
285 self
.sddl
= self
.con
.get_descriptor_sddl(self
.dn
)
286 self
.dacl_list
= self
.extract_dacl()
287 if self
.con
.sort_aces
:
288 self
.dacl_list
.sort()
290 def extract_dacl(self
):
291 """ Extracts the DACL as a list of ACE string (with the brakets).
294 if "S:" in self
.sddl
:
295 res
= re
.search("D:(.*?)(\(.*?\))S:", self
.sddl
).group(2)
297 res
= re
.search("D:(.*?)(\(.*\))", self
.sddl
).group(2)
298 except AttributeError:
300 return re
.findall("(\(.*?\))", res
)
302 def fix_guid(self
, ace
):
304 guids
= re
.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res
)
305 # If there are not GUIDs to replace return the same ACE
310 name
= self
.con
.guid_map
[guid
.lower()]
311 res
= res
.replace(guid
, name
)
313 # Do not bother if the GUID is not found in
314 # cn=Schema or cn=Extended-Rights
318 def fix_sid(self
, ace
):
320 sids
= re
.findall("S-[-0-9]+", res
)
321 # If there are not SIDs to replace return the same ACE
326 name
= self
.con
.sid_map
[sid
]
327 res
= res
.replace(sid
, name
)
329 # Do not bother if the SID is not found in baseDN
333 def fixit(self
, ace
):
334 """ Combine all replacement methods in one
337 res
= self
.fix_guid(res
)
338 res
= self
.fix_sid(res
)
341 def diff_1(self
, other
):
343 if len(self
.dacl_list
) != len(other
.dacl_list
):
344 res
+= 4*" " + "Difference in ACE count:\n"
345 res
+= 8*" " + "=> %s\n" % len(self
.dacl_list
)
346 res
+= 8*" " + "=> %s\n" % len(other
.dacl_list
)
354 self_ace
= "%s" % self
.dacl_list
[i
]
359 other_ace
= "%s" % other
.dacl_list
[i
]
362 if len(self_ace
) + len(other_ace
) == 0:
364 self_ace_fixed
= "%s" % self
.fixit(self_ace
)
365 other_ace_fixed
= "%s" % other
.fixit(other_ace
)
366 if self_ace_fixed
!= other_ace_fixed
:
367 res
+= "%60s * %s\n" % ( self_ace_fixed
, other_ace_fixed
)
370 res
+= "%60s | %s\n" % ( self_ace_fixed
, other_ace_fixed
)
374 def diff_2(self
, other
):
376 if len(self
.dacl_list
) != len(other
.dacl_list
):
377 res
+= 4*" " + "Difference in ACE count:\n"
378 res
+= 8*" " + "=> %s\n" % len(self
.dacl_list
)
379 res
+= 8*" " + "=> %s\n" % len(other
.dacl_list
)
384 self_dacl_list_fixed
= []
385 other_dacl_list_fixed
= []
386 [self_dacl_list_fixed
.append( self
.fixit(ace
) ) for ace
in self
.dacl_list
]
387 [other_dacl_list_fixed
.append( other
.fixit(ace
) ) for ace
in other
.dacl_list
]
388 for ace
in self_dacl_list_fixed
:
390 other_dacl_list_fixed
.index(ace
)
392 self_aces
.append(ace
)
394 common_aces
.append(ace
)
395 self_aces
= sorted(self_aces
)
396 if len(self_aces
) > 0:
397 res
+= 4*" " + "ACEs found only in %s:\n" % self
.con
.host
398 for ace
in self_aces
:
399 res
+= 8*" " + ace
+ "\n"
401 for ace
in other_dacl_list_fixed
:
403 self_dacl_list_fixed
.index(ace
)
405 other_aces
.append(ace
)
407 common_aces
.append(ace
)
408 other_aces
= sorted(other_aces
)
409 if len(other_aces
) > 0:
410 res
+= 4*" " + "ACEs found only in %s:\n" % other
.con
.host
411 for ace
in other_aces
:
412 res
+= 8*" " + ace
+ "\n"
414 common_aces
= sorted(list(set(common_aces
)))
416 res
+= 4*" " + "ACEs found in both:\n"
417 for ace
in common_aces
:
418 res
+= 8*" " + ace
+ "\n"
419 return (self_aces
== [] and other_aces
== [], res
)
421 class LDAPObject(object):
422 def __init__(self
, connection
, dn
, summary
, filter_list
,
423 outf
=sys
.stdout
, errf
=sys
.stderr
):
426 self
.con
= connection
427 self
.two_domains
= self
.con
.two_domains
428 self
.quiet
= self
.con
.quiet
429 self
.verbose
= self
.con
.verbose
430 self
.summary
= summary
431 self
.dn
= dn
.replace("${DOMAIN_DN}", self
.con
.base_dn
)
432 self
.dn
= self
.dn
.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self
.con
.domain_netbios
)
433 for x
in self
.con
.server_names
:
434 self
.dn
= self
.dn
.replace("CN=${SERVER_NAME}", "CN=%s" % x
)
435 self
.attributes
= self
.con
.get_attributes(self
.dn
)
436 # Attributes that are considered always to be different e.g based on timestamp etc.
438 # One domain - two domain controllers
439 self
.ignore_attributes
= [
440 # Default Naming Context
441 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
442 "operatingSystemVersion","oEMInformation",
443 # Configuration Naming Context
444 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
445 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
446 # Schema Naming Context
449 self
.ignore_attributes
+= filter_list
451 self
.dn_attributes
= []
452 self
.domain_attributes
= []
453 self
.servername_attributes
= []
454 self
.netbios_attributes
= []
455 self
.other_attributes
= []
456 # Two domains - two domain controllers
459 self
.ignore_attributes
+= [
460 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
461 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
462 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
463 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
464 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
465 # After Exchange preps
466 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
468 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
469 self
.dn_attributes
= [
470 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
471 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
472 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
473 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
474 # After Exchange preps
475 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
476 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
477 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
478 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
479 self
.dn_attributes
= [x
.upper() for x
in self
.dn_attributes
]
481 # Attributes that contain the Domain name e.g. 'samba.org'
482 self
.domain_attributes
= [
483 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
484 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
485 self
.domain_attributes
= [x
.upper() for x
in self
.domain_attributes
]
487 # May contain DOMAIN_NETBIOS and SERVER_NAME
488 self
.servername_attributes
= [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
489 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
490 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
491 self
.servername_attributes
= [x
.upper() for x
in self
.servername_attributes
]
493 self
.netbios_attributes
= [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
494 self
.netbios_attributes
= [x
.upper() for x
in self
.netbios_attributes
]
496 self
.other_attributes
= [ "name", "DC",]
497 self
.other_attributes
= [x
.upper() for x
in self
.other_attributes
]
499 self
.ignore_attributes
= [x
.upper() for x
in self
.ignore_attributes
]
503 Log on the screen if there is no --quiet oprion set
506 self
.outf
.write(msg
+"\n")
510 if not self
.two_domains
:
512 if res
.upper().endswith(self
.con
.base_dn
.upper()):
513 res
= res
[:len(res
)-len(self
.con
.base_dn
)] + "${DOMAIN_DN}"
516 def fix_domain_name(self
, s
):
518 if not self
.two_domains
:
520 res
= res
.replace(self
.con
.domain_name
.lower(), self
.con
.domain_name
.upper())
521 res
= res
.replace(self
.con
.domain_name
.upper(), "${DOMAIN_NAME}")
524 def fix_domain_netbios(self
, s
):
526 if not self
.two_domains
:
528 res
= res
.replace(self
.con
.domain_netbios
.lower(), self
.con
.domain_netbios
.upper())
529 res
= res
.replace(self
.con
.domain_netbios
.upper(), "${DOMAIN_NETBIOS}")
532 def fix_server_name(self
, s
):
534 if not self
.two_domains
or len(self
.con
.server_names
) > 1:
536 for x
in self
.con
.server_names
:
537 res
= res
.upper().replace(x
, "${SERVER_NAME}")
540 def __eq__(self
, other
):
541 if self
.con
.descriptor
:
542 return self
.cmp_desc(other
)
543 return self
.cmp_attrs(other
)
545 def cmp_desc(self
, other
):
546 d1
= Descriptor(self
.con
, self
.dn
, outf
=self
.outf
, errf
=self
.errf
)
547 d2
= Descriptor(other
.con
, other
.dn
, outf
=self
.outf
, errf
=self
.errf
)
548 if self
.con
.view
== "section":
550 elif self
.con
.view
== "collision":
553 raise Exception("Unknown --view option value.")
555 self
.screen_output
= res
[1][:-1]
556 other
.screen_output
= res
[1][:-1]
560 def cmp_attrs(self
, other
):
562 self
.unique_attrs
= []
563 self
.df_value_attrs
= []
564 other
.unique_attrs
= []
565 if self
.attributes
.keys() != other
.attributes
.keys():
567 title
= 4*" " + "Attributes found only in %s:" % self
.con
.host
568 for x
in self
.attributes
.keys():
569 if not x
in other
.attributes
.keys() and \
570 not x
.upper() in [q
.upper() for q
in other
.ignore_attributes
]:
574 res
+= 8*" " + x
+ "\n"
575 self
.unique_attrs
.append(x
)
577 title
= 4*" " + "Attributes found only in %s:" % other
.con
.host
578 for x
in other
.attributes
.keys():
579 if not x
in self
.attributes
.keys() and \
580 not x
.upper() in [q
.upper() for q
in self
.ignore_attributes
]:
584 res
+= 8*" " + x
+ "\n"
585 other
.unique_attrs
.append(x
)
587 missing_attrs
= [x
.upper() for x
in self
.unique_attrs
]
588 missing_attrs
+= [x
.upper() for x
in other
.unique_attrs
]
589 title
= 4*" " + "Difference in attribute values:"
590 for x
in self
.attributes
.keys():
591 if x
.upper() in self
.ignore_attributes
or x
.upper() in missing_attrs
:
593 if isinstance(self
.attributes
[x
], list) and isinstance(other
.attributes
[x
], list):
594 self
.attributes
[x
] = sorted(self
.attributes
[x
])
595 other
.attributes
[x
] = sorted(other
.attributes
[x
])
596 if self
.attributes
[x
] != other
.attributes
[x
]:
601 # First check if the difference can be fixed but shunting the first part
602 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
603 if x
.upper() in self
.other_attributes
:
604 p
= [self
.con
.domain_name
.split(".")[0] == j
for j
in self
.attributes
[x
]]
605 q
= [other
.con
.domain_name
.split(".")[0] == j
for j
in other
.attributes
[x
]]
608 # Attribute values that are list that contain DN based values that may differ
609 elif x
.upper() in self
.dn_attributes
:
613 m
= self
.attributes
[x
]
614 n
= other
.attributes
[x
]
615 p
= [self
.fix_dn(j
) for j
in m
]
616 q
= [other
.fix_dn(j
) for j
in n
]
619 # Attributes that contain the Domain name in them
620 if x
.upper() in self
.domain_attributes
:
624 m
= self
.attributes
[x
]
625 n
= other
.attributes
[x
]
626 p
= [self
.fix_domain_name(j
) for j
in m
]
627 q
= [other
.fix_domain_name(j
) for j
in n
]
631 if x
.upper() in self
.servername_attributes
:
632 # Attributes with SERVER_NAME
636 m
= self
.attributes
[x
]
637 n
= other
.attributes
[x
]
638 p
= [self
.fix_server_name(j
) for j
in m
]
639 q
= [other
.fix_server_name(j
) for j
in n
]
643 if x
.upper() in self
.netbios_attributes
:
644 # Attributes with NETBIOS Domain name
648 m
= self
.attributes
[x
]
649 n
= other
.attributes
[x
]
650 p
= [self
.fix_domain_netbios(j
) for j
in m
]
651 q
= [other
.fix_domain_netbios(j
) for j
in n
]
659 res
+= 8*" " + x
+ " => \n%s\n%s" % (p
, q
) + "\n"
661 res
+= 8*" " + x
+ " => \n%s\n%s" % (self
.attributes
[x
], other
.attributes
[x
]) + "\n"
662 self
.df_value_attrs
.append(x
)
664 if self
.unique_attrs
+ other
.unique_attrs
!= []:
665 assert self
.unique_attrs
!= other
.unique_attrs
666 self
.summary
["unique_attrs"] += self
.unique_attrs
667 self
.summary
["df_value_attrs"] += self
.df_value_attrs
668 other
.summary
["unique_attrs"] += other
.unique_attrs
669 other
.summary
["df_value_attrs"] += self
.df_value_attrs
# they are the same
671 self
.screen_output
= res
[:-1]
672 other
.screen_output
= res
[:-1]
677 class LDAPBundel(object):
679 def __init__(self
, connection
, context
, dn_list
=None, filter_list
=None,
680 outf
=sys
.stdout
, errf
=sys
.stderr
):
683 self
.con
= connection
684 self
.two_domains
= self
.con
.two_domains
685 self
.quiet
= self
.con
.quiet
686 self
.verbose
= self
.con
.verbose
687 self
.search_base
= self
.con
.search_base
688 self
.search_scope
= self
.con
.search_scope
690 self
.summary
["unique_attrs"] = []
691 self
.summary
["df_value_attrs"] = []
692 self
.summary
["known_ignored_dn"] = []
693 self
.summary
["abnormal_ignored_dn"] = []
694 self
.filter_list
= filter_list
696 self
.dn_list
= dn_list
697 elif context
.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
698 self
.context
= context
.upper()
699 self
.dn_list
= self
.get_dn_list(context
)
701 raise Exception("Unknown initialization data for LDAPBundel().")
703 while counter
< len(self
.dn_list
) and self
.two_domains
:
704 # Use alias reference
705 tmp
= self
.dn_list
[counter
]
706 tmp
= tmp
[:len(tmp
)-len(self
.con
.base_dn
)] + "${DOMAIN_DN}"
707 tmp
= tmp
.replace("CN=%s" % self
.con
.domain_netbios
, "CN=${DOMAIN_NETBIOS}")
708 if len(self
.con
.server_names
) == 1:
709 for x
in self
.con
.server_names
:
710 tmp
= tmp
.replace("CN=%s" % x
, "CN=${SERVER_NAME}")
711 self
.dn_list
[counter
] = tmp
713 self
.dn_list
= list(set(self
.dn_list
))
714 self
.dn_list
= sorted(self
.dn_list
)
715 self
.size
= len(self
.dn_list
)
719 Log on the screen if there is no --quiet oprion set
722 self
.outf
.write(msg
+"\n")
724 def update_size(self
):
725 self
.size
= len(self
.dn_list
)
726 self
.dn_list
= sorted(self
.dn_list
)
728 def __eq__(self
, other
):
730 if self
.size
!= other
.size
:
731 self
.log( "\n* DN lists have different size: %s != %s" % (self
.size
, other
.size
) )
734 # This is the case where we want to explicitly compare two objects with different DNs.
735 # It does not matter if they are in the same DC, in two DC in one domain or in two
737 if self
.search_scope
!= SCOPE_BASE
:
738 title
= "\n* DNs found only in %s:" % self
.con
.host
739 for x
in self
.dn_list
:
740 if not x
.upper() in [q
.upper() for q
in other
.dn_list
]:
745 self
.log( 4*" " + x
)
746 self
.dn_list
[self
.dn_list
.index(x
)] = ""
747 self
.dn_list
= [x
for x
in self
.dn_list
if x
]
749 title
= "\n* DNs found only in %s:" % other
.con
.host
750 for x
in other
.dn_list
:
751 if not x
.upper() in [q
.upper() for q
in self
.dn_list
]:
756 self
.log( 4*" " + x
)
757 other
.dn_list
[other
.dn_list
.index(x
)] = ""
758 other
.dn_list
= [x
for x
in other
.dn_list
if x
]
762 assert self
.size
== other
.size
763 assert sorted([x
.upper() for x
in self
.dn_list
]) == sorted([x
.upper() for x
in other
.dn_list
])
764 self
.log( "\n* Objects to be compared: %s" % self
.size
)
767 while index
< self
.size
:
770 object1
= LDAPObject(connection
=self
.con
,
771 dn
=self
.dn_list
[index
],
772 summary
=self
.summary
,
773 filter_list
=self
.filter_list
,
774 outf
=self
.outf
, errf
=self
.errf
)
775 except LdbError
, (enum
, estr
):
776 if enum
== ERR_NO_SUCH_OBJECT
:
777 self
.log( "\n!!! Object not found: %s" % self
.dn_list
[index
] )
781 object2
= LDAPObject(connection
=other
.con
,
782 dn
=other
.dn_list
[index
],
783 summary
=other
.summary
,
784 filter_list
=self
.filter_list
,
785 outf
=self
.outf
, errf
=self
.errf
)
786 except LdbError
, (enum
, estr
):
787 if enum
== ERR_NO_SUCH_OBJECT
:
788 self
.log( "\n!!! Object not found: %s" % other
.dn_list
[index
] )
794 if object1
== object2
:
796 self
.log( "\nComparing:" )
797 self
.log( "'%s' [%s]" % (object1
.dn
, object1
.con
.host
) )
798 self
.log( "'%s' [%s]" % (object2
.dn
, object2
.con
.host
) )
799 self
.log( 4*" " + "OK" )
801 self
.log( "\nComparing:" )
802 self
.log( "'%s' [%s]" % (object1
.dn
, object1
.con
.host
) )
803 self
.log( "'%s' [%s]" % (object2
.dn
, object2
.con
.host
) )
804 self
.log( object1
.screen_output
)
805 self
.log( 4*" " + "FAILED" )
807 self
.summary
= object1
.summary
808 other
.summary
= object2
.summary
813 def get_dn_list(self
, context
):
814 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
815 Parse all DNs and filter those that are 'strange' or abnormal.
817 if context
.upper() == "DOMAIN":
818 search_base
= self
.con
.base_dn
819 elif context
.upper() == "CONFIGURATION":
820 search_base
= self
.con
.config_dn
821 elif context
.upper() == "SCHEMA":
822 search_base
= self
.con
.schema_dn
823 elif context
.upper() == "DNSDOMAIN":
824 search_base
= "DC=DomainDnsZones,%s" % self
.con
.base_dn
825 elif context
.upper() == "DNSFOREST":
826 search_base
= "DC=ForestDnsZones,%s" % self
.con
.root_dn
829 if not self
.search_base
:
830 self
.search_base
= search_base
831 self
.search_scope
= self
.search_scope
.upper()
832 if self
.search_scope
== "SUB":
833 self
.search_scope
= SCOPE_SUBTREE
834 elif self
.search_scope
== "BASE":
835 self
.search_scope
= SCOPE_BASE
836 elif self
.search_scope
== "ONE":
837 self
.search_scope
= SCOPE_ONELEVEL
839 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
841 res
= self
.con
.ldb
.search(base
=self
.search_base
, scope
=self
.search_scope
, attrs
=["dn"])
842 except LdbError
, (enum
, estr
):
843 self
.outf
.write("Failed search of base=%s\n" % self
.search_base
)
846 dn_list
.append(x
["dn"].get_linearized())
852 def print_summary(self
):
853 self
.summary
["unique_attrs"] = list(set(self
.summary
["unique_attrs"]))
854 self
.summary
["df_value_attrs"] = list(set(self
.summary
["df_value_attrs"]))
856 if self
.summary
["unique_attrs"]:
857 self
.log( "\nAttributes found only in %s:" % self
.con
.host
)
858 self
.log( "".join([str("\n" + 4*" " + x
) for x
in self
.summary
["unique_attrs"]]) )
860 if self
.summary
["df_value_attrs"]:
861 self
.log( "\nAttributes with different values:" )
862 self
.log( "".join([str("\n" + 4*" " + x
) for x
in self
.summary
["df_value_attrs"]]) )
863 self
.summary
["df_value_attrs"] = []
866 class cmd_ldapcmp(Command
):
867 """compare two ldap databases"""
868 synopsis
= "%prog ldapcmp <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
870 takes_optiongroups
= {
871 "sambaopts": options
.SambaOptions
,
872 "versionopts": options
.VersionOptions
,
873 "credopts": options
.CredentialsOptionsDouble
,
876 takes_optiongroups
= {
877 "sambaopts": options
.SambaOptions
,
878 "versionopts": options
.VersionOptions
,
879 "credopts": options
.CredentialsOptionsDouble
,
882 takes_args
= ["URL1", "URL2", "context1?", "context2?", "context3?"]
885 Option("-w", "--two", dest
="two", action
="store_true", default
=False,
886 help="Hosts are in two different domains"),
887 Option("-q", "--quiet", dest
="quiet", action
="store_true", default
=False,
888 help="Do not print anything but relay on just exit code"),
889 Option("-v", "--verbose", dest
="verbose", action
="store_true", default
=False,
890 help="Print all DN pairs that have been compared"),
891 Option("--sd", dest
="descriptor", action
="store_true", default
=False,
892 help="Compare nTSecurityDescriptor attibutes only"),
893 Option("--sort-aces", dest
="sort_aces", action
="store_true", default
=False,
894 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
895 Option("--view", dest
="view", default
="section",
896 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
897 Option("--base", dest
="base", default
="",
898 help="Pass search base that will build DN list for the first DC."),
899 Option("--base2", dest
="base2", default
="",
900 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
901 Option("--scope", dest
="scope", default
="SUB",
902 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
903 Option("--filter", dest
="filter", default
="",
904 help="List of comma separated attributes to ignore in the comparision"),
907 def run(self
, URL1
, URL2
,
908 context1
=None, context2
=None, context3
=None,
909 two
=False, quiet
=False, verbose
=False, descriptor
=False, sort_aces
=False,
910 view
="section", base
="", base2
="", scope
="SUB", filter="",
911 credopts
=None, sambaopts
=None, versionopts
=None):
913 lp
= sambaopts
.get_loadparm()
915 using_ldap
= URL1
.startswith("ldap") or URL2
.startswith("ldap")
918 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
921 creds2
= credopts
.get_credentials2(lp
, guess
=False)
922 if creds2
.is_anonymous():
925 creds2
.set_domain("")
926 creds2
.set_workstation("")
927 if using_ldap
and not creds
.authentication_requested():
928 raise CommandError("You must supply at least one username/password pair")
930 # make a list of contexts to compare in
934 # If search bases are specified context is defaulted to
935 # DOMAIN so the given search bases can be verified.
936 contexts
= ["DOMAIN"]
938 # if no argument given, we compare all contexts
939 contexts
= ["DOMAIN", "CONFIGURATION", "SCHEMA"]
941 for c
in [context1
, context2
, context3
]:
944 if not c
.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
945 raise CommandError("Incorrect argument: %s" % c
)
946 contexts
.append(c
.upper())
948 if verbose
and quiet
:
949 raise CommandError("You cannot set --verbose and --quiet together")
950 if (not base
and base2
) or (base
and not base2
):
951 raise CommandError("You need to specify both --base and --base2 at the same time")
952 if descriptor
and view
.upper() not in ["SECTION", "COLLISION"]:
953 raise CommandError("Invalid --view value. Choose from: section or collision")
954 if not scope
.upper() in ["SUB", "ONE", "BASE"]:
955 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
957 con1
= LDAPBase(URL1
, creds
, lp
,
958 two
=two
, quiet
=quiet
, descriptor
=descriptor
, sort_aces
=sort_aces
,
959 verbose
=verbose
,view
=view
, base
=base
, scope
=scope
,
960 outf
=self
.outf
, errf
=self
.errf
)
961 assert len(con1
.base_dn
) > 0
963 con2
= LDAPBase(URL2
, creds2
, lp
,
964 two
=two
, quiet
=quiet
, descriptor
=descriptor
, sort_aces
=sort_aces
,
965 verbose
=verbose
, view
=view
, base
=base2
, scope
=scope
,
966 outf
=self
.outf
, errf
=self
.errf
)
967 assert len(con2
.base_dn
) > 0
969 filter_list
= filter.split(",")
972 for context
in contexts
:
974 self
.outf
.write("\n* Comparing [%s] context...\n" % context
)
976 b1
= LDAPBundel(con1
, context
=context
, filter_list
=filter_list
,
977 outf
=self
.outf
, errf
=self
.errf
)
978 b2
= LDAPBundel(con2
, context
=context
, filter_list
=filter_list
,
979 outf
=self
.outf
, errf
=self
.errf
)
983 self
.outf
.write("\n* Result for [%s]: SUCCESS\n" %
987 self
.outf
.write("\n* Result for [%s]: FAILURE\n" % context
)
989 assert len(b1
.summary
["df_value_attrs"]) == len(b2
.summary
["df_value_attrs"])
990 b2
.summary
["df_value_attrs"] = []
991 self
.outf
.write("\nSUMMARY\n")
992 self
.outf
.write("---------\n")
995 # mark exit status as FAILURE if a least one comparison failed
998 raise CommandError("Compare failed: %d" % status
)