minor tweaks
[sfa.git] / sfa / client / sfascan.py
1 #!/usr/bin/python
2
3 import sys
4 import socket
5 import re
6
7 import pygraphviz
8
9 from optparse import OptionParser
10
11 from sfa.client.sfi import Sfi
12 from sfa.util.sfalogging import sfa_logger,sfa_logger_goes_to_console
13 import sfa.util.xmlrpcprotocol as xmlrpcprotocol
14
15 m_url_with_proto=re.compile("\w+://(?P<hostname>[\w\-\.]+):(?P<port>[0-9]+).*")
16 m_url_without_proto=re.compile("(?P<hostname>[\w\-\.]+):(?P<port>[0-9]+).*")
17 def url_to_hostname_port (url):
18     match=m_url_with_proto.match(url)
19     if match:
20         return (match.group('hostname'),match.group('port'))
21     match=m_url_without_proto.match(url)
22     if match:
23         return (match.group('hostname'),match.group('port'))
24     return ('undefined','???')
25
26 ###
27 class Interface:
28
29     def __init__ (self,url):
30         try:
31             (self.hostname,self.port)=url_to_hostname_port(url)
32             self.ip=socket.gethostbyname(self.hostname)
33             self.probed=False
34         except:
35             import traceback
36             traceback.print_exc()
37             self.hostname="unknown"
38             self.ip='0.0.0.0'
39             self.port="???"
40             self.probed=True
41             self._version={}
42
43     def url(self):
44         return "http://%s:%s/"%(self.hostname,self.port)
45
46     # this is used as a key for creating graph nodes and to avoid duplicates
47     def uid (self):
48         return "%s:%s"%(self.ip,self.port)
49
50     # connect to server and trigger GetVersion
51     def get_version(self):
52         if self.probed:
53             return self._version
54         # dummy to meet Sfi's expectations for its 'options' field
55         class DummyOptions:
56             pass
57         options=DummyOptions()
58         options.verbose=False
59         try:
60             client=Sfi(options)
61             client.read_config()
62             key_file = client.get_key_file()
63             cert_file = client.get_cert_file(key_file)
64             url="http://%s:%s/"%(self.hostname,self.port)
65             sfa_logger().info('issuing get version at %s'%url)
66             server=xmlrpcprotocol.get_server(url, key_file, cert_file, options)
67             self._version=server.GetVersion()
68         except:
69             self._version={}
70         self.probed=True
71         return self._version
72
73     # default is for when we can't determine the type of the service
74     # typically the server is down, or we can't authenticate, or it's too old code
75     shapes = {"registry": "diamond", "slicemgr":"ellipse", "aggregate":"box", 'default':'plaintext'}
76
77     def get_label(self):
78         version=self.get_version()
79         if 'hrn' not in version: return self.url()
80         hrn=version['hrn']
81         result=hrn
82         if 'code_tag' in version: 
83             result += " %s"%version['code_tag']
84         if 'testbed' in version:
85             # could not get so-called HTML-like labels to work
86             #"<TABLE><TR><TD>%s</TD></TR><TR><TD>%s</TD></TR></TABLE>"%(result,version['testbed'])
87             result += " (%s)"%version['testbed']
88         return result
89
90     def get_shape(self):
91         default=Interface.shapes['default']
92         try:
93             version=self.get_version()
94             return Interface.shapes.get(version['interface'],default)
95         except:
96             return default
97
98 class SfaScan:
99
100     # provide the entry points (a list of interfaces)
101     def __init__ (self):
102         pass
103
104     def graph (self,entry_points):
105         graph=pygraphviz.AGraph(directed=True)
106         self.scan(entry_points,graph)
107         return graph
108     
109     # scan from the given interfaces as entry points
110     def scan(self,interfaces,graph):
111         if not isinstance(interfaces,list):
112             interfaces=[interfaces]
113
114         # remember node to interface mapping
115         node2interface={}
116         # add entry points right away using the interface uid's as a key
117         to_scan=interfaces
118         for i in interfaces: 
119             graph.add_node(i.uid())
120             node2interface[graph.get_node(i.uid())]=i
121         scanned=[]
122         # keep on looping until we reach a fixed point
123         # don't worry about abels and shapes that will get fixed later on
124         while to_scan:
125             for interface in to_scan:
126                 # performing xmlrpc call
127                 version=interface.get_version()
128                 # 'sfa' is expected if the call succeeded at all
129                 # 'peers' is needed as well as AMs typically don't have peers
130                 if 'sfa' in version and 'peers' in version: 
131                     # proceed with neighbours
132                     for (next_name,next_url) in version['peers'].items():
133                         next_interface=Interface(next_url)
134                         # locate or create node in graph
135                         try:
136                             # if found, we're good with this one
137                             next_node=graph.get_node(next_interface.uid())
138                         except:
139                             # otherwise, let's move on with it
140                             graph.add_node(next_interface.uid())
141                             next_node=graph.get_node(next_interface.uid())
142                             node2interface[next_node]=next_interface
143                             to_scan.append(next_interface)
144                         graph.add_edge(interface.uid(),next_interface.uid())
145                 scanned.append(interface)
146                 to_scan.remove(interface)
147             # we've scanned the whole graph, let's get the labels and shapes right
148             for node in graph.nodes():
149                 interface=node2interface.get(node,None)
150                 if interface:
151                     node.attr['label']=interface.get_label()
152                     node.attr['shape']=interface.get_shape()
153                     node.attr['href']=interface.url()
154                 else:
155                     sfa_logger().info("MISSED interface with node %s"%node)
156     
157
158 default_entry_points=["http://www.planet-lab.eu:12345/"]
159 default_outfiles=['sfa.png']
160
161 def main():
162     sfa_logger_goes_to_console()
163     parser=OptionParser()
164     parser.add_option("-e","--entry",action='append',dest='entry_points',default=[],
165                       help="Specify entry points - defaults are %r"%default_entry_points)
166     parser.add_option("-o","--output",action='append',dest='outfiles',default=[],
167                       help="Output filenames - defaults are %r"%default_outfiles)
168     (options,args)=parser.parse_args()
169     if args:
170         parser.print_help()
171         sys.exit(1)
172     if not options.entry_points:
173         options.entry_points=default_entry_points
174     if not options.outfiles:
175         options.outfiles=default_outfiles
176     scanner=SfaScan()
177     entries = [ Interface(entry) for entry in options.entry_points ]
178     g=scanner.graph(entries)
179     sfa_logger().info("creating layout")
180     g.layout(prog='dot')
181     for outfile in options.outfiles:
182         sfa_logger().info("drawing in %s"%outfile)
183         g.draw(outfile)
184     sfa_logger().info("done")
185
186 if __name__ == '__main__':
187     main()