source: fedd/compose.py @ 4b6909e

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

comment

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