source: fedd/compose.py @ b0581ac

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

pretty serviceable now

  • Property mode set to 100644
File size: 9.2 KB
Line 
1#!/usr/local/bin/python
2
3import sys
4import re
5import os
6
7from optparse import OptionParser
8from federation.remote_service import service_caller
9from federation import topdl
10
11class constraint:
12    def __init__(self, name=None, required=False, accepts=None, provides=None,
13            match=None):
14        self.name = name
15        self.required = required
16        self.accepts = accepts or []
17        self.provides = provides or []
18        self.match = match
19
20    def __str__(self):
21        return "%s:%s:%s:%s" % (self.name, self.required,
22                ",".join(self.provides), ",".join(self.accepts))
23
24
25def add_to_map(exp, mark, provides, accepts):
26    """
27    Create a constraint from a string of the form
28        name:required:provides:accepts
29    where name is the name of the node in the current topology, if the second
30    field is "required" this is a required constraint, a list of attributes
31    that this connection point provides and a list that it accepts.  The
32    provides and accepts lists are comma-separated.  Note that the string
33    passed in as exp should have whitespace removed. The constraint is added to
34    the mark dict under the name key and entries in the provides and accepts
35    are made to teh constraing for each attribute that it provides or accepts.
36    """
37    nn, r, p, a = exp.split(":")
38    p = p.split(",")
39    a = a.split(",")
40    c = constraint(name=nn, required=(r == 'required'), provides=p, accepts=a)
41    mark[nn] = c
42
43    for attr, dict in ((p, provides), (a, accepts)):
44        for a in attr:
45            if a not in dict: dict[a] = [c]
46            else: dict[a].append(c)
47
48def make_new_name(names, prefix="name"):
49    """
50    Generate an identifier not present in names by appending an integer to
51    prefix.  The new name is inserted into names and returned.
52    """
53    i = 0
54    n = "%s%d" % (prefix,i)
55    while n in names:
56        i += 1
57        n = "%s%d" % (prefix,i)
58    names.add(n)
59    return n
60
61def add_interfaces(top, mark):
62    """
63    Add unconnected interfaces to the nodes whose names are keys in the mark
64    dict.  As the interface is added change the name field of the contstraint
65    in mark into a tuple containing the node name, interface name, and topology
66    in which they reside.
67    """
68    for e in top.elements:
69        if e.name in mark:
70            con = mark[e.name]
71            ii = make_new_name(set([x.name for x in e.interface]), "inf")
72            e.interface.append(
73                    topdl.Interface(substrate=[], name=ii, element=e))
74            con.name = (e.name, ii, top)
75
76def localize_names(top, names, marks):
77    """
78    Take a topology and rename any substrates or elements that share a name
79    with an existing computer or substrate.  Keep the original name as a
80    localized_name attribute.  In addition, remap any constraints or interfaces
81    that point to the old name over to the new one.  Constraints are found in
82    the marks dict, indexed by node name.  Those constraints name attributes
83    have already been converted to triples (node name, interface name,
84    topology) so only the node name needs to be changed.
85    """
86    sub_map = { }
87    for s in top.substrates:
88        s.set_attribute('localized_name', s.name)
89        if s.name in names:
90            sub_map[s.name] = n = make_new_name(names, "substrate")
91            s.name = n
92        else:
93            names.add(s.name)
94
95    for e in [ e for e in top.elements if isinstance(e, topdl.Computer)]:
96        e.set_attribute('localized_name', e.name)
97        if e.name in names:
98            nn= make_new_name(names, "computer")
99            if e.name in marks:
100                n, i, t = marks[e.name].name
101                marks[e.name].name = (nn, i, t)
102            e.name = nn
103        else:
104            names.add(e.name)
105
106        # Interface mapping.  The list comprehension creates a list of
107        # substrate names where each element in the list is replaced by the
108        # entry in sub_map indexed by it if present and left alone otherwise.
109        for i in e.interface:
110            i.substrate = [ sub_map.get(ii, ii) for ii in i.substrate ]
111
112def meet_constraints(candidates, provides, accepts):
113    """
114    Try to meet all the constraints in candidates using the information in the
115    provides and accepts dicts (which index constraints that provide or accept
116    the given attribute). A constraint is met if it can be matched with another
117    constraint that provides an attribute that the first constraint accepts.
118    Only one match per pair is allowed, and we always prefer matching a
119    required constraint to an unreqired one.  If all the candidates can be
120    matches, return True and return False otherwise.
121    """
122    got_all = True
123    for c in candidates:
124        if not c.match:
125            match = None
126            for a in c.accepts:
127                for can in provides.get(a,[]):
128                    # A constraint cannot satisfy itself, nor do we allow loops
129                    # within the same topology.  This also excludes matched
130                    # constraints.
131                    if can.name != c.name and can.name[2] != c.name[2] and \
132                            not can.match:
133                        match = can
134                        # Within providers, prefer matches against required
135                        # composition points
136                        if can.required:
137                            break
138                # Within acceptance categories, prefer matches against required
139                # composition points
140                if match and match.required:
141                    break
142
143            # Done checking all possible matches.
144            if match:
145                match.match = c
146                c.match = match
147            else:
148                got_all = False
149    return got_all
150
151def remote_ns2topdl(uri, desc, cert):
152    """
153    Call a remote service to convert the ns2 to topdl (and in fact all the way
154    to a topdl.Topology.
155    """
156
157    req = { 'description' : { 'ns2description': desc }, }
158
159    try:
160        r = service_caller('Ns2Topdl')(uri, req, cert)
161    except:
162        return None
163
164    if r.has_key('Ns2TopdlResponseBody'):
165        r = r['Ns2TopdlResponseBody']
166        ed = r.get('experimentdescription', None)
167        if 'topdldescription' in ed:
168            return topdl.Topology(**ed['topdldescription'])
169        else:
170            return None
171    else:
172        return None
173
174def connect_composition_points(top, contraints):
175    """
176    top is a topology containing copies of all the topologies represented in
177    the contsraints, flattened into one name space.  This routine inserts the
178    additional substrates required to interconnect the topology as described by
179    the constraints.  After the links are made, unused connection points are
180    purged.
181    """
182    done = set()
183    for c in constraints:
184        if c not in done and c.match:
185            # c is an unprocessed matched constraint.  Create a substrate
186            # and attach it to the interfaces named by c and c.match.
187            sn = make_new_name(names, "sub")
188            s = topdl.Substrate(name=sn)
189            connected = 0
190            # Walk through all the computers in the topology
191            for e in [ e for e in top.elements \
192                    if isinstance(e, topdl.Computer)]:
193                # if the computer matches the computer and interface name in
194                # either c or c.match, connect it.  Once both computers have
195                # been found and connected, exit the loop walking through all
196                # computers.
197                for comp, inf in (c.name[0:2], c.match.name[0:2]):
198                    if e.name == comp:
199                        for i in e.interface:
200                            if i.name == inf:
201                                i.substrate.append(sn)
202                                connected += 1
203                                break
204                        break
205                # Connected both, so add the substrate to the topology
206                if connected == 2: 
207                    top.substrates.append(s)
208                    break
209            # c and c.match have been processed, so add them to the done set
210            done.add(c)
211            done.add(c.match)
212
213    # All interfaces with matched constraints have been connected.  Cull any
214    # interfaces unassigned to a substrate
215    for e in [ e for e in top.elements if isinstance(e, topdl.Computer)]:
216        if any([ not i.substrate for i in e.interface]):
217            e.interface = [ i for i in e.interface if i.substrate ]
218
219    top.incorporate_elements()
220
221
222
223const_re = re.compile("\s*#\s*COMPOSITION:\s*([^:]+:[^:]+:.*)")
224
225parser = OptionParser()
226parser.add_option('--url', dest='url', default="http://localhost:13235",
227        help='url of ns2 to topdl service')
228parser.add_option('--certfile', dest='cert', default=None,
229        help='Certificate to use as identity')
230
231opts, args = parser.parse_args()
232
233
234if opts.cert:
235    cert = opts.cert
236elif os.access(os.path.expanduser("~/.ssl/emulab.pem"), os.R_OK):
237    cert = os.path.expanduser("~/.ssl/emulab.pem")
238else:
239    cert = None
240
241comps = [ ]
242names = set()
243constraints = [ ]
244provides = { }
245accepts = { }
246for fn in args:
247    marks = { }
248    contents = ""
249    try:
250        f = open(fn, "r")
251        for l in f:
252            contents += l
253            m = const_re.search(l)
254            if m:
255                add_to_map(re.sub('\s', '', m.group(1)), marks, provides, 
256                        accepts)
257    except EnvironmentError, e:
258        print >>sys.stderr, "Error on %s: %s" % (fn, e)
259        continue
260
261    top = remote_ns2topdl(opts.url, contents, cert)
262    add_interfaces(top, marks)
263    localize_names(top, names, marks)
264    top.incorporate_elements()
265    constraints.extend(marks.values())
266    comps.append(top)
267
268# Now the various components live in the same namespace and are marked with
269# their composition requirements.
270
271if not meet_constraints([c for c in constraints if c.required], 
272        provides, accepts):
273    sys.exit("Could not meet all required constraints")
274meet_constraints([ c for c in constraints if not c.match ], provides, accepts)
275
276# Make a topology containing all elements and substrates from components that
277# had matches.
278comp = topdl.Topology()
279for t in set([ c.name[2] for c in constraints if c.match]):
280    comp.elements.extend([e.clone() for e in t.elements])
281    comp.substrates.extend([s.clone() for s in t.substrates])
282
283# Add substrates and connections corresponding to composition points
284connect_composition_points(comp, constraints)
285
286print topdl.topology_to_xml(comp, top='experiment')
Note: See TracBrowser for help on using the repository browser.