6 from urlparse import urlparse
10 from optparse import OptionParser
12 from sfa.client.sfi import Sfi
13 from sfa.util.sfalogging import logger, DEBUG
14 import sfa.util.xmlrpcprotocol as xmlrpcprotocol
16 def url_hostname_port (url):
19 parsed_url=urlparse(url)
20 # 0(scheme) returns protocol
22 if parsed_url[0]=='https': default_port='443'
23 # 1(netloc) returns the hostname+port part
24 parts=parsed_url[1].split(":")
27 return (url,parts[0],default_port)
29 return (url,parts[0],parts[1])
34 def __init__ (self,url):
37 (self._url,self.hostname,self.port)=url_hostname_port(url)
38 self.ip=socket.gethostbyname(self.hostname)
41 self.hostname="unknown"
51 # this is used as a key for creating graph nodes and to avoid duplicates
53 return "%s:%s"%(self.ip,self.port)
55 # connect to server and trigger GetVersion
56 def get_version(self):
59 # dummy to meet Sfi's expectations for its 'options' field
62 options=DummyOptions()
68 key_file = client.get_key_file()
69 cert_file = client.get_cert_file(key_file)
71 logger.info('issuing get version at %s'%url)
72 logger.debug("GetVersion, using timeout=%d"%options.timeout)
73 server=xmlrpcprotocol.get_server(url, key_file, cert_file, timeout=options.timeout, verbose=options.verbose)
74 self._version=server.GetVersion()
81 def multi_lines_label(*lines):
82 result='<<TABLE BORDER="0" CELLBORDER="0"><TR><TD>' + \
83 '</TD></TR><TR><TD>'.join(lines) + \
87 # default is for when we can't determine the type of the service
88 # typically the server is down, or we can't authenticate, or it's too old code
89 shapes = {"registry": "diamond", "slicemgr":"ellipse", "aggregate":"box", 'default':'plaintext'}
90 abbrevs = {"registry": "REG", "slicemgr":"SA", "aggregate":"AM", 'default':'[unknown interface]'}
92 # return a dictionary that translates into the node's attr
93 def get_layout (self):
95 ### retrieve cached GetVersion
96 version=self.get_version()
97 # set the href; xxx would make sense to try and 'guess' the web URL, not the API's one...
98 layout['href']=self.url()
99 ### set html-style label
100 ### see http://www.graphviz.org/doc/info/shapes.html#html
101 # if empty the service is unreachable
106 try: abbrev=Interface.abbrevs[version['interface']]
107 except: abbrev=Interface.abbrevs['default']
109 if 'hrn' in version: label += " %s"%version['hrn']
110 else: label += "[no hrn]"
111 if 'code_tag' in version:
112 label += " %s"%version['code_tag']
113 if 'testbed' in version:
114 label += " (%s)"%version['testbed']
115 layout['label']=Interface.multi_lines_label(self.url(),label)
117 try: shape=Interface.shapes[version['interface']]
118 except: shape=Interface.shapes['default']
119 layout['shape']=shape
120 ### fill color to outline wrongly configured bodies
121 if 'geni_api' not in version and 'sfa' not in version:
122 layout['style']='filled'
123 layout['fillcolor']='gray'
128 # provide the entry points (a list of interfaces)
129 def __init__ (self, left_to_right=False, verbose=False):
131 self.left_to_right=left_to_right
133 def graph (self,entry_points):
134 graph=pygraphviz.AGraph(directed=True)
135 if self.left_to_right:
136 graph.graph_attr['rankdir']='LR'
137 self.scan(entry_points,graph)
140 # scan from the given interfaces as entry points
141 def scan(self,interfaces,graph):
142 if not isinstance(interfaces,list):
143 interfaces=[interfaces]
145 # remember node to interface mapping
147 # add entry points right away using the interface uid's as a key
150 graph.add_node(i.uid())
151 node2interface[graph.get_node(i.uid())]=i
153 # keep on looping until we reach a fixed point
154 # don't worry about abels and shapes that will get fixed later on
156 for interface in to_scan:
157 # performing xmlrpc call
158 version=interface.get_version()
160 logger.info("GetVersion at interface %s"%interface.url())
162 logger.info("<EMPTY GetVersion(); offline or cannot authenticate>")
164 for (k,v) in version.iteritems():
165 if not isinstance(v,dict):
166 logger.info("\r\t%s:%s"%(k,v))
169 for (k1,v1) in v.iteritems():
170 logger.info("\r\t\t%s:%s"%(k1,v1))
171 # 'geni_api' is expected if the call succeeded at all
172 # 'peers' is needed as well as AMs typically don't have peers
173 if 'geni_api' in version and 'peers' in version:
174 # proceed with neighbours
175 for (next_name,next_url) in version['peers'].iteritems():
176 next_interface=Interface(next_url)
177 # locate or create node in graph
179 # if found, we're good with this one
180 next_node=graph.get_node(next_interface.uid())
182 # otherwise, let's move on with it
183 graph.add_node(next_interface.uid())
184 next_node=graph.get_node(next_interface.uid())
185 node2interface[next_node]=next_interface
186 to_scan.append(next_interface)
187 graph.add_edge(interface.uid(),next_interface.uid())
188 scanned.append(interface)
189 to_scan.remove(interface)
190 # we've scanned the whole graph, let's get the labels and shapes right
191 for node in graph.nodes():
192 interface=node2interface.get(node,None)
194 for (k,v) in interface.get_layout().iteritems():
197 logger.error("MISSED interface with node %s"%node)
200 default_outfiles=['sfa.png','sfa.svg','sfa.dot']
203 usage="%prog [options] url-entry-point(s)"
204 parser=OptionParser(usage=usage)
205 parser.add_option("-o","--output",action='append',dest='outfiles',default=[],
206 help="output filenames (cumulative) - defaults are %r"%default_outfiles)
207 parser.add_option("-l","--left-to-right",action="store_true",dest="left_to_right",default=False,
208 help="instead of top-to-bottom")
209 parser.add_option("-v","--verbose",action='store_true',dest='verbose',default=False,
211 parser.add_option("-d","--debug",action='store_true',dest='debug',default=False,
213 (options,args)=parser.parse_args()
217 if not options.outfiles:
218 options.outfiles=default_outfiles
219 logger.enable_console()
222 logger.setLevel(DEBUG)
223 scanner=SfaScan(left_to_right=options.left_to_right, verbose=options.verbose)
224 entries = [ Interface(entry) for entry in args ]
225 g=scanner.graph(entries)
226 logger.info("creating layout")
228 for outfile in options.outfiles:
229 logger.info("drawing in %s"%outfile)
233 if __name__ == '__main__':