New RSpec format
[sfa.git] / sfa / rspecs / aggregates / vini / utils.py
1 from __future__ import with_statement
2 import re
3 import socket
4 from sfa.util.faults import *
5 from sfa.rspecs.aggregates.vini.topology import *
6 from xmlbuilder import XMLBuilder
7 import sys
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 Node:
71     def __init__(self, node, bps = 1000 * 1000000):
72         self.id = node['node_id']
73         self.idtag = "n%s" % self.id
74         self.hostname = node['hostname']
75         self.name = self.shortname = self.hostname.replace('.vini-veritas.net', '')
76         self.site_id = node['site_id']
77         self.ipaddr = socket.gethostbyname(self.hostname)
78         self.bps = bps
79         self.links = set()
80         self.sliver = False
81
82     def get_link_id(self, remote):
83         if self.id < remote.id:
84             link = (self.id<<7) + remote.id
85         else:
86             link = (remote.id<<7) + self.id
87         return link
88         
89     def get_iface_id(self, remote):
90         if self.id < remote.id:
91             iface = 1
92         else:
93             iface = 2
94         return iface
95     
96     def get_virt_ip(self, remote):
97         link = self.get_link_id(remote)
98         iface = self.get_iface_id(remote)
99         first = link >> 6
100         second = ((link & 0x3f)<<2) + iface
101         return "192.168.%d.%d" % (first, second)
102
103     def get_virt_net(self, remote):
104         link = self.get_link_id(remote)
105         first = link >> 6
106         second = (link & 0x3f)<<2
107         return "192.168.%d.%d/30" % (first, second)
108         
109     def get_site(self, sites):
110         return sites[self.site_id]
111     
112     def get_topo_rspec(self, link):
113         if link.end1 == self:
114             remote = link.end2
115         elif link.end2 == self:
116             remote = link.end1
117         else:
118             raise Error("Link does not connect to Node")
119             
120         my_ip = self.get_virt_ip(remote)
121         remote_ip = remote.get_virt_ip(self)
122         net = self.get_virt_net(remote)
123         bw = format_tc_rate(link.bps)
124         return (remote.id, remote.ipaddr, bw, my_ip, remote_ip, net)
125         
126     def add_link(self, link):
127         self.links.add(link)
128         
129     def add_tag(self, sites):
130         s = self.get_site(sites)
131         words = self.hostname.split(".")
132         index = words[0].replace("node", "")
133         if index.isdigit():
134             self.tag = s.tag + index
135         else:
136             self.tag = None
137
138     # Assumes there is at most one Link between two sites
139     def get_sitelink(self, node, sites):
140         site1 = sites[self.site_id]
141         site2 = sites[node.site_id]
142         sl = site1.links.intersection(site2.links)
143         if len(sl):
144             return sl.pop()
145         return None
146
147     def add_sliver(self):
148         self.sliver = True
149
150     def toxml(self, xml, hrn):
151         if not self.tag:
152             return
153         with xml.node(id = self.idtag):
154             with xml.hostname:
155                 xml << self.hostname
156             with xml.kbps:
157                 xml << str(int(self.bps/1000))
158             if self.sliver:
159                 with xml.sliver:
160                     pass
161     
162
163 class Link:
164     def __init__(self, end1, end2, bps = 1000 * 1000000, parent = None):
165         self.end1 = end1
166         self.end2 = end2
167         self.bps = bps
168         self.parent = parent
169         self.children = []
170
171         end1.add_link(self)
172         end2.add_link(self)
173         
174         if self.parent:
175             self.parent.children.append(self)
176             
177     def toxml(self, xml):
178         end_ids = "%s %s" % (self.end1.idtag, self.end2.idtag)
179
180         if self.parent:
181             element = xml.vlink(endpoints=end_ids)
182         else:
183             element = xml.link(endpoints=end_ids)
184
185         with element:
186             with xml.description:
187                 xml << "%s -- %s" % (self.end1.name, self.end2.name)
188             with xml.kbps:
189                 xml << str(int(self.bps/1000))
190             for child in self.children:
191                 child.toxml(xml)
192         
193
194 class Site:
195     def __init__(self, site):
196         self.id = site['site_id']
197         self.idtag = "s%s" % self.id
198         self.node_ids = site['node_ids']
199         self.name = site['abbreviated_name'].replace(" ", "_")
200         self.tag = site['login_base']
201         self.public = site['is_public']
202         self.enabled = site['enabled']
203         self.links = set()
204
205     def get_sitenodes(self, nodes):
206         n = []
207         for i in self.node_ids:
208             n.append(nodes[i])
209         return n
210     
211     def add_link(self, link):
212         self.links.add(link)
213
214     def toxml(self, xml, hrn, nodes):
215         if not (self.public and self.enabled and self.node_ids):
216             return
217         with xml.site(id = self.idtag):
218             with xml.name:
219                 xml << self.name
220                 
221             for node in self.get_sitenodes(nodes):
222                 node.toxml(xml, hrn)
223    
224     
225 class Slice:
226     def __init__(self, slice):
227         self.id = slice['slice_id']
228         self.name = slice['name']
229         self.node_ids = set(slice['node_ids'])
230         self.slice_tag_ids = slice['slice_tag_ids']
231     
232     def get_tag(self, tagname, slicetags, node = None):
233         for i in self.slice_tag_ids:
234             tag = slicetags[i]
235             if tag.tagname == tagname:
236                 if (not node) or (node.id == tag.node_id):
237                     return tag
238         else:
239             return None
240         
241     def get_nodes(self, nodes):
242         n = []
243         for id in self.node_ids:
244             n.append(nodes[id])
245         return n
246              
247     
248     # Add a new slice tag   
249     def add_tag(self, tagname, value, slicetags, node = None):
250         record = {'slice_tag_id':None, 'slice_id':self.id, 'tagname':tagname, 'value':value}
251         if node:
252             record['node_id'] = node.id
253         else:
254             record['node_id'] = None
255         tag = Slicetag(record)
256         slicetags[tag.id] = tag
257         self.slice_tag_ids.append(tag.id)
258         tag.changed = True       
259         tag.updated = True
260         return tag
261     
262     # Update a slice tag if it exists, else add it             
263     def update_tag(self, tagname, value, slicetags, node = None):
264         tag = self.get_tag(tagname, slicetags, node)
265         if tag and tag.value == value:
266             value = "no change"
267         elif tag:
268             tag.value = value
269             tag.changed = True
270         else:
271             tag = self.add_tag(tagname, value, slicetags, node)
272         tag.updated = True
273             
274     def assign_egre_key(self, slicetags):
275         if not self.get_tag('egre_key', slicetags):
276             try:
277                 key = free_egre_key(slicetags)
278                 self.update_tag('egre_key', key, slicetags)
279             except:
280                 # Should handle this case...
281                 pass
282         return
283             
284     def turn_on_netns(self, slicetags):
285         tag = self.get_tag('netns', slicetags)
286         if (not tag) or (tag.value != '1'):
287             self.update_tag('netns', '1', slicetags)
288         return
289    
290     def turn_off_netns(self, slicetags):
291         tag = self.get_tag('netns', slicetags)
292         if tag and (tag.value != '0'):
293             tag.delete()
294         return
295     
296     def add_cap_net_admin(self, slicetags):
297         tag = self.get_tag('capabilities', slicetags)
298         if tag:
299             caps = tag.value.split(',')
300             for cap in caps:
301                 if cap == "CAP_NET_ADMIN":
302                     return
303             else:
304                 newcaps = "CAP_NET_ADMIN," + tag.value
305                 self.update_tag('capabilities', newcaps, slicetags)
306         else:
307             self.add_tag('capabilities', 'CAP_NET_ADMIN', slicetags)
308         return
309     
310     def remove_cap_net_admin(self, slicetags):
311         tag = self.get_tag('capabilities', slicetags)
312         if tag:
313             caps = tag.value.split(',')
314             newcaps = []
315             for cap in caps:
316                 if cap != "CAP_NET_ADMIN":
317                     newcaps.append(cap)
318             if newcaps:
319                 value = ','.join(newcaps)
320                 self.update_tag('capabilities', value, slicetags)
321             else:
322                 tag.delete()
323         return
324
325     # Update the vsys/setup-link and vsys/setup-nat slice tags.
326     def add_vsys_tags(self, slicetags):
327         link = nat = False
328         for i in self.slice_tag_ids:
329             tag = slicetags[i]
330             if tag.tagname == 'vsys':
331                 if tag.value == 'setup-link':
332                     link = True
333                 elif tag.value == 'setup-nat':
334                     nat = True
335         if not link:
336             self.add_tag('vsys', 'setup-link', slicetags)
337         if not nat:
338             self.add_tag('vsys', 'setup-nat', slicetags)
339         return
340
341
342 class Slicetag:
343     newid = -1 
344     def __init__(self, tag):
345         self.id = tag['slice_tag_id']
346         if not self.id:
347             # Make one up for the time being...
348             self.id = Slicetag.newid
349             Slicetag.newid -= 1
350         self.slice_id = tag['slice_id']
351         self.tagname = tag['tagname']
352         self.value = tag['value']
353         self.node_id = tag['node_id']
354         self.updated = False
355         self.changed = False
356         self.deleted = False
357     
358     # Mark a tag as deleted
359     def delete(self):
360         self.deleted = True
361         self.updated = True
362     
363     def write(self, api):
364         if self.changed:
365             if int(self.id) > 0:
366                 api.plshell.UpdateSliceTag(api.plauth, self.id, self.value)
367             else:
368                 api.plshell.AddSliceTag(api.plauth, self.slice_id, 
369                                         self.tagname, self.value, self.node_id)
370         elif self.deleted and int(self.id) > 0:
371             api.plshell.DeleteSliceTag(api.plauth, self.id)
372
373
374 """
375 A topology is a compound object consisting of:
376 * a dictionary mapping site IDs to Site objects
377 * a dictionary mapping node IDs to Node objects
378 * the Site objects are connected via SiteLink objects representing
379   the physical topology and available bandwidth
380 * the Node objects are connected via Link objects representing
381   the requested or assigned virtual topology of a slice
382 """
383 class Topology:
384     def __init__(self, api):
385         self.api = api
386         self.sites = get_sites(api)
387         self.nodes = get_nodes(api)
388         self.tags = get_slice_tags(api)
389         self.sitelinks = []
390         self.nodelinks = []
391     
392         for (s1, s2) in PhysicalLinks:
393             self.sitelinks.append(Link(self.sites[s1], self.sites[s2]))
394         
395         for id in self.nodes:
396             self.nodes[id].add_tag(self.sites)
397         
398         for t in self.tags:
399             tag = self.tags[t]
400             if tag.tagname == 'topo_rspec':
401                 node1 = self.nodes[tag.node_id]
402                 l = eval(tag.value)
403                 for (id, realip, bw, lvip, rvip, vnet) in l:
404                     allocbps = get_tc_rate(bw)
405                     node1.bps -= allocbps
406                     try:
407                         node2 = self.nodes[id]
408                         if node1.id < node2.id:
409                             sl = node1.get_sitelink(node2, self.sites)
410                             sl.bps -= allocbps
411                     except:
412                         pass
413
414     
415     """ Lookup site based on id or idtag value """
416     def lookupSite(self, id):
417         val = None
418         if isinstance(id, basestring):
419             id = int(id.lstrip('s'))
420         try:
421             val = self.sites[id]
422         except:
423             raise KeyError("site ID %s not found" % id)
424         return val
425     
426     def getSites(self):
427         sites = []
428         for s in self.sites:
429             sites.append(self.sites[s])
430         return sites
431         
432     """ Lookup node based on id or idtag value """
433     def lookupNode(self, id):
434         val = None
435         if isinstance(id, basestring):
436             id = int(id.lstrip('n'))
437         try:
438             val = self.nodes[id]
439         except:
440             raise KeyError("node ID %s not found" % id)
441         return val
442     
443     def getNodes(self):
444         nodes = []
445         for n in self.nodes:
446             nodes.append(self.nodes[n])
447         return nodes
448     
449     def nodesInTopo(self):
450         nodes = []
451         for n in self.nodes:
452             node = self.nodes[n]
453             if node.sliver:
454                 nodes.append(node)
455         return nodes
456             
457     def lookupSliceTag(self, id):
458         val = None
459         try:
460             val = self.tags[id]
461         except:
462             raise KeyError("slicetag ID %s not found" % id)
463         return val
464     
465     def getSliceTags(self):
466         tags = []
467         for t in self.tags:
468             tags.append(self.tags[t])
469         return tags
470     
471     def lookupSiteLink(self, node1, node2):
472         site1 = self.sites[node1.site_id]
473         site2 = self.sites[node2.site_id]
474         for link in self.sitelinks:
475             if site1 == link.end1 and site2 == link.end2:
476                 return link
477             if site2 == link.end1 and site1 == link.end2:
478                 return link
479         return None
480     
481
482     def __add_vlink(self, vlink, slicenodes, parent = None):
483         n1 = n2 = None
484         if 'endpoints' in vlink:
485             end = vlink['endpoints'].split()
486             n1 = self.lookupNode(end[0])
487             n2 = self.lookupNode(end[1])
488         elif parent:
489             """ Try to infer the endpoints """
490             (n1, n2) = self.__infer_endpoints(parent['endpoints'],slicenodes)
491         else:
492             raise Error("no endpoints given")
493
494         #print "Added virtual link: %s -- %s" % (n1.tag, n2.tag)
495         bps = int(vlink['kbps'][0]) * 1000
496         sitelink = self.lookupSiteLink(n1, n2)
497         if not sitelink:
498             raise PermissionError("nodes %s and %s not adjacent" % 
499                                   (n1.idtag, n2.idtag))
500         self.nodelinks.append(Link(n1, n2, bps, sitelink))
501         return
502
503     """ 
504     Infer the endpoints of the virtual link.  If the slice exists on 
505     only a single node at each end of the physical link, we'll assume that
506     the user wants the virtual link to terminate at these nodes.
507     """
508     def __infer_endpoints(self, endpoints, slicenodes):
509         n = []
510         ends = endpoints.split()
511         for end in ends:
512             found = 0
513             site = self.lookupSite(end)
514             for id in site.node_ids:
515                 if id in slicenodes:
516                     n.append(slicenodes[id])
517                     found += 1
518             if found != 1:
519                 raise Error("could not infer endpoint for site %s" % site.id)
520         #print "Inferred endpoints: %s %s" % (n[0].idtag, n[1].idtag)
521         return n
522         
523     def nodeTopoFromRSpec(self, rspec):
524         if self.nodelinks:
525             raise Error("virtual topology already present")
526             
527         rspecdict = rspec.toDict()
528         nodedict = {}
529         for node in self.getNodes():
530             nodedict[node.idtag] = node
531             
532         slicenodes = {}
533
534         top = rspecdict['rspec']
535         if ('network' in top):
536             sites = top['network'][0]['site']
537             for site in sites:
538                 for node in site['node']:
539                     if 'sliver' in node:
540                         n = nodedict[node['id']]
541                         slicenodes[n.id] = n
542                         n.add_sliver()
543             links = top['network'][0]['link']
544             for link in links:
545                 if 'vlink' in link:
546                     for vlink in link['vlink']:
547                         self.__add_vlink(vlink, slicenodes, link)
548         elif ('request' in top):
549             for sliver in top['request'][0]['sliver']:
550                 n = nodedict[sliver['nodeid']]
551                 slicenodes[n.id] = n
552                 n.add_sliver()
553             for vlink in top['request'][0]['vlink']:
554                 self.__add_vlink(vlink, slicenodes)
555         return
556
557     def nodeTopoFromSliceTags(self, slice):
558         if self.nodelinks:
559             raise Error("virtual topology already present")
560             
561         for node in slice.get_nodes(self.nodes):
562             node.sliver = True
563             linktag = slice.get_tag('topo_rspec', self.tags, node)
564             if linktag:
565                 l = eval(linktag.value)
566                 for (id, realip, bw, lvip, rvip, vnet) in l:
567                     if node.id < id:
568                         bps = get_tc_rate(bw)
569                         remote = self.lookupNode(id)
570                         sitelink = self.lookupSiteLink(node, remote)
571                         self.nodelinks.append(Link(node,remote,bps,sitelink))
572
573     def updateSliceTags(self, slice):
574         if not self.nodelinks:
575             return
576  
577         slice.update_tag('vini_topo', 'manual', self.tags)
578         slice.assign_egre_key(self.tags)
579         slice.turn_on_netns(self.tags)
580         slice.add_cap_net_admin(self.tags)
581
582         for node in slice.get_nodes(self.nodes):
583             linkdesc = []
584             for link in node.links:
585                 linkdesc.append(node.get_topo_rspec(link))
586             if linkdesc:
587                 topo_str = "%s" % linkdesc
588                 slice.update_tag('topo_rspec', topo_str, self.tags, node)
589
590         # Update slice tags in database
591         for tag in self.getSliceTags():
592             if tag.slice_id == slice.id:
593                 if tag.tagname == 'topo_rspec' and not tag.updated:
594                     tag.delete()
595                 tag.write(self.api)
596                 
597     """
598     Check the requested topology against the available topology and capacity
599     """
600     def verifyNodeTopo(self, hrn, topo):
601         for link in self.nodelinks:
602             if link.bps <= 0:
603                 raise GeniInvalidArgument(bw, "BW")
604                 
605             n1 = link.end1
606             n2 = link.end2
607             sitelink = self.lookupSiteLink(n1, n2)
608             if not sitelink:
609                 raise PermissionError("%s: nodes %s and %s not adjacent" % (hrn, n1.tag, n2.tag))
610             if sitelink.bps < link.bps:
611                 raise PermissionError("%s: insufficient capacity between %s and %s" % (hrn, n1.tag, n2.tag))
612                 
613     """
614     Produce XML directly from the topology specification.
615     """
616     def toxml(self, hrn = None):
617         xml = XMLBuilder()
618         with xml.rspec(type="VINI"):
619             if hrn:
620                 element = xml.network(name="Public VINI", slice=hrn)
621             else:
622                 element = xml.network(name="Public VINI")
623                 
624             with element:
625                 for site in self.getSites():
626                     site.toxml(xml, hrn, self.nodes)
627                 for link in self.sitelinks:
628                     link.toxml(xml)
629
630         return str(xml)
631
632 """
633 Create a dictionary of site objects keyed by site ID
634 """
635 def get_sites(api):
636     tmp = []
637     for site in api.plshell.GetSites(api.plauth):
638         t = site['site_id'], Site(site)
639         tmp.append(t)
640     return dict(tmp)
641
642
643 """
644 Create a dictionary of node objects keyed by node ID
645 """
646 def get_nodes(api):
647     tmp = []
648     for node in api.plshell.GetNodes(api.plauth):
649         t = node['node_id'], Node(node)
650         tmp.append(t)
651     return dict(tmp)
652
653 """
654 Create a dictionary of slice objects keyed by slice ID
655 """
656 def get_slice(api, slicename):
657     slice = api.plshell.GetSlices(api.plauth, [slicename])
658     if slice:
659         return Slice(slice[0])
660     else:
661         return None
662
663 """
664 Create a dictionary of slicetag objects keyed by slice tag ID
665 """
666 def get_slice_tags(api):
667     tmp = []
668     for tag in api.plshell.GetSliceTags(api.plauth):
669         t = tag['slice_tag_id'], Slicetag(tag)
670         tmp.append(t)
671     return dict(tmp)
672     
673 """
674 Find a free EGRE key
675 """
676 def free_egre_key(slicetags):
677     used = set()
678     for i in slicetags:
679         tag = slicetags[i]
680         if tag.tagname == 'egre_key':
681             used.add(int(tag.value))
682                 
683     for i in range(1, 256):
684         if i not in used:
685             key = i
686             break
687     else:
688         raise KeyError("No more EGRE keys available")
689         
690     return "%s" % key
691