python/uptodateness: cope with unknown invocation ID
[metze/samba/wip.git] / python / samba / uptodateness.py
1 # Uptodateness utils
2 #
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>
6 #
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.
11 #
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.
16 #
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
20
21 import sys
22 import time
23
24 from ldb import SCOPE_BASE, LdbError
25
26 from samba import nttime2unix, dsdb
27 from samba.netcmd import CommandError
28 from samba.samdb import SamDB
29 from samba.kcc import KCC
30
31
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)
37
38     dsa_list = kcc.list_dsas()
39     dsas = set(dsa_list)
40     if len(dsas) != len(dsa_list):
41         print("There seem to be duplicate dsas", file=sys.stderr)
42
43     return kcc, dsas
44
45
46 def get_partition_maps(samdb):
47     """Generate dictionaries mapping short partition names to the
48     appropriate DNs."""
49     base_dn = samdb.domain_dn()
50     short_to_long = {
51         "DOMAIN": base_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
56     }
57
58     long_to_short = {}
59     for s, l in short_to_long.items():
60         long_to_short[l] = s
61
62     return short_to_long, long_to_short
63
64
65 def get_partition(samdb, part):
66     # Allow people to say "--partition=DOMAIN" rather than
67     # "--partition=DC=blah,DC=..."
68     if part is not None:
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)
73     return part
74
75
76 def get_utdv(samdb, dn):
77     """This finds the uptodateness vector in the database."""
78     cursors = []
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         try:
87             settings_dn = str(res[0]["distinguishedName"][0])
88             prefix, dsa_dn = settings_dn.split(',', 1)
89         except IndexError as e:
90             print("Unknown invocation ID %s" % inv_id,
91                   file=sys.stderr)
92             continue
93         if prefix != 'CN=NTDS Settings':
94             raise CommandError("Expected NTDS Settings DN, got %s" %
95                                settings_dn)
96
97         cursors.append((dsa_dn,
98                         inv_id,
99                         int(c.highest_usn),
100                         nttime2unix(c.last_sync_success)))
101     return cursors
102
103
104 def get_own_cursor(samdb):
105     res = samdb.search(base="",
106                        scope=SCOPE_BASE,
107                        attrs=["highestCommittedUSN"])
108     usn = int(res[0]["highestCommittedUSN"][0])
109     now = int(time.time())
110     return (usn, now)
111
112
113 def get_utdv_edges(local_kcc, dsas, part_dn, lp, creds):
114     # we talk to each remote and make a matrix of the vectors
115     # for each partition
116     # normalise by oldest
117     utdv_edges = {}
118     for dsa_dn in dsas:
119         res = local_kcc.samdb.search(dsa_dn,
120                                      scope=SCOPE_BASE,
121                                      attrs=["dNSHostName"])
122         ldap_url = "ldap://%s" % res[0]["dNSHostName"][0]
123         try:
124             samdb = SamDB(url=ldap_url, credentials=creds, lp=lp)
125             cursors = get_utdv(samdb, part_dn)
126             own_usn, own_time = get_own_cursor(samdb)
127             remotes = {dsa_dn: own_usn}
128             for dn, guid, usn, t in cursors:
129                 remotes[dn] = usn
130         except LdbError as e:
131             print("Could not contact %s (%s)" % (ldap_url, e),
132                   file=sys.stderr)
133             continue
134         utdv_edges[dsa_dn] = remotes
135     return utdv_edges
136
137
138 def get_utdv_distances(utdv_edges, dsas):
139     distances = {}
140     for dn1 in dsas:
141         try:
142             peak = utdv_edges[dn1][dn1]
143         except KeyError as e:
144             peak = 0
145         d = {}
146         distances[dn1] = d
147         for dn2 in dsas:
148             if dn2 in utdv_edges:
149                 if dn1 in utdv_edges[dn2]:
150                     dist = peak - utdv_edges[dn2][dn1]
151                     d[dn2] = dist
152                 else:
153                     print("Missing dn %s from UTD vector" % dn1,
154                           file=sys.stderr)
155             else:
156                 print("missing dn %s from UTD vector list" % dn2,
157                       file=sys.stderr)
158     return distances
159
160
161 def get_utdv_max_distance(distances):
162     max_distance = 0
163     for vector in distances.values():
164         for distance in vector.values():
165             max_distance = max(max_distance, distance)
166     return max_distance
167
168
169 def get_utdv_summary(distances, filters=None):
170     maximum = failure = 0
171     median = 0.0  # could be average of 2 median values
172     values = []
173     # put all values into a list, exclude self to self ones
174     for dn_outer, vector in distances.items():
175         for dn_inner, distance in vector.items():
176             if dn_outer != dn_inner:
177                 values.append(distance)
178
179     if values:
180         values.sort()
181         maximum = values[-1]
182         length = len(values)
183         if length % 2 == 0:
184             index = length//2 - 1
185             median = (values[index] + values[index+1])/2.0
186             median = round(median, 1)  # keep only 1 decimal digit like 2.5
187         else:
188             index = (length - 1)//2
189             median = values[index]
190             median = float(median)  # ensure median is always a float like 1.0
191         # if value not exist, that's a failure
192         expected_length = len(distances) * (len(distances) - 1)
193         failure = expected_length - length
194
195     summary = {
196         'maximum': maximum,
197         'median': median,
198         'failure': failure,
199     }
200
201     if filters:
202         return {key: summary[key] for key in filters}
203     else:
204         return summary