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
12
# This program is free software; you can redistribute it and/or modify
13
# it under the terms of the GNU General Public License as published by
14
# the Free Software Foundation; either version 3 of the License, or
15
# (at your option) any later version.
17
# This program is distributed in the hope that it will be useful,
18
# but WITHOUT ANY WARRANTY; without even the implied warranty of
19
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
# GNU General Public License for more details.
22
# You should have received a copy of the GNU General Public License
23
# along with this program. If not, see <http://www.gnu.org/licenses/>.
31
import samba.getopt as options
33
from samba.ndr import ndr_pack, ndr_unpack
34
from samba.dcerpc import security
35
from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, ERR_NO_SUCH_OBJECT, LdbError
36
from samba.netcmd import (
46
class LDAPBase(object):
48
def __init__(self, host, creds, lp,
49
two=False, quiet=False, descriptor=False, sort_aces=False, verbose=False,
50
view="section", base="", scope="SUB"):
54
if os.path.isfile(host):
55
samdb_url = "tdb://%s" % host
57
samdb_url = "ldap://%s" % host
58
# use 'paged_search' module when connecting remotely
59
if samdb_url.lower().startswith("ldap://"):
60
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.base_dn = self.find_basedn()
75
self.domain_netbios = self.find_netbios()
76
self.server_names = self.find_servers()
77
self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
78
self.domain_sid = self.find_domain_sid()
82
# Log some domain controller specific place-holers that are being used
83
# when compare content of two DCs. Uncomment for DEBUG purposes.
84
if self.two_domains and not self.quiet:
85
print "\n* Place-holders for %s:" % self.host
86
print 4*" " + "${DOMAIN_DN} => %s" % self.base_dn
87
print 4*" " + "${DOMAIN_NETBIOS} => %s" % self.domain_netbios
88
print 4*" " + "${SERVER_NAME} => %s" % self.server_names
89
print 4*" " + "${DOMAIN_NAME} => %s" % self.domain_name
91
def find_domain_sid(self):
92
res = self.ldb.search(base=self.base_dn, expression="(objectClass=*)", scope=SCOPE_BASE)
93
return ndr_unpack(security.dom_sid,res[0]["objectSid"][0])
95
def find_servers(self):
98
res = self.ldb.search(base="OU=Domain Controllers,%s" % self.base_dn, \
99
scope=SCOPE_SUBTREE, expression="(objectClass=computer)", attrs=["cn"])
103
srv.append(x["cn"][0])
106
def find_netbios(self):
107
res = self.ldb.search(base="CN=Partitions,CN=Configuration,%s" % self.base_dn, \
108
scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
111
if "nETBIOSName" in x.keys():
112
return x["nETBIOSName"][0]
114
def find_basedn(self):
115
res = self.ldb.search(base="", expression="(objectClass=*)", scope=SCOPE_BASE,
116
attrs=["defaultNamingContext"])
118
return res[0]["defaultNamingContext"][0]
120
def object_exists(self, object_dn):
123
res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
124
except LdbError, (enum, estr):
125
if enum == ERR_NO_SUCH_OBJECT:
130
def delete_force(self, object_dn):
132
self.ldb.delete(object_dn)
133
except Ldb.LdbError, e:
134
assert "No such object" in str(e)
136
def get_attribute_name(self, key):
137
""" Returns the real attribute name
138
It resolved ranged results e.g. member;range=0-1499
141
r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
149
def get_attribute_values(self, object_dn, key, vals):
150
""" Returns list with all attribute values
151
It resolved ranged results e.g. member;range=0-1499
154
r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
158
# no range, just return the values
164
# get additional values in a loop
165
# until we get a response with '*' at the end
168
n = "%s;range=%d-*" % (attr, hi + 1)
169
res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
177
for key in res.keys():
183
if m.group(1) != attr:
187
fvals = list(res[key])
194
if fm.group(3) == "*":
195
# if we got "*" we're done
198
assert int(fm.group(2)) == hi + 1
199
hi = int(fm.group(3))
203
def get_attributes(self, object_dn):
204
""" Returns dict with all default visible attributes
206
res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
209
# 'Dn' element is not iterable and we have it as 'distinguishedName'
211
for key in res.keys():
212
vals = list(res[key])
214
name = self.get_attribute_name(key)
215
res[name] = self.get_attribute_values(object_dn, key, vals)
219
def get_descriptor_sddl(self, object_dn):
220
res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
221
desc = res[0]["nTSecurityDescriptor"][0]
222
desc = ndr_unpack(security.descriptor, desc)
223
return desc.as_sddl(self.domain_sid)
225
def guid_as_string(self, guid_blob):
226
""" Translate binary representation of schemaIDGUID to standard string representation.
227
@gid_blob: binary schemaIDGUID
229
blob = "%s" % guid_blob
230
stops = [4, 2, 2, 2, 6]
234
while x < len(stops):
238
c = hex(ord(blob[index])).replace("0x", "")
239
c = [None, "0" + c, c][len(c)]
240
if 2 * index < len(blob):
248
assert index == len(blob)
249
return res.strip().replace(" ", "-")
251
def get_guid_map(self):
252
""" Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
255
res = self.ldb.search(base="cn=schema,cn=configuration,%s" % self.base_dn, \
256
expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
258
self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
260
res = self.ldb.search(base="cn=extended-rights,cn=configuration,%s" % self.base_dn, \
261
expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
263
self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
265
def get_sid_map(self):
266
""" Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
269
res = self.ldb.search(base="%s" % self.base_dn, \
270
expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
273
self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
277
class Descriptor(object):
278
def __init__(self, connection, dn):
279
self.con = connection
281
self.sddl = self.con.get_descriptor_sddl(self.dn)
282
self.dacl_list = self.extract_dacl()
283
if self.con.sort_aces:
284
self.dacl_list.sort()
286
def extract_dacl(self):
287
""" Extracts the DACL as a list of ACE string (with the brakets).
290
if "S:" in self.sddl:
291
res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
293
res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
294
except AttributeError:
296
return re.findall("(\(.*?\))", res)
298
def fix_guid(self, ace):
300
guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
301
# If there are not GUIDs to replace return the same ACE
306
name = self.con.guid_map[guid.lower()]
307
res = res.replace(guid, name)
309
# Do not bother if the GUID is not found in
310
# cn=Schema or cn=Extended-Rights
314
def fix_sid(self, ace):
316
sids = re.findall("S-[-0-9]+", res)
317
# If there are not SIDs to replace return the same ACE
322
name = self.con.sid_map[sid]
323
res = res.replace(sid, name)
325
# Do not bother if the SID is not found in baseDN
329
def fixit(self, ace):
330
""" Combine all replacement methods in one
333
res = self.fix_guid(res)
334
res = self.fix_sid(res)
337
def diff_1(self, other):
339
if len(self.dacl_list) != len(other.dacl_list):
340
res += 4*" " + "Difference in ACE count:\n"
341
res += 8*" " + "=> %s\n" % len(self.dacl_list)
342
res += 8*" " + "=> %s\n" % len(other.dacl_list)
350
self_ace = "%s" % self.dacl_list[i]
355
other_ace = "%s" % other.dacl_list[i]
358
if len(self_ace) + len(other_ace) == 0:
360
self_ace_fixed = "%s" % self.fixit(self_ace)
361
other_ace_fixed = "%s" % other.fixit(other_ace)
362
if self_ace_fixed != other_ace_fixed:
363
res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
366
res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
370
def diff_2(self, other):
372
if len(self.dacl_list) != len(other.dacl_list):
373
res += 4*" " + "Difference in ACE count:\n"
374
res += 8*" " + "=> %s\n" % len(self.dacl_list)
375
res += 8*" " + "=> %s\n" % len(other.dacl_list)
380
self_dacl_list_fixed = []
381
other_dacl_list_fixed = []
382
[self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
383
[other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
384
for ace in self_dacl_list_fixed:
386
other_dacl_list_fixed.index(ace)
388
self_aces.append(ace)
390
common_aces.append(ace)
391
self_aces = sorted(self_aces)
392
if len(self_aces) > 0:
393
res += 4*" " + "ACEs found only in %s:\n" % self.con.host
394
for ace in self_aces:
395
res += 8*" " + ace + "\n"
397
for ace in other_dacl_list_fixed:
399
self_dacl_list_fixed.index(ace)
401
other_aces.append(ace)
403
common_aces.append(ace)
404
other_aces = sorted(other_aces)
405
if len(other_aces) > 0:
406
res += 4*" " + "ACEs found only in %s:\n" % other.con.host
407
for ace in other_aces:
408
res += 8*" " + ace + "\n"
410
common_aces = sorted(list(set(common_aces)))
412
res += 4*" " + "ACEs found in both:\n"
413
for ace in common_aces:
414
res += 8*" " + ace + "\n"
415
return (self_aces == [] and other_aces == [], res)
417
class LDAPObject(object):
418
def __init__(self, connection, dn, summary):
419
self.con = connection
420
self.two_domains = self.con.two_domains
421
self.quiet = self.con.quiet
422
self.verbose = self.con.verbose
423
self.summary = summary
424
self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
425
self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
426
for x in self.con.server_names:
427
self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
428
self.attributes = self.con.get_attributes(self.dn)
429
# Attributes that are considered always to be different e.g based on timestamp etc.
431
# One domain - two domain controllers
432
self.ignore_attributes = [
433
# Default Naming Context
434
"lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
435
"operatingSystemVersion","oEMInformation",
436
# Configuration Naming Context
437
"repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
438
"replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
439
# Schema Naming Context
441
self.dn_attributes = []
442
self.domain_attributes = []
443
self.servername_attributes = []
444
self.netbios_attributes = []
445
self.other_attributes = []
446
# Two domains - two domain controllers
449
self.ignore_attributes += [
450
"objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
451
"modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
452
"fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
453
"badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
454
"ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
455
# After Exchange preps
456
"targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
458
# Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
459
self.dn_attributes = [
460
"distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
461
"homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
462
"msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
463
"msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
464
# After Exchange preps
465
"msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
466
"msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
467
"addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
468
"msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
469
self.dn_attributes = [x.upper() for x in self.dn_attributes]
471
# Attributes that contain the Domain name e.g. 'samba.org'
472
self.domain_attributes = [
473
"proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
474
"dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
475
self.domain_attributes = [x.upper() for x in self.domain_attributes]
477
# May contain DOMAIN_NETBIOS and SERVER_NAME
478
self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
479
"servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
480
"msDS-IsDomainFor", "interSiteTopologyGenerator",]
481
self.servername_attributes = [x.upper() for x in self.servername_attributes]
483
self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
484
self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
486
self.other_attributes = [ "name", "DC",]
487
self.other_attributes = [x.upper() for x in self.other_attributes]
489
self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
493
Log on the screen if there is no --quiet oprion set
500
if not self.two_domains:
502
if res.upper().endswith(self.con.base_dn.upper()):
503
res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
506
def fix_domain_name(self, s):
508
if not self.two_domains:
510
res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
511
res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
514
def fix_domain_netbios(self, s):
516
if not self.two_domains:
518
res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
519
res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
522
def fix_server_name(self, s):
524
if not self.two_domains or len(self.con.server_names) > 1:
526
for x in self.con.server_names:
527
res = res.upper().replace(x, "${SERVER_NAME}")
530
def __eq__(self, other):
531
if self.con.descriptor:
532
return self.cmp_desc(other)
533
return self.cmp_attrs(other)
535
def cmp_desc(self, other):
536
d1 = Descriptor(self.con, self.dn)
537
d2 = Descriptor(other.con, other.dn)
538
if self.con.view == "section":
540
elif self.con.view == "collision":
543
raise Exception("Unknown --view option value.")
545
self.screen_output = res[1][:-1]
546
other.screen_output = res[1][:-1]
550
def cmp_attrs(self, other):
552
self.unique_attrs = []
553
self.df_value_attrs = []
554
other.unique_attrs = []
555
if self.attributes.keys() != other.attributes.keys():
557
title = 4*" " + "Attributes found only in %s:" % self.con.host
558
for x in self.attributes.keys():
559
if not x in other.attributes.keys() and \
560
not x.upper() in [q.upper() for q in other.ignore_attributes]:
564
res += 8*" " + x + "\n"
565
self.unique_attrs.append(x)
567
title = 4*" " + "Attributes found only in %s:" % other.con.host
568
for x in other.attributes.keys():
569
if not x in self.attributes.keys() and \
570
not x.upper() in [q.upper() for q in self.ignore_attributes]:
574
res += 8*" " + x + "\n"
575
other.unique_attrs.append(x)
577
missing_attrs = [x.upper() for x in self.unique_attrs]
578
missing_attrs += [x.upper() for x in other.unique_attrs]
579
title = 4*" " + "Difference in attribute values:"
580
for x in self.attributes.keys():
581
if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
583
if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
584
self.attributes[x] = sorted(self.attributes[x])
585
other.attributes[x] = sorted(other.attributes[x])
586
if self.attributes[x] != other.attributes[x]:
591
# First check if the difference can be fixed but shunting the first part
592
# of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
593
if x.upper() in self.other_attributes:
594
p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
595
q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
598
# Attribute values that are list that contain DN based values that may differ
599
elif x.upper() in self.dn_attributes:
603
m = self.attributes[x]
604
n = other.attributes[x]
605
p = [self.fix_dn(j) for j in m]
606
q = [other.fix_dn(j) for j in n]
609
# Attributes that contain the Domain name in them
610
if x.upper() in self.domain_attributes:
614
m = self.attributes[x]
615
n = other.attributes[x]
616
p = [self.fix_domain_name(j) for j in m]
617
q = [other.fix_domain_name(j) for j in n]
621
if x.upper() in self.servername_attributes:
622
# Attributes with SERVER_NAME
626
m = self.attributes[x]
627
n = other.attributes[x]
628
p = [self.fix_server_name(j) for j in m]
629
q = [other.fix_server_name(j) for j in n]
633
if x.upper() in self.netbios_attributes:
634
# Attributes with NETBIOS Domain name
638
m = self.attributes[x]
639
n = other.attributes[x]
640
p = [self.fix_domain_netbios(j) for j in m]
641
q = [other.fix_domain_netbios(j) for j in n]
649
res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
651
res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
652
self.df_value_attrs.append(x)
654
if self.unique_attrs + other.unique_attrs != []:
655
assert self.unique_attrs != other.unique_attrs
656
self.summary["unique_attrs"] += self.unique_attrs
657
self.summary["df_value_attrs"] += self.df_value_attrs
658
other.summary["unique_attrs"] += other.unique_attrs
659
other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
661
self.screen_output = res[:-1]
662
other.screen_output = res[:-1]
667
class LDAPBundel(object):
668
def __init__(self, connection, context, dn_list=None):
669
self.con = connection
670
self.two_domains = self.con.two_domains
671
self.quiet = self.con.quiet
672
self.verbose = self.con.verbose
673
self.search_base = self.con.search_base
674
self.search_scope = self.con.search_scope
676
self.summary["unique_attrs"] = []
677
self.summary["df_value_attrs"] = []
678
self.summary["known_ignored_dn"] = []
679
self.summary["abnormal_ignored_dn"] = []
681
self.dn_list = dn_list
682
elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
683
self.context = context.upper()
684
self.dn_list = self.get_dn_list(context)
686
raise Exception("Unknown initialization data for LDAPBundel().")
688
while counter < len(self.dn_list) and self.two_domains:
689
# Use alias reference
690
tmp = self.dn_list[counter]
691
tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
692
tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
693
if len(self.con.server_names) == 1:
694
for x in self.con.server_names:
695
tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
696
self.dn_list[counter] = tmp
698
self.dn_list = list(set(self.dn_list))
699
self.dn_list = sorted(self.dn_list)
700
self.size = len(self.dn_list)
704
Log on the screen if there is no --quiet oprion set
709
def update_size(self):
710
self.size = len(self.dn_list)
711
self.dn_list = sorted(self.dn_list)
713
def __eq__(self, other):
715
if self.size != other.size:
716
self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
719
# This is the case where we want to explicitly compare two objects with different DNs.
720
# It does not matter if they are in the same DC, in two DC in one domain or in two
722
if self.search_scope != SCOPE_BASE:
723
title= "\n* DNs found only in %s:" % self.con.host
724
for x in self.dn_list:
725
if not x.upper() in [q.upper() for q in other.dn_list]:
730
self.log( 4*" " + x )
731
self.dn_list[self.dn_list.index(x)] = ""
732
self.dn_list = [x for x in self.dn_list if x]
734
title= "\n* DNs found only in %s:" % other.con.host
735
for x in other.dn_list:
736
if not x.upper() in [q.upper() for q in self.dn_list]:
741
self.log( 4*" " + x )
742
other.dn_list[other.dn_list.index(x)] = ""
743
other.dn_list = [x for x in other.dn_list if x]
747
assert self.size == other.size
748
assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
749
self.log( "\n* Objects to be compared: %s" % self.size )
752
while index < self.size:
755
object1 = LDAPObject(connection=self.con,
756
dn=self.dn_list[index],
757
summary=self.summary)
758
except LdbError, (enum, estr):
759
if enum == ERR_NO_SUCH_OBJECT:
760
self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
764
object2 = LDAPObject(connection=other.con,
765
dn=other.dn_list[index],
766
summary=other.summary)
767
except LdbError, (enum, estr):
768
if enum == ERR_NO_SUCH_OBJECT:
769
self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
775
if object1 == object2:
777
self.log( "\nComparing:" )
778
self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
779
self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
780
self.log( 4*" " + "OK" )
782
self.log( "\nComparing:" )
783
self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
784
self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
785
self.log( object1.screen_output )
786
self.log( 4*" " + "FAILED" )
788
self.summary = object1.summary
789
other.summary = object2.summary
794
def get_dn_list(self, context):
795
""" Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
796
Parse all DNs and filter those that are 'strange' or abnormal.
798
if context.upper() == "DOMAIN":
799
search_base = "%s" % self.con.base_dn
800
elif context.upper() == "CONFIGURATION":
801
search_base = "CN=Configuration,%s" % self.con.base_dn
802
elif context.upper() == "SCHEMA":
803
search_base = "CN=Schema,CN=Configuration,%s" % self.con.base_dn
806
if not self.search_base:
807
self.search_base = search_base
808
self.search_scope = self.search_scope.upper()
809
if self.search_scope == "SUB":
810
self.search_scope = SCOPE_SUBTREE
811
elif self.search_scope == "BASE":
812
self.search_scope = SCOPE_BASE
813
elif self.search_scope == "ONE":
814
self.search_scope = SCOPE_ONELEVEL
816
raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
817
if not self.search_base.upper().endswith(search_base.upper()):
818
raise StandardError("Invalid search base specified: %s" % self.search_base)
819
res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
821
dn_list.append(x["dn"].get_linearized())
827
def print_summary(self):
828
self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
829
self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
831
if self.summary["unique_attrs"]:
832
self.log( "\nAttributes found only in %s:" % self.con.host )
833
self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
835
if self.summary["df_value_attrs"]:
836
self.log( "\nAttributes with different values:" )
837
self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
838
self.summary["df_value_attrs"] = []
840
class cmd_ldapcmp(Command):
841
"""compare two ldap databases"""
842
synopsis = "ldapcmp URL1 URL2 <domain|configuration|schema> [options]"
844
takes_optiongroups = {
845
"sambaopts": options.SambaOptions,
846
"versionopts": options.VersionOptions,
847
"credopts": options.CredentialsOptionsDouble,
850
takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
853
Option("-w", "--two", dest="two", action="store_true", default=False,
854
help="Hosts are in two different domains"),
855
Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
856
help="Do not print anything but relay on just exit code"),
857
Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
858
help="Print all DN pairs that have been compared"),
859
Option("--sd", dest="descriptor", action="store_true", default=False,
860
help="Compare nTSecurityDescriptor attibutes only"),
861
Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
862
help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
863
Option("--view", dest="view", default="section",
864
help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
865
Option("--base", dest="base", default="",
866
help="Pass search base that will build DN list for the first DC."),
867
Option("--base2", dest="base2", default="",
868
help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
869
Option("--scope", dest="scope", default="SUB",
870
help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
873
def run(self, URL1, URL2,
874
context1=None, context2=None, context3=None,
875
two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False, view="section",
876
base="", base2="", scope="SUB", credopts=None, sambaopts=None, versionopts=None):
877
lp = sambaopts.get_loadparm()
878
creds = credopts.get_credentials(lp, fallback_machine=True)
879
creds2 = credopts.get_credentials2(lp, guess=False)
880
if creds2.is_anonymous():
883
creds2.set_domain("")
884
creds2.set_workstation("")
885
if not creds.authentication_requested():
886
raise CommandError("You must supply at least one username/password pair")
888
# make a list of contexts to compare in
892
# If search bases are specified context is defaulted to
893
# DOMAIN so the given search bases can be verified.
894
contexts = ["DOMAIN"]
896
# if no argument given, we compare all contexts
897
contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
899
for c in [context1, context2, context3]:
902
if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA"]:
903
raise CommandError("Incorrect argument: %s" % c)
904
contexts.append(c.upper())
906
if verbose and quiet:
907
raise CommandError("You cannot set --verbose and --quiet together")
908
if (not base and base2) or (base and not base2):
909
raise CommandError("You need to specify both --base and --base2 at the same time")
910
if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
911
raise CommandError("Invalid --view value. Choose from: section or collision")
912
if not scope.upper() in ["SUB", "ONE", "BASE"]:
913
raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
915
con1 = LDAPBase(URL1, creds, lp,
916
two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
917
verbose=verbose,view=view, base=base, scope=scope)
918
assert len(con1.base_dn) > 0
920
con2 = LDAPBase(URL2, creds2, lp,
921
two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
922
verbose=verbose, view=view, base=base2, scope=scope)
923
assert len(con2.base_dn) > 0
926
for context in contexts:
928
print "\n* Comparing [%s] context..." % context
930
b1 = LDAPBundel(con1, context=context)
931
b2 = LDAPBundel(con2, context=context)
935
print "\n* Result for [%s]: SUCCESS" % context
938
print "\n* Result for [%s]: FAILURE" % context
940
assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
941
b2.summary["df_value_attrs"] = []
946
# mark exit status as FAILURE if a least one comparison failed
949
raise CommandError("Compare failed: %d" % status)