source: fedd/fedd_image.py @ 218ffe0

compt_changesinfo-ops
Last change on this file since 218ffe0 was 218ffe0, checked in by Ted Faber <faber@…>, 12 years ago

Fix Proof handling

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