source: fedd/fedd_image.py @ 451fb96

compt_changesinfo-ops
Last change on this file since 451fb96 was de2ef5c, checked in by Ted Faber <faber@…>, 13 years ago

PLaceholder error message. Graphviz returns non-zero error codes on
things like font sizing. Need to fix this: see #25

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