source: fedd/compose.py @ 89e2138

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

Remove the phantom interfaces as well. It was sort of dumb to add them and
then remove them. Along the way I also cleaned up constraint names. They no
longer morph from string to tuple as they're localized. I added a topology
pointer.

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