Combine PlanetLab and VINI RSpec handling code
[sfa.git] / sfa / managers / vini / vini_network.py
1 from __future__ import with_statement
2 from sfa.util.faults import *
3 from xmlbuilder import XMLBuilder
4 from lxml import etree
5 import sys
6 from sfa.plc.network import *
7 from sfa.managers.vini.topology import PhysicalLinks
8
9 # Taken from bwlimit.py
10 #
11 # See tc_util.c and http://physics.nist.gov/cuu/Units/binary.html. Be
12 # warned that older versions of tc interpret "kbps", "mbps", "mbit",
13 # and "kbit" to mean (in this system) "kibps", "mibps", "mibit", and
14 # "kibit" and that if an older version is installed, all rates will
15 # be off by a small fraction.
16 suffixes = {
17     "":         1,
18     "bit":      1,
19     "kibit":    1024,
20     "kbit":     1000,
21     "mibit":    1024*1024,
22     "mbit":     1000000,
23     "gibit":    1024*1024*1024,
24     "gbit":     1000000000,
25     "tibit":    1024*1024*1024*1024,
26     "tbit":     1000000000000,
27     "bps":      8,
28     "kibps":    8*1024,
29     "kbps":     8000,
30     "mibps":    8*1024*1024,
31     "mbps":     8000000,
32     "gibps":    8*1024*1024*1024,
33     "gbps":     8000000000,
34     "tibps":    8*1024*1024*1024*1024,
35     "tbps":     8000000000000
36 }
37
38
39 def get_tc_rate(s):
40     """
41     Parses an integer or a tc rate string (e.g., 1.5mbit) into bits/second
42     """
43
44     if type(s) == int:
45         return s
46     m = re.match(r"([0-9.]+)(\D*)", s)
47     if m is None:
48         return -1
49     suffix = m.group(2).lower()
50     if suffixes.has_key(suffix):
51         return int(float(m.group(1)) * suffixes[suffix])
52     else:
53         return -1
54
55 def format_tc_rate(rate):
56     """
57     Formats a bits/second rate into a tc rate string
58     """
59
60     if rate >= 1000000000 and (rate % 1000000000) == 0:
61         return "%.0fgbit" % (rate / 1000000000.)
62     elif rate >= 1000000 and (rate % 1000000) == 0:
63         return "%.0fmbit" % (rate / 1000000.)
64     elif rate >= 1000:
65         return "%.0fkbit" % (rate / 1000.)
66     else:
67         return "%.0fbit" % rate
68
69
70 class ViniNode(Node):
71     def __init__(self, network, node, bps = 1000 * 1000000):
72         Node.__init__(self, network, node)
73         self.bps = bps
74         self.links = set()
75         self.name = self.hostname.replace('.vini-veritas.net', '')
76
77     def get_link_id(self, remote):
78         if self.id < remote.id:
79             link = (self.id<<7) + remote.id
80         else:
81             link = (remote.id<<7) + self.id
82         return link
83         
84     def get_iface_id(self, remote):
85         if self.id < remote.id:
86             iface = 1
87         else:
88             iface = 2
89         return iface
90     
91     def get_virt_ip(self, remote):
92         link = self.get_link_id(remote)
93         iface = self.get_iface_id(remote)
94         first = link >> 6
95         second = ((link & 0x3f)<<2) + iface
96         return "192.168.%d.%d" % (first, second)
97
98     def get_virt_net(self, remote):
99         link = self.get_link_id(remote)
100         first = link >> 6
101         second = (link & 0x3f)<<2
102         return "192.168.%d.%d/30" % (first, second)
103         
104     def get_topo_rspec(self, link):
105         if link.end1 == self:
106             remote = link.end2
107         elif link.end2 == self:
108             remote = link.end1
109         else:
110             raise Error("Link does not connect to Node")
111             
112         my_ip = self.get_virt_ip(remote)
113         remote_ip = remote.get_virt_ip(self)
114         net = self.get_virt_net(remote)
115         bw = format_tc_rate(link.bps)
116         return (remote.id, remote.ipaddr, bw, my_ip, remote_ip, net)
117         
118     def add_link(self, link):
119         self.links.add(link)
120         
121     # Assumes there is at most one Link between two sites
122     def get_sitelink(self, node):
123         site1 = self.network.sites[self.site_id]
124         site2 = self.network.sites[node.site_id]
125         sl = site1.links.intersection(site2.links)
126         if len(sl):
127             return sl.pop()
128         return None
129
130     def toxml(self, xml):
131         slice = self.network.slice
132         if self.whitelist and not self.sliver:
133             if not slice or slice.id not in self.whitelist:
134                 return
135
136         with xml.node(id = self.idtag):
137             with xml.hostname:
138                 xml << self.hostname
139             with xml.bw_unallocated(units="kbps"):
140                 xml << str(int(self.bps/1000))
141             for iface in self.get_ifaces():
142                 iface.toxml(xml)
143             if self.sliver:
144                 self.sliver.toxml(xml)
145
146
147 class ViniSite(Site):
148     def __init__(self, network, site):
149         Site.__init__(self, network, site)
150         self.links = set()
151
152     def add_link(self, link):
153         self.links.add(link)
154
155 class ViniSlice(Slice):
156     def assign_egre_key(self):
157         if not self.get_tag('egre_key'):
158             try:
159                 key = free_egre_key()
160                 self.update_tag('egre_key', key)
161             except:
162                 # Should handle this case...
163                 pass
164         return
165             
166     def turn_on_netns(self):
167         tag = self.get_tag('netns')
168         if (not tag) or (tag.value != '1'):
169             self.update_tag('netns', '1')
170         return
171    
172     def turn_off_netns(self):
173         tag = self.get_tag('netns')
174         if tag and (tag.value != '0'):
175             tag.delete()
176         return
177     
178     def add_cap_net_admin(self):
179         tag = self.get_tag('capabilities')
180         if tag:
181             caps = tag.value.split(',')
182             for cap in caps:
183                 if cap == "CAP_NET_ADMIN":
184                     return
185             else:
186                 newcaps = "CAP_NET_ADMIN," + tag.value
187                 self.update_tag('capabilities', newcaps)
188         else:
189             self.add_tag('capabilities', 'CAP_NET_ADMIN')
190         return
191     
192     def remove_cap_net_admin(self):
193         tag = self.get_tag('capabilities')
194         if tag:
195             caps = tag.value.split(',')
196             newcaps = []
197             for cap in caps:
198                 if cap != "CAP_NET_ADMIN":
199                     newcaps.append(cap)
200             if newcaps:
201                 value = ','.join(newcaps)
202                 self.update_tag('capabilities', value)
203             else:
204                 tag.delete()
205         return
206
207     # Update the vsys/setup-link and vsys/setup-nat slice tags.
208     def add_vsys_tags(self):
209         link = nat = False
210         for tag in self.network.getSliceTags():
211             if tag.tagname == 'vsys':
212                 if tag.value == 'setup-link':
213                     link = True
214                 elif tag.value == 'setup-nat':
215                     nat = True
216         if not link:
217             self.add_tag('vsys', 'setup-link')
218         if not nat:
219             self.add_tag('vsys', 'setup-nat')
220         return
221
222
223
224 class Link:
225     def __init__(self, end1, end2, bps = 1000 * 1000000, parent = None):
226         self.end1 = end1
227         self.end2 = end2
228         self.bps = bps
229         self.parent = parent
230         self.children = []
231
232         end1.add_link(self)
233         end2.add_link(self)
234         
235         if self.parent:
236             self.parent.children.append(self)
237             
238     def toxml(self, xml):
239         end_ids = "%s %s" % (self.end1.idtag, self.end2.idtag)
240
241         if self.parent:
242             with  xml.vlink(endpoints=end_ids):
243                 with xml.description:
244                     xml << "%s -- %s" % (self.end1.name, self.end2.name)
245                 with xml.bw(units="kbps"):
246                     xml << str(int(self.bps/1000))
247         else:
248             with xml.link(endpoints=end_ids):
249                 with xml.description:
250                     xml << "%s -- %s" % (self.end1.name, self.end2.name)
251                 with xml.bw_unallocated(units="kbps"):
252                     xml << str(int(self.bps/1000))
253                 for child in self.children:
254                     child.toxml(xml)
255         
256
257
258 class ViniNetwork(Network):
259     def __init__(self, api, type = "VINI"):
260         Network.__init__(self, api, type)
261         self.sitelinks = []
262         self.nodelinks = []
263     
264         for (s1, s2) in PhysicalLinks:
265             self.sitelinks.append(Link(self.sites[s1], self.sites[s2]))
266         
267         for t in self.tags:
268             tag = self.tags[t]
269             if tag.tagname == 'topo_rspec':
270                 node1 = self.nodes[tag.node_id]
271                 l = eval(tag.value)
272                 for (id, realip, bw, lvip, rvip, vnet) in l:
273                     allocbps = get_tc_rate(bw)
274                     node1.bps -= allocbps
275                     try:
276                         node2 = self.nodes[id]
277                         if node1.id < node2.id:
278                             sl = node1.get_sitelink(node2)
279                             sl.bps -= allocbps
280                     except:
281                         pass
282
283     def lookupSiteLink(self, node1, node2):
284         site1 = self.sites[node1.site_id]
285         site2 = self.sites[node2.site_id]
286         for link in self.sitelinks:
287             if site1 == link.end1 and site2 == link.end2:
288                 return link
289             if site2 == link.end1 and site1 == link.end2:
290                 return link
291         return None
292     
293
294     """
295     Check the requested topology against the available topology and capacity
296     """
297     def verifyTopology(self):
298         for link in self.nodelinks:
299             if link.bps <= 0:
300                 raise InvalidRSpec("must request positive bandwidth")
301                 
302             n1 = link.end1
303             n2 = link.end2
304             sitelink = self.lookupSiteLink(n1, n2)
305             if not sitelink:
306                 raise InvalidRSpec("nodes %s and %s are not adjacent" % 
307                                    (n1.idtag, n2.idtag))
308             if sitelink.bps < link.bps:
309                 raise InvalidRSpec("not enough capacity between %s and %s" % 
310                                    (n1.idtag, n2.idtag))
311                 
312     def __add_vlink(self, vlink, parent = None):
313         n1 = n2 = None
314         endpoints = vlink.get("endpoints")
315         if endpoints:
316             (end1, end2) = endpoints.split()
317             n1 = self.lookupNode(end1)
318             n2 = self.lookupNode(end2)
319         elif parent:
320             """ Try to infer the endpoints for the virtual link """
321             site_endpoints = parent.get("endpoints")
322             (n1, n2) = self.__infer_endpoints(site_endpoints)
323         else:
324             raise InvalidRSpec("no endpoints given")
325
326         #print "Added virtual link: %s -- %s" % (n1.tag, n2.tag)
327         bps = int(vlink.findtext("kbps")) * 1000
328         sitelink = self.lookupSiteLink(n1, n2)
329         if not sitelink:
330             raise InvalidRSpec("nodes %s and %s are not adjacent" % 
331                                   (n1.idtag, n2.idtag))
332         self.nodelinks.append(Link(n1, n2, bps, sitelink))
333         return
334
335     """ 
336     Infer the endpoints of the virtual link.  If the slice exists on 
337     only a single node at each end of the physical link, we'll assume that
338     the user wants the virtual link to terminate at these nodes.
339     """
340     def __infer_endpoints(self, endpoints):
341         n = []
342         ends = endpoints.split()
343         for end in ends:
344             found = 0
345             site = self.lookupSite(end)
346             for id in site.node_ids:
347                 if id in self.nodedict:
348                     n.append(self.nodedict[id])
349                     found += 1
350             if found != 1:
351                 raise InvalidRSpec("could not infer endpoint for site %s" % 
352                                    site.idtag)
353         #print "Inferred endpoints: %s %s" % (n[0].idtag, n[1].idtag)
354         return n
355         
356     def addRSpec(self, xml, schema = None):
357         Network.addRSpec(self, xml, schema)
358         self.nodedict = {}
359         for node in self.nodesWithSlivers():
360             self.nodedict[node.id] = node
361         
362         # Find vlinks under link elements
363         for vlink in self.rspec.iterfind("./network/link/vlink"):
364             link = vlink.getparent()
365             self.__add_vlink(vlink, slicenodes, link)
366
367         # Find vlinks that specify endpoints
368         for vlink in rspec.iterfind("./request/vlink[@endpoints]"):
369             self.__add_vlink(vlink, slicenodes)
370
371
372     def addSlice(self):
373         Network.addSlice(self)
374
375         for node in self.slice.get_nodes():
376             linktag = self.slice.get_tag('topo_rspec', node)
377             if linktag:
378                 l = eval(linktag.value)
379                 for (id, realip, bw, lvip, rvip, vnet) in l:
380                     if node.id < id:
381                         bps = get_tc_rate(bw)
382                         remote = self.lookupNode(id)
383                         sitelink = self.lookupSiteLink(node, remote)
384                         self.nodelinks.append(Link(node,remote,bps,sitelink))
385
386
387     def updateSliceTags(self):
388         slice = self.slice
389
390         slice.update_tag('vini_topo', 'manual')
391         slice.assign_egre_key()
392         slice.turn_on_netns()
393         slice.add_cap_net_admin()
394
395         for node in slice.get_nodes():
396             linkdesc = []
397             for link in node.links:
398                 linkdesc.append(node.get_topo_rspec(link))
399             if linkdesc:
400                 topo_str = "%s" % linkdesc
401                 slice.update_tag('topo_rspec', topo_str, node)
402
403         Network.updateSliceTags(self)
404
405     """
406     Produce XML directly from the topology specification.
407     """
408     def toxml(self):
409         xml = XMLBuilder(format = True, tab_step = "  ")
410         with xml.RSpec(type=self.type):
411             name = "Public_" + self.type
412             if self.slice:
413                 element = xml.network(name=name, slice=self.slice.hrn)
414             else:
415                 element = xml.network(name=name)
416
417             with element:
418                 if self.slice:
419                     self.slice.toxml(xml)
420                 for site in self.getSites():
421                     site.toxml(xml)
422                 for link in self.sitelinks:
423                     link.toxml(xml)
424
425         header = '<?xml version="1.0"?>\n'
426         return header + str(xml)
427
428     """
429     Create a dictionary of ViniSite objects keyed by site ID
430     """
431     def get_sites(self, api):
432         tmp = []
433         for site in api.plshell.GetSites(api.plauth, {'peer_id': None}):
434             t = site['site_id'], ViniSite(self, site)
435             tmp.append(t)
436         return dict(tmp)
437
438
439     """
440     Create a dictionary of ViniNode objects keyed by node ID
441     """
442     def get_nodes(self, api):
443         tmp = []
444         for node in api.plshell.GetNodes(api.plauth, {'peer_id': None}):
445             t = node['node_id'], ViniNode(self, node)
446             tmp.append(t)
447         return dict(tmp)
448
449     """
450     Return a ViniSlice object for a single slice
451     """
452     def get_slice(self, api, hrn):
453         slicename = hrn_to_pl_slicename(hrn)
454         slice = api.plshell.GetSlices(api.plauth, [slicename])
455         if slice:
456             self.slice = ViniSlice(self, slicename, slice[0])
457             return self.slice
458         else:
459             return None
460
461
462