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 %s"%self.url())
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
151 key_file = client.private_key
152 cert_file = client.my_gid
153 logger.debug("using key %s & cert %s"%(key_file,cert_file))
155 logger.info('issuing GetVersion at %s'%url)
156 # setting timeout here seems to get the call to fail - even though the response time is fast
157 #server=SfaServerProxy(url, key_file, cert_file, verbose=self.verbose, timeout=options.timeout)
158 server=SfaServerProxy(url, key_file, cert_file, verbose=self.verbose)
159 self._version=server.GetVersion()
161 logger.log_exc("failed to get version")
163 # so that next run from this process will find out
165 # store in version cache so next processes will remember for an hour
167 cache.set(self.url(),self._version)
169 logger.debug("Saved version for url=%s in version cache"%self.url())
174 def multi_lines_label(*lines):
175 result='<<TABLE BORDER="0" CELLBORDER="0"><TR><TD>' + \
176 '</TD></TR><TR><TD>'.join(lines) + \
177 '</TD></TR></TABLE>>'
180 # default is for when we can't determine the type of the service
181 # typically the server is down, or we can't authenticate, or it's too old code
182 shapes = {"registry": "diamond", "slicemgr":"ellipse", "aggregate":"box", 'default':'plaintext'}
183 abbrevs = {"registry": "REG", "slicemgr":"SA", "aggregate":"AM", 'default':'[unknown interface]'}
185 # return a dictionary that translates into the node's attr
186 def get_layout (self):
188 ### retrieve cached GetVersion
189 version=self.get_version()
190 # set the href; xxx would make sense to try and 'guess' the web URL, not the API's one...
191 layout['href']=self.url()
192 ### set html-style label
193 ### see http://www.graphviz.org/doc/info/shapes.html#html
194 # if empty the service is unreachable
199 try: abbrev=Interface.abbrevs[version['interface']]
200 except: abbrev=Interface.abbrevs['default']
202 if 'hrn' in version: label += " %s"%version['hrn']
203 else: label += "[no hrn]"
204 if 'code_tag' in version:
205 label += " %s"%version['code_tag']
206 if 'testbed' in version:
207 label += " (%s)"%version['testbed']
208 layout['label']=Interface.multi_lines_label(self.url(),label)
210 try: shape=Interface.shapes[version['interface']]
211 except: shape=Interface.shapes['default']
212 layout['shape']=shape
213 ### fill color to outline wrongly configured bodies
214 if 'geni_api' not in version and 'sfa' not in version:
215 layout['style']='filled'
216 layout['fillcolor']='gray'
221 # provide the entry points (a list of interfaces)
222 def __init__ (self, left_to_right=False, verbose=False):
224 self.left_to_right=left_to_right
226 def graph (self,entry_points):
227 graph=pygraphviz.AGraph(directed=True)
228 if self.left_to_right:
229 graph.graph_attr['rankdir']='LR'
230 self.scan(entry_points,graph)
233 # scan from the given interfaces as entry points
234 def scan(self,interfaces,graph):
235 if not isinstance(interfaces,list):
236 interfaces=[interfaces]
238 # remember node to interface mapping
240 # add entry points right away using the interface uid's as a key
243 graph.add_node(i.uid())
244 node2interface[graph.get_node(i.uid())]=i
246 # keep on looping until we reach a fixed point
247 # don't worry about abels and shapes that will get fixed later on
249 for interface in to_scan:
250 # performing xmlrpc call
251 logger.info("retrieving/fetching version at interface %s"%interface.url())
252 version=interface.get_version()
254 logger.info("<EMPTY GetVersion(); offline or cannot authenticate>")
256 for (k,v) in version.iteritems():
257 if not isinstance(v,dict):
258 logger.debug("\r\t%s:%s"%(k,v))
261 for (k1,v1) in v.iteritems():
262 logger.debug("\r\t\t%s:%s"%(k1,v1))
263 # 'geni_api' is expected if the call succeeded at all
264 # 'peers' is needed as well as AMs typically don't have peers
265 if 'geni_api' in version and 'peers' in version:
266 # proceed with neighbours
267 for (next_name,next_url) in version['peers'].iteritems():
268 next_interface=Interface(next_url)
269 # locate or create node in graph
271 # if found, we're good with this one
272 next_node=graph.get_node(next_interface.uid())
274 # otherwise, let's move on with it
275 graph.add_node(next_interface.uid())
276 next_node=graph.get_node(next_interface.uid())
277 node2interface[next_node]=next_interface
278 to_scan.append(next_interface)
279 graph.add_edge(interface.uid(),next_interface.uid())
280 scanned.append(interface)
281 to_scan.remove(interface)
282 # we've scanned the whole graph, let's get the labels and shapes right
283 for node in graph.nodes():
284 interface=node2interface.get(node,None)
286 for (k,v) in interface.get_layout().iteritems():
289 logger.error("MISSED interface with node %s"%node)
294 default_outfiles=['sfa.png','sfa.svg','sfa.dot']
297 usage="%prog [options] url-entry-point(s)"
298 parser=OptionParser(usage=usage)
299 parser.add_option("-d", "--dir", dest="sfi_dir",
300 help="config & working directory - default is " + Sfi.default_sfi_dir(),
301 metavar="PATH", default=Sfi.default_sfi_dir())
302 parser.add_option("-o","--output",action='append',dest='outfiles',default=[],
303 help="output filenames (cumulative) - defaults are %r"%SfaScan.default_outfiles)
304 parser.add_option("-l","--left-to-right",action="store_true",dest="left_to_right",default=False,
305 help="instead of top-to-bottom")
306 parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
307 help="verbose - can be repeated for more verbosity")
308 parser.add_option("-c", "--clean-cache",action='store_true',
309 dest='clean_cache',default=False,
310 help='clean/trash version cache and exit')
311 parser.add_option("-s","--show-cache",action='store_true',
312 dest='show_cache',default=False,
313 help='show/display version cache')
315 (options,args)=parser.parse_args()
316 logger.enable_console()
317 # apply current verbosity to logger
318 logger.setLevelFromOptVerbose(options.verbose)
319 # figure if we need to be verbose for these local classes that only have a bool flag
320 bool_verbose=logger.getBoolVerboseFromOpt(options.verbose)
322 if options.show_cache:
323 VersionCache().show()
325 if options.clean_cache:
326 VersionCache().clean()
332 if not options.outfiles:
333 options.outfiles=SfaScan.default_outfiles
334 scanner=Scanner(left_to_right=options.left_to_right, verbose=bool_verbose)
335 entries = [ Interface(entry) for entry in args ]
337 g=scanner.graph(entries)
338 logger.info("creating layout")
340 for outfile in options.outfiles:
341 logger.info("drawing in %s"%outfile)
344 # test mode when pygraphviz is not available
347 print "GetVersion at %s returned %s"%(entry.url(),entry.get_version())