source: fedd/fedd_image.py @ 2f45140

Last change on this file since 2f45140 was ef7d689, checked in by Ted Faber <faber@…>, 12 years ago

Allow call w/o credential - url was undefined

  • Property mode set to 100755
File size: 14.7 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
352url = None
353
354# Note that if we're converting a local topdl file, the absence of a cert or
355# abac directory may not be a big deal.  It's checked if needed below
356try:
357    cert, fid, url = wrangle_standard_options(opts)
358except RuntimeError, e:
359    cert = None
360    print >>sys.stderr, "Warning: %s" % e
361
362
363# Figure out where output is going and in what format.  If format is given and
364# understood, that's the output format.  Otherwise we try the last three
365# letters of the file as a suffix.  If all that fails, we fall back to
366# dot/neato format.
367if opts.format and opts.outfile:
368    fmt = opts.format
369    file = opts.outfile
370elif not opts.format and opts.outfile:
371    fmt = opts.outfile[-3:]
372    if fmt not in ("png", "jpg", "dot", "svg"):
373        print >>sys.stderr, \
374                "Unexpected file suffix (%s) and no format given, using %s" % \
375                (fmt, "dot")
376        fmt = "dot"
377    file = opts.outfile
378elif opts.format and not opts.outfile:
379    fmt = opts.format
380    file = None
381else:
382    fmt="dot"
383    file = None
384
385# Get the topology from a file or an experiment
386if not opts.file:
387    if not cert:
388        sys.exit("Couldn't find an identity certificate, RPC (Info) needed.")
389    try:
390        top = get_experiment_topo(opts, cert, url)
391    except RPCException, e:
392        exit_with_fault(e, 'image', opts)
393    except RuntimeError, e:
394        sys.exit("%s" % e)
395else:
396    if opts.exp_name or opts.exp_certfile:
397        print >>sys.stderr, \
398                "Ignoring experiment name or certificate, reading %s" \
399                % opts.file
400    try:
401        top = get_file_topo(opts, cert, url)
402    except RPCException, e:
403        print >>sys.stderr, "Cannot extract a topology from %s" % opts.file
404        exit_with_fault(e, 'image', opts)
405    except RuntimeError, e:
406        sys.exit("Cannot extract a topology from %s: %s" % (opts.file, e))
407    except EnvironmentError, e:
408        sys.exit("Cannot extract a topology from %s: %s" % (opts.file, e))
409
410    if not top:
411        sys.exit("Cannot extract a topology from %s" % opts.file)
412
413# and make an image
414if not opts.serialize_only: 
415    try:
416        if gen_image(top, file, fmt, opts.neato, opts.labels, opts.pixels,
417                opts.group) !=0:
418            sys.exit("Error rendering graph (subcommand returned non-0)" +
419                    "  Check output, this may be a non-fatal graphviz warning")
420    except EnvironmentError, e:
421        sys.exit("Failed to draw graph: %s" %e)
422    except RuntimeError, e:
423        sys.exit("Failed to draw graph: %s" %e)
424else:
425    sys.exit(0)
Note: See TracBrowser for help on using the repository browser.