source: fedd/compose.py @ baf19c6

axis_examplecompt_changesinfo-opsversion-3.01version-3.02
Last change on this file since baf19c6 was baf19c6, checked in by Ted Faber <faber@…>, 14 years ago

Missed a typo in that huge commit

  • Property mode set to 100644
File size: 18.8 KB
Line 
1#!/usr/local/bin/python
2
3import sys
4import re
5import os
6import random
7import copy
8
9from optparse import OptionParser, OptionValueError
10from federation.remote_service import service_caller
11from federation.service_error import service_error
12from federation import topdl
13
14class constraint:
15    def __init__(self, name=None, required=False, accepts=None, provides=None,
16            match=None):
17        self.name = name
18        self.required = required
19        self.accepts = accepts or []
20        self.provides = provides or []
21        self.match = match
22
23    def __str__(self):
24        return "%s:%s:%s:%s" % (self.name, self.required,
25                ",".join(self.provides), ",".join(self.accepts))
26
27       
28class ComposeOptionParser(OptionParser):
29    """
30    This class encapsulates the options to this script in one place.  It also
31    holds the callback for the multifile choice.
32    """
33    def __init__(self):
34        OptionParser.__init__(self)
35        self.add_option('--url', dest='url', default="http://localhost:13235",
36                help='url of ns2 to topdl service')
37        self.add_option('--certfile', dest='cert', default=None,
38                help='Certificate to use as identity')
39        self.add_option('--seed', dest='seed', type='int', default=None,
40                help='Random number seed')
41        self.add_option('--multifile', dest='files', default=[], type='string', 
42                action='callback', callback=self.multi_callback, 
43                help="Include file multiple times")
44        self.add_option('--output', dest='outfile', default=None,
45                help='Output file name')
46        self.add_option('--format', dest='format', default="xml",
47                help='Output file format')
48
49    @staticmethod
50    def multi_callback(option, opt_str, value, parser):
51        """
52        Parse a --multifile command line option.  The parameter is of the form
53        filename,count.  This splits the argument at the rightmost comma and
54        inserts the filename, count tuple into the "files" option.  It handles
55        a couple error cases, too.
56        """
57        idx = value.rfind(',')
58        if idx != -1:
59            try:
60                parser.values.files.append((value[0:idx], int(value[idx+1:])))
61            except ValueError, e:
62                raise OptionValueError(
63                        "Can't convert %s to int in multifile (%s)" % \
64                                (value[idx+1:], value))
65        else:
66            raise OptionValueError(
67                    "Bad format (need a comma) for multifile: %s" % value)
68
69def warn(msg):
70    """
71    Exactly what you think.  Print a message to stderr
72    """
73    print >>sys.stderr, msg
74
75
76def make_new_name(names, prefix="name"):
77    """
78    Generate an identifier not present in names by appending an integer to
79    prefix.  The new name is inserted into names and returned.
80    """
81    i = 0
82    n = "%s%d" % (prefix,i)
83    while n in names:
84        i += 1
85        n = "%s%d" % (prefix,i)
86    names.add(n)
87    return n
88
89def add_interfaces(top, mark):
90    """
91    Add unconnected interfaces to the nodes whose names are keys in the mark
92    dict.  As the interface is added change the name field of the contstraint
93    in mark into a tuple containing the node name, interface name, and topology
94    in which they reside.
95    """
96    for e in top.elements:
97        if e.name in mark:
98            con = mark[e.name]
99            ii = make_new_name(set([x.name for x in e.interface]), "inf")
100            e.interface.append(
101                    topdl.Interface(substrate=[], name=ii, element=e))
102            con.name = (e.name, ii, top)
103
104def localize_names(top, names, marks):
105    """
106    Take a topology and rename any substrates or elements that share a name
107    with an existing computer or substrate.  Keep the original name as a
108    localized_name attribute.  In addition, remap any constraints or interfaces
109    that point to the old name over to the new one.  Constraints are found in
110    the marks dict, indexed by node name.  Those constraints name attributes
111    have already been converted to triples (node name, interface name,
112    topology) so only the node name needs to be changed.
113    """
114    sub_map = { }
115    for s in top.substrates:
116        s.set_attribute('localized_name', s.name)
117        if s.name in names:
118            sub_map[s.name] = n = make_new_name(names, "substrate")
119            s.name = n
120        else:
121            names.add(s.name)
122
123    for e in [ e for e in top.elements if isinstance(e, topdl.Computer)]:
124        e.set_attribute('localized_name', e.name)
125        if e.name in names:
126            nn= make_new_name(names, "computer")
127            if e.name in marks:
128                n, i, t = marks[e.name].name
129                marks[e.name].name = (nn, i, t)
130            e.name = nn
131        else:
132            names.add(e.name)
133
134        # Interface mapping.  The list comprehension creates a list of
135        # substrate names where each element in the list is replaced by the
136        # entry in sub_map indexed by it if present and left alone otherwise.
137        for i in e.interface:
138            i.substrate = [ sub_map.get(ii, ii) for ii in i.substrate ]
139
140def meet_constraints(candidates, provides, accepts):
141    """
142    Try to meet all the constraints in candidates using the information in the
143    provides and accepts dicts (which index constraints that provide or accept
144    the given attribute). A constraint is met if it can be matched with another
145    constraint that provides an attribute that the first constraint accepts.
146    Only one match per pair is allowed, and we always prefer matching a
147    required constraint to an unreqired one.  If all the candidates can be
148    matches, return True and return False otherwise.
149    """
150    got_all = True
151    for c in candidates:
152        if not c.match:
153            match = None
154            for a in c.accepts:
155                for can in provides.get(a,[]):
156                    # A constraint cannot satisfy itself, nor do we allow loops
157                    # within the same topology.  This also excludes matched
158                    # constraints.
159                    if can.name != c.name and can.name[2] != c.name[2] and \
160                            not can.match:
161                        match = can
162                        # Within providers, prefer matches against required
163                        # composition points
164                        if can.required:
165                            break
166                # Within acceptance categories, prefer matches against required
167                # composition points
168                if match and match.required:
169                    break
170
171            # Done checking all possible matches.
172            if match:
173                match.match = c
174                c.match = match
175            else:
176                got_all = False
177    return got_all
178
179def randomize_constraint_order(indexes):
180    """
181    Randomly reorder the lists of constraints that provides and accepts hold.
182    """
183    if not isinstance(indexes, tuple):
184        indexes = (indexes,)
185
186    for idx in indexes:
187        for k in idx.keys():
188            random.shuffle(idx[k])
189
190def remote_ns2topdl(uri, desc, cert):
191    """
192    Call a remote service to convert the ns2 to topdl (and in fact all the way
193    to a topdl.Topology.
194    """
195
196    req = { 'description' : { 'ns2description': desc }, }
197
198    r = service_caller('Ns2Topdl')(uri, req, cert)
199
200    if r.has_key('Ns2TopdlResponseBody'):
201        r = r['Ns2TopdlResponseBody']
202        ed = r.get('experimentdescription', None)
203        if 'topdldescription' in ed:
204            return topdl.Topology(**ed['topdldescription'])
205        else:
206            return None
207    else:
208        return None
209
210def connect_composition_points(top, contraints, names):
211    """
212    top is a topology containing copies of all the topologies represented in
213    the contsraints, flattened into one name space.  This routine inserts the
214    additional substrates required to interconnect the topology as described by
215    the constraints.  After the links are made, unused connection points are
216    purged.
217    """
218    done = set()
219    for c in constraints:
220        if c not in done and c.match:
221            # c is an unprocessed matched constraint.  Create a substrate
222            # and attach it to the interfaces named by c and c.match.
223            sn = make_new_name(names, "sub")
224            s = topdl.Substrate(name=sn)
225            connected = 0
226            # Walk through all the computers in the topology
227            for e in [ e for e in top.elements \
228                    if isinstance(e, topdl.Computer)]:
229                # if the computer matches the computer and interface name in
230                # either c or c.match, connect it.  Once both computers have
231                # been found and connected, exit the loop walking through all
232                # computers.
233                for comp, inf in (c.name[0:2], c.match.name[0:2]):
234                    if e.name == comp:
235                        for i in e.interface:
236                            if i.name == inf:
237                                i.substrate.append(sn)
238                                connected += 1
239                                break
240                        break
241                # Connected both, so add the substrate to the topology
242                if connected == 2: 
243                    top.substrates.append(s)
244                    break
245            # c and c.match have been processed, so add them to the done set
246            done.add(c)
247            done.add(c.match)
248
249    # All interfaces with matched constraints have been connected.  Cull any
250    # interfaces unassigned to a substrate
251    for e in [ e for e in top.elements if isinstance(e, topdl.Computer)]:
252        if any([ not i.substrate for i in e.interface]):
253            e.interface = [ i for i in e.interface if i.substrate ]
254
255    top.incorporate_elements()
256
257def import_ns2_constraints(contents):
258    """
259    Contents is a list containing the lines of an annotated ns2 file.  This
260    routine extracts the constraint comment lines and convertes them into
261    constraints in the namespace of the tcl experiment, as well as inserting
262    them in the accepts and provides indices.
263
264    Constraints are given in lines of the form
265        name:required:provides:accepts
266    where name is the name of the node in the current topology, if the second
267    field is "required" this is a required constraint, a list of attributes
268    that this connection point provides and a list that it accepts.  The
269    provides and accepts lists are comma-separated.  The constraint is added to
270    the marks dict under the name key and that dict is returned.
271    """
272
273    const_re = re.compile("\s*#\s*COMPOSITION:\s*([^:]+:[^:]+:.*)")
274
275    marks = { }
276    for l in contents:
277        m = const_re.search(l)
278        if m:
279            exp = re.sub('\s', '', m.group(1))
280            nn, r, p, a = exp.split(":")
281            if nn.find('(') != -1:
282                # Convert array references into topdl names
283                nn = re.sub('\\((\\d)\\)', '-\\1', nn)
284            p = p.split(",")
285            a = a.split(",")
286            c = constraint(name=nn, required=(r == 'required'),
287                    provides=p, accepts=a)
288            marks[nn] = c
289    return marks
290
291def add_constraint_attributes(top, marks):
292    """
293    Take the constraints in marks and add attributes representing them to the
294    nodes in top.
295    """
296    for e in [e for e in top.elements if isinstance(e, topdl.Computer)]:
297        if e.name in marks and not e.get_attribute('composition_point'):
298            c = marks[e.name]
299            e.set_attribute('composition_point', 'true')
300            e.set_attribute('required', '%s' % c.required)
301            e.set_attribute('provides', ",".join(c.provides))
302            e.set_attribute('accepts', ",".join(c.accepts))
303
304def remove_constraint_attributes(top, marks):
305    """
306    Remove constraint attributes that match the ones in the dict from the
307    topology.
308    """
309    for e in [e for e in top.elements \
310            if isinstance(e, topdl.Computer) and e.name in marks]:
311        e.remove_attribute('composition_point')
312        e.remove_attribute('required')
313        e.remove_attribute('provides')
314        e.remove_attribute('accepts')
315
316def import_ns2_component(fn):
317    """
318    Pull a composition component in from an ns2 description.  The Constraints
319    are parsed from the comments using import_ns2_constraints and the topology
320    is created using a call to a fedd exporting the Ns2Topdl function.  A
321    topdl.Topology object rempresenting the component's topology and a dict
322    mapping constraints from the components names to the conttraints is
323    returned.  If either the file read or the conversion fails, appropriate
324    Exceptions are thrown.
325    """
326    f = open(fn, "r")
327    contents = [ l for l in f ]
328
329    marks = import_ns2_constraints(contents)
330    top = remote_ns2topdl(opts.url, "".join(contents), cert)
331    if not top:
332        raise RuntimeError("Cannot create topology from: %s" % fn)
333    add_constraint_attributes(top, marks)
334
335    return (top, marks)
336
337def import_topdl_component(fn):
338    """
339    Pull a component in from a topdl description.  The topology generation is
340    straightforward and the constraints are pulled from the attributes of
341    Computer nodes in the experiment.  The compisition_point attribute being
342    true marks a node as a composition point.  The required, provides and
343    accepts attributes map directly into those constraint fields.  The dict of
344    constraints and the topology are returned.
345    """
346    top = topdl.topology_from_xml(filename=fn, top='experiment')
347    marks = { }
348    for e in [ e for e in top.elements if isinstance(e, topdl.Computer)]:
349        if e.get_attribute('composition_point'):
350            r = e.get_attribute('required') or 'false'
351            a = e.get_attribute('accepts') 
352            p = e.get_attribute('provides')
353            if a and p:
354                c = constraint(name=e.name, required=(r == 'required'),
355                    provides=p.split(','), accepts=a.split(','))
356                marks[e.name] = c
357    return (top, marks)
358
359def index_constraints(constraints, provides, accepts):
360    """
361    Add constraints to the provides and accepts indices based on the attributes
362    of the contstraints.
363    """
364    for c in constraints:
365        for attr, dict in ((c.provides, provides), (c.accepts, accepts)):
366            for a in attr:
367                if a not in dict: dict[a] = [c]
368                else: dict[a].append(c)
369
370def get_suffix(fn):
371    """
372    We get filename suffixes a couple places.  It;s worth using the same code.
373    This gets the shortest . separated suffix from a filename, or None.
374    """
375    idx = fn.rfind('.')
376    if idx != -1: return fn[idx+1:]
377    else: return None
378
379
380def output_composition(top, constraints, outfile=None, format=None):
381    """
382    Output the composition to the file named by outfile (if any) in the format
383    given by format if any.  If both are None, output to stdout in topdl
384    """
385    def topdl_out(f, top, constraints):
386        """
387        Output into topdl.  Just call the topdl output, as the constraint
388        attributes should be in the topology.
389        """
390        print >>f, topdl.topology_to_xml(comp, top='experiment')
391
392    def ns2_out(f, top, constraints):
393        """
394        Reformat the constraint data structures into ns2 constraint comments
395        and output the ns2 using the topdl routine.
396        """
397        # Inner routines
398        # Deal with the possibility that the single string name is still in the
399        # constraint.
400        def name_format(n):
401            if isinstance(n, tuple): return n[0]
402            else: return n
403           
404           
405        # Format the required field back into a string. (ns2_out inner
406        def required_format(x):
407            if x: return "required"
408            else: return "optional"
409       
410        # ns2_out main line
411        for c in constraints:
412            print >>f, "# COMPOSITION: %s:%s:%s:%s" % (
413                    topdl.to_tcl_name(name_format(c.name)),
414                    required_format(c.required), ",".join(c.provides),
415                    ",".join(c.accepts))
416        print >>f, topdl.topology_to_ns2(top)
417
418    # Info to map from format to output routine. 
419    exporters = {
420            'xml':topdl_out, 'topdl':topdl_out,
421            'tcl': ns2_out, 'ns': ns2_out,
422            }
423
424    if format:
425        # Explicit format set, use it
426        if format in exporters:
427            exporter = exporters[format]
428        else:
429            raise RuntimeError("Unknown format %s" % format)
430    elif outfile:
431        # Determine the format from the suffix (if any)
432        s = get_suffix(outfile)
433        if s and s in exporters:
434            exporter = exporters[s]
435        else:
436            raise RuntimeError("Unknown format (suffix) %s" % outfile)
437    else:
438        # Both outfile and format are empty
439        exporter = topdl_out
440
441    # The actual output.  Open the file, if any, and call the exporter
442    if outfile: f = open(outfile, "w")
443    else: f = sys.stdout
444    exporter(f, top, constraints)
445    if outfile: f.close()
446
447def import_components(files):
448    """
449    Pull in the components.  The input is a sequence of tuples where each tuple
450    includes the name of the file to pull the component from and the number of
451    copies to make of it.  The routine to read a file is picked by its suffix.
452    On completion, a tuple containing the number of components successfully
453    read, a list of the topologies, a set of names in use accross all
454    topologies (for inserting later names), the constraints extracted, and
455    indexes mapping the attribute provices and accepted tinto the lists of
456    constraints that provide or accept them is returned.
457    """
458    importers = {
459            'tcl': import_ns2_component, 
460            'ns': import_ns2_component, 
461            'xml':import_topdl_component,
462            'topdl':import_topdl_component,
463            }
464    names = set()
465    constraints = [ ]
466    provides = { }
467    accepts = { }
468    components = 0
469    topos = [ ]
470
471    for fn, cnt in files:
472        top = None
473        try:
474            s = get_suffix(fn)
475            if s and s in importers:
476                top, marks = importers[s](fn)
477            else:
478                warn("Unknown suffix on file %s.  Ignored" % fn)
479                continue
480        except service_error, e:
481            warn("Remote error on %s: %s" % (fn, e))
482            continue
483        except EnvironmentError, e:
484            warn("Error on %s: %s" % (fn, e))
485            continue
486
487        # Parsed the component and sonctraints, now work through the rest of
488        # the pre-processing.  We do this once per copy of the component
489        # requested, cloning topologies and constraints each time.
490        for i in range(0, cnt):
491            components += 1
492            t = top.clone()
493            m = copy.deepcopy(marks)
494            index_constraints(m.values(), provides, accepts)
495            add_interfaces(t, m)
496            localize_names(t, names, m)
497            t.incorporate_elements()
498            constraints.extend(m.values())
499            topos.append(t)
500
501    return (components, topos, names, constraints, provides, accepts)
502
503# Main line begins
504parser = ComposeOptionParser()
505
506opts, args = parser.parse_args()
507
508if opts.cert:
509    cert = opts.cert
510elif os.access(os.path.expanduser("~/.ssl/emulab.pem"), os.R_OK):
511    cert = os.path.expanduser("~/.ssl/emulab.pem")
512else:
513    cert = None
514
515random.seed(opts.seed)
516
517files = opts.files
518files.extend([ (a, 1) for a in args])
519
520# Pull 'em in.
521components, topos, names, constraints, provides, accepts = \
522        import_components(files)
523
524# Let the user know if they messed up on specifying constraints.
525if any([ not isinstance(c.name, tuple) for c in constraints]):
526    warn("nodes not found for constraints on %s" % \
527            ",".join([ c.name for c in constraints \
528            if isinstance(c.name, basestring)]))
529    constraints = [ c for c in constraints if isinstance(c.name, tuple )]
530
531# If more than one component was given, actually do the composition, otherwise
532# this is probably a format conversion.
533if components > 1:
534    # Mix up the constraint indexes
535    randomize_constraint_order((provides, accepts))
536
537    # Now the various components live in the same namespace and are marked with
538    # their composition requirements.
539
540    if not meet_constraints([c for c in constraints if c.required], 
541            provides, accepts):
542        sys.exit("Could not meet all required constraints")
543
544    meet_constraints([ c for c in constraints if not c.match ], 
545            provides, accepts)
546
547    # Make a topology containing all elements and substrates from components
548    # that had matches.
549    comp = topdl.Topology()
550    for t in set([ c.name[2] for c in constraints if c.match]):
551        comp.elements.extend([e.clone() for e in t.elements])
552        comp.substrates.extend([s.clone() for s in t.substrates])
553
554    # Add substrates and connections corresponding to composition points
555    connect_composition_points(comp, constraints, names)
556
557    # Remove composition attributes for satisfied constraints
558    remove_constraint_attributes(comp, dict(
559        [(c.name[0], c) for c in constraints if c.match]))
560elif topos == 1:
561    comp = topos[0]
562else:
563    sys.exit("Did not read any components.")
564
565# Put out the composition with only the unmatched constriaints
566output_composition(comp, [c for c in constraints if not c.match], 
567        opts.outfile, opts.format)
Note: See TracBrowser for help on using the repository browser.