source: fedd/fedd_image.py @ 6bff6d9

version-3.01version-3.02
Last change on this file since 6bff6d9 was d743d60, checked in by Ted Faber <faber@…>, 15 years ago

Totally refactor fedd_client.py into component scripts. The previous layout
have become a twisty hell of misdirected OOP and learning python run amok.
This version is actually pretty readable and will be much easier to build on.

  • Property mode set to 100755
File size: 11.3 KB
Line 
1#!/usr/local/bin/python
2
3import sys
4import os
5import tempfile
6import subprocess
7import xml.parsers.expat
8
9from federation import topdl
10from federation.remote_service import service_caller
11from federation.client_lib import client_opts, exit_with_fault, RPCException, \
12        wrangle_standard_options, do_rpc, get_experiment_names, save_certfile
13
14
15class 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
42def 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
92def 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
164def 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
183def 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
213def 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
248parser = 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
253try:
254    cert, fid = wrangle_standard_options(opts)
255except 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.
264if opts.format and opts.outfile:
265    fmt = opts.format
266    file = opts.outfile
267elif 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
275elif opts.format and not opts.outfile:
276    fmt = opts.format
277    file = None
278else:
279    fmt="dot"
280    file = None
281
282# Get the topology from a file or an experiment
283if 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)
292else:
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
311if 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)
319else:
320    sys.exit(0)
Note: See TracBrowser for help on using the repository browser.