fix sfascan (don't pass timeout to xmlrpcprotocol) and review
[sfa.git] / sfa / client / sfascan.py
1 #!/usr/bin/env python
2
3 import sys
4 import socket
5 import traceback
6 from urlparse import urlparse
7
8 import pygraphviz
9
10 from optparse import OptionParser
11
12 from sfa.client.sfi import Sfi
13 from sfa.util.sfalogging import logger, DEBUG
14 import sfa.client.xmlrpcprotocol as xmlrpcprotocol
15
16 def url_hostname_port (url):
17     if url.find("://")<0:
18         url="http://"+url
19     parsed_url=urlparse(url)
20     # 0(scheme) returns protocol
21     default_port='80'
22     if parsed_url[0]=='https': default_port='443'
23     # 1(netloc) returns the hostname+port part
24     parts=parsed_url[1].split(":")
25     # just a hostname
26     if len(parts)==1:
27         return (url,parts[0],default_port)
28     else:
29         return (url,parts[0],parts[1])
30
31 ###
32 class Interface:
33
34     def __init__ (self,url,verbose=False):
35         self._url=url
36         self.verbose=verbose
37         try:
38             (self._url,self.hostname,self.port)=url_hostname_port(url)
39             self.ip=socket.gethostbyname(self.hostname)
40             self.probed=False
41         except:
42             self.hostname="unknown"
43             self.ip='0.0.0.0'
44             self.port="???"
45             # don't really try it
46             self.probed=True
47             self._version={}
48
49     def url(self):
50         return self._url
51
52     # this is used as a key for creating graph nodes and to avoid duplicates
53     def uid (self):
54         return "%s:%s"%(self.ip,self.port)
55
56     # connect to server and trigger GetVersion
57     def get_version(self):
58         if self.probed:
59             return self._version
60         # dummy to meet Sfi's expectations for its 'options' field
61         class DummyOptions:
62             pass
63         options=DummyOptions()
64         options.verbose=self.verbose
65         options.timeout=10
66         try:
67             client=Sfi(options)
68             client.read_config()
69             key_file = client.get_key_file()
70             cert_file = client.get_cert_file(key_file)
71             url=self.url()
72             logger.info('issuing GetVersion at %s'%url)
73             logger.debug("GetVersion, using key_file=%s"%key_file)
74             logger.debug("GetVersion, using cert_file=%s"%cert_file)
75             logger.debug("GetVersion, using timeout=%s"%options.timeout)
76             # setting timeout here seems to get the call to fail - even though the response time is fast
77             #server=xmlrpcprotocol.server_proxy(url, key_file, cert_file, verbose=self.verbose, timeout=options.timeout)
78             server=xmlrpcprotocol.server_proxy(url, key_file, cert_file, verbose=self.verbose)
79             self._version=server.GetVersion()
80         except:
81             logger.log_exc("failed to get version")
82             self._version={}
83         self.probed=True
84         return self._version
85
86     @staticmethod
87     def multi_lines_label(*lines):
88         result='<<TABLE BORDER="0" CELLBORDER="0"><TR><TD>' + \
89             '</TD></TR><TR><TD>'.join(lines) + \
90             '</TD></TR></TABLE>>'
91         return result
92
93     # default is for when we can't determine the type of the service
94     # typically the server is down, or we can't authenticate, or it's too old code
95     shapes = {"registry": "diamond", "slicemgr":"ellipse", "aggregate":"box", 'default':'plaintext'}
96     abbrevs = {"registry": "REG", "slicemgr":"SA", "aggregate":"AM", 'default':'[unknown interface]'}
97
98     # return a dictionary that translates into the node's attr
99     def get_layout (self):
100         layout={}
101         ### retrieve cached GetVersion
102         version=self.get_version()
103         # set the href; xxx would make sense to try and 'guess' the web URL, not the API's one...
104         layout['href']=self.url()
105         ### set html-style label
106         ### see http://www.graphviz.org/doc/info/shapes.html#html
107         # if empty the service is unreachable
108         if not version:
109             label="offline"
110         else:
111             label=''
112             try: abbrev=Interface.abbrevs[version['interface']]
113             except: abbrev=Interface.abbrevs['default']
114             label += abbrev
115             if 'hrn' in version: label += " %s"%version['hrn']
116             else:                label += "[no hrn]"
117             if 'code_tag' in version: 
118                 label += " %s"%version['code_tag']
119             if 'testbed' in version:
120                 label += " (%s)"%version['testbed']
121         layout['label']=Interface.multi_lines_label(self.url(),label)
122         ### set shape
123         try: shape=Interface.shapes[version['interface']]
124         except: shape=Interface.shapes['default']
125         layout['shape']=shape
126         ### fill color to outline wrongly configured bodies
127         if 'geni_api' not in version and 'sfa' not in version:
128             layout['style']='filled'
129             layout['fillcolor']='gray'
130         return layout
131
132 class SfaScan:
133
134     # provide the entry points (a list of interfaces)
135     def __init__ (self, left_to_right=False, verbose=False):
136         self.verbose=verbose
137         self.left_to_right=left_to_right
138
139     def graph (self,entry_points):
140         graph=pygraphviz.AGraph(directed=True)
141         if self.left_to_right: 
142             graph.graph_attr['rankdir']='LR'
143         self.scan(entry_points,graph)
144         return graph
145     
146     # scan from the given interfaces as entry points
147     def scan(self,interfaces,graph):
148         if not isinstance(interfaces,list):
149             interfaces=[interfaces]
150
151         # remember node to interface mapping
152         node2interface={}
153         # add entry points right away using the interface uid's as a key
154         to_scan=interfaces
155         for i in interfaces: 
156             graph.add_node(i.uid())
157             node2interface[graph.get_node(i.uid())]=i
158         scanned=[]
159         # keep on looping until we reach a fixed point
160         # don't worry about abels and shapes that will get fixed later on
161         while to_scan:
162             for interface in to_scan:
163                 # performing xmlrpc call
164                 version=interface.get_version()
165                 if self.verbose:
166                     logger.info("GetVersion at interface %s"%interface.url())
167                     if not version:
168                         logger.info("<EMPTY GetVersion(); offline or cannot authenticate>")
169                     else: 
170                         for (k,v) in version.iteritems(): 
171                             if not isinstance(v,dict):
172                                 logger.info("\r\t%s:%s"%(k,v))
173                             else:
174                                 logger.info(k)
175                                 for (k1,v1) in v.iteritems():
176                                     logger.info("\r\t\t%s:%s"%(k1,v1))
177                 # 'geni_api' is expected if the call succeeded at all
178                 # 'peers' is needed as well as AMs typically don't have peers
179                 if 'geni_api' in version and 'peers' in version: 
180                     # proceed with neighbours
181                     for (next_name,next_url) in version['peers'].iteritems():
182                         next_interface=Interface(next_url)
183                         # locate or create node in graph
184                         try:
185                             # if found, we're good with this one
186                             next_node=graph.get_node(next_interface.uid())
187                         except:
188                             # otherwise, let's move on with it
189                             graph.add_node(next_interface.uid())
190                             next_node=graph.get_node(next_interface.uid())
191                             node2interface[next_node]=next_interface
192                             to_scan.append(next_interface)
193                         graph.add_edge(interface.uid(),next_interface.uid())
194                 scanned.append(interface)
195                 to_scan.remove(interface)
196             # we've scanned the whole graph, let's get the labels and shapes right
197             for node in graph.nodes():
198                 interface=node2interface.get(node,None)
199                 if interface:
200                     for (k,v) in interface.get_layout().iteritems():
201                         node.attr[k]=v
202                 else:
203                     logger.error("MISSED interface with node %s"%node)
204     
205
206 default_outfiles=['sfa.png','sfa.svg','sfa.dot']
207
208 def main():
209     usage="%prog [options] url-entry-point(s)"
210     parser=OptionParser(usage=usage)
211     parser.add_option("-o","--output",action='append',dest='outfiles',default=[],
212                       help="output filenames (cumulative) - defaults are %r"%default_outfiles)
213     parser.add_option("-l","--left-to-right",action="store_true",dest="left_to_right",default=False,
214                       help="instead of top-to-bottom")
215     parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0,
216                       help="verbose - can be repeated for more verbosity")
217     (options,args)=parser.parse_args()
218     if not args:
219         parser.print_help()
220         sys.exit(1)
221     if not options.outfiles:
222         options.outfiles=default_outfiles
223     logger.enable_console()
224     # apply current verbosity to logger
225     logger.setLevelFromOptVerbose(options.verbose)
226     # figure if we need to be verbose for these local classes that only have a bool flag
227     bool_verbose=logger.getBoolVerboseFromOpt(options.verbose)
228     scanner=SfaScan(left_to_right=options.left_to_right, verbose=bool_verbose)
229     entries = [ Interface(entry) for entry in args ]
230     g=scanner.graph(entries)
231     logger.info("creating layout")
232     g.layout(prog='dot')
233     for outfile in options.outfiles:
234         logger.info("drawing in %s"%outfile)
235         g.draw(outfile)
236     logger.info("done")
237
238 if __name__ == '__main__':
239     main()