3 # Copyright (C) Andrew Bartlett 2015, 2018
4 # Copyright (C) Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
5 # Copyright (C) Joe Guo <joeg@catalyst.net.nz>
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 from __future__ import print_function
24 from ldb import SCOPE_BASE, LdbError
26 from samba import nttime2unix, dsdb
27 from samba.netcmd import CommandError
28 from samba.samdb import SamDB
29 from samba.kcc import KCC
32 def get_kcc_and_dsas(url, lp, creds):
33 """Get a readonly KCC object and the list of DSAs it knows about."""
34 unix_now = int(time.time())
35 kcc = KCC(unix_now, readonly=True)
36 kcc.load_samdb(url, lp, creds)
38 dsa_list = kcc.list_dsas()
40 if len(dsas) != len(dsa_list):
41 print("There seem to be duplicate dsas", file=sys.stderr)
46 def get_partition_maps(samdb):
47 """Generate dictionaries mapping short partition names to the
49 base_dn = samdb.domain_dn()
52 "CONFIGURATION": str(samdb.get_config_basedn()),
53 "SCHEMA": "CN=Schema,%s" % samdb.get_config_basedn(),
54 "DNSDOMAIN": "DC=DomainDnsZones,%s" % base_dn,
55 "DNSFOREST": "DC=ForestDnsZones,%s" % base_dn
59 for s, l in short_to_long.items():
62 return short_to_long, long_to_short
65 def get_partition(samdb, part):
66 # Allow people to say "--partition=DOMAIN" rather than
67 # "--partition=DC=blah,DC=..."
69 short_partitions, long_partitions = get_partition_maps(samdb)
70 part = short_partitions.get(part.upper(), part)
71 if part not in long_partitions:
72 raise CommandError("unknown partition %s" % part)
76 def get_utdv(samdb, dn):
77 """This finds the uptodateness vector in the database."""
79 config_dn = samdb.get_config_basedn()
80 for c in dsdb._dsdb_load_udv_v2(samdb, dn):
81 inv_id = str(c.source_dsa_invocation_id)
82 res = samdb.search(base=config_dn,
83 expression=("(&(invocationId=%s)"
84 "(objectClass=nTDSDSA))" % inv_id),
85 attrs=["distinguishedName", "invocationId"])
86 settings_dn = str(res[0]["distinguishedName"][0])
87 prefix, dsa_dn = settings_dn.split(',', 1)
88 if prefix != 'CN=NTDS Settings':
89 raise CommandError("Expected NTDS Settings DN, got %s" %
92 cursors.append((dsa_dn,
95 nttime2unix(c.last_sync_success)))
99 def get_own_cursor(samdb):
100 res = samdb.search(base="",
102 attrs=["highestCommittedUSN"])
103 usn = int(res[0]["highestCommittedUSN"][0])
104 now = int(time.time())
108 def get_utdv_edges(local_kcc, dsas, part_dn, lp, creds):
109 # we talk to each remote and make a matrix of the vectors
111 # normalise by oldest
114 res = local_kcc.samdb.search(dsa_dn,
116 attrs=["dNSHostName"])
117 ldap_url = "ldap://%s" % res[0]["dNSHostName"][0]
119 samdb = SamDB(url=ldap_url, credentials=creds, lp=lp)
120 cursors = get_utdv(samdb, part_dn)
121 own_usn, own_time = get_own_cursor(samdb)
122 remotes = {dsa_dn: own_usn}
123 for dn, guid, usn, t in cursors:
125 except LdbError as e:
126 print("Could not contact %s (%s)" % (ldap_url, e),
129 utdv_edges[dsa_dn] = remotes
133 def get_utdv_distances(utdv_edges, dsas):
137 peak = utdv_edges[dn1][dn1]
138 except KeyError as e:
143 if dn2 in utdv_edges:
144 if dn1 in utdv_edges[dn2]:
145 dist = peak - utdv_edges[dn2][dn1]
148 print("Missing dn %s from UTD vector" % dn1,
151 print("missing dn %s from UTD vector list" % dn2,
156 def get_utdv_max_distance(distances):
158 for vector in distances.values():
159 for distance in vector.values():
160 max_distance = max(max_distance, distance)
164 def get_utdv_summary(distances, filters=None):
165 maximum = failure = 0
166 median = 0.0 # could be average of 2 median values
168 # put all values into a list, exclude self to self ones
169 for dn_outer, vector in distances.items():
170 for dn_inner, distance in vector.items():
171 if dn_outer != dn_inner:
172 values.append(distance)
180 median = (values[index] + values[index+1])/2.0
181 median = round(median, 1) # keep only 1 decimal digit like 2.5
183 index = (length - 1)/2
184 median = values[index]
185 median = float(median) # ensure median is always a float like 1.0
186 # if value not exist, that's a failure
187 expected_length = len(distances) * (len(distances) - 1)
188 failure = expected_length - length
197 return {key: summary[key] for key in filters}