1 | #!/usr/local/bin/python |
---|
2 | |
---|
3 | import sys |
---|
4 | import os |
---|
5 | import tempfile |
---|
6 | import subprocess |
---|
7 | import xml.parsers.expat |
---|
8 | |
---|
9 | from federation import topdl |
---|
10 | from federation.remote_service import service_caller |
---|
11 | from federation.client_lib import client_opts, exit_with_fault, RPCException, \ |
---|
12 | wrangle_standard_options, do_rpc, get_experiment_names, save_certfile |
---|
13 | |
---|
14 | |
---|
15 | class image_opts(client_opts): |
---|
16 | def __init__(self): |
---|
17 | client_opts.__init__(self) |
---|
18 | self.add_option("--experiment_cert", dest="exp_certfile", |
---|
19 | type="string", help="experiment certificate file") |
---|
20 | self.add_option("--experiment_name", dest="exp_name", |
---|
21 | type="string", help="human readable experiment name") |
---|
22 | self.add_option("--output", dest="outfile", type="string", |
---|
23 | help="output image file") |
---|
24 | self.add_option("--format", dest="format", type="choice", |
---|
25 | choices=("jpg", "png", "dot", "svg"), |
---|
26 | help="Output file format override") |
---|
27 | self.add_option("--program", dest="neato", default=None, |
---|
28 | type="string", |
---|
29 | help="Program compatible with dot (from graphviz) used to " + \ |
---|
30 | "render image") |
---|
31 | self.add_option("--labels", dest='labels', action='store_true', |
---|
32 | default=True, help='Label nodes and edges') |
---|
33 | self.add_option("--no_labels", dest='labels', |
---|
34 | default=True, action='store_false', |
---|
35 | help='Label nodes and edges') |
---|
36 | self.add_option('--pixels', dest="pixels", default=None, |
---|
37 | type="int", |
---|
38 | help="Size of output in pixels (diagrams are square") |
---|
39 | self.add_option("--file", dest="file", |
---|
40 | help="experiment description file") |
---|
41 | |
---|
42 | def gen_dot_topo(t, labels, dotfile): |
---|
43 | """ |
---|
44 | Write a dot description of the topology in t, a topdl.Topology to the open |
---|
45 | file dotfile. the gen_image function below has already put some of that |
---|
46 | file together. This function only generates the nodes and connections. If |
---|
47 | labels is true, label the nodes. |
---|
48 | """ |
---|
49 | |
---|
50 | # The routine draws a circle for substrates with more than 2 interfaces |
---|
51 | # connected to it, so this makes lists of each of those. |
---|
52 | lans = [ s for s in t.substrates if len(s.interfaces) != 2] |
---|
53 | links = [ s for s in t.substrates if len(s.interfaces) == 2] |
---|
54 | |
---|
55 | # Output the nodes. Deal with the possibility that there's no name, but |
---|
56 | # that's unlikely. |
---|
57 | for i, n in enumerate(t.elements): |
---|
58 | if n.name: |
---|
59 | print >>dotfile, '\t"%s" [shape=box,style=filled,\\' % n.name |
---|
60 | else: |
---|
61 | print >>dotfile, \ |
---|
62 | '\t"unnamed_node%d" [shape=box,style=filled,\\' % i |
---|
63 | print >>dotfile, '\t\tcolor=black,fillcolor="#60f8c0",regular=1]' |
---|
64 | |
---|
65 | # Encode the lans and the links |
---|
66 | for l in lans: |
---|
67 | print >>dotfile, '\t"%s" [shape=ellipse, style=filled,\\' % l.name |
---|
68 | print >>dotfile,'\t\tcolor=black,fillcolor="#80c0f8",regular=1]' |
---|
69 | for i in l.interfaces: |
---|
70 | ip = i.get_attribute('ip4_address') |
---|
71 | if labels and ip: |
---|
72 | print >>dotfile, '\t"%s" -- "%s" [headlabel="%s"]' % \ |
---|
73 | (l.name, i.element.name, ip) |
---|
74 | else: |
---|
75 | print >>dotfile, '\t"%s" -- "%s"' % \ |
---|
76 | (l.name, i.element.name) |
---|
77 | |
---|
78 | for l in links: |
---|
79 | s, d = l.interfaces[0:2] |
---|
80 | sip = s.get_attribute('ip4_address') |
---|
81 | dip = d.get_attribute('ip4_address') |
---|
82 | if labels and sip and dip: |
---|
83 | print >>dotfile, \ |
---|
84 | ('\t"%s" -- "%s" [label="%s",taillabel="%s",' + \ |
---|
85 | 'headlabel="%s"]') % \ |
---|
86 | (s.element.name, d.element.name, l.name, |
---|
87 | sip, dip) |
---|
88 | else: |
---|
89 | print >>dotfile, '\t"%s" -- "%s" ' % \ |
---|
90 | (s.element.name, d.element.name) |
---|
91 | |
---|
92 | def gen_image(top, file, fmt, neato, labels, pix=None): |
---|
93 | """ |
---|
94 | Create a dot formatted input file from the topdl.Topology in top and run |
---|
95 | dot on it, storing the output in a file named file. The image format it in |
---|
96 | fmt, and it has to be one of the formats understood by dot or neato. |
---|
97 | Labels is true if the graph should be noted with node names and addresses. |
---|
98 | Pix sets the sixe of the output in pixels (square images). Neato is a |
---|
99 | user-specified graph drawing programs that takes neato/dot compaitble |
---|
100 | inputs. If no neato is specified, the code looks for a suitable |
---|
101 | executable. Return the command's return code. |
---|
102 | """ |
---|
103 | |
---|
104 | # Open up a temporary file for dot to turn into a visualization. This |
---|
105 | # may raise an exception |
---|
106 | df, dotname = tempfile.mkstemp(prefix='fedd_client', suffix=".dot") |
---|
107 | dotfile = os.fdopen(df, 'w') |
---|
108 | |
---|
109 | # Look for neato if the executable is unspecified. |
---|
110 | if not neato: |
---|
111 | for f in ['/usr/bin/neato', '/usr/local/bin/neato', |
---|
112 | '/usr/bin/dot', '/usr/local/bin/dot']: |
---|
113 | if os.access(f, os.X_OK): |
---|
114 | neato = f |
---|
115 | break |
---|
116 | else: |
---|
117 | raise RuntimeError("Cannot find graph rendering program") |
---|
118 | |
---|
119 | # Some arguments bases on size, outputformat, destination, etc. |
---|
120 | cmd = [neato, '-Gsplines=true'] |
---|
121 | if fmt != 'dot': cmd.append('-T%s' % fmt) |
---|
122 | if file: |
---|
123 | cmd.append('-o') |
---|
124 | cmd.append(file) |
---|
125 | cmd.append(dotname) |
---|
126 | |
---|
127 | nodes = len(top.elements) |
---|
128 | if nodes < 10: size = 5 |
---|
129 | elif nodes < 50: size = 10 |
---|
130 | else: size = 18 |
---|
131 | |
---|
132 | if pix: |
---|
133 | dpi = pix / size |
---|
134 | else: |
---|
135 | dpi = None |
---|
136 | |
---|
137 | # Begin composing the graph description |
---|
138 | print >>dotfile, "graph G {" |
---|
139 | if dpi: |
---|
140 | print >>dotfile, '\tgraph [size="%i,%i", dpi="%i", ratio=fill];' \ |
---|
141 | % (size, size, dpi) |
---|
142 | else: |
---|
143 | print >>dotfile, '\tgraph [size="%i,%i", ratio=fill];' \ |
---|
144 | % (size, size) |
---|
145 | |
---|
146 | if labels: |
---|
147 | print >>dotfile, '\tnode [fontname=arial,fontsize=9,label="\N"];' |
---|
148 | print >>dotfile, '\tedge [fontname=arial,fontsize=9];\n' |
---|
149 | else: |
---|
150 | print >>dotfile, '\tnode [label=""];' |
---|
151 | |
---|
152 | gen_dot_topo(top, labels, dotfile) |
---|
153 | print >>dotfile, "}" |
---|
154 | dotfile.close() |
---|
155 | |
---|
156 | # Redirect the drawing program stderr, and draw |
---|
157 | dev_null = open("/dev/null", "w") |
---|
158 | rv = subprocess.call(cmd, stderr=dev_null) |
---|
159 | os.remove(dotname) |
---|
160 | dev_null.close() |
---|
161 | |
---|
162 | return rv |
---|
163 | |
---|
164 | def extract_topo_from_message(msg, serialize): |
---|
165 | """ |
---|
166 | Extract the topdl.Topology from either an Ns2Topdl or Info response. In |
---|
167 | the unlikely event that the message is misformatted, a RuntimeError is |
---|
168 | raised. If the overall script is only serializing messages, this routine |
---|
169 | probably returns None (unless do_rpc is modified to return a valid response |
---|
170 | when serializing only). |
---|
171 | """ |
---|
172 | if 'experimentdescription' in msg: |
---|
173 | if 'topdldescription' in msg['experimentdescription']: |
---|
174 | exp = msg['experimentdescription']['topdldescription'] |
---|
175 | return topdl.Topology(**exp) |
---|
176 | else: |
---|
177 | raise RuntimeError("Bad response: could not translate") |
---|
178 | elif serialize: |
---|
179 | return None |
---|
180 | else: |
---|
181 | raise RuntimeError("Bad response. %s" % e.message) |
---|
182 | |
---|
183 | def get_experiment_topo(opts, cert): |
---|
184 | """ |
---|
185 | Get the topology of an existing experiment from a running fedd. That is, |
---|
186 | pull the experimentdescription from an Info operation. Extract the |
---|
187 | experiment identifier from opts and raise a RuntimeError if insufficient or |
---|
188 | ambiguous information is present there. The Topology is returned. |
---|
189 | """ |
---|
190 | |
---|
191 | # Figure out what experiment to retrieve |
---|
192 | if opts.exp_name and opts.exp_certfile: |
---|
193 | raise RuntimeError("Only one of --experiment_cert and " + \ |
---|
194 | "--experiment_name permitted") |
---|
195 | elif opts.exp_certfile: |
---|
196 | exp_id = { 'fedid': fedid(file=opts.exp_certfile) } |
---|
197 | elif opts.exp_name: |
---|
198 | exp_id = { 'localname' : opts.exp_name } |
---|
199 | else: |
---|
200 | raise RuntimeError("specify one of --experiment_cert, " + \ |
---|
201 | "--experiment_name, or --file") |
---|
202 | |
---|
203 | # Get the topology and return it |
---|
204 | req = { 'experiment': exp_id } |
---|
205 | resp_dict = do_rpc(req, |
---|
206 | opts.url, opts.transport, cert, opts.trusted, |
---|
207 | serialize_only=opts.serialize_only, |
---|
208 | tracefile=opts.tracefile, |
---|
209 | caller=service_caller('Info'), responseBody="InfoResponseBody") |
---|
210 | |
---|
211 | return extract_topo_from_message(resp_dict, opts.serialize_only) |
---|
212 | |
---|
213 | def get_file_topo(opts, cert): |
---|
214 | """ |
---|
215 | Parse a topology file or convert an ns2 file into a topdl.Topology. XML |
---|
216 | encodings of the topology can be converted completely locally, but ns2 |
---|
217 | files must be converted by a fedd. The topology is returned or a |
---|
218 | RuntimeError, RPCError, or EnvironmentError (if the file is unreadable) is |
---|
219 | raised. |
---|
220 | """ |
---|
221 | # Try the easy case first. Note that if the file is unreadable rather than |
---|
222 | # unparsable, this attempt will raise an EnvironmentError. |
---|
223 | try: |
---|
224 | return topdl.topology_from_xml(filename=opts.file, top='experiment') |
---|
225 | except xml.parsers.expat.ExpatError, e: |
---|
226 | # Print an error message if debugging |
---|
227 | if opts.debug > 1: print >>sys.stderr, "XML parse failed: %s" %e |
---|
228 | |
---|
229 | # If we get here the parser above failed and we assume that the file is |
---|
230 | # ns2/tcl. Call out to the converter. The exceptions above can come out |
---|
231 | # of here, though EnvironmentErrors are unlikely. |
---|
232 | if not cert: |
---|
233 | raise RuntimeError("Couldn't find an identity certificate, " + \ |
---|
234 | "RPC (Ns2Topdl) needed.") |
---|
235 | |
---|
236 | contents = "".join([l for l in open(opts.file, "r")]) |
---|
237 | msg = { 'description': { 'ns2description': contents }, } |
---|
238 | if opts.debug > 1: print >>sys.stderr, msg |
---|
239 | resp_dict = do_rpc(msg, |
---|
240 | opts.url, opts.transport, cert, opts.trusted, |
---|
241 | serialize_only=opts.serialize_only, |
---|
242 | tracefile=opts.tracefile, |
---|
243 | caller=service_caller('Ns2Topdl'), |
---|
244 | responseBody="Ns2TopdlResponseBody") |
---|
245 | |
---|
246 | return extract_topo_from_message(resp_dict, opts.serialize_only) |
---|
247 | |
---|
248 | parser = image_opts() |
---|
249 | (opts, args) = parser.parse_args() |
---|
250 | |
---|
251 | # Note that if we're converting a local topdl file, the absence of a cert may |
---|
252 | # not be a big deal. It's checked if needed below |
---|
253 | try: |
---|
254 | cert, fid = wrangle_standard_options(opts) |
---|
255 | except RuntimeError, e: |
---|
256 | cert = None |
---|
257 | print >>sys.stderr, "Warning: %s e" |
---|
258 | |
---|
259 | |
---|
260 | # Figure out where output is going and in what format. If format is given and |
---|
261 | # understood, that's the output format. Otherwise we try the last three |
---|
262 | # letters of the file as a suffix. If all that fails, we fall back to |
---|
263 | # dot/neato format. |
---|
264 | if opts.format and opts.outfile: |
---|
265 | fmt = opts.format |
---|
266 | file = opts.outfile |
---|
267 | elif not opts.format and opts.outfile: |
---|
268 | fmt = opts.outfile[-3:] |
---|
269 | if fmt not in ("png", "jpg", "dot", "svg"): |
---|
270 | print >>sys.stderr, \ |
---|
271 | "Unexpected file suffix (%s) and no format given, using %s" % \ |
---|
272 | (fmt, "dot") |
---|
273 | fmt = "dot" |
---|
274 | file = opts.outfile |
---|
275 | elif opts.format and not opts.outfile: |
---|
276 | fmt = opts.format |
---|
277 | file = None |
---|
278 | else: |
---|
279 | fmt="dot" |
---|
280 | file = None |
---|
281 | |
---|
282 | # Get the topology from a file or an experiment |
---|
283 | if not opts.file: |
---|
284 | if not cert: |
---|
285 | sys.exit("Couldn't find an identity certificate, RPC (Info) needed.") |
---|
286 | try: |
---|
287 | top = get_experiment_topo(opts, cert) |
---|
288 | except RPCException, e: |
---|
289 | exit_with_fault(e) |
---|
290 | except RuntimeError, e: |
---|
291 | sys.exit("%s" % e) |
---|
292 | else: |
---|
293 | if opts.exp_name or opts.exp_certfile: |
---|
294 | print >>sys.stderr, \ |
---|
295 | "Ignoring experiment name or certificate, reading %s" \ |
---|
296 | % opts.file |
---|
297 | try: |
---|
298 | top = get_file_topo(opts, cert) |
---|
299 | except RPCException, e: |
---|
300 | print >>sys.stderr, "Cannot extract a topology from %s" % opts.file |
---|
301 | exit_with_fault(e) |
---|
302 | except RuntimeError, e: |
---|
303 | sys.exit("Cannot extract a topology from %s: %s" % (opts.file, e)) |
---|
304 | except EnvironmentError, e: |
---|
305 | sys.exit("Cannot extract a topology from %s: %s" % (opts.file, e)) |
---|
306 | |
---|
307 | if not top: |
---|
308 | sys.exit("Cannot extract a topology from %s" % opts.file) |
---|
309 | |
---|
310 | # and make an image |
---|
311 | if not opts.serialize_only: |
---|
312 | try: |
---|
313 | if gen_image(top, file, fmt, opts.neato, opts.labels, opts.pixels) !=0: |
---|
314 | sys.exit("Error rendering graph (subcommand returned non-0)") |
---|
315 | except EnvironmentError, e: |
---|
316 | sys.exit("Failed to draw graph: %s" %e) |
---|
317 | except RuntimeError, e: |
---|
318 | sys.exit("Failed to draw graph: %s" %e) |
---|
319 | else: |
---|
320 | sys.exit(0) |
---|