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 (
40 RE_RANGED_RESULT
= re
.compile(r
"^([^;]+);range=(\d+)-(\d+|\*)$")
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"])
108 return [str(x
["cn"][0]) for x
in res
]
110 def find_netbios(self
):
112 res
= self
.ldb
.search(base
="CN=Partitions,%s" % self
.config_dn
,
113 scope
=SCOPE_SUBTREE
, attrs
=["nETBIOSName"])
114 except LdbError
as e
:
116 if estr
in ["Operation unavailable without authentication"]:
117 raise CommandError(estr
, e
)
120 raise CommandError("Could not find netbios name")
123 if "nETBIOSName" in x
:
124 return x
["nETBIOSName"][0].decode()
126 def object_exists(self
, object_dn
):
129 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
)
130 except LdbError
as e2
:
131 (enum
, estr
) = e2
.args
132 if enum
== ERR_NO_SUCH_OBJECT
:
137 def delete_force(self
, object_dn
):
139 self
.ldb
.delete(object_dn
)
140 except Ldb
.LdbError
as e
:
141 assert "No such object" in str(e
)
143 def get_attribute_name(self
, key
):
144 """ Returns the real attribute name
145 It resolved ranged results e.g. member;range=0-1499
148 m
= RE_RANGED_RESULT
.match(key
)
154 def get_attribute_values(self
, object_dn
, key
, vals
):
155 """ Returns list with all attribute values
156 It resolved ranged results e.g. member;range=0-1499
159 m
= RE_RANGED_RESULT
.match(key
)
161 # no range, just return the values
167 # get additional values in a loop
168 # until we get a response with '*' at the end
171 n
= "%s;range=%d-*" % (attr
, hi
+ 1)
172 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
, attrs
=[n
])
181 m
= RE_RANGED_RESULT
.match(key
)
186 if m
.group(1) != attr
:
190 fvals
= list(res
[key
])
197 if fm
.group(3) == "*":
198 # if we got "*" we're done
201 assert int(fm
.group(2)) == hi
+ 1
202 hi
= int(fm
.group(3))
206 def get_attributes(self
, object_dn
):
207 """ Returns dict with all default visible attributes
209 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
, attrs
=["*"])
212 # 'Dn' element is not iterable and we have it as 'distinguishedName'
216 for key
, vals
in res
.items():
217 name
= self
.get_attribute_name(key
)
218 # sort vals and return a list, help to compare
220 attributes
[name
] = self
.get_attribute_values(object_dn
, key
, vals
)
224 def get_descriptor_sddl(self
, object_dn
):
225 res
= self
.ldb
.search(base
=object_dn
, scope
=SCOPE_BASE
, attrs
=["nTSecurityDescriptor"])
226 desc
= res
[0]["nTSecurityDescriptor"][0]
227 desc
= ndr_unpack(security
.descriptor
, desc
)
228 return desc
.as_sddl(self
.domain_sid
)
230 def get_sid_map(self
):
231 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
234 res
= self
.ldb
.search(base
=self
.base_dn
,
235 expression
="(objectSid=*)", scope
=SCOPE_SUBTREE
, attrs
=["objectSid", "sAMAccountName"])
238 self
.sid_map
["%s" % ndr_unpack(security
.dom_sid
, item
["objectSid"][0])] = str(item
["sAMAccountName"][0])
243 class Descriptor(object):
244 def __init__(self
, connection
, dn
, outf
=sys
.stdout
, errf
=sys
.stderr
):
247 self
.con
= connection
249 self
.sddl
= self
.con
.get_descriptor_sddl(self
.dn
)
250 self
.dacl_list
= self
.extract_dacl()
251 if self
.con
.sort_aces
:
252 self
.dacl_list
.sort()
254 def extract_dacl(self
):
255 """ Extracts the DACL as a list of ACE string (with the brackets).
258 if "S:" in self
.sddl
:
259 res
= re
.search(r
"D:(.*?)(\(.*?\))S:", self
.sddl
).group(2)
261 res
= re
.search(r
"D:(.*?)(\(.*\))", self
.sddl
).group(2)
262 except AttributeError:
264 return re
.findall(r
"(\(.*?\))", res
)
266 def fix_sid(self
, ace
):
268 sids
= re
.findall("S-[-0-9]+", res
)
269 # If there are not SIDs to replace return the same ACE
274 name
= self
.con
.sid_map
[sid
]
275 res
= res
.replace(sid
, name
)
277 # Do not bother if the SID is not found in baseDN
281 def diff_1(self
, other
):
283 if len(self
.dacl_list
) != len(other
.dacl_list
):
284 res
+= 4 * " " + "Difference in ACE count:\n"
285 res
+= 8 * " " + "=> %s\n" % len(self
.dacl_list
)
286 res
+= 8 * " " + "=> %s\n" % len(other
.dacl_list
)
294 self_ace
= "%s" % self
.dacl_list
[i
]
299 other_ace
= "%s" % other
.dacl_list
[i
]
302 if len(self_ace
) + len(other_ace
) == 0:
304 self_ace_fixed
= "%s" % self
.fix_sid(self_ace
)
305 other_ace_fixed
= "%s" % other
.fix_sid(other_ace
)
306 if self_ace_fixed
!= other_ace_fixed
:
307 res
+= "%60s * %s\n" % (self_ace_fixed
, other_ace_fixed
)
310 res
+= "%60s | %s\n" % (self_ace_fixed
, other_ace_fixed
)
314 def diff_2(self
, other
):
316 if len(self
.dacl_list
) != len(other
.dacl_list
):
317 res
+= 4 * " " + "Difference in ACE count:\n"
318 res
+= 8 * " " + "=> %s\n" % len(self
.dacl_list
)
319 res
+= 8 * " " + "=> %s\n" % len(other
.dacl_list
)
324 self_dacl_list_fixed
= [self
.fix_sid(ace
) for ace
in self
.dacl_list
]
325 other_dacl_list_fixed
= [other
.fix_sid(ace
) for ace
in other
.dacl_list
]
326 for ace
in self_dacl_list_fixed
:
328 other_dacl_list_fixed
.index(ace
)
330 self_aces
.append(ace
)
332 common_aces
.append(ace
)
333 self_aces
= sorted(self_aces
)
334 if len(self_aces
) > 0:
335 res
+= 4 * " " + "ACEs found only in %s:\n" % self
.con
.host
336 for ace
in self_aces
:
337 res
+= 8 * " " + ace
+ "\n"
339 for ace
in other_dacl_list_fixed
:
341 self_dacl_list_fixed
.index(ace
)
343 other_aces
.append(ace
)
345 common_aces
.append(ace
)
346 other_aces
= sorted(other_aces
)
347 if len(other_aces
) > 0:
348 res
+= 4 * " " + "ACEs found only in %s:\n" % other
.con
.host
349 for ace
in other_aces
:
350 res
+= 8 * " " + ace
+ "\n"
352 common_aces
= sorted(list(set(common_aces
)))
354 res
+= 4 * " " + "ACEs found in both:\n"
355 for ace
in common_aces
:
356 res
+= 8 * " " + ace
+ "\n"
357 return (self_aces
== [] and other_aces
== [], res
)
360 class LDAPObject(object):
361 def __init__(self
, connection
, dn
, summary
, filter_list
,
362 outf
=sys
.stdout
, errf
=sys
.stderr
):
365 self
.con
= connection
366 self
.two_domains
= self
.con
.two_domains
367 self
.quiet
= self
.con
.quiet
368 self
.verbose
= self
.con
.verbose
369 self
.summary
= summary
370 self
.dn
= dn
.replace("${DOMAIN_DN}", self
.con
.base_dn
)
371 self
.dn
= self
.dn
.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self
.con
.domain_netbios
)
372 for x
in self
.con
.server_names
:
373 self
.dn
= self
.dn
.replace("CN=${SERVER_NAME}", "CN=%s" % x
)
374 self
.attributes
= self
.con
.get_attributes(self
.dn
)
375 # One domain - two domain controllers
377 # Some attributes are defined as FLAG_ATTR_NOT_REPLICATED
379 # The following list was generated by
380 # egrep '^systemFlags: |^ldapDisplayName: |^linkID: ' \
381 # source4/setup/ad-schema/MS-AD_Schema_2K8_R2_Attributes.txt | \
382 # grep -B1 FLAG_ATTR_NOT_REPLICATED | \
383 # grep ldapDisplayName | \
385 self
.non_replicated_attributes
= [
388 "dSCorePropagationData",
393 "msDS-Cached-Membership",
394 "msDS-Cached-Membership-Time-Stamp",
395 "msDS-EnabledFeatureBL",
396 "msDS-ExecuteScriptPassword",
398 "msDS-ReplicationEpoch",
399 "msDS-RetiredReplNCSignatures",
400 "msDS-USNLastSyncSuccess",
401 # "distinguishedName", # This is implicitly replicated
402 # "objectGUID", # This is implicitly replicated
403 "partialAttributeDeletionList",
404 "partialAttributeSet",
407 "replPropertyMetaData",
408 "replUpToDateVector",
412 "rIDPreviousAllocationPool",
419 "whenChanged", # This is implicitly replicated, but may diverge on updates of non-replicated attributes
421 self
.ignore_attributes
= self
.non_replicated_attributes
422 self
.ignore_attributes
+= ["msExchServer1HighestUSN"]
424 self
.ignore_attributes
+= filter_list
426 self
.dn_attributes
= []
427 self
.domain_attributes
= []
428 self
.servername_attributes
= []
429 self
.netbios_attributes
= []
430 self
.other_attributes
= []
431 # Two domains - two domain controllers
434 self
.ignore_attributes
+= [
435 "objectCategory", "objectGUID", "objectSid", "whenCreated",
436 "whenChanged", "pwdLastSet", "uSNCreated", "creationTime",
437 "modifiedCount", "priorSetTime", "rIDManagerReference",
438 "gPLink", "ipsecNFAReference", "fRSPrimaryMember",
439 "fSMORoleOwner", "masteredBy", "ipsecOwnersReference",
440 "wellKnownObjects", "otherWellKnownObjects", "badPwdCount",
441 "ipsecISAKMPReference", "ipsecFilterReference",
442 "msDs-masteredBy", "lastSetTime",
443 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath",
444 "accountExpires", "invocationId",
445 "operatingSystem", "operatingSystemVersion",
446 "oEMInformation", "schemaInfo",
447 # After Exchange preps
448 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
450 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
451 self
.dn_attributes
= [
452 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
453 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
454 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
455 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
456 # After Exchange preps
457 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
458 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
459 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
460 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",
461 # After 2012 R2 functional preparation
462 "msDS-MembersOfResourcePropertyListBL",
463 "msDS-ValueTypeReference",
464 "msDS-MembersOfResourcePropertyList",
465 "msDS-ValueTypeReferenceBL",
466 "msDS-ClaimTypeAppliesToClass",
468 self
.dn_attributes
= [x
.upper() for x
in self
.dn_attributes
]
470 # Attributes that contain the Domain name e.g. 'samba.org'
471 self
.domain_attributes
= [
472 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
473 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName", ]
474 self
.domain_attributes
= [x
.upper() for x
in self
.domain_attributes
]
476 # May contain DOMAIN_NETBIOS and SERVER_NAME
477 self
.servername_attributes
= ["distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
478 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
479 "msDS-IsDomainFor", "interSiteTopologyGenerator", ]
480 self
.servername_attributes
= [x
.upper() for x
in self
.servername_attributes
]
482 self
.netbios_attributes
= ["servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name", ]
483 self
.netbios_attributes
= [x
.upper() for x
in self
.netbios_attributes
]
485 self
.other_attributes
= ["name", "DC", ]
486 self
.other_attributes
= [x
.upper() for x
in self
.other_attributes
]
488 self
.ignore_attributes
= set([x
.upper() for x
in self
.ignore_attributes
])
492 Log on the screen if there is no --quiet option set
495 self
.outf
.write(msg
+"\n")
499 if not self
.two_domains
:
501 if res
.upper().endswith(self
.con
.base_dn
.upper()):
502 res
= res
[:len(res
) - len(self
.con
.base_dn
)] + "${DOMAIN_DN}"
505 def fix_domain_name(self
, s
):
507 if not self
.two_domains
:
509 res
= res
.replace(self
.con
.domain_name
.lower(), self
.con
.domain_name
.upper())
510 res
= res
.replace(self
.con
.domain_name
.upper(), "${DOMAIN_NAME}")
513 def fix_domain_netbios(self
, s
):
515 if not self
.two_domains
:
517 res
= res
.replace(self
.con
.domain_netbios
.lower(), self
.con
.domain_netbios
.upper())
518 res
= res
.replace(self
.con
.domain_netbios
.upper(), "${DOMAIN_NETBIOS}")
521 def fix_server_name(self
, s
):
523 if not self
.two_domains
or len(self
.con
.server_names
) > 1:
525 for x
in self
.con
.server_names
:
526 res
= res
.upper().replace(x
, "${SERVER_NAME}")
529 def __eq__(self
, other
):
530 if self
.con
.descriptor
:
531 return self
.cmp_desc(other
)
532 return self
.cmp_attrs(other
)
534 def cmp_desc(self
, other
):
535 d1
= Descriptor(self
.con
, self
.dn
, outf
=self
.outf
, errf
=self
.errf
)
536 d2
= Descriptor(other
.con
, other
.dn
, outf
=self
.outf
, errf
=self
.errf
)
537 if self
.con
.view
== "section":
539 elif self
.con
.view
== "collision":
542 raise ValueError(f
"Unknown --view option value: {self.con.view}")
544 self
.screen_output
= res
[1]
545 other
.screen_output
= res
[1]
549 def cmp_attrs(self
, other
):
551 self
.df_value_attrs
= []
553 self_attrs
= set([attr
.upper() for attr
in self
.attributes
])
554 other_attrs
= set([attr
.upper() for attr
in other
.attributes
])
556 self_unique_attrs
= self_attrs
- other_attrs
- other
.ignore_attributes
557 if self_unique_attrs
:
558 res
+= 4 * " " + "Attributes found only in %s:" % self
.con
.host
559 for x
in self_unique_attrs
:
560 res
+= 8 * " " + x
+ "\n"
562 other_unique_attrs
= other_attrs
- self_attrs
- self
.ignore_attributes
563 if other_unique_attrs
:
564 res
+= 4 * " " + "Attributes found only in %s:" % other
.con
.host
565 for x
in other_unique_attrs
:
566 res
+= 8 * " " + x
+ "\n"
568 missing_attrs
= self_unique_attrs
& other_unique_attrs
569 title
= 4 * " " + "Difference in attribute values:"
570 for x
in self
.attributes
:
571 if x
.upper() in self
.ignore_attributes
or x
.upper() in missing_attrs
:
573 ours
= self
.attributes
[x
]
574 theirs
= other
.attributes
.get(x
)
576 if isinstance(ours
, list) and isinstance(theirs
, list):
578 theirs
= sorted(theirs
)
585 # First check if the difference can be fixed but shunting the first part
586 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
587 if x
.upper() in self
.other_attributes
:
588 p
= [self
.con
.domain_name
.split(".")[0] == j
for j
in ours
]
589 q
= [other
.con
.domain_name
.split(".")[0] == j
for j
in theirs
]
592 # Attribute values that are list that contain DN based values that may differ
593 elif x
.upper() in self
.dn_attributes
:
596 p
= [self
.fix_dn(j
) for j
in m
]
597 q
= [other
.fix_dn(j
) for j
in n
]
600 # Attributes that contain the Domain name in them
601 if x
.upper() in self
.domain_attributes
:
607 p
= [self
.fix_domain_name(j
) for j
in m
]
608 q
= [other
.fix_domain_name(j
) for j
in n
]
612 if x
.upper() in self
.servername_attributes
:
613 # Attributes with SERVER_NAME
619 p
= [self
.fix_server_name(j
) for j
in m
]
620 q
= [other
.fix_server_name(j
) for j
in n
]
624 if x
.upper() in self
.netbios_attributes
:
625 # Attributes with NETBIOS Domain name
631 p
= [self
.fix_domain_netbios(j
) for j
in m
]
632 q
= [other
.fix_domain_netbios(j
) for j
in n
]
640 res
+= 8 * " " + x
+ " => \n%s\n%s" % (p
, q
) + "\n"
642 res
+= 8 * " " + x
+ " => \n%s\n%s" % (ours
, theirs
) + "\n"
643 self
.df_value_attrs
.append(x
)
646 assert self_unique_attrs
!= other_unique_attrs
647 self
.summary
["unique_attrs"] += list(self_unique_attrs
)
648 self
.summary
["df_value_attrs"] += self
.df_value_attrs
649 other
.summary
["unique_attrs"] += list(other_unique_attrs
)
650 other
.summary
["df_value_attrs"] += self
.df_value_attrs
# they are the same
652 self
.screen_output
= res
653 other
.screen_output
= res
658 class LDAPBundle(object):
660 def __init__(self
, connection
, context
, dn_list
=None, filter_list
=None,
661 outf
=sys
.stdout
, errf
=sys
.stderr
):
664 self
.con
= connection
665 self
.two_domains
= self
.con
.two_domains
666 self
.quiet
= self
.con
.quiet
667 self
.verbose
= self
.con
.verbose
668 self
.search_base
= self
.con
.search_base
669 self
.search_scope
= self
.con
.search_scope
670 self
.skip_missing_dn
= self
.con
.skip_missing_dn
672 self
.summary
["unique_attrs"] = []
673 self
.summary
["df_value_attrs"] = []
674 self
.summary
["known_ignored_dn"] = []
675 self
.summary
["abnormal_ignored_dn"] = []
676 self
.filter_list
= filter_list
678 self
.dn_list
= dn_list
679 elif context
.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
680 self
.context
= context
.upper()
681 self
.dn_list
= self
.get_dn_list(context
)
683 raise Exception("Unknown initialization data for LDAPBundle().")
685 while counter
< len(self
.dn_list
) and self
.two_domains
:
686 # Use alias reference
687 tmp
= self
.dn_list
[counter
]
688 tmp
= tmp
[:len(tmp
) - len(self
.con
.base_dn
)] + "${DOMAIN_DN}"
689 tmp
= tmp
.replace("CN=%s" % self
.con
.domain_netbios
, "CN=${DOMAIN_NETBIOS}")
690 if len(self
.con
.server_names
) == 1:
691 for x
in self
.con
.server_names
:
692 tmp
= tmp
.replace("CN=%s" % x
, "CN=${SERVER_NAME}")
693 self
.dn_list
[counter
] = tmp
695 self
.dn_list
= list(set(self
.dn_list
))
696 self
.dn_list
= sorted(self
.dn_list
)
697 self
.size
= len(self
.dn_list
)
701 Log on the screen if there is no --quiet option set
704 self
.outf
.write(msg
+ "\n")
706 def update_size(self
):
707 self
.size
= len(self
.dn_list
)
708 self
.dn_list
= sorted(self
.dn_list
)
710 def diff(self
, other
):
712 if self
.size
!= other
.size
:
713 self
.log("\n* DN lists have different size: %s != %s" % (self
.size
, other
.size
))
714 if not self
.skip_missing_dn
:
717 self_dns
= set([q
.upper() for q
in self
.dn_list
])
718 other_dns
= set([q
.upper() for q
in other
.dn_list
])
721 # This is the case where we want to explicitly compare two objects with different DNs.
722 # It does not matter if they are in the same DC, in two DC in one domain or in two
724 if self
.search_scope
!= SCOPE_BASE
and not self
.skip_missing_dn
:
726 self_only
= self_dns
- other_dns
# missing in other
729 self
.log("\n* DNs found only in %s:" % self
.con
.host
)
730 for x
in sorted(self_only
):
731 self
.log(4 * " " + x
)
733 other_only
= other_dns
- self_dns
# missing in self
736 self
.log("\n* DNs found only in %s:" % other
.con
.host
)
737 for x
in sorted(other_only
):
738 self
.log(4 * " " + x
)
740 common_dns
= self_dns
& other_dns
741 self
.log("\n* Objects to be compared: %d" % len(common_dns
))
743 for dn
in common_dns
:
746 object1
= LDAPObject(connection
=self
.con
,
748 summary
=self
.summary
,
749 filter_list
=self
.filter_list
,
750 outf
=self
.outf
, errf
=self
.errf
)
751 except LdbError
as e
:
752 self
.log("LdbError for dn %s: %s" % (dn
, e
))
756 object2
= LDAPObject(connection
=other
.con
,
758 summary
=other
.summary
,
759 filter_list
=self
.filter_list
,
760 outf
=self
.outf
, errf
=self
.errf
)
761 except LdbError
as e
:
762 self
.log("LdbError for dn %s: %s" % (dn
, e
))
765 if object1
== object2
:
767 self
.log("\nComparing:")
768 self
.log("'%s' [%s]" % (object1
.dn
, object1
.con
.host
))
769 self
.log("'%s' [%s]" % (object2
.dn
, object2
.con
.host
))
770 self
.log(4 * " " + "OK")
772 self
.log("\nComparing:")
773 self
.log("'%s' [%s]" % (object1
.dn
, object1
.con
.host
))
774 self
.log("'%s' [%s]" % (object2
.dn
, object2
.con
.host
))
775 self
.log(object1
.screen_output
)
776 self
.log(4 * " " + "FAILED")
778 self
.summary
= object1
.summary
779 other
.summary
= object2
.summary
783 def get_dn_list(self
, context
):
784 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
785 Parse all DNs and filter those that are 'strange' or abnormal.
787 if context
.upper() == "DOMAIN":
788 search_base
= self
.con
.base_dn
789 elif context
.upper() == "CONFIGURATION":
790 search_base
= self
.con
.config_dn
791 elif context
.upper() == "SCHEMA":
792 search_base
= self
.con
.schema_dn
793 elif context
.upper() == "DNSDOMAIN":
794 search_base
= "DC=DomainDnsZones,%s" % self
.con
.base_dn
795 elif context
.upper() == "DNSFOREST":
796 search_base
= "DC=ForestDnsZones,%s" % self
.con
.root_dn
799 if not self
.search_base
:
800 self
.search_base
= search_base
801 self
.search_scope
= self
.search_scope
.upper()
802 if self
.search_scope
== "SUB":
803 self
.search_scope
= SCOPE_SUBTREE
804 elif self
.search_scope
== "BASE":
805 self
.search_scope
= SCOPE_BASE
806 elif self
.search_scope
== "ONE":
807 self
.search_scope
= SCOPE_ONELEVEL
809 raise ValueError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
811 res
= self
.con
.ldb
.search(base
=self
.search_base
, scope
=self
.search_scope
, attrs
=["dn"])
812 except LdbError
as e3
:
813 (enum
, estr
) = e3
.args
814 self
.outf
.write("Failed search of base=%s\n" % self
.search_base
)
817 dn_list
.append(x
["dn"].get_linearized())
820 def print_summary(self
):
821 self
.summary
["unique_attrs"] = list(set(self
.summary
["unique_attrs"]))
822 self
.summary
["df_value_attrs"] = list(set(self
.summary
["df_value_attrs"]))
824 if self
.summary
["unique_attrs"]:
825 self
.log("\nAttributes found only in %s:" % self
.con
.host
)
826 self
.log("".join([str("\n" + 4 * " " + x
) for x
in self
.summary
["unique_attrs"]]))
828 if self
.summary
["df_value_attrs"]:
829 self
.log("\nAttributes with different values:")
830 self
.log("".join([str("\n" + 4 * " " + x
) for x
in self
.summary
["df_value_attrs"]]))
831 self
.summary
["df_value_attrs"] = []
834 class cmd_ldapcmp(Command
):
835 """Compare two ldap databases."""
836 synopsis
= "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
838 takes_optiongroups
= {
839 "sambaopts": options
.SambaOptions
,
840 "versionopts": options
.VersionOptions
,
841 "credopts": options
.CredentialsOptionsDouble
,
844 takes_args
= ["URL1", "URL2", "context1?", "context2?", "context3?", "context4?", "context5?"]
847 Option("-w", "--two", dest
="two", action
="store_true", default
=False,
848 help="Hosts are in two different domains"),
849 Option("-q", "--quiet", dest
="quiet", action
="store_true", default
=False,
850 help="Do not print anything but relay on just exit code"),
851 Option("-v", "--verbose", dest
="verbose", action
="store_true", default
=False,
852 help="Print all DN pairs that have been compared"),
853 Option("--sd", dest
="descriptor", action
="store_true", default
=False,
854 help="Compare nTSecurityDescriptor attributes only"),
855 Option("--sort-aces", dest
="sort_aces", action
="store_true", default
=False,
856 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
857 Option("--view", dest
="view", default
="section", choices
=["section", "collision"],
858 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
859 Option("--base", dest
="base", default
="",
860 help="Pass search base that will build DN list for the first DC."),
861 Option("--base2", dest
="base2", default
="",
862 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
863 Option("--scope", dest
="scope", default
="SUB", choices
=["SUB", "ONE", "BASE"],
864 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
865 Option("--filter", dest
="filter", default
="",
866 help="List of comma separated attributes to ignore in the comparison"),
867 Option("--skip-missing-dn", dest
="skip_missing_dn", action
="store_true", default
=False,
868 help="Skip report and failure due to missing DNs in one server or another"),
871 def run(self
, URL1
, URL2
,
872 context1
=None, context2
=None, context3
=None, context4
=None, context5
=None,
873 two
=False, quiet
=False, verbose
=False, descriptor
=False, sort_aces
=False,
874 view
="section", base
="", base2
="", scope
="SUB", filter="",
875 credopts
=None, sambaopts
=None, versionopts
=None, skip_missing_dn
=False):
877 lp
= sambaopts
.get_loadparm()
879 using_ldap
= URL1
.startswith("ldap") or URL2
.startswith("ldap")
882 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
885 creds2
= credopts
.get_credentials2(lp
, guess
=False)
886 if creds2
.is_anonymous():
889 creds2
.set_domain("")
890 creds2
.set_workstation("")
891 if using_ldap
and not creds
.authentication_requested():
892 raise CommandError("You must supply at least one username/password pair")
894 # make a list of contexts to compare in
898 # If search bases are specified context is defaulted to
899 # DOMAIN so the given search bases can be verified.
900 contexts
= ["DOMAIN"]
902 # if no argument given, we compare all contexts
903 contexts
= ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
905 for c
in [context1
, context2
, context3
, context4
, context5
]:
908 if not c
.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
909 raise CommandError("Incorrect argument: %s" % c
)
910 contexts
.append(c
.upper())
912 if verbose
and quiet
:
913 raise CommandError("You cannot set --verbose and --quiet together")
914 if (not base
and base2
) or (base
and not base2
):
915 raise CommandError("You need to specify both --base and --base2 at the same time")
917 con1
= LDAPBase(URL1
, creds
, lp
,
918 two
=two
, quiet
=quiet
, descriptor
=descriptor
, sort_aces
=sort_aces
,
919 verbose
=verbose
, view
=view
, base
=base
, scope
=scope
,
920 outf
=self
.outf
, errf
=self
.errf
, skip_missing_dn
=skip_missing_dn
)
921 assert len(con1
.base_dn
) > 0
923 con2
= LDAPBase(URL2
, creds2
, lp
,
924 two
=two
, quiet
=quiet
, descriptor
=descriptor
, sort_aces
=sort_aces
,
925 verbose
=verbose
, view
=view
, base
=base2
, scope
=scope
,
926 outf
=self
.outf
, errf
=self
.errf
, skip_missing_dn
=skip_missing_dn
)
927 assert len(con2
.base_dn
) > 0
929 filter_list
= filter.split(",")
932 for context
in contexts
:
934 self
.outf
.write("\n* Comparing [%s] context...\n" % context
)
936 b1
= LDAPBundle(con1
, context
=context
, filter_list
=filter_list
,
937 outf
=self
.outf
, errf
=self
.errf
)
938 b2
= LDAPBundle(con2
, context
=context
, filter_list
=filter_list
,
939 outf
=self
.outf
, errf
=self
.errf
)
943 self
.outf
.write("\n* Result for [%s]: SUCCESS\n" %
947 self
.outf
.write("\n* Result for [%s]: FAILURE\n" % context
)
949 assert len(b1
.summary
["df_value_attrs"]) == len(b2
.summary
["df_value_attrs"])
950 b2
.summary
["df_value_attrs"] = []
951 self
.outf
.write("\nSUMMARY\n")
952 self
.outf
.write("---------\n")
955 # mark exit status as FAILURE if a least one comparison failed
958 raise CommandError("Compare failed: %d" % status
)