8 from urlparse import urlparse
13 print 'Warning, could not import pygraphviz, test mode only'
15 from optparse import OptionParser
17 from sfa.client.sfi import Sfi
18 from sfa.util.sfalogging import logger, DEBUG
19 import sfa.client.sfaprotocol as sfaprotocol
21 def url_hostname_port (url):
24 parsed_url=urlparse(url)
25 # 0(scheme) returns protocol
27 if parsed_url[0]=='https': default_port='443'
28 # 1(netloc) returns the hostname+port part
29 parts=parsed_url[1].split(":")
32 return (url,parts[0],default_port)
34 return (url,parts[0],parts[1])
36 ### a very simple cache mechanism so that successive runs (see make)
37 ### will go *much* faster
38 ### assuming everything is sequential, as simple as it gets
39 ### { url -> (timestamp,version)}
41 def __init__ (self, filename=None, expires=60*60):
42 # default is to store cache in the same dir as argv[0]
44 filename=os.path.join(os.path.dirname(sys.argv[0]),"sfascan-version-cache.pickle")
45 self.filename=filename
52 infile=file(self.filename,'r')
53 self.url2version=pickle.load(infile)
56 logger.debug("Cannot load version cache, restarting from scratch")
58 logger.debug("loaded version cache with %d entries %s"%(len(self.url2version),self.url2version.keys()))
62 outfile=file(self.filename,'w')
63 pickle.dump(self.url2version,outfile)
66 logger.log_exc ("Cannot save version cache into %s"%self.filename)
69 retcod=os.unlink(self.filename)
70 logger.info("Cleaned up version cache %s, retcod=%d"%(self.filename,retcod))
72 logger.info ("Could not unlink version cache %s"%self.filename)
75 entries=len(self.url2version)
76 print "version cache from file %s has %d entries"%(self.filename,entries)
77 key_values=self.url2version.items()
78 def old_first (kv1,kv2): return int(kv1[1][0]-kv2[1][0])
79 key_values.sort(old_first)
80 for key_value in key_values:
81 (url,tuple) = key_value
82 (timestamp,version) = tuple
83 how_old = time.time()-timestamp
84 if how_old<=self.expires:
85 print url,"-- %d seconds ago"%how_old
87 print "OUTDATED",url,"(%d seconds ago, expires=%d)"%(how_old,self.expires)
89 # turns out we might have trailing slashes or not
90 def normalize (self, url):
93 def set (self,url,version):
94 url=self.normalize(url)
95 self.url2version[url]=( time.time(), version)
97 url=self.normalize(url)
99 (timestamp,version)=self.url2version[url]
100 how_old = time.time()-timestamp
101 if how_old<=self.expires: return version
109 def __init__ (self,url,verbose=False):
113 (self._url,self.hostname,self.port)=url_hostname_port(url)
114 self.ip=socket.gethostbyname(self.hostname)
117 self.hostname="unknown"
120 # don't really try it
127 # this is used as a key for creating graph nodes and to avoid duplicates
129 return "%s:%s"%(self.ip,self.port)
131 # connect to server and trigger GetVersion
132 def get_version(self):
133 ### if we already know the answer:
136 ### otherwise let's look in the cache file
137 logger.debug("searching in version cache %s"%self.url())
138 cached_version = VersionCache().get(self.url())
139 if cached_version is not None:
140 logger.info("Retrieved version info from cache")
141 return cached_version
142 ### otherwise let's do the hard work
143 # dummy to meet Sfi's expectations for its 'options' field
146 options=DummyOptions()
147 options.verbose=self.verbose
152 key_file = client.get_key_file()
153 cert_file = client.get_cert_file(key_file)
154 logger.debug("using key %s & cert %s"%(key_file,cert_file))
156 logger.info('issuing GetVersion at %s'%url)
157 # setting timeout here seems to get the call to fail - even though the response time is fast
158 #server=sfaprotocol.server_proxy(url, key_file, cert_file, verbose=self.verbose, timeout=options.timeout)
159 server=sfaprotocol.server_proxy(url, key_file, cert_file, verbose=self.verbose)
160 self._version=server.GetVersion()
162 logger.log_exc("failed to get version")
164 # so that next run from this process will find out
166 # store in version cache so next processes will remember for an hour
168 cache.set(self.url(),self._version)
170 logger.debug("Saved version for url=%s in version cache"%self.url())
175 def multi_lines_label(*lines):
176 result='<<TABLE BORDER="0" CELLBORDER="0"><TR><TD>' + \
177 '</TD></TR><TR><TD>'.join(lines) + \
178 '</TD></TR></TABLE>>'
181 # default is for when we can't determine the type of the service
182 # typically the server is down, or we can't authenticate, or it's too old code
183 shapes = {"registry": "diamond", "slicemgr":"ellipse", "aggregate":"box", 'default':'plaintext'}
184 abbrevs = {"registry": "REG", "slicemgr":"SA", "aggregate":"AM", 'default':'[unknown interface]'}
186 # return a dictionary that translates into the node's attr
187 def get_layout (self):
189 ### retrieve cached GetVersion
190 version=self.get_version()
191 # set the href; xxx would make sense to try and 'guess' the web URL, not the API's one...
192 layout['href']=self.url()
193 ### set html-style label
194 ### see http://www.graphviz.org/doc/info/shapes.html#html
195 # if empty the service is unreachable
200 try: abbrev=Interface.abbrevs[version['interface']]
201 except: abbrev=Interface.abbrevs['default']
203 if 'hrn' in version: label += " %s"%version['hrn']
204 else: label += "[no hrn]"
205 if 'code_tag' in version:
206 label += " %s"%version['code_tag']
207 if 'testbed' in version:
208 label += " (%s)"%version['testbed']
209 layout['label']=Interface.multi_lines_label(self.url(),label)
211 try: shape=Interface.shapes[version['interface']]
212 except: shape=Interface.shapes['default']
213 layout['shape']=shape
214 ### fill color to outline wrongly configured bodies
215 if 'geni_api' not in version and 'sfa' not in version:
216 layout['style']='filled'
217 layout['fillcolor']='gray'
222 # provide the entry points (a list of interfaces)
223 def __init__ (self, left_to_right=False, verbose=False):
225 self.left_to_right=left_to_right
227 def graph (self,entry_points):
228 graph=pygraphviz.AGraph(directed=True)
229 if self.left_to_right:
230 graph.graph_attr['rankdir']='LR'
231 self.scan(entry_points,graph)
234 # scan from the given interfaces as entry points
235 def scan(self,interfaces,graph):
236 if not isinstance(interfaces,list):
237 interfaces=[interfaces]
239 # remember node to interface mapping
241 # add entry points right away using the interface uid's as a key
244 graph.add_node(i.uid())
245 node2interface[graph.get_node(i.uid())]=i
247 # keep on looping until we reach a fixed point
248 # don't worry about abels and shapes that will get fixed later on
250 for interface in to_scan:
251 # performing xmlrpc call
252 logger.info("retrieving/fetching version at interface %s"%interface.url())
253 version=interface.get_version()
255 logger.info("<EMPTY GetVersion(); offline or cannot authenticate>")
257 for (k,v) in version.iteritems():
258 if not isinstance(v,dict):
259 logger.debug("\r\t%s:%s"%(k,v))
262 for (k1,v1) in v.iteritems():
263 logger.debug("\r\t\t%s:%s"%(k1,v1))
264 # 'geni_api' is expected if the call succeeded at all
265 # 'peers' is needed as well as AMs typically don't have peers
266 if 'geni_api' in version and 'peers' in version:
267 # proceed with neighbours
268 for (next_name,next_url) in version['peers'].iteritems():
269 next_interface=Interface(next_url)
270 # locate or create node in graph
272 # if found, we're good with this one
273 next_node=graph.get_node(next_interface.uid())
275 # otherwise, let's move on with it
276 graph.add_node(next_interface.uid())
277 next_node=graph.get_node(next_interface.uid())
278 node2interface[next_node]=next_interface
279 to_scan.append(next_interface)
280 graph.add_edge(interface.uid(),next_interface.uid())
281 scanned.append(interface)
282 to_scan.remove(interface)
283 # we've scanned the whole graph, let's get the labels and shapes right
284 for node in graph.nodes():
285 interface=node2interface.get(node,None)
287 for (k,v) in interface.get_layout().iteritems():
290 logger.error("MISSED interface with node %s"%node)
293 default_outfiles=['sfa.png','sfa.svg','sfa.dot']
296 usage="%prog [options] url-entry-point(s)"
297 parser=OptionParser(usage=usage)
298 parser.add_option("-d", "--dir", dest="sfi_dir",
299 help="config & working directory - default is " + Sfi.default_sfi_dir(),
300 metavar="PATH", default=Sfi.default_sfi_dir())
301 parser.add_option("-o","--output",action='append',dest='outfiles',default=[],
302 help="output filenames (cumulative) - defaults are %r"%default_outfiles)
303 parser.add_option("-l","--left-to-right",action="store_true",dest="left_to_right",default=False,
304 help="instead of top-to-bottom")
305 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
306 help="verbose - can be repeated for more verbosity")
307 parser.add_option("-c", "--clean-cache",action='store_true',
308 dest='clean_cache',default=False,
309 help='clean/trash version cache and exit')
310 parser.add_option("-s","--show-cache",action='store_true',
311 dest='show_cache',default=False,
312 help='show/display version cache')
314 (options,args)=parser.parse_args()
315 logger.enable_console()
316 # apply current verbosity to logger
317 logger.setLevelFromOptVerbose(options.verbose)
318 # figure if we need to be verbose for these local classes that only have a bool flag
319 bool_verbose=logger.getBoolVerboseFromOpt(options.verbose)
321 if options.show_cache:
322 VersionCache().show()
324 if options.clean_cache:
325 VersionCache().clean()
331 if not options.outfiles:
332 options.outfiles=default_outfiles
333 scanner=SfaScan(left_to_right=options.left_to_right, verbose=bool_verbose)
334 entries = [ Interface(entry) for entry in args ]
336 g=scanner.graph(entries)
337 logger.info("creating layout")
339 for outfile in options.outfiles:
340 logger.info("drawing in %s"%outfile)
343 # test mode when pygraphviz is not available
346 print "GetVersion at %s returned %s"%(entry.url(),entry.get_version())
348 if __name__ == '__main__':