Merge branch 'thgeneric' of ssh://git.onelab.eu/git/sfa into thgeneric
[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         ipaddr = remote.get_primary_iface().ipv4
117         return (remote.id, ipaddr, bw, my_ip, remote_ip, net)
118         
119     def add_link(self, link):
120         self.links.add(link)
121         
122     # Assumes there is at most one Link between two sites
123     def get_sitelink(self, node):
124         site1 = self.network.sites[self.site_id]
125         site2 = self.network.sites[node.site_id]
126         sl = site1.links.intersection(site2.links)
127         if len(sl):
128             return sl.pop()
129         return None
130
131     def toxml(self, xml):
132         slice = self.network.slice
133         if self.whitelist and not self.sliver:
134             if not slice or slice.id not in self.whitelist:
135                 return
136
137         with xml.node(id = self.idtag):
138             with xml.hostname:
139                 xml << self.hostname
140             with xml.bw_unallocated(units="kbps"):
141                 xml << str(int(self.bps/1000))
142             self.get_primary_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         tag = self.get_tag('egre_key')
158         if not tag:
159             try:
160                 key = self.network.free_egre_key()
161             except:
162                 raise InvalidRSpec("ran out of EGRE keys!")
163             tag = self.update_tag('egre_key', key, None, 'admin')
164         return
165             
166     def turn_on_netns(self):
167         tag = self.get_tag('netns')
168         if (not tag) or (tag.value != '1'):
169             tag = self.update_tag('netns', '1', None, 'admin')
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                     newcaps = tag.value
185                     break
186             else:
187                 newcaps = "CAP_NET_ADMIN," + tag.value
188             self.update_tag('capabilities', newcaps, None, 'admin')
189         else:
190             tag = self.add_tag('capabilities', 'CAP_NET_ADMIN', None, 'admin')
191         return
192     
193     def remove_cap_net_admin(self):
194         tag = self.get_tag('capabilities')
195         if tag:
196             caps = tag.value.split(',')
197             newcaps = []
198             for cap in caps:
199                 if cap != "CAP_NET_ADMIN":
200                     newcaps.append(cap)
201             if newcaps:
202                 value = ','.join(newcaps)
203                 self.update_tag('capabilities', value, None, 'admin')
204             else:
205                 tag.delete()
206         return
207
208 class Link:
209     def __init__(self, end1, end2, bps = 1000 * 1000000, parent = None):
210         self.end1 = end1
211         self.end2 = end2
212         self.bps = bps
213         self.parent = parent
214         self.children = []
215
216         end1.add_link(self)
217         end2.add_link(self)
218         
219         if self.parent:
220             self.parent.children.append(self)
221             
222     def toxml(self, xml):
223         end_ids = "%s %s" % (self.end1.idtag, self.end2.idtag)
224
225         if self.parent:
226             with  xml.vlink(endpoints=end_ids):
227                 with xml.description:
228                     xml << "%s -- %s" % (self.end1.name, self.end2.name)
229                 with xml.kbps:
230                     xml << str(int(self.bps/1000))
231         else:
232             with xml.link(endpoints=end_ids):
233                 with xml.description:
234                     xml << "%s -- %s" % (self.end1.name, self.end2.name)
235                 with xml.bw_unallocated(units="kbps"):
236                     xml << str(int(self.bps/1000))
237                 for child in self.children:
238                     child.toxml(xml)
239         
240
241
242 class ViniNetwork(Network):
243     def __init__(self, api, type = "SFA"):
244         Network.__init__(self, api, type)
245         self.sitelinks = []
246         self.nodelinks = []
247     
248         for (s1, s2) in PhysicalLinks:
249             self.sitelinks.append(Link(self.sites[s1], self.sites[s2]))
250         
251         for t in self.tags:
252             tag = self.tags[t]
253             if tag.tagname == 'topo_rspec':
254                 node1 = self.nodes[tag.node_id]
255                 l = eval(tag.value)
256                 for (id, realip, bw, lvip, rvip, vnet) in l:
257                     allocbps = get_tc_rate(bw)
258                     node1.bps -= allocbps
259                     try:
260                         node2 = self.nodes[id]
261                         if node1.id < node2.id:
262                             sl = node1.get_sitelink(node2)
263                             sl.bps -= allocbps
264                     except:
265                         pass
266
267     def lookupSiteLink(self, node1, node2):
268         site1 = self.sites[node1.site_id]
269         site2 = self.sites[node2.site_id]
270         for link in self.sitelinks:
271             if site1 == link.end1 and site2 == link.end2:
272                 return link
273             if site2 == link.end1 and site1 == link.end2:
274                 return link
275         return None
276     
277
278     """
279     Check the requested topology against the available topology and capacity
280     """
281     def verifyTopology(self):
282         for link in self.nodelinks:
283             if link.bps <= 0:
284                 raise InvalidRSpec("must request positive bandwidth")
285                 
286             n1 = link.end1
287             n2 = link.end2
288             sitelink = self.lookupSiteLink(n1, n2)
289             if not sitelink:
290                 raise InvalidRSpec("nodes %s and %s are not adjacent" % 
291                                    (n1.idtag, n2.idtag))
292             if sitelink.bps < link.bps:
293                 raise InvalidRSpec("not enough capacity between %s and %s" % 
294                                    (n1.idtag, n2.idtag))
295                 
296     def __add_vlink(self, vlink, parent = None):
297         n1 = n2 = None
298         endpoints = vlink.get("endpoints")
299         if endpoints:
300             (end1, end2) = endpoints.split()
301             n1 = self.lookupNode(end1)
302             n2 = self.lookupNode(end2)
303         elif parent:
304             """ Try to infer the endpoints for the virtual link """
305             site_endpoints = parent.get("endpoints")
306             (n1, n2) = self.__infer_endpoints(site_endpoints)
307         else:
308             raise InvalidRSpec("no endpoints given")
309
310         #print "Added virtual link: %s -- %s" % (n1.tag, n2.tag)
311         bps = int(vlink.findtext("kbps")) * 1000
312         sitelink = self.lookupSiteLink(n1, n2)
313         if not sitelink:
314             raise InvalidRSpec("nodes %s and %s are not adjacent" % 
315                                   (n1.idtag, n2.idtag))
316         self.nodelinks.append(Link(n1, n2, bps, sitelink))
317         return
318
319     """ 
320     Infer the endpoints of the virtual link.  If the slice exists on 
321     only a single node at each end of the physical link, we'll assume that
322     the user wants the virtual link to terminate at these nodes.
323     """
324     def __infer_endpoints(self, endpoints):
325         n = []
326         ends = endpoints.split()
327         for end in ends:
328             found = 0
329             site = self.lookupSite(end)
330             for id in site.node_ids:
331                 if id in self.nodedict:
332                     n.append(self.nodedict[id])
333                     found += 1
334             if found != 1:
335                 raise InvalidRSpec("could not infer endpoint for site %s" % 
336                                    site.idtag)
337         #print "Inferred endpoints: %s %s" % (n[0].idtag, n[1].idtag)
338         return n
339         
340     def addRSpec(self, xml, schema = None):
341         Network.addRSpec(self, xml, schema)
342         self.nodedict = {}
343         for node in self.nodesWithSlivers():
344             self.nodedict[node.id] = node
345         
346         # Find vlinks under link elements
347         for vlink in self.rspec.iterfind("./network/link/vlink"):
348             link = vlink.getparent()
349             self.__add_vlink(vlink, link)
350
351         # Find vlinks that specify endpoints
352         for vlink in self.rspec.iterfind("./request/vlink[@endpoints]"):
353             self.__add_vlink(vlink)
354
355
356     def addSlice(self):
357         Network.addSlice(self)
358
359         for node in self.slice.get_nodes():
360             linktag = self.slice.get_tag('topo_rspec', node)
361             if linktag:
362                 l = eval(linktag.value)
363                 for (id, realip, bw, lvip, rvip, vnet) in l:
364                     if node.id < id:
365                         bps = get_tc_rate(bw)
366                         remote = self.lookupNode(id)
367                         sitelink = self.lookupSiteLink(node, remote)
368                         self.nodelinks.append(Link(node,remote,bps,sitelink))
369
370
371     def updateSliceTags(self):
372         slice = self.slice
373
374         tag = slice.update_tag('vini_topo', 'manual', None, 'admin')
375         slice.assign_egre_key()
376         slice.turn_on_netns()
377         slice.add_cap_net_admin()
378
379         for node in self.nodesWithSlivers():
380             linkdesc = []
381             for link in node.links:
382                 linkdesc.append(node.get_topo_rspec(link))
383             if linkdesc:
384                 topo_str = "%s" % linkdesc
385                 tag = slice.update_tag('topo_rspec', topo_str, node, 'admin')
386
387         # Expire the un-updated topo_rspec tags
388         for tag in self.getSliceTags():
389             if tag.tagname in ['topo_rspec']:
390                 if not tag.was_updated():
391                     tag.delete()
392
393         Network.updateSliceTags(self)
394
395     """
396     Find a free EGRE key
397     """
398     def free_egre_key(self):
399         used = set()
400         for tag in self.getSliceTags():
401             if tag.tagname == 'egre_key':
402                 used.add(int(tag.value))
403                 
404         for i in range(1, 256):
405             if i not in used:
406                 key = i
407                 break
408         else:
409             raise KeyError("No more EGRE keys available")
410         
411         return str(key)
412
413     """
414     Produce XML directly from the topology specification.
415     """
416     def toxml(self):
417         xml = XMLBuilder(format = True, tab_step = "  ")
418         with xml.RSpec(type=self.type):
419             if self.slice:
420                 element = xml.network(name=self.api.hrn, slice=self.slice.hrn)
421             else:
422                 element = xml.network(name=self.api.hrn)
423
424             with element:
425                 if self.slice:
426                     self.slice.toxml(xml)
427                 for site in self.getSites():
428                     site.toxml(xml)
429                 for link in self.sitelinks:
430                     link.toxml(xml)
431
432         header = '<?xml version="1.0"?>\n'
433         return header + str(xml)
434
435     """
436     Create a dictionary of ViniSite objects keyed by site ID
437     """
438     def get_sites(self, api):
439         tmp = []
440         for site in api.driver.GetSites({'peer_id': None}):
441             t = site['site_id'], ViniSite(self, site)
442             tmp.append(t)
443         return dict(tmp)
444
445
446     """
447     Create a dictionary of ViniNode objects keyed by node ID
448     """
449     def get_nodes(self, api):
450         tmp = []
451         for node in api.driver.GetNodes({'peer_id': None}):
452             t = node['node_id'], ViniNode(self, node)
453             tmp.append(t)
454         return dict(tmp)
455
456     """
457     Return a ViniSlice object for a single slice
458     """
459     def get_slice(self, api, hrn):
460         slicename = hrn_to_pl_slicename(hrn)
461         slice = api.driver.GetSlices([slicename])
462         if slice:
463             self.slice = ViniSlice(self, slicename, slice[0])
464             return self.slice
465         else:
466             return None
467
468
469