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