source: fedd/fedd_image.py @ 1f356d3

axis_examplecompt_changesinfo-ops
Last change on this file since 1f356d3 was 1f356d3, checked in by Ted Faber <faber@…>, 9 years ago

Generate images with nodes paritioned by attribute

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