source: fedd/fedd_image.py @ e65150a

axis_examplecompt_changesinfo-ops
Last change on this file since e65150a was 52b6ebc, checked in by Ted Faber <faber@…>, 14 years ago

Typo in the error handler

  • Property mode set to 100755
File size: 14.2 KB
RevLine 
[d743d60]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",
[62f3dd9]19                action='callback', callback=self.expand_file, type='str',
20                help="experiment certificate file")
[d743d60]21        self.add_option("--experiment_name", dest="exp_name",
22                type="string", help="human readable experiment name")
[62f3dd9]23        self.add_option("--output", dest="outfile",
24                action='callback', callback=self.expand_file, type='str',
[d743d60]25                help="output image file")
26        self.add_option("--format", dest="format", type="choice", 
27                choices=("jpg", "png", "dot", "svg"),
28                help="Output file format override")
29        self.add_option("--program", dest="neato", default=None,
30                type="string",
31                help="Program compatible with dot (from graphviz) used to " + \
32                        "render image")
33        self.add_option("--labels", dest='labels', action='store_true',
34                default=True, help='Label nodes and edges')
35        self.add_option("--no_labels", dest='labels',
36                default=True, action='store_false',
37                help='Label nodes and edges')
38        self.add_option('--pixels', dest="pixels", default=None,
39                type="int",
40                help="Size of output in pixels (diagrams are square")
41        self.add_option("--file", dest="file", 
[62f3dd9]42                action='callback', callback=self.expand_file, type='str',
[d743d60]43                help="experiment description file")
[5c3d542]44        self.add_option("--group", dest="group", action="append", default=[], 
[1f356d3]45                help="Group nodes by this attribute")
[d743d60]46
[5c3d542]47def make_subgraph(topelems, attrs):
[1f356d3]48    """
49    Take a list of topdl elements (Computers and Substrates ) and partition
50    them into groups based on the value of the given attribute.  All computers
51    with the same value for the attribute are in a subgraph. A substrate is in
52    the subgraph if all its interfaces are, and in the "default" subgraph if
53    not.
54    """
55
56    def add_to_map(m, a, v):
57        """
58        Add this value to the list in m[a] if that list exists, or create it.
59        """
60        if a in m: m[a].append(v)
61        else: m[a] = [ v]
62
[5c3d542]63    if not attrs: return { }
64
[1f356d3]65    sg = { }
[5c3d542]66    attr, rest = attrs[0], attrs[1:]
[1f356d3]67    for e in topelems:
68        if isinstance(e, topdl.Computer):
69            add_to_map(sg, e.get_attribute(attr) or 'default', e)
70        elif isinstance(e, topdl.Substrate):
71            # A little hairy.  Make a set of the values of the given attribute
72            # for all the elements in the substrate's interfaces.  If that set
73            # has one element, put the substrate in that subgraph.  Otherwise
74            # put it in default.
75            atts = set([i.element.get_attribute(attr) or 'default' \
76                    for i in e.interfaces])
77
78            if len(atts) == 1: add_to_map(sg, atts.pop(), e)
79            else: add_to_map(sg, 'default', e)
80
[5c3d542]81    if rest:
82        for k in sg.keys():
83            sg[k] = make_subgraph(sg[k], rest)
84
[1f356d3]85    return sg
86
[5c3d542]87def output_subgraphs(sg, dotfile):
88
89    # Construct subgraphs featurning nodes from those graphs
90    for sn, nodes in sg.items():
91        # Output the computers and lans in subgraphs, if necessary.  Subgraphs
92        # treated as clusters are designated by "cluster" starting the name.
93        # (The default subgraph is not treated as a cluster.
94        if sn != 'default': 
95            print >>dotfile, 'subgraph "cluster-%s" {' % sn
96            print >>dotfile, 'label="%s"' % sn
97        else: 
98            print >>dotfile, 'subgraph {'
99        print >>dotfile, 'color="#08c0f8"'
100        print >>dotfile, 'clusterrank="local"'
101
102        if isinstance(nodes, dict): output_subgraphs(nodes, dotfile)
103        elif isinstance(nodes, list):
104            # For each node in the subgraph, output its representation
105            for i, n in enumerate(nodes):
106                if isinstance(n, topdl.Computer):
107                    if n.name:
108                        print >>dotfile, \
109                                '\t"%s" [shape=box,style=filled,\\' % n.name
110                    else:
111                        print >>dotfile, \
112                                '\t"unnamed_node%d" [shape=box,style=filled,\\'\
113                                % i
114                    print >>dotfile, \
115                            '\t\tcolor=black,fillcolor="#60f8c0",regular=1]'
116                elif isinstance(n, topdl.Substrate):
117                    print >>dotfile, '\t"%s" [shape=ellipse, style=filled,\\' \
118                            % n.name
119                    print >>dotfile,\
120                            '\t\tcolor=black,fillcolor="#80c0f8",regular=1]'
121        print >>dotfile, '}'
122
123def gen_dot_topo(t, labels, dotfile, sg_attr):
[d743d60]124    """
125    Write a dot description of the topology in t, a topdl.Topology to the open
126    file dotfile.  the gen_image function below has already put some of that
127    file together. This function only generates the nodes and connections.  If
128    labels is true, label the nodes.
129    """
130
[1f356d3]131
[d743d60]132    # The routine draws a circle for substrates with more than 2 interfaces
133    # connected to it, so this makes lists of each of those.
134    lans = [ s for s in t.substrates if len(s.interfaces) != 2]
135    links = [ s for s in t.substrates if len(s.interfaces) == 2]
136
[1f356d3]137    # Break into subgraphs by the attribute of one's given
138    if sg_attr: sg = make_subgraph(t.elements + lans, sg_attr)
139    else: sg = { 'default': t.elements + lans }
140
141
[5c3d542]142    output_subgraphs(sg, dotfile)
[1f356d3]143
144    # Pull the edges out ot the lans and links
[d743d60]145    for l in lans:
146        for i in l.interfaces:
147            ip = i.get_attribute('ip4_address')
148            if labels and ip:
149                print >>dotfile, '\t"%s" -- "%s" [headlabel="%s"]' % \
150                        (l.name, i.element.name, ip)
151            else:
152                print >>dotfile, '\t"%s" -- "%s"' % \
153                        (l.name, i.element.name)
154
155    for l in links:
156        s, d = l.interfaces[0:2] 
157        sip = s.get_attribute('ip4_address')
158        dip = d.get_attribute('ip4_address')
159        if labels and sip and dip:
160            print >>dotfile, \
161                    ('\t"%s" -- "%s" [label="%s",taillabel="%s",' + \
162                    'headlabel="%s"]') % \
163                    (s.element.name, d.element.name, l.name,
164                        sip, dip)
165        else:
166            print >>dotfile, '\t"%s" -- "%s" ' % \
167                    (s.element.name, d.element.name)
168
[1f356d3]169def gen_image(top, file, fmt, neato, labels, pix=None, sg_attr=None):
[d743d60]170    """
171    Create a dot formatted input file from the topdl.Topology in top and run
172    dot on it, storing the output in a file named file.  The image format it in
173    fmt, and it has to be one of the formats understood by dot or neato.
174    Labels is true if the graph should be noted with node names and addresses.
175    Pix sets the sixe of the output in pixels (square images).  Neato is a
176    user-specified graph drawing programs that takes neato/dot compaitble
177    inputs.  If no neato is specified, the code looks for a suitable
178    executable.  Return the command's return code.
179    """
180
181    # Open up a temporary file for dot to turn into a visualization.  This
182    # may raise an exception
183    df, dotname = tempfile.mkstemp(prefix='fedd_client', suffix=".dot")
184    dotfile = os.fdopen(df, 'w')
185
186    # Look for neato if the executable is unspecified.
187    if not neato:
[1f356d3]188        # If a subgraph attribute has been specified, prefer dot to neato.  dot
189        # is more "natural" looking graphs, but neato segregates better.
190        if sg_attr: 
191            search_list = ['/usr/bin/dot', '/usr/local/bin/dot',
192                    '/usr/bin/neato', '/usr/local/bin/neato']
193        else:
194            search_list = ['/usr/bin/neato', '/usr/local/bin/neato', 
195                '/usr/bin/dot', '/usr/local/bin/dot']
196
197        for f in search_list:
[d743d60]198            if os.access(f, os.X_OK):
199                neato = f
200                break
201        else:
202            raise RuntimeError("Cannot find graph rendering program")
203
204    # Some arguments bases on size, outputformat, destination, etc.
205    cmd = [neato, '-Gsplines=true']
206    if fmt != 'dot': cmd.append('-T%s' % fmt)
207    if file:
208        cmd.append('-o')
209        cmd.append(file)
210    cmd.append(dotname)
211
212    nodes = len(top.elements)
213    if nodes < 10: size = 5
214    elif nodes < 50: size = 10
215    else: size = 18
216
217    if pix:
218        dpi = pix / size
219    else:
220        dpi = None
221
222    # Begin composing the graph description
223    print >>dotfile, "graph G {"
224    if dpi:
[5c3d542]225        print >>dotfile, \
226            '\tgraph [size="%i,%i", dpi="%i", ratio=fill, compound="true"];' \
227            % (size, size, dpi)
[d743d60]228    else:
[5c3d542]229        print >>dotfile, \
230            '\tgraph [size="%i,%i", ratio=fill, compound="true"];' \
231            % (size, size)
[d743d60]232
233    if labels:
234        print >>dotfile, '\tnode [fontname=arial,fontsize=9,label="\N"];'
235        print >>dotfile, '\tedge [fontname=arial,fontsize=9];\n'
236    else:
237        print >>dotfile, '\tnode [label=""];'
238
[1f356d3]239    gen_dot_topo(top, labels, dotfile, sg_attr=sg_attr)
[d743d60]240    print >>dotfile, "}"
241    dotfile.close()
242
243    # Redirect the drawing program stderr, and draw
244    dev_null = open("/dev/null", "w")
245    rv = subprocess.call(cmd, stderr=dev_null)
246    os.remove(dotname)
247    dev_null.close()
248
249    return rv
250
251def extract_topo_from_message(msg, serialize):
252    """
253    Extract the topdl.Topology from either an Ns2Topdl or Info response.  In
254    the unlikely event that the message is misformatted, a RuntimeError is
255    raised.  If the overall script is only serializing messages, this routine
256    probably returns None (unless do_rpc is modified to return a valid response
257    when serializing only).
258    """
259    if 'experimentdescription' in msg:
260        if 'topdldescription' in msg['experimentdescription']:
261            exp = msg['experimentdescription']['topdldescription']
262            return topdl.Topology(**exp)
263        else:
264            raise RuntimeError("Bad response: could not translate")
265    elif serialize:
266        return None
267    else:
268        raise RuntimeError("Bad response. %s" % e.message)
269
[5d854e1]270def get_experiment_topo(opts, cert, url):
[d743d60]271    """
272    Get the topology of an existing experiment from a running fedd.  That is,
273    pull the experimentdescription from an Info operation. Extract the
274    experiment identifier from opts and raise a RuntimeError if insufficient or
275    ambiguous information is present there.  The Topology is returned.
276    """
277
278    # Figure out what experiment to retrieve
279    if opts.exp_name and opts.exp_certfile:
280        raise RuntimeError("Only one of --experiment_cert and " + \
281                "--experiment_name permitted")
282    elif opts.exp_certfile:
283        exp_id = { 'fedid': fedid(file=opts.exp_certfile) }
284    elif opts.exp_name:
285        exp_id = { 'localname' : opts.exp_name }
286    else:
287        raise RuntimeError("specify one of --experiment_cert, " + \
288                "--experiment_name, or --file")
289
290    # Get the topology and return it
291    req = { 'experiment': exp_id }
292    resp_dict = do_rpc(req,
[5d854e1]293            url, opts.transport, cert, opts.trusted, 
[d743d60]294            serialize_only=opts.serialize_only,
295            tracefile=opts.tracefile,
296            caller=service_caller('Info'), responseBody="InfoResponseBody")
297
298    return extract_topo_from_message(resp_dict, opts.serialize_only)
299
[5d854e1]300def get_file_topo(opts, cert, url):
[d743d60]301    """
302    Parse a topology file or convert an ns2 file into a topdl.Topology.  XML
303    encodings of the topology can be converted completely locally, but ns2
304    files must be converted by a fedd.  The topology is returned or a
305    RuntimeError, RPCError, or EnvironmentError (if the file is unreadable) is
306    raised.
307    """
308    # Try the easy case first.  Note that if the file is unreadable rather than
309    # unparsable, this attempt will raise an EnvironmentError.
310    try:
311        return topdl.topology_from_xml(filename=opts.file, top='experiment')
312    except xml.parsers.expat.ExpatError, e:
313        # Print an error message if debugging
314        if opts.debug > 1: print >>sys.stderr, "XML parse failed: %s" %e
315
316    # If we get here the parser above failed and we assume that the file is
317    # ns2/tcl.  Call out to the converter.  The exceptions above can come out
318    # of here, though EnvironmentErrors are unlikely.
319    if not cert:
320        raise RuntimeError("Couldn't find an identity certificate, " + \
321                "RPC (Ns2Topdl) needed.")
322
323    contents = "".join([l for l in open(opts.file, "r")])
324    msg = { 'description': { 'ns2description': contents }, }
325    if opts.debug > 1: print >>sys.stderr, msg
326    resp_dict = do_rpc(msg, 
[5d854e1]327            url, opts.transport, cert, opts.trusted, 
[d743d60]328            serialize_only=opts.serialize_only,
329            tracefile=opts.tracefile,
330            caller=service_caller('Ns2Topdl'),
331            responseBody="Ns2TopdlResponseBody")
332
333    return extract_topo_from_message(resp_dict, opts.serialize_only)
334
335parser = image_opts()
336(opts, args) = parser.parse_args()
337
[52b6ebc]338# Note that if we're converting a local topdl file, the absence of a cert or
339# abac directory may not be a big deal.  It's checked if needed below
[d743d60]340try:
[5d854e1]341    cert, fid, url = wrangle_standard_options(opts)
[d743d60]342except RuntimeError, e:
343    cert = None
[52b6ebc]344    print >>sys.stderr, "Warning: %s" % e
[d743d60]345
346
347# Figure out where output is going and in what format.  If format is given and
348# understood, that's the output format.  Otherwise we try the last three
349# letters of the file as a suffix.  If all that fails, we fall back to
350# dot/neato format.
351if opts.format and opts.outfile:
352    fmt = opts.format
353    file = opts.outfile
354elif not opts.format and opts.outfile:
355    fmt = opts.outfile[-3:]
356    if fmt not in ("png", "jpg", "dot", "svg"):
357        print >>sys.stderr, \
358                "Unexpected file suffix (%s) and no format given, using %s" % \
359                (fmt, "dot")
360        fmt = "dot"
361    file = opts.outfile
362elif opts.format and not opts.outfile:
363    fmt = opts.format
364    file = None
365else:
366    fmt="dot"
367    file = None
368
369# Get the topology from a file or an experiment
370if not opts.file:
371    if not cert:
372        sys.exit("Couldn't find an identity certificate, RPC (Info) needed.")
373    try:
[5d854e1]374        top = get_experiment_topo(opts, cert, url)
[d743d60]375    except RPCException, e:
376        exit_with_fault(e)
377    except RuntimeError, e:
378        sys.exit("%s" % e)
379else:
380    if opts.exp_name or opts.exp_certfile:
381        print >>sys.stderr, \
382                "Ignoring experiment name or certificate, reading %s" \
383                % opts.file
384    try:
[5d854e1]385        top = get_file_topo(opts, cert, url)
[d743d60]386    except RPCException, e:
387        print >>sys.stderr, "Cannot extract a topology from %s" % opts.file
388        exit_with_fault(e)
389    except RuntimeError, e:
390        sys.exit("Cannot extract a topology from %s: %s" % (opts.file, e))
391    except EnvironmentError, e:
392        sys.exit("Cannot extract a topology from %s: %s" % (opts.file, e))
393
394    if not top:
395        sys.exit("Cannot extract a topology from %s" % opts.file)
396
397# and make an image
398if not opts.serialize_only: 
399    try:
[1f356d3]400        if gen_image(top, file, fmt, opts.neato, opts.labels, opts.pixels,
401                opts.group) !=0:
[d743d60]402            sys.exit("Error rendering graph (subcommand returned non-0)")
403    except EnvironmentError, e:
404        sys.exit("Failed to draw graph: %s" %e)
405    except RuntimeError, e:
406        sys.exit("Failed to draw graph: %s" %e)
407else:
408    sys.exit(0)
Note: See TracBrowser for help on using the repository browser.