ctdb-daemon: Exit if setting the session ID fails
[Samba.git] / python / samba / netcmd / ldapcmp.py
blob89c175bf61e5be2b5e415657f120ac7dfab17bbe
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
6 # above partitions.
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/>.
24 import os
25 import re
26 import sys
28 import samba
29 import samba.getopt as options
30 from samba import Ldb
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 (
35 Command,
36 CommandError,
37 Option,
40 global summary
41 summary = {}
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):
49 ldb_options = []
50 samdb_url = host
51 if not "://" in host:
52 if os.path.isfile(host):
53 samdb_url = "tdb://%s" % host
54 else:
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"]
59 self.outf = outf
60 self.errf = errf
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.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()
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_sid_map(self):
253 """ Build dictionary that maps GUID to 'name' attribute found in Schema or Extended-Rights.
255 self.sid_map = {}
256 res = self.ldb.search(base=self.base_dn,
257 expression="(objectSid=*)", scope=SCOPE_SUBTREE, attrs=["objectSid", "sAMAccountName"])
258 for item in res:
259 try:
260 self.sid_map["%s" % ndr_unpack(security.dom_sid, item["objectSid"][0])] = item["sAMAccountName"][0]
261 except KeyError:
262 pass
264 class Descriptor(object):
265 def __init__(self, connection, dn, outf=sys.stdout, errf=sys.stderr):
266 self.outf = outf
267 self.errf = errf
268 self.con = connection
269 self.dn = dn
270 self.sddl = self.con.get_descriptor_sddl(self.dn)
271 self.dacl_list = self.extract_dacl()
272 if self.con.sort_aces:
273 self.dacl_list.sort()
275 def extract_dacl(self):
276 """ Extracts the DACL as a list of ACE string (with the brakets).
278 try:
279 if "S:" in self.sddl:
280 res = re.search("D:(.*?)(\(.*?\))S:", self.sddl).group(2)
281 else:
282 res = re.search("D:(.*?)(\(.*\))", self.sddl).group(2)
283 except AttributeError:
284 return []
285 return re.findall("(\(.*?\))", res)
287 def fix_sid(self, ace):
288 res = "%s" % ace
289 sids = re.findall("S-[-0-9]+", res)
290 # If there are not SIDs to replace return the same ACE
291 if len(sids) == 0:
292 return res
293 for sid in sids:
294 try:
295 name = self.con.sid_map[sid]
296 res = res.replace(sid, name)
297 except KeyError:
298 # Do not bother if the SID is not found in baseDN
299 pass
300 return res
302 def diff_1(self, other):
303 res = ""
304 if len(self.dacl_list) != len(other.dacl_list):
305 res += 4*" " + "Difference in ACE count:\n"
306 res += 8*" " + "=> %s\n" % len(self.dacl_list)
307 res += 8*" " + "=> %s\n" % len(other.dacl_list)
309 i = 0
310 flag = True
311 while True:
312 self_ace = None
313 other_ace = None
314 try:
315 self_ace = "%s" % self.dacl_list[i]
316 except IndexError:
317 self_ace = ""
319 try:
320 other_ace = "%s" % other.dacl_list[i]
321 except IndexError:
322 other_ace = ""
323 if len(self_ace) + len(other_ace) == 0:
324 break
325 self_ace_fixed = "%s" % self.fix_sid(self_ace)
326 other_ace_fixed = "%s" % other.fix_sid(other_ace)
327 if self_ace_fixed != other_ace_fixed:
328 res += "%60s * %s\n" % ( self_ace_fixed, other_ace_fixed )
329 flag = False
330 else:
331 res += "%60s | %s\n" % ( self_ace_fixed, other_ace_fixed )
332 i += 1
333 return (flag, res)
335 def diff_2(self, other):
336 res = ""
337 if len(self.dacl_list) != len(other.dacl_list):
338 res += 4*" " + "Difference in ACE count:\n"
339 res += 8*" " + "=> %s\n" % len(self.dacl_list)
340 res += 8*" " + "=> %s\n" % len(other.dacl_list)
342 common_aces = []
343 self_aces = []
344 other_aces = []
345 self_dacl_list_fixed = []
346 other_dacl_list_fixed = []
347 [self_dacl_list_fixed.append( self.fix_sid(ace) ) for ace in self.dacl_list]
348 [other_dacl_list_fixed.append( other.fix_sid(ace) ) for ace in other.dacl_list]
349 for ace in self_dacl_list_fixed:
350 try:
351 other_dacl_list_fixed.index(ace)
352 except ValueError:
353 self_aces.append(ace)
354 else:
355 common_aces.append(ace)
356 self_aces = sorted(self_aces)
357 if len(self_aces) > 0:
358 res += 4*" " + "ACEs found only in %s:\n" % self.con.host
359 for ace in self_aces:
360 res += 8*" " + ace + "\n"
362 for ace in other_dacl_list_fixed:
363 try:
364 self_dacl_list_fixed.index(ace)
365 except ValueError:
366 other_aces.append(ace)
367 else:
368 common_aces.append(ace)
369 other_aces = sorted(other_aces)
370 if len(other_aces) > 0:
371 res += 4*" " + "ACEs found only in %s:\n" % other.con.host
372 for ace in other_aces:
373 res += 8*" " + ace + "\n"
375 common_aces = sorted(list(set(common_aces)))
376 if self.con.verbose:
377 res += 4*" " + "ACEs found in both:\n"
378 for ace in common_aces:
379 res += 8*" " + ace + "\n"
380 return (self_aces == [] and other_aces == [], res)
382 class LDAPObject(object):
383 def __init__(self, connection, dn, summary, filter_list,
384 outf=sys.stdout, errf=sys.stderr):
385 self.outf = outf
386 self.errf = errf
387 self.con = connection
388 self.two_domains = self.con.two_domains
389 self.quiet = self.con.quiet
390 self.verbose = self.con.verbose
391 self.summary = summary
392 self.dn = dn.replace("${DOMAIN_DN}", self.con.base_dn)
393 self.dn = self.dn.replace("CN=${DOMAIN_NETBIOS}", "CN=%s" % self.con.domain_netbios)
394 for x in self.con.server_names:
395 self.dn = self.dn.replace("CN=${SERVER_NAME}", "CN=%s" % x)
396 self.attributes = self.con.get_attributes(self.dn)
397 # Attributes that are considered always to be different e.g based on timestamp etc.
399 # One domain - two domain controllers
400 self.ignore_attributes = [
401 # Default Naming Context
402 "lastLogon", "lastLogoff", "badPwdCount", "logonCount", "badPasswordTime", "modifiedCount",
403 "operatingSystemVersion","oEMInformation",
404 "ridNextRID", "rIDPreviousAllocationPool",
405 # Configuration Naming Context
406 "repsFrom", "dSCorePropagationData", "msExchServer1HighestUSN",
407 "replUpToDateVector", "repsTo", "whenChanged", "uSNChanged", "uSNCreated",
408 # Schema Naming Context
409 "prefixMap"]
410 if filter_list:
411 self.ignore_attributes += filter_list
413 self.dn_attributes = []
414 self.domain_attributes = []
415 self.servername_attributes = []
416 self.netbios_attributes = []
417 self.other_attributes = []
418 # Two domains - two domain controllers
420 if self.two_domains:
421 self.ignore_attributes += [
422 "objectCategory", "objectGUID", "objectSid", "whenCreated", "pwdLastSet", "uSNCreated", "creationTime",
423 "modifiedCount", "priorSetTime", "rIDManagerReference", "gPLink", "ipsecNFAReference",
424 "fRSPrimaryMember", "fSMORoleOwner", "masteredBy", "ipsecOwnersReference", "wellKnownObjects",
425 "badPwdCount", "ipsecISAKMPReference", "ipsecFilterReference", "msDs-masteredBy", "lastSetTime",
426 "ipsecNegotiationPolicyReference", "subRefs", "gPCFileSysPath", "accountExpires", "invocationId",
427 # After Exchange preps
428 "targetAddress", "msExchMailboxGuid", "siteFolderGUID"]
430 # Attributes that contain the unique DN tail part e.g. 'DC=samba,DC=org'
431 self.dn_attributes = [
432 "distinguishedName", "defaultObjectCategory", "member", "memberOf", "siteList", "nCName",
433 "homeMDB", "homeMTA", "interSiteTopologyGenerator", "serverReference",
434 "msDS-HasInstantiatedNCs", "hasMasterNCs", "msDS-hasMasterNCs", "msDS-HasDomainNCs", "dMDLocation",
435 "msDS-IsDomainFor", "rIDSetReferences", "serverReferenceBL",
436 # After Exchange preps
437 "msExchHomeRoutingGroup", "msExchResponsibleMTAServer", "siteFolderServer", "msExchRoutingMasterDN",
438 "msExchRoutingGroupMembersBL", "homeMDBBL", "msExchHomePublicMDB", "msExchOwningServer", "templateRoots",
439 "addressBookRoots", "msExchPolicyRoots", "globalAddressList", "msExchOwningPFTree",
440 "msExchResponsibleMTAServerBL", "msExchOwningPFTreeBL",]
441 self.dn_attributes = [x.upper() for x in self.dn_attributes]
443 # Attributes that contain the Domain name e.g. 'samba.org'
444 self.domain_attributes = [
445 "proxyAddresses", "mail", "userPrincipalName", "msExchSmtpFullyQualifiedDomainName",
446 "dnsHostName", "networkAddress", "dnsRoot", "servicePrincipalName",]
447 self.domain_attributes = [x.upper() for x in self.domain_attributes]
449 # May contain DOMAIN_NETBIOS and SERVER_NAME
450 self.servername_attributes = [ "distinguishedName", "name", "CN", "sAMAccountName", "dNSHostName",
451 "servicePrincipalName", "rIDSetReferences", "serverReference", "serverReferenceBL",
452 "msDS-IsDomainFor", "interSiteTopologyGenerator",]
453 self.servername_attributes = [x.upper() for x in self.servername_attributes]
455 self.netbios_attributes = [ "servicePrincipalName", "CN", "distinguishedName", "nETBIOSName", "name",]
456 self.netbios_attributes = [x.upper() for x in self.netbios_attributes]
458 self.other_attributes = [ "name", "DC",]
459 self.other_attributes = [x.upper() for x in self.other_attributes]
461 self.ignore_attributes = [x.upper() for x in self.ignore_attributes]
463 def log(self, msg):
465 Log on the screen if there is no --quiet option set
467 if not self.quiet:
468 self.outf.write(msg+"\n")
470 def fix_dn(self, s):
471 res = "%s" % s
472 if not self.two_domains:
473 return res
474 if res.upper().endswith(self.con.base_dn.upper()):
475 res = res[:len(res)-len(self.con.base_dn)] + "${DOMAIN_DN}"
476 return res
478 def fix_domain_name(self, s):
479 res = "%s" % s
480 if not self.two_domains:
481 return res
482 res = res.replace(self.con.domain_name.lower(), self.con.domain_name.upper())
483 res = res.replace(self.con.domain_name.upper(), "${DOMAIN_NAME}")
484 return res
486 def fix_domain_netbios(self, s):
487 res = "%s" % s
488 if not self.two_domains:
489 return res
490 res = res.replace(self.con.domain_netbios.lower(), self.con.domain_netbios.upper())
491 res = res.replace(self.con.domain_netbios.upper(), "${DOMAIN_NETBIOS}")
492 return res
494 def fix_server_name(self, s):
495 res = "%s" % s
496 if not self.two_domains or len(self.con.server_names) > 1:
497 return res
498 for x in self.con.server_names:
499 res = res.upper().replace(x, "${SERVER_NAME}")
500 return res
502 def __eq__(self, other):
503 if self.con.descriptor:
504 return self.cmp_desc(other)
505 return self.cmp_attrs(other)
507 def cmp_desc(self, other):
508 d1 = Descriptor(self.con, self.dn, outf=self.outf, errf=self.errf)
509 d2 = Descriptor(other.con, other.dn, outf=self.outf, errf=self.errf)
510 if self.con.view == "section":
511 res = d1.diff_2(d2)
512 elif self.con.view == "collision":
513 res = d1.diff_1(d2)
514 else:
515 raise Exception("Unknown --view option value.")
517 self.screen_output = res[1][:-1]
518 other.screen_output = res[1][:-1]
520 return res[0]
522 def cmp_attrs(self, other):
523 res = ""
524 self.unique_attrs = []
525 self.df_value_attrs = []
526 other.unique_attrs = []
527 if self.attributes.keys() != other.attributes.keys():
529 title = 4*" " + "Attributes found only in %s:" % self.con.host
530 for x in self.attributes.keys():
531 if not x in other.attributes.keys() and \
532 not x.upper() in [q.upper() for q in other.ignore_attributes]:
533 if title:
534 res += title + "\n"
535 title = None
536 res += 8*" " + x + "\n"
537 self.unique_attrs.append(x)
539 title = 4*" " + "Attributes found only in %s:" % other.con.host
540 for x in other.attributes.keys():
541 if not x in self.attributes.keys() and \
542 not x.upper() in [q.upper() for q in self.ignore_attributes]:
543 if title:
544 res += title + "\n"
545 title = None
546 res += 8*" " + x + "\n"
547 other.unique_attrs.append(x)
549 missing_attrs = [x.upper() for x in self.unique_attrs]
550 missing_attrs += [x.upper() for x in other.unique_attrs]
551 title = 4*" " + "Difference in attribute values:"
552 for x in self.attributes.keys():
553 if x.upper() in self.ignore_attributes or x.upper() in missing_attrs:
554 continue
555 if isinstance(self.attributes[x], list) and isinstance(other.attributes[x], list):
556 self.attributes[x] = sorted(self.attributes[x])
557 other.attributes[x] = sorted(other.attributes[x])
558 if self.attributes[x] != other.attributes[x]:
559 p = None
560 q = None
561 m = None
562 n = None
563 # First check if the difference can be fixed but shunting the first part
564 # of the DomainHostName e.g. 'mysamba4.test.local' => 'mysamba4'
565 if x.upper() in self.other_attributes:
566 p = [self.con.domain_name.split(".")[0] == j for j in self.attributes[x]]
567 q = [other.con.domain_name.split(".")[0] == j for j in other.attributes[x]]
568 if p == q:
569 continue
570 # Attribute values that are list that contain DN based values that may differ
571 elif x.upper() in self.dn_attributes:
572 m = p
573 n = q
574 if not p and not q:
575 m = self.attributes[x]
576 n = other.attributes[x]
577 p = [self.fix_dn(j) for j in m]
578 q = [other.fix_dn(j) for j in n]
579 if p == q:
580 continue
581 # Attributes that contain the Domain name in them
582 if x.upper() in self.domain_attributes:
583 m = p
584 n = q
585 if not p and not q:
586 m = self.attributes[x]
587 n = other.attributes[x]
588 p = [self.fix_domain_name(j) for j in m]
589 q = [other.fix_domain_name(j) for j in n]
590 if p == q:
591 continue
593 if x.upper() in self.servername_attributes:
594 # Attributes with SERVER_NAME
595 m = p
596 n = q
597 if not p and not q:
598 m = self.attributes[x]
599 n = other.attributes[x]
600 p = [self.fix_server_name(j) for j in m]
601 q = [other.fix_server_name(j) for j in n]
602 if p == q:
603 continue
605 if x.upper() in self.netbios_attributes:
606 # Attributes with NETBIOS Domain name
607 m = p
608 n = q
609 if not p and not q:
610 m = self.attributes[x]
611 n = other.attributes[x]
612 p = [self.fix_domain_netbios(j) for j in m]
613 q = [other.fix_domain_netbios(j) for j in n]
614 if p == q:
615 continue
617 if title:
618 res += title + "\n"
619 title = None
620 if p and q:
621 res += 8*" " + x + " => \n%s\n%s" % (p, q) + "\n"
622 else:
623 res += 8*" " + x + " => \n%s\n%s" % (self.attributes[x], other.attributes[x]) + "\n"
624 self.df_value_attrs.append(x)
626 if self.unique_attrs + other.unique_attrs != []:
627 assert self.unique_attrs != other.unique_attrs
628 self.summary["unique_attrs"] += self.unique_attrs
629 self.summary["df_value_attrs"] += self.df_value_attrs
630 other.summary["unique_attrs"] += other.unique_attrs
631 other.summary["df_value_attrs"] += self.df_value_attrs # they are the same
633 self.screen_output = res[:-1]
634 other.screen_output = res[:-1]
636 return res == ""
639 class LDAPBundel(object):
641 def __init__(self, connection, context, dn_list=None, filter_list=None,
642 outf=sys.stdout, errf=sys.stderr):
643 self.outf = outf
644 self.errf = errf
645 self.con = connection
646 self.two_domains = self.con.two_domains
647 self.quiet = self.con.quiet
648 self.verbose = self.con.verbose
649 self.search_base = self.con.search_base
650 self.search_scope = self.con.search_scope
651 self.skip_missing_dn = self.con.skip_missing_dn
652 self.summary = {}
653 self.summary["unique_attrs"] = []
654 self.summary["df_value_attrs"] = []
655 self.summary["known_ignored_dn"] = []
656 self.summary["abnormal_ignored_dn"] = []
657 self.filter_list = filter_list
658 if dn_list:
659 self.dn_list = dn_list
660 elif context.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
661 self.context = context.upper()
662 self.dn_list = self.get_dn_list(context)
663 else:
664 raise Exception("Unknown initialization data for LDAPBundel().")
665 counter = 0
666 while counter < len(self.dn_list) and self.two_domains:
667 # Use alias reference
668 tmp = self.dn_list[counter]
669 tmp = tmp[:len(tmp)-len(self.con.base_dn)] + "${DOMAIN_DN}"
670 tmp = tmp.replace("CN=%s" % self.con.domain_netbios, "CN=${DOMAIN_NETBIOS}")
671 if len(self.con.server_names) == 1:
672 for x in self.con.server_names:
673 tmp = tmp.replace("CN=%s" % x, "CN=${SERVER_NAME}")
674 self.dn_list[counter] = tmp
675 counter += 1
676 self.dn_list = list(set(self.dn_list))
677 self.dn_list = sorted(self.dn_list)
678 self.size = len(self.dn_list)
680 def log(self, msg):
682 Log on the screen if there is no --quiet option set
684 if not self.quiet:
685 self.outf.write(msg+"\n")
687 def update_size(self):
688 self.size = len(self.dn_list)
689 self.dn_list = sorted(self.dn_list)
691 def __eq__(self, other):
692 res = True
693 if self.size != other.size:
694 self.log( "\n* DN lists have different size: %s != %s" % (self.size, other.size) )
695 if not self.skip_missing_dn:
696 res = False
698 # This is the case where we want to explicitly compare two objects with different DNs.
699 # It does not matter if they are in the same DC, in two DC in one domain or in two
700 # different domains.
701 if self.search_scope != SCOPE_BASE:
702 title= "\n* DNs found only in %s:" % self.con.host
703 for x in self.dn_list:
704 if not x.upper() in [q.upper() for q in other.dn_list]:
705 if title and not self.skip_missing_dn:
706 self.log( title )
707 title = None
708 res = False
709 self.log( 4*" " + x )
710 self.dn_list[self.dn_list.index(x)] = ""
711 self.dn_list = [x for x in self.dn_list if x]
713 title= "\n* DNs found only in %s:" % other.con.host
714 for x in other.dn_list:
715 if not x.upper() in [q.upper() for q in self.dn_list]:
716 if title and not self.skip_missing_dn:
717 self.log( title )
718 title = None
719 res = False
720 self.log( 4*" " + x )
721 other.dn_list[other.dn_list.index(x)] = ""
722 other.dn_list = [x for x in other.dn_list if x]
724 self.update_size()
725 other.update_size()
726 assert self.size == other.size
727 assert sorted([x.upper() for x in self.dn_list]) == sorted([x.upper() for x in other.dn_list])
728 self.log( "\n* Objects to be compared: %s" % self.size )
730 index = 0
731 while index < self.size:
732 skip = False
733 try:
734 object1 = LDAPObject(connection=self.con,
735 dn=self.dn_list[index],
736 summary=self.summary,
737 filter_list=self.filter_list,
738 outf=self.outf, errf=self.errf)
739 except LdbError, (enum, estr):
740 if enum == ERR_NO_SUCH_OBJECT:
741 self.log( "\n!!! Object not found: %s" % self.dn_list[index] )
742 skip = True
743 raise
744 try:
745 object2 = LDAPObject(connection=other.con,
746 dn=other.dn_list[index],
747 summary=other.summary,
748 filter_list=self.filter_list,
749 outf=self.outf, errf=self.errf)
750 except LdbError, (enum, estr):
751 if enum == ERR_NO_SUCH_OBJECT:
752 self.log( "\n!!! Object not found: %s" % other.dn_list[index] )
753 skip = True
754 raise
755 if skip:
756 index += 1
757 continue
758 if object1 == object2:
759 if self.con.verbose:
760 self.log( "\nComparing:" )
761 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
762 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
763 self.log( 4*" " + "OK" )
764 else:
765 self.log( "\nComparing:" )
766 self.log( "'%s' [%s]" % (object1.dn, object1.con.host) )
767 self.log( "'%s' [%s]" % (object2.dn, object2.con.host) )
768 self.log( object1.screen_output )
769 self.log( 4*" " + "FAILED" )
770 res = False
771 self.summary = object1.summary
772 other.summary = object2.summary
773 index += 1
775 return res
777 def get_dn_list(self, context):
778 """ Query LDAP server about the DNs of certain naming self.con.ext Domain (or Default), Configuration, Schema.
779 Parse all DNs and filter those that are 'strange' or abnormal.
781 if context.upper() == "DOMAIN":
782 search_base = self.con.base_dn
783 elif context.upper() == "CONFIGURATION":
784 search_base = self.con.config_dn
785 elif context.upper() == "SCHEMA":
786 search_base = self.con.schema_dn
787 elif context.upper() == "DNSDOMAIN":
788 search_base = "DC=DomainDnsZones,%s" % self.con.base_dn
789 elif context.upper() == "DNSFOREST":
790 search_base = "DC=ForestDnsZones,%s" % self.con.root_dn
792 dn_list = []
793 if not self.search_base:
794 self.search_base = search_base
795 self.search_scope = self.search_scope.upper()
796 if self.search_scope == "SUB":
797 self.search_scope = SCOPE_SUBTREE
798 elif self.search_scope == "BASE":
799 self.search_scope = SCOPE_BASE
800 elif self.search_scope == "ONE":
801 self.search_scope = SCOPE_ONELEVEL
802 else:
803 raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")
804 try:
805 res = self.con.ldb.search(base=self.search_base, scope=self.search_scope, attrs=["dn"])
806 except LdbError, (enum, estr):
807 self.outf.write("Failed search of base=%s\n" % self.search_base)
808 raise
809 for x in res:
810 dn_list.append(x["dn"].get_linearized())
812 global summary
814 return dn_list
816 def print_summary(self):
817 self.summary["unique_attrs"] = list(set(self.summary["unique_attrs"]))
818 self.summary["df_value_attrs"] = list(set(self.summary["df_value_attrs"]))
820 if self.summary["unique_attrs"]:
821 self.log( "\nAttributes found only in %s:" % self.con.host )
822 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["unique_attrs"]]) )
824 if self.summary["df_value_attrs"]:
825 self.log( "\nAttributes with different values:" )
826 self.log( "".join([str("\n" + 4*" " + x) for x in self.summary["df_value_attrs"]]) )
827 self.summary["df_value_attrs"] = []
830 class cmd_ldapcmp(Command):
831 """Compare two ldap databases."""
832 synopsis = "%prog <URL1> <URL2> (domain|configuration|schema|dnsdomain|dnsforest) [options]"
834 takes_optiongroups = {
835 "sambaopts": options.SambaOptions,
836 "versionopts": options.VersionOptions,
837 "credopts": options.CredentialsOptionsDouble,
840 takes_optiongroups = {
841 "sambaopts": options.SambaOptions,
842 "versionopts": options.VersionOptions,
843 "credopts": options.CredentialsOptionsDouble,
846 takes_args = ["URL1", "URL2", "context1?", "context2?", "context3?", "context4?", "context5?"]
848 takes_options = [
849 Option("-w", "--two", dest="two", action="store_true", default=False,
850 help="Hosts are in two different domains"),
851 Option("-q", "--quiet", dest="quiet", action="store_true", default=False,
852 help="Do not print anything but relay on just exit code"),
853 Option("-v", "--verbose", dest="verbose", action="store_true", default=False,
854 help="Print all DN pairs that have been compared"),
855 Option("--sd", dest="descriptor", action="store_true", default=False,
856 help="Compare nTSecurityDescriptor attibutes only"),
857 Option("--sort-aces", dest="sort_aces", action="store_true", default=False,
858 help="Sort ACEs before comparison of nTSecurityDescriptor attribute"),
859 Option("--view", dest="view", default="section",
860 help="Display mode for nTSecurityDescriptor results. Possible values: section or collision."),
861 Option("--base", dest="base", default="",
862 help="Pass search base that will build DN list for the first DC."),
863 Option("--base2", dest="base2", default="",
864 help="Pass search base that will build DN list for the second DC. Used when --two or when compare two different DNs."),
865 Option("--scope", dest="scope", default="SUB",
866 help="Pass search scope that builds DN list. Options: SUB, ONE, BASE"),
867 Option("--filter", dest="filter", default="",
868 help="List of comma separated attributes to ignore in the comparision"),
869 Option("--skip-missing-dn", dest="skip_missing_dn", action="store_true", default=False,
870 help="Skip report and failure due to missing DNs in one server or another"),
873 def run(self, URL1, URL2,
874 context1=None, context2=None, context3=None, context4=None, context5=None,
875 two=False, quiet=False, verbose=False, descriptor=False, sort_aces=False,
876 view="section", base="", base2="", scope="SUB", filter="",
877 credopts=None, sambaopts=None, versionopts=None, skip_missing_dn=False):
879 lp = sambaopts.get_loadparm()
881 using_ldap = URL1.startswith("ldap") or URL2.startswith("ldap")
883 if using_ldap:
884 creds = credopts.get_credentials(lp, fallback_machine=True)
885 else:
886 creds = None
887 creds2 = credopts.get_credentials2(lp, guess=False)
888 if creds2.is_anonymous():
889 creds2 = creds
890 else:
891 creds2.set_domain("")
892 creds2.set_workstation("")
893 if using_ldap and not creds.authentication_requested():
894 raise CommandError("You must supply at least one username/password pair")
896 # make a list of contexts to compare in
897 contexts = []
898 if context1 is None:
899 if base and base2:
900 # If search bases are specified context is defaulted to
901 # DOMAIN so the given search bases can be verified.
902 contexts = ["DOMAIN"]
903 else:
904 # if no argument given, we compare all contexts
905 contexts = ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]
906 else:
907 for c in [context1, context2, context3, context4, context5]:
908 if c is None:
909 continue
910 if not c.upper() in ["DOMAIN", "CONFIGURATION", "SCHEMA", "DNSDOMAIN", "DNSFOREST"]:
911 raise CommandError("Incorrect argument: %s" % c)
912 contexts.append(c.upper())
914 if verbose and quiet:
915 raise CommandError("You cannot set --verbose and --quiet together")
916 if (not base and base2) or (base and not base2):
917 raise CommandError("You need to specify both --base and --base2 at the same time")
918 if descriptor and view.upper() not in ["SECTION", "COLLISION"]:
919 raise CommandError("Invalid --view value. Choose from: section or collision")
920 if not scope.upper() in ["SUB", "ONE", "BASE"]:
921 raise CommandError("Invalid --scope value. Choose from: SUB, ONE, BASE")
923 con1 = LDAPBase(URL1, creds, lp,
924 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
925 verbose=verbose,view=view, base=base, scope=scope,
926 outf=self.outf, errf=self.errf)
927 assert len(con1.base_dn) > 0
929 con2 = LDAPBase(URL2, creds2, lp,
930 two=two, quiet=quiet, descriptor=descriptor, sort_aces=sort_aces,
931 verbose=verbose, view=view, base=base2, scope=scope,
932 outf=self.outf, errf=self.errf)
933 assert len(con2.base_dn) > 0
935 filter_list = filter.split(",")
937 status = 0
938 for context in contexts:
939 if not quiet:
940 self.outf.write("\n* Comparing [%s] context...\n" % context)
942 b1 = LDAPBundel(con1, context=context, filter_list=filter_list,
943 outf=self.outf, errf=self.errf)
944 b2 = LDAPBundel(con2, context=context, filter_list=filter_list,
945 outf=self.outf, errf=self.errf)
947 if b1 == b2:
948 if not quiet:
949 self.outf.write("\n* Result for [%s]: SUCCESS\n" %
950 context)
951 else:
952 if not quiet:
953 self.outf.write("\n* Result for [%s]: FAILURE\n" % context)
954 if not descriptor:
955 assert len(b1.summary["df_value_attrs"]) == len(b2.summary["df_value_attrs"])
956 b2.summary["df_value_attrs"] = []
957 self.outf.write("\nSUMMARY\n")
958 self.outf.write("---------\n")
959 b1.print_summary()
960 b2.print_summary()
961 # mark exit status as FAILURE if a least one comparison failed
962 status = -1
963 if status != 0:
964 raise CommandError("Compare failed: %d" % status)