source: fedd/fedd_image.py @ 2e46f35

axis_examplecompt_changesinfo-ops
Last change on this file since 2e46f35 was 2e46f35, checked in by mikeryan <mikeryan@…>, 8 years ago

switch to /usr/bin/env python to run python

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