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