source: fedd/fedd_image.py @ 25930db

compt_changes
Last change on this file since 25930db was 6bedbdba, checked in by Ted Faber <faber@…>, 13 years ago

Split topdl and fedid out to different packages. Add differential
installs

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