-
Notifications
You must be signed in to change notification settings - Fork 0
/
plotrepl.py
297 lines (254 loc) · 12.3 KB
/
plotrepl.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# Replication plot
# Author: Konrads Klints <konrads.klints@kpmg.co.uk>
import sys
import logging
import adsilib
import csv
import argparse
import win32com
import win32com.client as win32
import collections
import pprint
import pydot
import os,glob,tempfile,subprocess,re
import ldapaccess
import math
sys.setrecursionlimit(3000)
def _filter_node_badchars(n):
assert type(n)==str or type(n)==unicode
return re.sub("[^A-Za-z0-9_]","_",n)
#return n.replace("&- ","_")
# def _makeTreeEntry(node,rel):
# assert rel in "forward reverse none".split(" "),\
# "Unknown relationship type between nodes %s" % rel
# tree={'name':node['name'],'attrs':node,'users':[],'groups':[],'rel':rel}
# return tree
def _find_graphviz_dot():
candidates=glob.glob(os.path.join(os.environ['PROGRAMFILES'],"GraphViz*","bin","dot.exe"))
if len(candidates):
dotfile=candidates[0]
if os.path.exists(dotfile):
return dotfile
return None
def getSiteLinks(source):
attributes="cost cn replInterval siteList".split(" ")
results=source.search('(objectClass=siteLink)',attributes)
return results
def getSites(source):
attributes="cn location".split(" ")
results=source.search('(objectClass=site)',attributes)
return results
def main():
format_types="graph-dot graph".split(" ")
#lookup_directions="reverse forward both".split(" ")
lookup_directions="reverse forward".split(" ")
parser = argparse.ArgumentParser(description="Retrieve and plot Active Directory sites and replication.\nIf no credentials are supplied current credentials of logged on user will be used",
epilog=\
"""
Supported output formats are:
* graph-dot - output a .dot file which is a graph defintion as understood by GraphViz.
* graph - build a pretty picture using the .dot file
By Konrads Klints <konrads.klints@kpmg.co.uk>.
""".strip())
# parser.add_argument('groupName',metavar='OBJECT',type=str,nargs=1,
# help='Object (group or user) to expand. You can specify both DN and NT names')
parser.add_argument('--debug',dest='debug', action='store_true', default=False,
help='Enable debugging output')
parser.add_argument('-o','--output',dest='output', type=str,metavar="FILE",
help='Output file, defaults to stdout')
# parser.add_argument('--no-recursive',dest='recursive', action="store_false",default=True,
# help='Do not expand child groups')
parser.add_argument('-f','--format',dest='format_type', type=lambda x: x.lower(),choices=format_types,
default=format_types[0],
help="Output format")
parser.add_argument("-s","--data-source",
help="Specify data source. Defaults to global catalogue GC:",
type=str,default="GC:",dest='source')
# dumpwhat=parser.add_mutually_exclusive_group()
# dumpwhat.add_argument("-d","--direction",
# help="""Lookup direction. Forward looks up the members of the group (only for groups),
# reverse - groups that the specified object is a member of.
# The default for groups is forward, while for users - reverse""",
# type=lambda x: x.lower(),choices=lookup_directions,
# dest='direction')
# dumpwhat.add_argument("-A","--dump-all",
# help="Dump every group",
# dest='dump_all',default=False,action="store_true")
# ugg=parser.add_argument_group("User expansion",
# """
# Options related to user expansion. By default, if input object is group, then users will be graphed,
# if user, then not"""
# ).add_mutually_exclusive_group()
# ugg.add_argument('--fetch-users',dest='always_fetch_users', action="store_true", default=None ,
# help="Always force users")
# ugg.add_argument('--no-fetch-users',dest='never_fetch_users', action="store_true", default=None,
# help="Never fetch users")
csvgroup=parser.add_argument_group('csv','Arguments relating to CSV output')
csvgroup.add_argument('--no-dn',dest='csv_output_dn', action="store_false",default=True,
help="Do not print DNs in CSV")
graphgroup=parser.add_argument_group("graphing","Options related to graph generation")
dotfile=_find_graphviz_dot()
if dotfile is None:
dotfile="NOT FOUND"
graphgroup.add_argument('--graphviz-dot',dest='dotfile', type=str,default=dotfile,
help="Path to GraphViz dot.exe, best guess: %s" % dotfile)
# graphgroup.add_argument('--graph-users',dest='graph_users', action="store_true",default=False,
# help="Expand graph users, don't summarise")
group = parser.add_argument_group("creds","credential management").add_mutually_exclusive_group(required=False)
group.add_argument('--credentials',dest='credentials', type=str,
help='Credentials separated by column like bp1\user:password')
group.add_argument('--credfile',dest='credfile', type=str,
help='Credential file which contains one line with rot13 + base64 encoded credentials like bp1\user:password')
global args
args=parser.parse_args()
# This is for py2exe to work:
if win32com.client.gencache.is_readonly == True:
#allow gencache to create the cached wrapper objects
win32com.client.gencache.is_readonly = False
# under p2exe the call in gencache to __init__() does not happen
# so we use Rebuild() to force the creation of the gen_py folder
win32com.client.gencache.Rebuild(int(args.debug))
# NB You must ensure that the python...\win32com.client.gen_py dir does not exist
# to allow creation of the cache in %temp%
if args.format_type=="graph-dot" and args.output is None:
print >>sys.stderr, "ERROR: If you specify graph-dot, you must specify output!\n\n"
parser.print_help()
sys.exit(-1)
# if args.format_type=="csv" and args.output:
# ext=args.output.split(".")[-1]
# if not ext.lower().endswith("csv"):
# logging.warning("You requested CSV output, but specified output file with a different extension: %s" % ext)
level=logging.INFO
if args.debug:
level=logging.DEBUG
# if args.format_type=="csv" and args.direction=="both":
# print >>sys.stderr, "Direction 'both' is not supported for CSV"
# sys.exit(-1)
logging.basicConfig(level=level,stream=sys.stderr)
output=sys.stdout
if args.output and args.format_type!="graph-dot":
output=file(args.output,"wb")
if args.credentials:
(username,password)=args.credentials.split(":")
elif args.credfile:
(username,password)=file(args.credfile,'rb').read().decode('base64').decode('rot13').split(":")
else:
username=None
password=None
global datasource
if args.source.upper().startswith("LDAP"):
datasource=ldapaccess.LDAPAccess(args.source,username,password)
root="" # Cheating
else:
datasource=adsilib.UsefulADSISearches(args.source,username,password)
root=datasource.getDefaultNamingContenxt()
# fulltree=_recurse(tree,args.recursive)
# connections=_buildConnections(datasource)
# if args.format_type=="pretty-print":
# pprint.PrettyPrinter(stream=output,indent=2,depth=40).pprint(connections)
# elif args.format_type=="csv":
# raise NotImplementedError("Sorry, this feature is not implented yet!")
# # writer=csv.writer(output,quoting=csv.QUOTE_MINIMAL)
# # _write_csv_nested(writer,[],fulltree)
if args.format_type=="graph-dot" or args.format_type=="graph":
# print "lalalal"
graph=pydot.Dot(graph_type='digraph',fontname="Verdana",
rankdir='LR'#,label="Direction: %s" % args.direction,labelloc="top"
)
_build_graph(graph,getSites(datasource),getSiteLinks(datasource))
# Add legend
# legend=pydot.Node("legend",shape="none",margin="0",label=\
# """
# <<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">
# <tr><td colspan="2">Legend</td></tr>
# <tr><td colspan="2">Site replication map</td></tr>
# <TR>
# <TD> Regular group </TD>
# <TD BGCOLOR="#bfefff"> <br/></TD>
# </TR>
# <TR>
# <TD>Group contributing to multiple inheritance</TD>
# <TD BGCOLOR="#ff69b4"> </TD>
# </TR>
# <TR>
# <TD>Mutually inherited groups</TD>
# <TD BGCOLOR="#ff6a6a"> </TD>
# </TR>
# </TABLE>>""")
# graph.add_node(legend)
#Monkeypatch pydot to convert unicode to utf8
graph.mp_to_string=graph.to_string
def _encodeme():
try:
return graph.mp_to_string().encode('utf8')
except UnicodeDecodeError:
return graph.mp_to_string()
graph.to_string=_encodeme
try:
if args.format_type == "graph":
(fd,tmpf)=tempfile.mkstemp()
logging.debug("Tempfile is %s" % tmpf)
os.close(fd)
graph.write(tmpf)
ext=args.output.split(".")[-1]
if not os.path.exists(args.dotfile):
logging.CRITICAL("dot.exe does not exist, I got path %s" % args.dotfile)
sys.exit(-1)
callargs=[args.dotfile,"-T%s" % ext.lower(),"-o",args.output,tmpf]
if args.debug:
callargs.append("-v")
logging.debug("Call args: %s" % str(callargs))
subprocess.call(callargs,shell=True)
else:
graph.write(args.output)
finally:
pass
#os.unlink(tmpf)
# def _make_graph_group(group):
# #n=pydot.Node(_filter_node_badchars(group['name']),shape="rect",color="lightblue2", style="filled",
# n=pydot.Node(group['name'],shape="rect",color="lightblue2", style="filled",
# #label="%s\n(%s)" % (group['name'],group['attrs']['distinguishedName'])
# label="%s" % (group['name'],)
# )
# return n
def _make_graph_site(site):
#n=pydot.Node(_filter_node_badchars(group['name']),shape="rect",color="lightblue2", style="filled",
n=pydot.Node(site['_DN'],shape="rect",color="lightblue2", style="filled",
#label="%s\n(%s)" % (group['name'],group['attrs']['distinguishedName'])
label="%s\n%s" % (site['cn'],site['location'])
)
return n
# global alreadyGraphNodes
# alreadyGraphedNodes={}
def _build_graph(graph,sites,links):
# Scan costs
costs=[]
for link in links:
cost=int(link['cost'])
if not cost in costs:
costs.append(cost)
costs.sort(reverse=True)
graphedLinks=[]
for site in sites:
graph.add_node(_make_graph_site(site))
# logging.DEBUG("Graphed %i sites" % len(sites) )
for link in links:
if "DefaultSiteLink" in link['cn']:
logging.log(logging.DEBUG, "Skipping %s" % link['cn'])
continue
else:
logging.log(logging.DEBUG, "Plotting %s" % link['cn'])
for firstLink in link['siteList']:
for secondLink in link['siteList']:
if firstLink == secondLink:
continue
if (firstLink,secondLink) in graphedLinks or (secondLink,firstLink) in graphedLinks:
continue
cost=int(link['cost'])
weight=costs.index(cost)+1
logging.log(logging.DEBUG,"Cost was %i, weight is %i" % (cost,weight))
edge=pydot.Edge(firstLink,secondLink,style="bold",weight=weight,label="%s/%s" % (link['cost'],link['replInterval']))
graph.add_edge(edge)
graphedLinks.append((firstLink,secondLink))
if __name__=="__main__":
main()