#!/usr/bin/env python
import sys, os.path
import pickle
import time
import socket
import traceback
from urlparse import urlparse
import pygraphviz
from optparse import OptionParser
from sfa.client.sfi import Sfi
from sfa.util.sfalogging import logger, DEBUG
import sfa.client.sfaprotocol as sfaprotocol
def url_hostname_port (url):
if url.find("://")<0:
url="http://"+url
parsed_url=urlparse(url)
# 0(scheme) returns protocol
default_port='80'
if parsed_url[0]=='https': default_port='443'
# 1(netloc) returns the hostname+port part
parts=parsed_url[1].split(":")
# just a hostname
if len(parts)==1:
return (url,parts[0],default_port)
else:
return (url,parts[0],parts[1])
### a very simple cache mechanism so that successive runs (see make)
### will go *much* faster
### assuming everything is sequential, as simple as it gets
### { url -> (timestamp,version)}
class VersionCache:
def __init__ (self, filename=None, expires=60*60):
# default is to store cache in the same dir as argv[0]
if filename is None:
filename=os.path.join(os.path.dirname(sys.argv[0]),"sfascan-version-cache.pickle")
self.filename=filename
self.expires=expires
self.url2version={}
self.load()
def load (self):
try:
infile=file(self.filename,'r')
self.url2version=pickle.load(infile)
infile.close()
except:
logger.debug("Cannot load version cache, restarting from scratch")
self.url2version = {}
logger.debug("loaded version cache with %d entries %s"%(len(self.url2version),self.url2version.keys()))
def save (self):
try:
outfile=file(self.filename,'w')
pickle.dump(self.url2version,outfile)
outfile.close()
except:
logger.log_exc ("Cannot save version cache into %s"%self.filename)
def clean (self):
try:
retcod=os.unlink(self.filename)
logger.info("Cleaned up version cache %s, retcod=%d"%(self.filename,retcod))
except:
logger.info ("Could not unlink version cache %s"%self.filename)
def show (self):
entries=len(self.url2version)
print "version cache from file %s has %d entries"%(self.filename,entries)
key_values=self.url2version.items()
def old_first (kv1,kv2): return int(kv1[1][0]-kv2[1][0])
key_values.sort(old_first)
for key_value in key_values:
(url,tuple) = key_value
(timestamp,version) = tuple
how_old = time.time()-timestamp
if how_old<=self.expires:
print url,"-- %d seconds ago"%how_old
else:
print "OUTDATED",url,"(%d seconds ago, expires=%d)"%(how_old,self.expires)
# turns out we might have trailing slashes or not
def normalize (self, url):
return url.strip("/")
def set (self,url,version):
url=self.normalize(url)
self.url2version[url]=( time.time(), version)
def get (self,url):
url=self.normalize(url)
try:
(timestamp,version)=self.url2version[url]
how_old = time.time()-timestamp
if how_old<=self.expires: return version
else: return None
except:
return None
###
class Interface:
def __init__ (self,url,verbose=False):
self._url=url
self.verbose=verbose
try:
(self._url,self.hostname,self.port)=url_hostname_port(url)
self.ip=socket.gethostbyname(self.hostname)
self.probed=False
except:
self.hostname="unknown"
self.ip='0.0.0.0'
self.port="???"
# don't really try it
self.probed=True
self._version={}
def url(self):
return self._url
# this is used as a key for creating graph nodes and to avoid duplicates
def uid (self):
return "%s:%s"%(self.ip,self.port)
# connect to server and trigger GetVersion
def get_version(self):
### if we already know the answer:
if self.probed:
return self._version
### otherwise let's look in the cache file
logger.debug("searching in version cache %s"%self.url())
cached_version = VersionCache().get(self.url())
if cached_version is not None:
logger.info("Retrieved version info from cache")
return cached_version
### otherwise let's do the hard work
# dummy to meet Sfi's expectations for its 'options' field
class DummyOptions:
pass
options=DummyOptions()
options.verbose=self.verbose
options.timeout=10
try:
client=Sfi(options)
client.read_config()
key_file = client.get_key_file()
cert_file = client.get_cert_file(key_file)
url=self.url()
logger.info('issuing GetVersion at %s'%url)
# setting timeout here seems to get the call to fail - even though the response time is fast
#server=sfaprotocol.server_proxy(url, key_file, cert_file, verbose=self.verbose, timeout=options.timeout)
server=sfaprotocol.server_proxy(url, key_file, cert_file, verbose=self.verbose)
self._version=server.GetVersion()
except:
logger.log_exc("failed to get version")
self._version={}
# so that next run from this process will find out
self.probed=True
# store in version cache so next processes will remember for an hour
cache=VersionCache()
cache.set(self.url(),self._version)
cache.save()
logger.debug("Saved version for url=%s in version cache"%self.url())
# that's our result
return self._version
@staticmethod
def multi_lines_label(*lines):
result='<
' + \
' |
'.join(lines) + \
' |
>'
return result
# default is for when we can't determine the type of the service
# typically the server is down, or we can't authenticate, or it's too old code
shapes = {"registry": "diamond", "slicemgr":"ellipse", "aggregate":"box", 'default':'plaintext'}
abbrevs = {"registry": "REG", "slicemgr":"SA", "aggregate":"AM", 'default':'[unknown interface]'}
# return a dictionary that translates into the node's attr
def get_layout (self):
layout={}
### retrieve cached GetVersion
version=self.get_version()
# set the href; xxx would make sense to try and 'guess' the web URL, not the API's one...
layout['href']=self.url()
### set html-style label
### see http://www.graphviz.org/doc/info/shapes.html#html
# if empty the service is unreachable
if not version:
label="offline"
else:
label=''
try: abbrev=Interface.abbrevs[version['interface']]
except: abbrev=Interface.abbrevs['default']
label += abbrev
if 'hrn' in version: label += " %s"%version['hrn']
else: label += "[no hrn]"
if 'code_tag' in version:
label += " %s"%version['code_tag']
if 'testbed' in version:
label += " (%s)"%version['testbed']
layout['label']=Interface.multi_lines_label(self.url(),label)
### set shape
try: shape=Interface.shapes[version['interface']]
except: shape=Interface.shapes['default']
layout['shape']=shape
### fill color to outline wrongly configured bodies
if 'geni_api' not in version and 'sfa' not in version:
layout['style']='filled'
layout['fillcolor']='gray'
return layout
class SfaScan:
# provide the entry points (a list of interfaces)
def __init__ (self, left_to_right=False, verbose=False):
self.verbose=verbose
self.left_to_right=left_to_right
def graph (self,entry_points):
graph=pygraphviz.AGraph(directed=True)
if self.left_to_right:
graph.graph_attr['rankdir']='LR'
self.scan(entry_points,graph)
return graph
# scan from the given interfaces as entry points
def scan(self,interfaces,graph):
if not isinstance(interfaces,list):
interfaces=[interfaces]
# remember node to interface mapping
node2interface={}
# add entry points right away using the interface uid's as a key
to_scan=interfaces
for i in interfaces:
graph.add_node(i.uid())
node2interface[graph.get_node(i.uid())]=i
scanned=[]
# keep on looping until we reach a fixed point
# don't worry about abels and shapes that will get fixed later on
while to_scan:
for interface in to_scan:
# performing xmlrpc call
logger.info("retrieving/fetching version at interface %s"%interface.url())
version=interface.get_version()
if not version:
logger.info("")
else:
for (k,v) in version.iteritems():
if not isinstance(v,dict):
logger.debug("\r\t%s:%s"%(k,v))
else:
logger.debug(k)
for (k1,v1) in v.iteritems():
logger.debug("\r\t\t%s:%s"%(k1,v1))
# 'geni_api' is expected if the call succeeded at all
# 'peers' is needed as well as AMs typically don't have peers
if 'geni_api' in version and 'peers' in version:
# proceed with neighbours
for (next_name,next_url) in version['peers'].iteritems():
next_interface=Interface(next_url)
# locate or create node in graph
try:
# if found, we're good with this one
next_node=graph.get_node(next_interface.uid())
except:
# otherwise, let's move on with it
graph.add_node(next_interface.uid())
next_node=graph.get_node(next_interface.uid())
node2interface[next_node]=next_interface
to_scan.append(next_interface)
graph.add_edge(interface.uid(),next_interface.uid())
scanned.append(interface)
to_scan.remove(interface)
# we've scanned the whole graph, let's get the labels and shapes right
for node in graph.nodes():
interface=node2interface.get(node,None)
if interface:
for (k,v) in interface.get_layout().iteritems():
node.attr[k]=v
else:
logger.error("MISSED interface with node %s"%node)
default_outfiles=['sfa.png','sfa.svg','sfa.dot']
def main():
usage="%prog [options] url-entry-point(s)"
parser=OptionParser(usage=usage)
parser.add_option("-o","--output",action='append',dest='outfiles',default=[],
help="output filenames (cumulative) - defaults are %r"%default_outfiles)
parser.add_option("-l","--left-to-right",action="store_true",dest="left_to_right",default=False,
help="instead of top-to-bottom")
parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
help="verbose - can be repeated for more verbosity")
parser.add_option("-c", "--clean-cache",action='store_true',
dest='clean_cache',default=False,
help='clean/trash version cache and exit')
parser.add_option("-s","--show-cache",action='store_true',
dest='show_cache',default=False,
help='show/display version cache')
(options,args)=parser.parse_args()
logger.enable_console()
# apply current verbosity to logger
logger.setLevelFromOptVerbose(options.verbose)
# figure if we need to be verbose for these local classes that only have a bool flag
bool_verbose=logger.getBoolVerboseFromOpt(options.verbose)
if options.show_cache:
VersionCache().show()
sys.exit(0)
if options.clean_cache:
VersionCache().clean()
sys.exit(0)
if not args:
parser.print_help()
sys.exit(1)
if not options.outfiles:
options.outfiles=default_outfiles
scanner=SfaScan(left_to_right=options.left_to_right, verbose=bool_verbose)
entries = [ Interface(entry) for entry in args ]
g=scanner.graph(entries)
logger.info("creating layout")
g.layout(prog='dot')
for outfile in options.outfiles:
logger.info("drawing in %s"%outfile)
g.draw(outfile)
logger.info("done")
if __name__ == '__main__':
main()