source: fedd/fedd_image.py @ d894c21

axis_examplecompt_changesinfo-ops
Last change on this file since d894c21 was 5c3d542, checked in by Ted Faber <faber@…>, 13 years ago

Multiple attributes and recursive subgraphs.

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