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
.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
):
281 self
.con
= connection
283 self
.sddl
= self
.con
.get_descriptor_sddl(self
.dn
)
284 self
.dacl_list
= self
.extract_dacl()
285 if self
.con
.sort_aces
:
286 self
.dacl_list
.sort()
288 def extract_dacl(self
):
289 """ Extracts the DACL as a list of ACE string (with the brakets).
292 if "S:" in self
.sddl
:
293 res
= re
.search("D:(.*?)(\(.*?\))S:", self
.sddl
).group(2)
295 res
= re
.search("D:(.*?)(\(.*\))", self
.sddl
).group(2)
296 except AttributeError:
298 return re
.findall("(\(.*?\))", res
)
300 def fix_guid(self
, ace
):
302 guids
= re
.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res
)
303 # If there are not GUIDs to replace return the same ACE
308 name
= self
.con
.guid_map
[guid
.lower()]
309 res
= res
.replace(guid
, name
)
311 # Do not bother if the GUID is not found in
312 # cn=Schema or cn=Extended-Rights
316 def fix_sid(self
, ace
):
318 sids
= re
.findall("S-[-0-9]+", res
)
319 # If there are not SIDs to replace return the same ACE
324 name
= self
.con
.sid_map
[sid
]
325 res
= res
.replace(sid
, name
)
327 # Do not bother if the SID is not found in baseDN
331 def fixit(self
, ace
):
332 """ Combine all replacement methods in one
335 res
= self
.fix_guid(res
)
336 res
= self
.fix_sid(res
)
339 def diff_1(self
, other
):
341 if len(self
.dacl_list
) != len(other
.dacl_list
):
342 res
+= 4*" " + "Difference in ACE count:\n"
343 res
+= 8*" " + "=> %s\n" % len(self
.dacl_list
)
344 res
+= 8*" " + "=> %s\n" % len(other
.dacl_list
)
352 self_ace
= "%s" % self
.dacl_list
[i
]
357 other_ace
= "%s" % other
.dacl_list
[i
]
360 if len(self_ace
) + len(other_ace
) == 0:
362 self_ace_fixed
= "%s" % self
.fixit(self_ace
)
363 other_ace_fixed
= "%s" % other
.fixit(other_ace
)
364 if self_ace_fixed
!= other_ace_fixed
:
365 res
+= "%60s * %s\n" % ( self_ace_fixed
, other_ace_fixed
)
368 res
+= "%60s | %s\n" % ( self_ace_fixed
, other_ace_fixed
)
372 def diff_2(self
, other
):
374 if len(self
.dacl_list
) != len(other
.dacl_list
):
375 res
+= 4*" " + "Difference in ACE count:\n"
376 res
+= 8*" " + "=> %s\n" % len(self
.dacl_list
)
377 res
+= 8*" " + "=> %s\n" % len(other
.dacl_list
)
382 self_dacl_list_fixed
= []
383 other_dacl_list_fixed
= []
384 [self_dacl_list_fixed
.append( self
.fixit(ace
) ) for ace
in self
.dacl_list
]
385 [other_dacl_list_fixed
.append( other
.fixit(ace
) ) for ace
in other
.dacl_list
]
386 for ace
in self_dacl_list_fixed
:
388 other_dacl_list_fixed
.index(ace
)
390 self_aces
.append(ace
)
392 common_aces
.append(ace
)
393 self_aces
= sorted(self_aces
)
394 if len(self_aces
) > 0:
395 res
+= 4*" " + "ACEs found only in %s:\n" % self
.con
.host
396 for ace
in self_aces
:
397 res
+= 8*" " + ace
+ "\n"
399 for ace
in other_dacl_list_fixed
:
401 self_dacl_list_fixed
.index(ace
)
403 other_aces
.append(ace
)
405 common_aces
.append(ace
)
406 other_aces
= sorted(other_aces
)
407 if len(other_aces
) > 0:
408 res
+= 4*" " + "ACEs found only in %s:\n" % other
.con
.host
409 for ace
in other_aces
:
410 res
+= 8*" " + ace
+ "\n"
412 common_aces
= sorted(list(set(common_aces
)))
414 res
+= 4*" " + "ACEs found in both:\n"
415 for ace
in common_aces
:
416 res
+= 8*" " + ace
+ "\n"
417 return (self_aces
== [] and other_aces
== [], res
)
419 class LDAPObject(object):
420 def __init__(self
, connection
, dn
, summary
, filter_list
):
421 self
.con
= connection
422 self
.two_domains
= self
.con
.two_domains
423 self
.quiet
= self
.con
.quiet
424 self
.verbose
= self
.con
.verbose
425 self
.summary
= summary
426 self
.dn
= dn
.replace("${DOMAIN_DN}", self
.con
.base_dn
)
427 self
.dn
= self
.dn
.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self
.con
.domain_netbios
)
428 for x
in self
.con
.server_names
:
429 self
.dn
= self
.dn
.replace("CN=${SERVER_NAME}", "CN=%s" % x
)
430 self
.attributes
= self
.con
.get_attributes(self
.dn
)
431 # Attributes that are considered always to be different e.g based on timestamp etc.
433 # One domain - two domain controllers
434 self
.ignore_attributes
= [
435 # Default Naming Context
436 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
437 "operatingSystemVersion","oEMInformation",
438 # Configuration Naming Context
439 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
440 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
441 # Schema Naming Context
444 self
.ignore_attributes
+= filter_list
446 self
.dn_attributes
= []
447 self
.domain_attributes
= []
448 self
.servername_attributes
= []
449 self
.netbios_attributes
= []
450 self
.other_attributes
= []
451 # Two domains - two domain controllers
454 self
.ignore_attributes
+= [
455 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
456 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
457 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
458 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
459 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
460 # After Exchange preps
461 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
463 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
464 self
.dn_attributes
= [
465 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
466 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
467 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
468 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
469 # After Exchange preps
470 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
471 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
472 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
473 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
474 self
.dn_attributes
= [x
.upper() for x
in self
.dn_attributes
]
476 # Attributes that contain the Domain name e.g. 'samba.org'
477 self
.domain_attributes
= [
478 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
479 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
480 self
.domain_attributes
= [x
.upper() for x
in self
.domain_attributes
]
482 # May contain DOMAIN_NETBIOS and SERVER_NAME
483 self
.servername_attributes
= [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
484 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
485 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
486 self
.servername_attributes
= [x
.upper() for x
in self
.servername_attributes
]
488 self
.netbios_attributes
= [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
489 self
.netbios_attributes
= [x
.upper() for x
in self
.netbios_attributes
]
491 self
.other_attributes
= [ "name", "DC",]
492 self
.other_attributes
= [x
.upper() for x
in self
.other_attributes
]
494 self
.ignore_attributes
= [x
.upper() for x
in self
.ignore_attributes
]
498 Log on the screen if there is no --quiet oprion set
501 self
.outf
.write(msg
+"\n")
505 if not self
.two_domains
:
507 if res
.upper().endswith(self
.con
.base_dn
.upper()):
508 res
= res
[:len(res
)-len(self
.con
.base_dn
)] + "${DOMAIN_DN}"
511 def fix_domain_name(self
, s
):
513 if not self
.two_domains
:
515 res
= res
.replace(self
.con
.domain_name
.lower(), self
.con
.domain_name
.upper())
516 res
= res
.replace(self
.con
.domain_name
.upper(), "${DOMAIN_NAME}")
519 def fix_domain_netbios(self
, s
):
521 if not self
.two_domains
:
523 res
= res
.replace(self
.con
.domain_netbios
.lower(), self
.con
.domain_netbios
.upper())
524 res
= res
.replace(self
.con
.domain_netbios
.upper(), "${DOMAIN_NETBIOS}")
527 def fix_server_name(self
, s
):
529 if not self
.two_domains
or len(self
.con
.server_names
) > 1:
531 for x
in self
.con
.server_names
:
532 res
= res
.upper().replace(x
, "${SERVER_NAME}")
535 def __eq__(self
, other
):
536 if self
.con
.descriptor
:
537 return self
.cmp_desc(other
)
538 return self
.cmp_attrs(other
)
540 def cmp_desc(self
, other
):
541 d1
= Descriptor(self
.con
, self
.dn
)
542 d2
= Descriptor(other
.con
, other
.dn
)
543 if self
.con
.view
== "section":
545 elif self
.con
.view
== "collision":
548 raise Exception("Unknown --view option value.")
550 self
.screen_output
= res
[1][:-1]
551 other
.screen_output
= res
[1][:-1]
555 def cmp_attrs(self
, other
):
557 self
.unique_attrs
= []
558 self
.df_value_attrs
= []
559 other
.unique_attrs
= []
560 if self
.attributes
.keys() != other
.attributes
.keys():
562 title
= 4*" " + "Attributes found only in %s:" % self
.con
.host
563 for x
in self
.attributes
.keys():
564 if not x
in other
.attributes
.keys() and \
565 not x
.upper() in [q
.upper() for q
in other
.ignore_attributes
]:
569 res
+= 8*" " + x
+ "\n"
570 self
.unique_attrs
.append(x
)
572 title
= 4*" " + "Attributes found only in %s:" % other
.con
.host
573 for x
in other
.attributes
.keys():
574 if not x
in self
.attributes
.keys() and \
575 not x
.upper() in [q
.upper() for q
in self
.ignore_attributes
]:
579 res
+= 8*" " + x
+ "\n"
580 other
.unique_attrs
.append(x
)
582 missing_attrs
= [x
.upper() for x
in self
.unique_attrs
]
583 missing_attrs
+= [x
.upper() for x
in other
.unique_attrs
]
584 title
= 4*" " + "Difference in attribute values:"
585 for x
in self
.attributes
.keys():
586 if x
.upper() in self
.ignore_attributes
or x
.upper() in missing_attrs
:
588 if isinstance(self
.attributes
[x
], list) and isinstance(other
.attributes
[x
], list):
589 self
.attributes
[x
] = sorted(self
.attributes
[x
])
590 other
.attributes
[x
] = sorted(other
.attributes
[x
])
591 if self
.attributes
[x
] != other
.attributes
[x
]:
596 # First check if the difference can be fixed but shunting the first part
597 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
598 if x
.upper() in self
.other_attributes
:
599 p
= [self
.con
.domain_name
.split(".")[0] == j
for j
in self
.attributes
[x
]]
600 q
= [other
.con
.domain_name
.split(".")[0] == j
for j
in other
.attributes
[x
]]
603 # Attribute values that are list that contain DN based values that may differ
604 elif x
.upper() in self
.dn_attributes
:
608 m
= self
.attributes
[x
]
609 n
= other
.attributes
[x
]
610 p
= [self
.fix_dn(j
) for j
in m
]
611 q
= [other
.fix_dn(j
) for j
in n
]
614 # Attributes that contain the Domain name in them
615 if x
.upper() in self
.domain_attributes
:
619 m
= self
.attributes
[x
]
620 n
= other
.attributes
[x
]
621 p
= [self
.fix_domain_name(j
) for j
in m
]
622 q
= [other
.fix_domain_name(j
) for j
in n
]
626 if x
.upper() in self
.servername_attributes
:
627 # Attributes with SERVER_NAME
631 m
= self
.attributes
[x
]
632 n
= other
.attributes
[x
]
633 p
= [self
.fix_server_name(j
) for j
in m
]
634 q
= [other
.fix_server_name(j
) for j
in n
]
638 if x
.upper() in self
.netbios_attributes
:
639 # Attributes with NETBIOS Domain name
643 m
= self
.attributes
[x
]
644 n
= other
.attributes
[x
]
645 p
= [self
.fix_domain_netbios(j
) for j
in m
]
646 q
= [other
.fix_domain_netbios(j
) for j
in n
]
654 res
+= 8*" " + x
+ " => \n%s\n%s" % (p
, q
) + "\n"
656 res
+= 8*" " + x
+ " => \n%s\n%s" % (self
.attributes
[x
], other
.attributes
[x
]) + "\n"
657 self
.df_value_attrs
.append(x
)
659 if self
.unique_attrs
+ other
.unique_attrs
!= []:
660 assert self
.unique_attrs
!= other
.unique_attrs
661 self
.summary
["unique_attrs"] += self
.unique_attrs
662 self
.summary
["df_value_attrs"] += self
.df_value_attrs
663 other
.summary
["unique_attrs"] += other
.unique_attrs
664 other
.summary
["df_value_attrs"] += self
.df_value_attrs
# they are the same
666 self
.screen_output
= res
[:-1]
667 other
.screen_output
= res
[:-1]
672 class LDAPBundel(object):
674 def __init__(self
, connection
, context
, dn_list
=None, filter_list
=None):
675 self
.con
= connection
676 self
.two_domains
= self
.con
.two_domains
677 self
.quiet
= self
.con
.quiet
678 self
.verbose
= self
.con
.verbose
679 self
.search_base
= self
.con
.search_base
680 self
.search_scope
= self
.con
.search_scope
682 self
.summary
["unique_attrs"] = []
683 self
.summary
["df_value_attrs"] = []
684 self
.summary
["known_ignored_dn"] = []
685 self
.summary
["abnormal_ignored_dn"] = []
686 self
.filter_list
= filter_list
688 self
.dn_list
= dn_list
689 elif context
.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
690 self
.context
= context
.upper()
691 self
.dn_list
= self
.get_dn_list(context
)
693 raise Exception("Unknown initialization data for LDAPBundel().")
695 while counter
< len(self
.dn_list
) and self
.two_domains
:
696 # Use alias reference
697 tmp
= self
.dn_list
[counter
]
698 tmp
= tmp
[:len(tmp
)-len(self
.con
.base_dn
)] + "${DOMAIN_DN}"
699 tmp
= tmp
.replace("CN=%s" % self
.con
.domain_netbios
, "CN=${DOMAIN_NETBIOS}")
700 if len(self
.con
.server_names
) == 1:
701 for x
in self
.con
.server_names
:
702 tmp
= tmp
.replace("CN=%s" % x
, "CN=${SERVER_NAME}")
703 self
.dn_list
[counter
] = tmp
705 self
.dn_list
= list(set(self
.dn_list
))
706 self
.dn_list
= sorted(self
.dn_list
)
707 self
.size
= len(self
.dn_list
)
711 Log on the screen if there is no --quiet oprion set
714 self
.outf
.write(msg
+"\n")
716 def update_size(self
):
717 self
.size
= len(self
.dn_list
)
718 self
.dn_list
= sorted(self
.dn_list
)
720 def __eq__(self
, other
):
722 if self
.size
!= other
.size
:
723 self
.log( "\n* DN lists have different size: %s != %s" % (self
.size
, other
.size
) )
726 # This is the case where we want to explicitly compare two objects with different DNs.
727 # It does not matter if they are in the same DC, in two DC in one domain or in two
729 if self
.search_scope
!= SCOPE_BASE
:
730 title
= "\n* DNs found only in %s:" % self
.con
.host
731 for x
in self
.dn_list
:
732 if not x
.upper() in [q
.upper() for q
in other
.dn_list
]:
737 self
.log( 4*" " + x
)
738 self
.dn_list
[self
.dn_list
.index(x
)] = ""
739 self
.dn_list
= [x
for x
in self
.dn_list
if x
]
741 title
= "\n* DNs found only in %s:" % other
.con
.host
742 for x
in other
.dn_list
:
743 if not x
.upper() in [q
.upper() for q
in self
.dn_list
]:
748 self
.log( 4*" " + x
)
749 other
.dn_list
[other
.dn_list
.index(x
)] = ""
750 other
.dn_list
= [x
for x
in other
.dn_list
if x
]
754 assert self
.size
== other
.size
755 assert sorted([x
.upper() for x
in self
.dn_list
]) == sorted([x
.upper() for x
in other
.dn_list
])
756 self
.log( "\n* Objects to be compared: %s" % self
.size
)
759 while index
< self
.size
:
762 object1
= LDAPObject(connection
=self
.con
,
763 dn
=self
.dn_list
[index
],
764 summary
=self
.summary
,
765 filter_list
=self
.filter_list
)
766 except LdbError
, (enum
, estr
):
767 if enum
== ERR_NO_SUCH_OBJECT
:
768 self
.log( "\n!!! Object not found: %s" % self
.dn_list
[index
] )
772 object2
= LDAPObject(connection
=other
.con
,
773 dn
=other
.dn_list
[index
],
774 summary
=other
.summary
,
775 filter_list
=self
.filter_list
)
776 except LdbError
, (enum
, estr
):
777 if enum
== ERR_NO_SUCH_OBJECT
:
778 self
.log( "\n!!! Object not found: %s" % other
.dn_list
[index
] )
784 if object1
== object2
:
786 self
.log( "\nComparing:" )
787 self
.log( "'%s' [%s]" % (object1
.dn
, object1
.con
.host
) )
788 self
.log( "'%s' [%s]" % (object2
.dn
, object2
.con
.host
) )
789 self
.log( 4*" " + "OK" )
791 self
.log( "\nComparing:" )
792 self
.log( "'%s' [%s]" % (object1
.dn
, object1
.con
.host
) )
793 self
.log( "'%s' [%s]" % (object2
.dn
, object2
.con
.host
) )
794 self
.log( object1
.screen_output
)
795 self
.log( 4*" " + "FAILED" )
797 self
.summary
= object1
.summary
798 other
.summary
= object2
.summary
803 def get_dn_list(self
, context
):
804 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
805 Parse all DNs and filter those that are 'strange' or abnormal.
807 if context
.upper() == "DOMAIN":
808 search_base
= self
.con
.base_dn
809 elif context
.upper() == "CONFIGURATION":
810 search_base
= self
.con
.config_dn
811 elif context
.upper() == "SCHEMA":
812 search_base
= self
.con
.schema_dn
813 elif context
.upper() == "DNSDOMAIN":
814 search_base
= "DC=DomainDnsZones,%s" % self
.con
.base_dn
815 elif context
.upper() == "DNSFOREST":
816 search_base
= "DC=ForestDnsZones,%s" % self
.con
.root_dn
819 if not self
.search_base
:
820 self
.search_base
= search_base
821 self
.search_scope
= self
.search_scope
.upper()
822 if self
.search_scope
== "SUB":
823 self
.search_scope
= SCOPE_SUBTREE
824 elif self
.search_scope
== "BASE":
825 self
.search_scope
= SCOPE_BASE
826 elif self
.search_scope
== "ONE":
827 self
.search_scope
= SCOPE_ONELEVEL
829 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
831 res
= self
.con
.ldb
.search(base
=self
.search_base
, scope
=self
.search_scope
, attrs
=["dn"])
832 except LdbError
, (enum
, estr
):
833 self
.outf
.write("Failed search of base=%s\n" % self
.search_base
)
836 dn_list
.append(x
["dn"].get_linearized())
842 def print_summary(self
):
843 self
.summary
["unique_attrs"] = list(set(self
.summary
["unique_attrs"]))
844 self
.summary
["df_value_attrs"] = list(set(self
.summary
["df_value_attrs"]))
846 if self
.summary
["unique_attrs"]:
847 self
.log( "\nAttributes found only in %s:" % self
.con
.host
)
848 self
.log( "".join([str("\n" + 4*" " + x
) for x
in self
.summary
["unique_attrs"]]) )
850 if self
.summary
["df_value_attrs"]:
851 self
.log( "\nAttributes with different values:" )
852 self
.log( "".join([str("\n" + 4*" " + x
) for x
in self
.summary
["df_value_attrs"]]) )
853 self
.summary
["df_value_attrs"] = []
856 class cmd_ldapcmp(Command
):
857 """compare two ldap databases"""
858 synopsis
= "%prog ldapcmp <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
860 takes_optiongroups
= {
861 "sambaopts": options
.SambaOptions
,
862 "versionopts": options
.VersionOptions
,
863 "credopts": options
.CredentialsOptionsDouble
,
866 takes_args
= ["URL1", "URL2", "context1?", "context2?", "context3?"]
869 Option("-w", "--two", dest
="two", action
="store_true", default
=False,
870 help="Hosts are in two different domains"),
871 Option("-q", "--quiet", dest
="quiet", action
="store_true", default
=False,
872 help="Do not print anything but relay on just exit code"),
873 Option("-v", "--verbose", dest
="verbose", action
="store_true", default
=False,
874 help="Print all DN pairs that have been compared"),
875 Option("--sd", dest
="descriptor", action
="store_true", default
=False,
876 help="Compare nTSecurityDescriptor attibutes only"),
877 Option("--sort-aces", dest
="sort_aces", action
="store_true", default
=False,
878 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
879 Option("--view", dest
="view", default
="section",
880 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
881 Option("--base", dest
="base", default
="",
882 help="Pass search base that will build DN list for the first DC."),
883 Option("--base2", dest
="base2", default
="",
884 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
885 Option("--scope", dest
="scope", default
="SUB",
886 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
887 Option("--filter", dest
="filter", default
="",
888 help="List of comma separated attributes to ignore in the comparision"),
891 def run(self
, URL1
, URL2
,
892 context1
=None, context2
=None, context3
=None,
893 two
=False, quiet
=False, verbose
=False, descriptor
=False, sort_aces
=False,
894 view
="section", base
="", base2
="", scope
="SUB", filter="",
895 credopts
=None, sambaopts
=None, versionopts
=None):
897 lp
= sambaopts
.get_loadparm()
899 using_ldap
= URL1
.startswith("ldap") or URL2
.startswith("ldap")
902 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
905 creds2
= credopts
.get_credentials2(lp
, guess
=False)
906 if creds2
.is_anonymous():
909 creds2
.set_domain("")
910 creds2
.set_workstation("")
911 if using_ldap
and not creds
.authentication_requested():
912 raise CommandError("You must supply at least one username/password pair")
914 # make a list of contexts to compare in
918 # If search bases are specified context is defaulted to
919 # DOMAIN so the given search bases can be verified.
920 contexts
= ["DOMAIN"]
922 # if no argument given, we compare all contexts
923 contexts
= ["DOMAIN", "CONFIGURATION", "SCHEMA"]
925 for c
in [context1
, context2
, context3
]:
928 if not c
.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
929 raise CommandError("Incorrect argument: %s" % c
)
930 contexts
.append(c
.upper())
932 if verbose
and quiet
:
933 raise CommandError("You cannot set --verbose and --quiet together")
934 if (not base
and base2
) or (base
and not base2
):
935 raise CommandError("You need to specify both --base and --base2 at the same time")
936 if descriptor
and view
.upper() not in ["SECTION", "COLLISION"]:
937 raise CommandError("Invalid --view value. Choose from: section or collision")
938 if not scope
.upper() in ["SUB", "ONE", "BASE"]:
939 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
941 con1
= LDAPBase(URL1
, creds
, lp
,
942 two
=two
, quiet
=quiet
, descriptor
=descriptor
, sort_aces
=sort_aces
,
943 verbose
=verbose
,view
=view
, base
=base
, scope
=scope
)
944 assert len(con1
.base_dn
) > 0
946 con2
= LDAPBase(URL2
, creds2
, lp
,
947 two
=two
, quiet
=quiet
, descriptor
=descriptor
, sort_aces
=sort_aces
,
948 verbose
=verbose
, view
=view
, base
=base2
, scope
=scope
)
949 assert len(con2
.base_dn
) > 0
951 filter_list
= filter.split(",")
954 for context
in contexts
:
956 self
.outf
.write("\n* Comparing [%s] context...\n" % context
)
958 b1
= LDAPBundel(con1
, context
=context
, filter_list
=filter_list
)
959 b2
= LDAPBundel(con2
, context
=context
, filter_list
=filter_list
)
963 self
.outf
.write("\n* Result for [%s]: SUCCESS\n" %
967 self
.outf
.write("\n* Result for [%s]: FAILURE\n" % context
)
969 assert len(b1
.summary
["df_value_attrs"]) == len(b2
.summary
["df_value_attrs"])
970 b2
.summary
["df_value_attrs"] = []
971 self
.outf
.write("\nSUMMARY\n")
972 self
.outf
.write("---------\n")
975 # mark exit status as FAILURE if a least one comparison failed
978 raise CommandError("Compare failed: %d" % status
)