source: fedd/compose.py @ da5b93c

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

checkpoint

  • Property mode set to 100644
File size: 9.6 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# Main line begins
223
224const_re = re.compile("\s*#\s*COMPOSITION:\s*([^:]+:[^:]+:.*)")
225
226parser = OptionParser()
227parser.add_option('--url', dest='url', default="http://localhost:13235",
228        help='url of ns2 to topdl service')
229parser.add_option('--certfile', dest='cert', default=None,
230        help='Certificate to use as identity')
231
232opts, args = parser.parse_args()
233
234
235if opts.cert:
236    cert = opts.cert
237elif os.access(os.path.expanduser("~/.ssl/emulab.pem"), os.R_OK):
238    cert = os.path.expanduser("~/.ssl/emulab.pem")
239else:
240    cert = None
241
242comps = [ ]
243names = set()
244constraints = [ ]
245provides = { }
246accepts = { }
247for fn in args:
248    marks = { }
249    contents = ""
250    try:
251        f = open(fn, "r")
252        for l in f:
253            contents += l
254            m = const_re.search(l)
255            if m:
256                add_to_map(re.sub('\s', '', m.group(1)), marks, provides, 
257                        accepts)
258    except EnvironmentError, e:
259        print >>sys.stderr, "Error on %s: %s" % (fn, e)
260        continue
261
262    top = remote_ns2topdl(opts.url, contents, cert)
263    if not top:
264        sys.exit("Cannot create topology from: %s" % fn)
265    add_interfaces(top, marks)
266    localize_names(top, names, marks)
267    top.incorporate_elements()
268    constraints.extend(marks.values())
269    comps.append(top)
270
271# Let the user know if they messed up on specifying constraints.
272if any([ not isinstance(c.name, tuple) for c in constraints]):
273    print >>sys.stderr, "nodes not found for constraints on %s" % \
274            ",".join([ c.name for c in constraints \
275            if isinstance(c.name, basestring)])
276    constraints = [ c for c in constraints if isinstance(c.name, tuple )]
277
278# Now the various components live in the same namespace and are marked with
279# their composition requirements.
280
281if not meet_constraints([c for c in constraints if c.required], 
282        provides, accepts):
283    sys.exit("Could not meet all required constraints")
284meet_constraints([ c for c in constraints if not c.match ], provides, accepts)
285
286# Make a topology containing all elements and substrates from components that
287# had matches.
288comp = topdl.Topology()
289for t in set([ c.name[2] for c in constraints if c.match]):
290    comp.elements.extend([e.clone() for e in t.elements])
291    comp.substrates.extend([s.clone() for s in t.substrates])
292
293# Add substrates and connections corresponding to composition points
294connect_composition_points(comp, constraints)
295
296print topdl.topology_to_xml(comp, top='experiment')
Note: See TracBrowser for help on using the repository browser.