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
_v
2(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"])
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
,
93 if prefix
!= 'CN=NTDS Settings':
94 raise CommandError("Expected NTDS Settings DN, got %s" %
97 cursors
.append((dsa_dn
,
100 nttime2unix(c
.last_sync_success
)))
104 def get_own_cursor(samdb
):
105 res
= samdb
.search(base
="",
107 attrs
=["highestCommittedUSN"])
108 usn
= int(res
[0]["highestCommittedUSN"][0])
109 now
= int(time
.time())
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
116 # normalise by oldest
119 res
= local_kcc
.samdb
.search(dsa_dn
,
121 attrs
=["dNSHostName"])
122 ldap_url
= "ldap://%s" % res
[0]["dNSHostName"][0]
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
:
130 except LdbError
as e
:
131 print("Could not contact %s (%s)" % (ldap_url
, e
),
134 utdv_edges
[dsa_dn
] = remotes
138 def get_utdv_distances(utdv_edges
, dsas
):
142 peak
= utdv_edges
[dn1
][dn1
]
143 except KeyError as e
:
148 if dn2
in utdv_edges
:
149 if dn1
in utdv_edges
[dn2
]:
150 dist
= peak
- utdv_edges
[dn2
][dn1
]
153 print("Missing dn %s from UTD vector" % dn1
,
156 print("missing dn %s from UTD vector list" % dn2
,
161 def get_utdv_max_distance(distances
):
163 for vector
in distances
.values():
164 for distance
in vector
.values():
165 max_distance
= max(max_distance
, distance
)
169 def get_utdv_summary(distances
, filters
=None):
170 maximum
= failure
= 0
171 median
= 0.0 # could be average of 2 median 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
)
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
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
202 return {key
: summary
[key
] for key
in filters
}