s4-python: Remove execute flag from netcmd scripts.
[Samba/gebeck_regimport.git] / source4 / scripting / python / samba / netcmd / ldapcmp.py
blob75708decd01e4f791a1932cdf4479640374a9a70
1 #!/usr/bin/env python
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
8 # above partitions.
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/>.
26 import os
27 import re
28 import sys
30 import samba
31 import samba.getopt as options
32 from samba import Ldb
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 (
37 Command,
38 CommandError,
39 Option,
40 SuperCommand,
43 global summary
44 summary = {}
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"):
51 ldb_options = []
52 samdb_url = host
53 if not "://" in host:
54 if os.path.isfile(host):
55 samdb_url = "tdb://%s" % host
56 else:
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,
62 credentials=creds,
63 lp=lp,
64 options=ldb_options)
65 self.search_base = base
66 self.search_scope = scope
67 self.two_domains = two
68 self.quiet = quiet
69 self.descriptor = descriptor
70 self.sort_aces = sort_aces
71 self.view = view
72 self.verbose = verbose
73 self.host = host
74 self.base_dn = str(self.ldb.get_default_basedn())
75 self.root_dn = str(self.ldb.get_root_basedn())
76 self.config_dn = str(self.ldb.get_config_basedn())
77 self.schema_dn = str(self.ldb.get_schema_basedn())
78 self.domain_netbios = self.find_netbios()
79 self.server_names = self.find_servers()
80 self.domain_name = re.sub("[Dd][Cc]=", "", self.base_dn).replace(",", ".")
81 self.domain_sid = self.find_domain_sid()
82 self.get_guid_map()
83 self.get_sid_map()
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" %
90 self.base_dn)
91 self.outf.write(4*" " + "${DOMAIN_NETBIOS} => %s\n" %
92 self.domain_netbios)
93 self.outf.write(4*" " + "${SERVER_NAME} => %s\n" %
94 self.server_names)
95 self.outf.write(4*" " + "${DOMAIN_NAME} => %s\n" %
96 self.domain_name)
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"])
107 assert len(res) > 0
108 srv = []
109 for x in res:
110 srv.append(x["cn"][0])
111 return srv
113 def find_netbios(self):
114 res = self.ldb.search(base="CN=Partitions,%s" % self.config_dn, \
115 scope=SCOPE_SUBTREE, attrs=["nETBIOSName"])
116 assert len(res) > 0
117 for x in res:
118 if "nETBIOSName" in x.keys():
119 return x["nETBIOSName"][0]
121 def object_exists(self, object_dn):
122 res = None
123 try:
124 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE)
125 except LdbError, (enum, estr):
126 if enum == ERR_NO_SUCH_OBJECT:
127 return False
128 raise
129 return len(res) == 1
131 def delete_force(self, object_dn):
132 try:
133 self.ldb.delete(object_dn)
134 except Ldb.LdbError, e:
135 assert "No such object" in str(e)
137 def get_attribute_name(self, key):
138 """ Returns the real attribute name
139 It resolved ranged results e.g. member;range=0-1499
142 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
144 m = r.match(key)
145 if m is None:
146 return key
148 return m.group(1)
150 def get_attribute_values(self, object_dn, key, vals):
151 """ Returns list with all attribute values
152 It resolved ranged results e.g. member;range=0-1499
155 r = re.compile("^([^;]+);range=(\d+)-(\d+|\*)$")
157 m = r.match(key)
158 if m is None:
159 # no range, just return the values
160 return vals
162 attr = m.group(1)
163 hi = int(m.group(3))
165 # get additional values in a loop
166 # until we get a response with '*' at the end
167 while True:
169 n = "%s;range=%d-*" % (attr, hi + 1)
170 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=[n])
171 assert len(res) == 1
172 res = dict(res[0])
173 del res["dn"]
175 fm = None
176 fvals = None
178 for key in res.keys():
179 m = r.match(key)
181 if m is None:
182 continue
184 if m.group(1) != attr:
185 continue
187 fm = m
188 fvals = list(res[key])
189 break
191 if fm is None:
192 break
194 vals.extend(fvals)
195 if fm.group(3) == "*":
196 # if we got "*" we're done
197 break
199 assert int(fm.group(2)) == hi + 1
200 hi = int(fm.group(3))
202 return vals
204 def get_attributes(self, object_dn):
205 """ Returns dict with all default visible attributes
207 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["*"])
208 assert len(res) == 1
209 res = dict(res[0])
210 # 'Dn' element is not iterable and we have it as 'distinguishedName'
211 del res["dn"]
212 for key in res.keys():
213 vals = list(res[key])
214 del res[key]
215 name = self.get_attribute_name(key)
216 res[name] = self.get_attribute_values(object_dn, key, vals)
218 return res
220 def get_descriptor_sddl(self, object_dn):
221 res = self.ldb.search(base=object_dn, scope=SCOPE_BASE, attrs=["nTSecurityDescriptor"])
222 desc = res[0]["nTSecurityDescriptor"][0]
223 desc = ndr_unpack(security.descriptor, desc)
224 return desc.as_sddl(self.domain_sid)
226 def guid_as_string(self, guid_blob):
227 """ Translate binary representation of schemaIDGUID to standard string representation.
228 @gid_blob: binary schemaIDGUID
230 blob = "%s" % guid_blob
231 stops = [4, 2, 2, 2, 6]
232 index = 0
233 res = ""
234 x = 0
235 while x < len(stops):
236 tmp = ""
237 y = 0
238 while y < stops[x]:
239 c = hex(ord(blob[index])).replace("0x", "")
240 c = [None, "0" + c, c][len(c)]
241 if 2 * index < len(blob):
242 tmp = c + tmp
243 else:
244 tmp += c
245 index += 1
246 y += 1
247 res += tmp + " "
248 x += 1
249 assert index == len(blob)
250 return res.strip().replace(" ", "-")
252 def get_guid_map(self):
253 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
255 self.guid_map = {}
256 res = self.ldb.search(base=self.schema_dn,
257 expression="(schemaIdGuid=*)", scope=SCOPE_SUBTREE, attrs=["schemaIdGuid", "name"])
258 for item in res:
259 self.guid_map[self.guid_as_string(item["schemaIdGuid"]).lower()] = item["name"][0]
261 res = self.ldb.search(base="cn=extended-rights,%s" % self.config_dn,
262 expression="(rightsGuid=*)", scope=SCOPE_SUBTREE, attrs=["rightsGuid", "name"])
263 for item in res:
264 self.guid_map[str(item["rightsGuid"]).lower()] = item["name"][0]
266 def get_sid_map(self):
267 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
269 self.sid_map = {}
270 res = self.ldb.search(base=self.base_dn,
271 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
272 for item in res:
273 try:
274 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
275 except KeyError:
276 pass
278 class Descriptor(object):
279 def __init__(self, connection, dn):
280 self.con = connection
281 self.dn = dn
282 self.sddl = self.con.get_descriptor_sddl(self.dn)
283 self.dacl_list = self.extract_dacl()
284 if self.con.sort_aces:
285 self.dacl_list.sort()
287 def extract_dacl(self):
288 """ Extracts the DACL as a list of ACE string (with the brakets).
290 try:
291 if "S:" in self.sddl:
292 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
293 else:
294 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
295 except AttributeError:
296 return []
297 return re.findall("(\(.*?\))", res)
299 def fix_guid(self, ace):
300 res = "%s" % ace
301 guids = re.findall("[a-z0-9]+?-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+", res)
302 # If there are not GUIDs to replace return the same ACE
303 if len(guids) == 0:
304 return res
305 for guid in guids:
306 try:
307 name = self.con.guid_map[guid.lower()]
308 res = res.replace(guid, name)
309 except KeyError:
310 # Do not bother if the GUID is not found in
311 # cn=Schema or cn=Extended-Rights
312 pass
313 return res
315 def fix_sid(self, ace):
316 res = "%s" % ace
317 sids = re.findall("S-[-0-9]+", res)
318 # If there are not SIDs to replace return the same ACE
319 if len(sids) == 0:
320 return res
321 for sid in sids:
322 try:
323 name = self.con.sid_map[sid]
324 res = res.replace(sid, name)
325 except KeyError:
326 # Do not bother if the SID is not found in baseDN
327 pass
328 return res
330 def fixit(self, ace):
331 """ Combine all replacement methods in one
333 res = "%s" % ace
334 res = self.fix_guid(res)
335 res = self.fix_sid(res)
336 return res
338 def diff_1(self, other):
339 res = ""
340 if len(self.dacl_list) != len(other.dacl_list):
341 res += 4*" " + "Difference in ACE count:\n"
342 res += 8*" " + "=> %s\n" % len(self.dacl_list)
343 res += 8*" " + "=> %s\n" % len(other.dacl_list)
345 i = 0
346 flag = True
347 while True:
348 self_ace = None
349 other_ace = None
350 try:
351 self_ace = "%s" % self.dacl_list[i]
352 except IndexError:
353 self_ace = ""
355 try:
356 other_ace = "%s" % other.dacl_list[i]
357 except IndexError:
358 other_ace = ""
359 if len(self_ace) + len(other_ace) == 0:
360 break
361 self_ace_fixed = "%s" % self.fixit(self_ace)
362 other_ace_fixed = "%s" % other.fixit(other_ace)
363 if self_ace_fixed != other_ace_fixed:
364 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
365 flag = False
366 else:
367 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
368 i += 1
369 return (flag, res)
371 def diff_2(self, other):
372 res = ""
373 if len(self.dacl_list) != len(other.dacl_list):
374 res += 4*" " + "Difference in ACE count:\n"
375 res += 8*" " + "=> %s\n" % len(self.dacl_list)
376 res += 8*" " + "=> %s\n" % len(other.dacl_list)
378 common_aces = []
379 self_aces = []
380 other_aces = []
381 self_dacl_list_fixed = []
382 other_dacl_list_fixed = []
383 [self_dacl_list_fixed.append( self.fixit(ace) ) for ace in self.dacl_list]
384 [other_dacl_list_fixed.append( other.fixit(ace) ) for ace in other.dacl_list]
385 for ace in self_dacl_list_fixed:
386 try:
387 other_dacl_list_fixed.index(ace)
388 except ValueError:
389 self_aces.append(ace)
390 else:
391 common_aces.append(ace)
392 self_aces = sorted(self_aces)
393 if len(self_aces) > 0:
394 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
395 for ace in self_aces:
396 res += 8*" " + ace + "\n"
398 for ace in other_dacl_list_fixed:
399 try:
400 self_dacl_list_fixed.index(ace)
401 except ValueError:
402 other_aces.append(ace)
403 else:
404 common_aces.append(ace)
405 other_aces = sorted(other_aces)
406 if len(other_aces) > 0:
407 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
408 for ace in other_aces:
409 res += 8*" " + ace + "\n"
411 common_aces = sorted(list(set(common_aces)))
412 if self.con.verbose:
413 res += 4*" " + "ACEs found in both:\n"
414 for ace in common_aces:
415 res += 8*" " + ace + "\n"
416 return (self_aces == [] and other_aces == [], res)
418 class LDAPObject(object):
419 def __init__(self, connection, dn, summary, filter_list):
420 self.con = connection
421 self.two_domains = self.con.two_domains
422 self.quiet = self.con.quiet
423 self.verbose = self.con.verbose
424 self.summary = summary
425 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
426 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
427 for x in self.con.server_names:
428 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
429 self.attributes = self.con.get_attributes(self.dn)
430 # Attributes that are considered always to be different e.g based on timestamp etc.
432 # One domain - two domain controllers
433 self.ignore_attributes = [
434 # Default Naming Context
435 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
436 "operatingSystemVersion","oEMInformation",
437 # Configuration Naming Context
438 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
439 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
440 # Schema Naming Context
441 "prefixMap"]
442 if filter_list:
443 self.ignore_attributes += filter_list
445 self.dn_attributes = []
446 self.domain_attributes = []
447 self.servername_attributes = []
448 self.netbios_attributes = []
449 self.other_attributes = []
450 # Two domains - two domain controllers
452 if self.two_domains:
453 self.ignore_attributes += [
454 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
455 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
456 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
457 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
458 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
459 # After Exchange preps
460 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
462 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
463 self.dn_attributes = [
464 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
465 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
466 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
467 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
468 # After Exchange preps
469 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
470 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
471 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
472 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
473 self.dn_attributes = [x.upper() for x in self.dn_attributes]
475 # Attributes that contain the Domain name e.g. 'samba.org'
476 self.domain_attributes = [
477 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
478 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
479 self.domain_attributes = [x.upper() for x in self.domain_attributes]
481 # May contain DOMAIN_NETBIOS and SERVER_NAME
482 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
483 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
484 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
485 self.servername_attributes = [x.upper() for x in self.servername_attributes]
487 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
488 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
490 self.other_attributes = [ "name", "DC",]
491 self.other_attributes = [x.upper() for x in self.other_attributes]
493 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
495 def log(self, msg):
497 Log on the screen if there is no --quiet oprion set
499 if not self.quiet:
500 self.outf.write(msg+"\n")
502 def fix_dn(self, s):
503 res = "%s" % s
504 if not self.two_domains:
505 return res
506 if res.upper().endswith(self.con.base_dn.upper()):
507 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
508 return res
510 def fix_domain_name(self, s):
511 res = "%s" % s
512 if not self.two_domains:
513 return res
514 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
515 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
516 return res
518 def fix_domain_netbios(self, s):
519 res = "%s" % s
520 if not self.two_domains:
521 return res
522 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
523 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
524 return res
526 def fix_server_name(self, s):
527 res = "%s" % s
528 if not self.two_domains or len(self.con.server_names) > 1:
529 return res
530 for x in self.con.server_names:
531 res = res.upper().replace(x, "${SERVER_NAME}")
532 return res
534 def __eq__(self, other):
535 if self.con.descriptor:
536 return self.cmp_desc(other)
537 return self.cmp_attrs(other)
539 def cmp_desc(self, other):
540 d1 = Descriptor(self.con, self.dn)
541 d2 = Descriptor(other.con, other.dn)
542 if self.con.view == "section":
543 res = d1.diff_2(d2)
544 elif self.con.view == "collision":
545 res = d1.diff_1(d2)
546 else:
547 raise Exception("Unknown --view option value.")
549 self.screen_output = res[1][:-1]
550 other.screen_output = res[1][:-1]
552 return res[0]
554 def cmp_attrs(self, other):
555 res = ""
556 self.unique_attrs = []
557 self.df_value_attrs = []
558 other.unique_attrs = []
559 if self.attributes.keys() != other.attributes.keys():
561 title = 4*" " + "Attributes found only in %s:" % self.con.host
562 for x in self.attributes.keys():
563 if not x in other.attributes.keys() and \
564 not x.upper() in [q.upper() for q in other.ignore_attributes]:
565 if title:
566 res += title + "\n"
567 title = None
568 res += 8*" " + x + "\n"
569 self.unique_attrs.append(x)
571 title = 4*" " + "Attributes found only in %s:" % other.con.host
572 for x in other.attributes.keys():
573 if not x in self.attributes.keys() and \
574 not x.upper() in [q.upper() for q in self.ignore_attributes]:
575 if title:
576 res += title + "\n"
577 title = None
578 res += 8*" " + x + "\n"
579 other.unique_attrs.append(x)
581 missing_attrs = [x.upper() for x in self.unique_attrs]
582 missing_attrs += [x.upper() for x in other.unique_attrs]
583 title = 4*" " + "Difference in attribute values:"
584 for x in self.attributes.keys():
585 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
586 continue
587 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
588 self.attributes[x] = sorted(self.attributes[x])
589 other.attributes[x] = sorted(other.attributes[x])
590 if self.attributes[x] != other.attributes[x]:
591 p = None
592 q = None
593 m = None
594 n = None
595 # First check if the difference can be fixed but shunting the first part
596 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
597 if x.upper() in self.other_attributes:
598 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
599 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
600 if p == q:
601 continue
602 # Attribute values that are list that contain DN based values that may differ
603 elif x.upper() in self.dn_attributes:
604 m = p
605 n = q
606 if not p and not q:
607 m = self.attributes[x]
608 n = other.attributes[x]
609 p = [self.fix_dn(j) for j in m]
610 q = [other.fix_dn(j) for j in n]
611 if p == q:
612 continue
613 # Attributes that contain the Domain name in them
614 if x.upper() in self.domain_attributes:
615 m = p
616 n = q
617 if not p and not q:
618 m = self.attributes[x]
619 n = other.attributes[x]
620 p = [self.fix_domain_name(j) for j in m]
621 q = [other.fix_domain_name(j) for j in n]
622 if p == q:
623 continue
625 if x.upper() in self.servername_attributes:
626 # Attributes with SERVER_NAME
627 m = p
628 n = q
629 if not p and not q:
630 m = self.attributes[x]
631 n = other.attributes[x]
632 p = [self.fix_server_name(j) for j in m]
633 q = [other.fix_server_name(j) for j in n]
634 if p == q:
635 continue
637 if x.upper() in self.netbios_attributes:
638 # Attributes with NETBIOS Domain name
639 m = p
640 n = q
641 if not p and not q:
642 m = self.attributes[x]
643 n = other.attributes[x]
644 p = [self.fix_domain_netbios(j) for j in m]
645 q = [other.fix_domain_netbios(j) for j in n]
646 if p == q:
647 continue
649 if title:
650 res += title + "\n"
651 title = None
652 if p and q:
653 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
654 else:
655 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
656 self.df_value_attrs.append(x)
658 if self.unique_attrs + other.unique_attrs != []:
659 assert self.unique_attrs != other.unique_attrs
660 self.summary["unique_attrs"] += self.unique_attrs
661 self.summary["df_value_attrs"] += self.df_value_attrs
662 other.summary["unique_attrs"] += other.unique_attrs
663 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
665 self.screen_output = res[:-1]
666 other.screen_output = res[:-1]
668 return res == ""
671 class LDAPBundel(object):
673 def __init__(self, connection, context, dn_list=None, filter_list=None):
674 self.con = connection
675 self.two_domains = self.con.two_domains
676 self.quiet = self.con.quiet
677 self.verbose = self.con.verbose
678 self.search_base = self.con.search_base
679 self.search_scope = self.con.search_scope
680 self.summary = {}
681 self.summary["unique_attrs"] = []
682 self.summary["df_value_attrs"] = []
683 self.summary["known_ignored_dn"] = []
684 self.summary["abnormal_ignored_dn"] = []
685 self.filter_list = filter_list
686 if dn_list:
687 self.dn_list = dn_list
688 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
689 self.context = context.upper()
690 self.dn_list = self.get_dn_list(context)
691 else:
692 raise Exception("Unknown initialization data for LDAPBundel().")
693 counter = 0
694 while counter < len(self.dn_list) and self.two_domains:
695 # Use alias reference
696 tmp = self.dn_list[counter]
697 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
698 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
699 if len(self.con.server_names) == 1:
700 for x in self.con.server_names:
701 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
702 self.dn_list[counter] = tmp
703 counter += 1
704 self.dn_list = list(set(self.dn_list))
705 self.dn_list = sorted(self.dn_list)
706 self.size = len(self.dn_list)
708 def log(self, msg):
710 Log on the screen if there is no --quiet oprion set
712 if not self.quiet:
713 self.outf.write(msg+"\n")
715 def update_size(self):
716 self.size = len(self.dn_list)
717 self.dn_list = sorted(self.dn_list)
719 def __eq__(self, other):
720 res = True
721 if self.size != other.size:
722 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
723 res = False
725 # This is the case where we want to explicitly compare two objects with different DNs.
726 # It does not matter if they are in the same DC, in two DC in one domain or in two
727 # different domains.
728 if self.search_scope != SCOPE_BASE:
729 title= "\n* DNs found only in %s:" % self.con.host
730 for x in self.dn_list:
731 if not x.upper() in [q.upper() for q in other.dn_list]:
732 if title:
733 self.log( title )
734 title = None
735 res = False
736 self.log( 4*" " + x )
737 self.dn_list[self.dn_list.index(x)] = ""
738 self.dn_list = [x for x in self.dn_list if x]
740 title= "\n* DNs found only in %s:" % other.con.host
741 for x in other.dn_list:
742 if not x.upper() in [q.upper() for q in self.dn_list]:
743 if title:
744 self.log( title )
745 title = None
746 res = False
747 self.log( 4*" " + x )
748 other.dn_list[other.dn_list.index(x)] = ""
749 other.dn_list = [x for x in other.dn_list if x]
751 self.update_size()
752 other.update_size()
753 assert self.size == other.size
754 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
755 self.log( "\n* Objects to be compared: %s" % self.size )
757 index = 0
758 while index < self.size:
759 skip = False
760 try:
761 object1 = LDAPObject(connection=self.con,
762 dn=self.dn_list[index],
763 summary=self.summary,
764 filter_list=self.filter_list)
765 except LdbError, (enum, estr):
766 if enum == ERR_NO_SUCH_OBJECT:
767 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
768 skip = True
769 raise
770 try:
771 object2 = LDAPObject(connection=other.con,
772 dn=other.dn_list[index],
773 summary=other.summary,
774 filter_list=self.filter_list)
775 except LdbError, (enum, estr):
776 if enum == ERR_NO_SUCH_OBJECT:
777 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
778 skip = True
779 raise
780 if skip:
781 index += 1
782 continue
783 if object1 == object2:
784 if self.con.verbose:
785 self.log( "\nComparing:" )
786 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
787 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
788 self.log( 4*" " + "OK" )
789 else:
790 self.log( "\nComparing:" )
791 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
792 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
793 self.log( object1.screen_output )
794 self.log( 4*" " + "FAILED" )
795 res = False
796 self.summary = object1.summary
797 other.summary = object2.summary
798 index += 1
800 return res
802 def get_dn_list(self, context):
803 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
804 Parse all DNs and filter those that are 'strange' or abnormal.
806 if context.upper() == "DOMAIN":
807 search_base = self.con.base_dn
808 elif context.upper() == "CONFIGURATION":
809 search_base = self.con.config_dn
810 elif context.upper() == "SCHEMA":
811 search_base = self.con.schema_dn
812 elif context.upper() == "DNSDOMAIN":
813 search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
814 elif context.upper() == "DNSFOREST":
815 search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
817 dn_list = []
818 if not self.search_base:
819 self.search_base = search_base
820 self.search_scope = self.search_scope.upper()
821 if self.search_scope == "SUB":
822 self.search_scope = SCOPE_SUBTREE
823 elif self.search_scope == "BASE":
824 self.search_scope = SCOPE_BASE
825 elif self.search_scope == "ONE":
826 self.search_scope = SCOPE_ONELEVEL
827 else:
828 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
829 try:
830 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
831 except LdbError, (enum, estr):
832 self.outf.write("Failed search of base=%s\n" % self.search_base)
833 raise
834 for x in res:
835 dn_list.append(x["dn"].get_linearized())
837 global summary
839 return dn_list
841 def print_summary(self):
842 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
843 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
845 if self.summary["unique_attrs"]:
846 self.log( "\nAttributes found only in %s:" % self.con.host )
847 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
849 if self.summary["df_value_attrs"]:
850 self.log( "\nAttributes with different values:" )
851 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
852 self.summary["df_value_attrs"] = []
855 class cmd_ldapcmp(Command):
856 """compare two ldap databases"""
857 synopsis = "%prog ldapcmp <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
859 takes_optiongroups = {
860 "sambaopts": options.SambaOptions,
861 "versionopts": options.VersionOptions,
862 "credopts": options.CredentialsOptionsDouble,
865 takes_optiongroups = {
866 "sambaopts": options.SambaOptions,
867 "versionopts": options.VersionOptions,
868 "credopts": options.CredentialsOptionsDouble,
871 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?"]
873 takes_options = [
874 Option("-w", "--two", dest="two", action="store_true", default=False,
875 help="Hosts are in two different domains"),
876 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
877 help="Do not print anything but relay on just exit code"),
878 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
879 help="Print all DN pairs that have been compared"),
880 Option("--sd", dest="descriptor", action="store_true", default=False,
881 help="Compare nTSecurityDescriptor attibutes only"),
882 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
883 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
884 Option("--view", dest="view", default="section",
885 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
886 Option("--base", dest="base", default="",
887 help="Pass search base that will build DN list for the first DC."),
888 Option("--base2", dest="base2", default="",
889 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
890 Option("--scope", dest="scope", default="SUB",
891 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
892 Option("--filter", dest="filter", default="",
893 help="List of comma separated attributes to ignore in the comparision"),
896 def run(self, URL1, URL2,
897 context1=None, context2=None, context3=None,
898 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
899 view="section", base="", base2="", scope="SUB", filter="",
900 credopts=None, sambaopts=None, versionopts=None):
902 lp = sambaopts.get_loadparm()
904 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
906 if using_ldap:
907 creds = credopts.get_credentials(lp, fallback_machine=True)
908 else:
909 creds = None
910 creds2 = credopts.get_credentials2(lp, guess=False)
911 if creds2.is_anonymous():
912 creds2 = creds
913 else:
914 creds2.set_domain("")
915 creds2.set_workstation("")
916 if using_ldap and not creds.authentication_requested():
917 raise CommandError("You must supply at least one username/password pair")
919 # make a list of contexts to compare in
920 contexts = []
921 if context1 is None:
922 if base and base2:
923 # If search bases are specified context is defaulted to
924 # DOMAIN so the given search bases can be verified.
925 contexts = ["DOMAIN"]
926 else:
927 # if no argument given, we compare all contexts
928 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA"]
929 else:
930 for c in [context1, context2, context3]:
931 if c is None:
932 continue
933 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
934 raise CommandError("Incorrect argument: %s" % c)
935 contexts.append(c.upper())
937 if verbose and quiet:
938 raise CommandError("You cannot set --verbose and --quiet together")
939 if (not base and base2) or (base and not base2):
940 raise CommandError("You need to specify both --base and --base2 at the same time")
941 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
942 raise CommandError("Invalid --view value. Choose from: section or collision")
943 if not scope.upper() in ["SUB", "ONE", "BASE"]:
944 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
946 con1 = LDAPBase(URL1, creds, lp,
947 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
948 verbose=verbose,view=view, base=base, scope=scope)
949 assert len(con1.base_dn) > 0
951 con2 = LDAPBase(URL2, creds2, lp,
952 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
953 verbose=verbose, view=view, base=base2, scope=scope)
954 assert len(con2.base_dn) > 0
956 filter_list = filter.split(",")
958 status = 0
959 for context in contexts:
960 if not quiet:
961 self.outf.write("\n* Comparing [%s] context...\n" % context)
963 b1 = LDAPBundel(con1, context=context, filter_list=filter_list)
964 b2 = LDAPBundel(con2, context=context, filter_list=filter_list)
966 if b1 == b2:
967 if not quiet:
968 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
969 context)
970 else:
971 if not quiet:
972 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
973 if not descriptor:
974 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
975 b2.summary["df_value_attrs"] = []
976 self.outf.write("\nSUMMARY\n")
977 self.outf.write("---------\n")
978 b1.print_summary()
979 b2.print_summary()
980 # mark exit status as FAILURE if a least one comparison failed
981 status = -1
982 if status != 0:
983 raise CommandError("Compare failed: %d" % status)