6 from urlparse import urlparse
11 print 'Warning, could not import pygraphviz, test mode only'
13 from optparse import OptionParser
15 from sfa.client.sfi import Sfi
16 from sfa.util.sfalogging import logger, DEBUG
17 from sfa.client.sfaserverproxy import SfaServerProxy
19 def url_hostname_port (url):
22 parsed_url=urlparse(url)
23 # 0(scheme) returns protocol
25 if parsed_url[0]=='https': default_port='443'
26 # 1(netloc) returns the hostname+port part
27 parts=parsed_url[1].split(":")
30 return (url,parts[0],default_port)
32 return (url,parts[0],parts[1])
34 ### a very simple cache mechanism so that successive runs (see make)
35 ### will go *much* faster
36 ### assuming everything is sequential, as simple as it gets
37 ### { url -> (timestamp,version)}
39 def __init__ (self, filename=None, expires=60*60):
40 # default is to store cache in the same dir as argv[0]
42 filename=os.path.join(os.path.dirname(sys.argv[0]),"sfascan-version-cache.pickle")
43 self.filename=filename
50 infile=file(self.filename,'r')
51 self.url2version=pickle.load(infile)
54 logger.debug("Cannot load version cache, restarting from scratch")
56 logger.debug("loaded version cache with %d entries %s"%(len(self.url2version),self.url2version.keys()))
60 outfile=file(self.filename,'w')
61 pickle.dump(self.url2version,outfile)
64 logger.log_exc ("Cannot save version cache into %s"%self.filename)
67 retcod=os.unlink(self.filename)
68 logger.info("Cleaned up version cache %s, retcod=%d"%(self.filename,retcod))
70 logger.info ("Could not unlink version cache %s"%self.filename)
73 entries=len(self.url2version)
74 print "version cache from file %s has %d entries"%(self.filename,entries)
75 key_values=self.url2version.items()
76 def old_first (kv1,kv2): return int(kv1[1][0]-kv2[1][0])
77 key_values.sort(old_first)
78 for key_value in key_values:
79 (url,tuple) = key_value
80 (timestamp,version) = tuple
81 how_old = time.time()-timestamp
82 if how_old<=self.expires:
83 print url,"-- %d seconds ago"%how_old
85 print "OUTDATED",url,"(%d seconds ago, expires=%d)"%(how_old,self.expires)
87 # turns out we might have trailing slashes or not
88 def normalize (self, url):
91 def set (self,url,version):
92 url=self.normalize(url)
93 self.url2version[url]=( time.time(), version)
95 url=self.normalize(url)
97 (timestamp,version)=self.url2version[url]
98 how_old = time.time()-timestamp
99 if how_old<=self.expires: return version
107 def __init__ (self,url,verbose=False):
111 (self._url,self.hostname,self.port)=url_hostname_port(url)
112 self.ip=socket.gethostbyname(self.hostname)
115 self.hostname="unknown"
118 # don't really try it
125 # this is used as a key for creating graph nodes and to avoid duplicates
127 return "%s:%s"%(self.ip,self.port)
129 # connect to server and trigger GetVersion
130 def get_version(self):
131 ### if we already know the answer:
134 ### otherwise let's look in the cache file
135 logger.debug("searching in version cache %s"%self.url())
136 cached_version = VersionCache().get(self.url())
137 if cached_version is not None:
138 logger.info("Retrieved version info from cache")
139 return cached_version
140 ### otherwise let's do the hard work
141 # dummy to meet Sfi's expectations for its 'options' field
144 options=DummyOptions()
145 options.verbose=self.verbose
150 key_file = client.get_key_file()
151 cert_file = client.get_cert_file(key_file)
152 logger.debug("using key %s & cert %s"%(key_file,cert_file))
154 logger.info('issuing GetVersion at %s'%url)
155 # setting timeout here seems to get the call to fail - even though the response time is fast
156 #server=SfaServerProxy(url, key_file, cert_file, verbose=self.verbose, timeout=options.timeout)
157 server=SfaServerProxy(url, key_file, cert_file, verbose=self.verbose)
158 self._version=server.GetVersion()
160 logger.log_exc("failed to get version")
162 # so that next run from this process will find out
164 # store in version cache so next processes will remember for an hour
166 cache.set(self.url(),self._version)
168 logger.debug("Saved version for url=%s in version cache"%self.url())
173 def multi_lines_label(*lines):
174 result='<<TABLE BORDER="0" CELLBORDER="0"><TR><TD>' + \
175 '</TD></TR><TR><TD>'.join(lines) + \
176 '</TD></TR></TABLE>>'
179 # default is for when we can't determine the type of the service
180 # typically the server is down, or we can't authenticate, or it's too old code
181 shapes = {"registry": "diamond", "slicemgr":"ellipse", "aggregate":"box", 'default':'plaintext'}
182 abbrevs = {"registry": "REG", "slicemgr":"SA", "aggregate":"AM", 'default':'[unknown interface]'}
184 # return a dictionary that translates into the node's attr
185 def get_layout (self):
187 ### retrieve cached GetVersion
188 version=self.get_version()
189 # set the href; xxx would make sense to try and 'guess' the web URL, not the API's one...
190 layout['href']=self.url()
191 ### set html-style label
192 ### see http://www.graphviz.org/doc/info/shapes.html#html
193 # if empty the service is unreachable
198 try: abbrev=Interface.abbrevs[version['interface']]
199 except: abbrev=Interface.abbrevs['default']
201 if 'hrn' in version: label += " %s"%version['hrn']
202 else: label += "[no hrn]"
203 if 'code_tag' in version:
204 label += " %s"%version['code_tag']
205 if 'testbed' in version:
206 label += " (%s)"%version['testbed']
207 layout['label']=Interface.multi_lines_label(self.url(),label)
209 try: shape=Interface.shapes[version['interface']]
210 except: shape=Interface.shapes['default']
211 layout['shape']=shape
212 ### fill color to outline wrongly configured bodies
213 if 'geni_api' not in version and 'sfa' not in version:
214 layout['style']='filled'
215 layout['fillcolor']='gray'
220 # provide the entry points (a list of interfaces)
221 def __init__ (self, left_to_right=False, verbose=False):
223 self.left_to_right=left_to_right
225 def graph (self,entry_points):
226 graph=pygraphviz.AGraph(directed=True)
227 if self.left_to_right:
228 graph.graph_attr['rankdir']='LR'
229 self.scan(entry_points,graph)
232 # scan from the given interfaces as entry points
233 def scan(self,interfaces,graph):
234 if not isinstance(interfaces,list):
235 interfaces=[interfaces]
237 # remember node to interface mapping
239 # add entry points right away using the interface uid's as a key
242 graph.add_node(i.uid())
243 node2interface[graph.get_node(i.uid())]=i
245 # keep on looping until we reach a fixed point
246 # don't worry about abels and shapes that will get fixed later on
248 for interface in to_scan:
249 # performing xmlrpc call
250 logger.info("retrieving/fetching version at interface %s"%interface.url())
251 version=interface.get_version()
253 logger.info("<EMPTY GetVersion(); offline or cannot authenticate>")
255 for (k,v) in version.iteritems():
256 if not isinstance(v,dict):
257 logger.debug("\r\t%s:%s"%(k,v))
260 for (k1,v1) in v.iteritems():
261 logger.debug("\r\t\t%s:%s"%(k1,v1))
262 # 'geni_api' is expected if the call succeeded at all
263 # 'peers' is needed as well as AMs typically don't have peers
264 if 'geni_api' in version and 'peers' in version:
265 # proceed with neighbours
266 for (next_name,next_url) in version['peers'].iteritems():
267 next_interface=Interface(next_url)
268 # locate or create node in graph
270 # if found, we're good with this one
271 next_node=graph.get_node(next_interface.uid())
273 # otherwise, let's move on with it
274 graph.add_node(next_interface.uid())
275 next_node=graph.get_node(next_interface.uid())
276 node2interface[next_node]=next_interface
277 to_scan.append(next_interface)
278 graph.add_edge(interface.uid(),next_interface.uid())
279 scanned.append(interface)
280 to_scan.remove(interface)
281 # we've scanned the whole graph, let's get the labels and shapes right
282 for node in graph.nodes():
283 interface=node2interface.get(node,None)
285 for (k,v) in interface.get_layout().iteritems():
288 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"%SfaScan.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=SfaScan.default_outfiles
333 scanner=Scanner(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())