9 from urllib.parse import urlparse
14 print('Warning, could not import pygraphviz, test mode only')
16 from optparse import OptionParser
18 from sfa.client.return_value import ReturnValue
19 from sfa.client.sfi import Sfi
20 from sfa.util.sfalogging import logger, DEBUG
21 from sfa.client.sfaserverproxy import SfaServerProxy
24 def url_hostname_port(url):
25 if url.find("://") < 0:
27 parsed_url = urlparse(url)
28 # 0(scheme) returns protocol
30 if parsed_url[0] == 'https':
32 # 1(netloc) returns the hostname+port part
33 parts = parsed_url[1].split(":")
36 return (url, parts[0], default_port)
38 return (url, parts[0], parts[1])
40 # a very simple cache mechanism so that successive runs (see make)
41 # will go *much* faster
42 # assuming everything is sequential, as simple as it gets
43 # { url -> (timestamp, version)}
47 # default expiration period is 1h
49 def __init__(self, filename=None, expires=60 * 60):
50 # default is to store cache in the same dir as argv[0]
52 filename = os.path.join(os.path.dirname(
53 sys.argv[0]), "sfascan-version-cache.pickle")
54 self.filename = filename
55 self.expires = expires
61 infile = open(self.filename, 'r')
62 self.url2version = pickle.load(infile)
65 logger.debug("Cannot load version cache, restarting from scratch")
67 logger.debug("loaded version cache with %d entries %s" % (len(self.url2version),
68 list(self.url2version.keys())))
72 outfile = open(self.filename, 'w')
73 pickle.dump(self.url2version, outfile)
76 logger.log_exc("Cannot save version cache into %s" % self.filename)
80 retcod = os.unlink(self.filename)
81 logger.info("Cleaned up version cache %s, retcod=%d" %
82 (self.filename, retcod))
84 logger.info("Could not unlink version cache %s" % self.filename)
87 entries = len(self.url2version)
88 print("version cache from file %s has %d entries" %
89 (self.filename, entries))
90 key_values = list(self.url2version.items())
92 def old_first(kv1, kv2): return int(kv1[1][0] - kv2[1][0])
93 key_values.sort(old_first)
94 for key_value in key_values:
95 (url, tuple) = key_value
96 (timestamp, version) = tuple
97 how_old = time.time() - timestamp
98 if how_old <= self.expires:
99 print(url, "-- %d seconds ago" % how_old)
101 print("OUTDATED", url, "(%d seconds ago, expires=%d)" %
102 (how_old, self.expires))
104 # turns out we might have trailing slashes or not
105 def normalize(self, url):
106 return url.strip("/")
108 def set(self, url, version):
109 url = self.normalize(url)
110 self.url2version[url] = (time.time(), version)
113 url = self.normalize(url)
115 (timestamp, version) = self.url2version[url]
116 how_old = time.time() - timestamp
117 if how_old <= self.expires:
125 # non-existing hostnames happen...
126 # for better perfs we cache the result of gethostbyname too
131 def __init__(self, url, mentioned_in=None, verbose=False):
133 self.verbose = verbose
134 cache = VersionCache()
135 key = "interface:%s" % url
137 (self._url, self.hostname, self.port) = url_hostname_port(url)
138 # look for ip in the cache
139 tuple = cache.get(key)
141 (self.hostname, self.ip, self.port) = tuple
143 self.ip = socket.gethostbyname(self.hostname)
145 msg = "can't resolve hostname %s\n\tfound in url %s" % (
146 self.hostname, self._url)
148 msg += "\n\t(mentioned at %s)" % mentioned_in
150 self.hostname = "unknown"
154 cache.set(key, (self.hostname, self.ip, self.port,))
158 # mark unknown interfaces as probed to avoid unnecessary attempts
159 if self.hostname == 'unknown':
160 # don't really try it
167 # this is used as a key for creating graph nodes and to avoid duplicates
169 return "%s:%s" % (self.ip, self.port)
171 # connect to server and trigger GetVersion
172 def get_version(self):
173 # if we already know the answer:
176 # otherwise let's look in the cache file
177 logger.debug("searching in version cache %s" % self.url())
178 cached_version = VersionCache().get(self.url())
179 if cached_version is not None:
180 logger.info("Retrieved version info from cache %s" % self.url())
181 return cached_version
182 # otherwise let's do the hard work
183 # dummy to meet Sfi's expectations for its 'options' field
187 options = DummyOptions()
188 options.verbose = self.verbose
191 client = Sfi(options)
194 key_file = client.private_key
195 cert_file = client.my_gid
196 logger.debug("using key %s & cert %s" % (key_file, cert_file))
198 logger.info('issuing GetVersion at %s' % url)
199 # setting timeout here seems to get the call to fail - even though the response time is fast
200 #server=SfaServerProxy(url, key_file, cert_file, verbose=self.verbose, timeout=options.timeout)
201 server = SfaServerProxy(
202 url, key_file, cert_file, verbose=self.verbose)
203 self._version = ReturnValue.get_value(server.GetVersion())
205 logger.log_exc("failed to get version")
207 # so that next run from this process will find out
209 # store in version cache so next processes will remember for an hour
210 cache = VersionCache()
211 cache.set(self.url(), self._version)
213 logger.debug("Saved version for url=%s in version cache" % self.url())
218 def multi_lines_label(*lines):
219 result = '<<TABLE BORDER="0" CELLBORDER="0"><TR><TD>' + \
220 '</TD></TR><TR><TD>'.join(lines) + \
221 '</TD></TR></TABLE>>'
224 # default is for when we can't determine the type of the service
225 # typically the server is down, or we can't authenticate, or it's too old
227 shapes = {"registry": "diamond",
228 "aggregate": "box", 'default': 'plaintext'}
229 abbrevs = {"registry": "REG",
230 "aggregate": "AM", 'default': '[unknown interface]'}
232 # return a dictionary that translates into the node's attr
233 def get_layout(self):
235 # retrieve cached GetVersion
236 version = self.get_version()
237 # set the href; xxx would make sense to try and 'guess' the web URL,
238 # not the API's one...
239 layout['href'] = self.url()
240 # set html-style label
241 # see http://www.graphviz.org/doc/info/shapes.html#html
242 # if empty the service is unreachable
248 abbrev = Interface.abbrevs[version['interface']]
250 abbrev = Interface.abbrevs['default']
253 label += " %s" % version['hrn']
256 if 'code_tag' in version:
257 label += " %s" % version['code_tag']
258 if 'testbed' in version:
259 label += " (%s)" % version['testbed']
260 layout['label'] = Interface.multi_lines_label(self.url(), label)
263 shape = Interface.shapes[version['interface']]
265 shape = Interface.shapes['default']
266 layout['shape'] = shape
267 # fill color to outline wrongly configured or unreachable bodies
268 # as of sfa-2.0 registry doesn't have 'sfa' not 'geni_api',
269 # but have peer aggregates have 'geni_api' and 'sfa'
270 if 'geni_api' not in version and 'peers' not in version:
271 layout['style'] = 'filled'
272 layout['fillcolor'] = 'gray'
278 # provide the entry points (a list of interfaces)
279 def __init__(self, left_to_right=False, verbose=False):
280 self.verbose = verbose
281 self.left_to_right = left_to_right
283 def graph(self, entry_points):
284 graph = pygraphviz.AGraph(directed=True)
285 if self.left_to_right:
286 graph.graph_attr['rankdir'] = 'LR'
287 self.scan(entry_points, graph)
290 # scan from the given interfaces as entry points
291 def scan(self, interfaces, graph):
292 if not isinstance(interfaces, list):
293 interfaces = [interfaces]
295 # remember node to interface mapping
297 # add entry points right away using the interface uid's as a key
300 graph.add_node(i.uid())
301 node2interface[graph.get_node(i.uid())] = i
303 # keep on looping until we reach a fixed point
304 # don't worry about abels and shapes that will get fixed later on
306 for interface in to_scan:
307 # performing xmlrpc call
309 "retrieving/fetching version at interface %s" % interface.url())
310 version = interface.get_version()
313 "<EMPTY GetVersion(); offline or cannot authenticate>")
315 for (k, v) in version.items():
316 if not isinstance(v, dict):
317 logger.debug("\r\t%s:%s" % (k, v))
320 for (k1, v1) in v.items():
321 logger.debug("\r\t\t%s:%s" % (k1, v1))
322 # proceed with neighbours
323 if 'peers' in version:
324 for (next_name, next_url) in version['peers'].items():
325 next_interface = Interface(
326 next_url, mentioned_in=interface.url())
327 # locate or create node in graph
329 # if found, we're good with this one
330 next_node = graph.get_node(next_interface.uid())
332 # otherwise, let's move on with it
333 graph.add_node(next_interface.uid())
334 next_node = graph.get_node(next_interface.uid())
335 node2interface[next_node] = next_interface
336 to_scan.append(next_interface)
337 graph.add_edge(interface.uid(), next_interface.uid())
338 scanned.append(interface)
339 to_scan.remove(interface)
340 # we've scanned the whole graph, let's get the labels and shapes
342 for node in graph.nodes():
343 interface = node2interface.get(node, None)
345 for (k, v) in interface.get_layout().items():
348 logger.error("MISSED interface with node %s" % node)
353 default_outfiles = ['sfa.png', 'sfa.svg', 'sfa.dot']
356 usage = "%prog [options] url-entry-point(s)"
357 parser = OptionParser(usage=usage)
358 parser.add_option("-d", "--dir", dest="sfi_dir",
359 help="config & working directory - default is " + Sfi.default_sfi_dir(),
360 metavar="PATH", default=Sfi.default_sfi_dir())
361 parser.add_option("-o", "--output", action='append', dest='outfiles', default=[],
362 help="output filenames (cumulative) - defaults are %r" % SfaScan.default_outfiles)
363 parser.add_option("-l", "--left-to-right", action="store_true", dest="left_to_right", default=False,
364 help="instead of top-to-bottom")
365 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
366 help="verbose - can be repeated for more verbosity")
367 parser.add_option("-c", "--clean-cache", action='store_true',
368 dest='clean_cache', default=False,
369 help='clean/trash version cache and exit')
370 parser.add_option("-s", "--show-cache", action='store_true',
371 dest='show_cache', default=False,
372 help='show/display version cache')
374 (options, args) = parser.parse_args()
375 logger.enable_console()
376 # apply current verbosity to logger
377 logger.setLevelFromOptVerbose(options.verbose)
378 # figure if we need to be verbose for these local classes that only
380 bool_verbose = logger.getBoolVerboseFromOpt(options.verbose)
382 if options.show_cache:
383 VersionCache().show()
385 if options.clean_cache:
386 VersionCache().clean()
392 if not options.outfiles:
393 options.outfiles = SfaScan.default_outfiles
394 scanner = Scanner(left_to_right=options.left_to_right,
395 verbose=bool_verbose)
396 entries = [Interface(entry, mentioned_in="command line")
399 g = scanner.graph(entries)
400 logger.info("creating layout")
402 for outfile in options.outfiles:
403 logger.info("drawing in %s" % outfile)
406 # test mode when pygraphviz is not available
409 print("GetVersion at %s returned %s" %
410 (entry.url(), entry.get_version()))