source: fedd/compose.py @ 6df4b11

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

Check both sides of the constraint match (!). Add lax option to output
solutions that do not match all required constraints.

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