removed another bunch of references to geni
[sfa.git] / sfa / rspecs / aggregates / vini / utils.py
1 import re
2 import socket
3 from sfa.util.faults import *
4 from sfa.rspecs.aggregates.vini.topology import *
5
6 default_topo_xml = """
7             <LinkSpec>
8                 <endpoint>i2atla1</endpoint>
9                 <endpoint>i2chic1</endpoint>
10                 <kbps>1000</kbps>
11             </LinkSpec>
12             <LinkSpec>
13                 <endpoint>i2atla1</endpoint>
14                 <endpoint>i2hous1</endpoint>
15                 <kbps>1000</kbps>
16             </LinkSpec>
17             <LinkSpec>
18                 <endpoint>i2atla1</endpoint>
19                 <endpoint>i2wash1</endpoint>
20                 <kbps>1000</kbps>
21             </LinkSpec>
22             <LinkSpec>
23                 <endpoint>i2chic1</endpoint>
24                 <endpoint>i2kans1</endpoint>
25                 <kbps>1000</kbps>
26             </LinkSpec>
27             <LinkSpec>
28                 <endpoint>i2chic1</endpoint>
29                 <endpoint>i2wash1</endpoint>
30                 <kbps>1000</kbps>
31             </LinkSpec>
32             <LinkSpec>
33                 <endpoint>i2hous1</endpoint>
34                 <endpoint>i2kans1</endpoint>
35                 <kbps>1000</kbps>
36             </LinkSpec>
37             <LinkSpec>
38                 <endpoint>i2hous1</endpoint>
39                 <endpoint>i2losa1</endpoint>
40                 <kbps>1000</kbps>
41             </LinkSpec>
42             <LinkSpec>
43                 <endpoint>i2kans1</endpoint>
44                 <endpoint>i2salt1</endpoint>
45                 <kbps>1000</kbps>
46             </LinkSpec>
47             <LinkSpec>
48                 <endpoint>i2losa1</endpoint>
49                 <endpoint>i2salt1</endpoint>
50                 <kbps>1000</kbps>
51             </LinkSpec>
52             <LinkSpec>
53                 <endpoint>i2losa1</endpoint>
54                 <endpoint>i2seat1</endpoint>
55                 <kbps>1000</kbps>
56             </LinkSpec>
57             <LinkSpec>
58                 <endpoint>i2newy1</endpoint>
59                 <endpoint>i2wash1</endpoint>
60                 <kbps>1000</kbps>
61             </LinkSpec>
62             <LinkSpec>
63                 <endpoint>i2salt1</endpoint>
64                 <endpoint>i2seat1</endpoint>
65                 <kbps>1000</kbps>
66             </LinkSpec>"""
67       
68 # Taken from bwlimit.py
69 #
70 # See tc_util.c and http://physics.nist.gov/cuu/Units/binary.html. Be
71 # warned that older versions of tc interpret "kbps", "mbps", "mbit",
72 # and "kbit" to mean (in this system) "kibps", "mibps", "mibit", and
73 # "kibit" and that if an older version is installed, all rates will
74 # be off by a small fraction.
75 suffixes = {
76     "":         1,
77     "bit":      1,
78     "kibit":    1024,
79     "kbit":     1000,
80     "mibit":    1024*1024,
81     "mbit":     1000000,
82     "gibit":    1024*1024*1024,
83     "gbit":     1000000000,
84     "tibit":    1024*1024*1024*1024,
85     "tbit":     1000000000000,
86     "bps":      8,
87     "kibps":    8*1024,
88     "kbps":     8000,
89     "mibps":    8*1024*1024,
90     "mbps":     8000000,
91     "gibps":    8*1024*1024*1024,
92     "gbps":     8000000000,
93     "tibps":    8*1024*1024*1024*1024,
94     "tbps":     8000000000000
95 }
96
97
98 def get_tc_rate(s):
99     """
100     Parses an integer or a tc rate string (e.g., 1.5mbit) into bits/second
101     """
102
103     if type(s) == int:
104         return s
105     m = re.match(r"([0-9.]+)(\D*)", s)
106     if m is None:
107         return -1
108     suffix = m.group(2).lower()
109     if suffixes.has_key(suffix):
110         return int(float(m.group(1)) * suffixes[suffix])
111     else:
112         return -1
113
114 def format_tc_rate(rate):
115     """
116     Formats a bits/second rate into a tc rate string
117     """
118
119     if rate >= 1000000000 and (rate % 1000000000) == 0:
120         return "%.0fgbit" % (rate / 1000000000.)
121     elif rate >= 1000000 and (rate % 1000000) == 0:
122         return "%.0fmbit" % (rate / 1000000.)
123     elif rate >= 1000:
124         return "%.0fkbit" % (rate / 1000.)
125     else:
126         return "%.0fbit" % rate
127
128
129 class Node:
130     def __init__(self, node, bps = 1000 * 1000000):
131         self.id = node['node_id']
132         self.hostname = node['hostname']
133         self.shortname = self.hostname.replace('.vini-veritas.net', '')
134         self.site_id = node['site_id']
135         self.ipaddr = socket.gethostbyname(self.hostname)
136         self.bps = bps
137         self.links = set()
138
139     def get_link_id(self, remote):
140         if self.id < remote.id:
141             link = (self.id<<7) + remote.id
142         else:
143             link = (remote.id<<7) + self.id
144         return link
145         
146     def get_iface_id(self, remote):
147         if self.id < remote.id:
148             iface = 1
149         else:
150             iface = 2
151         return iface
152     
153     def get_virt_ip(self, remote):
154         link = self.get_link_id(remote)
155         iface = self.get_iface_id(remote)
156         first = link >> 6
157         second = ((link & 0x3f)<<2) + iface
158         return "192.168.%d.%d" % (first, second)
159
160     def get_virt_net(self, remote):
161         link = self.get_link_id(remote)
162         first = link >> 6
163         second = (link & 0x3f)<<2
164         return "192.168.%d.%d/30" % (first, second)
165         
166     def get_site(self, sites):
167         return sites[self.site_id]
168     
169     def get_topo_rspec(self, link):
170         if link.end1 == self:
171             remote = link.end2
172         elif link.end2 == self:
173             remote = link.end1
174         else:
175             raise Error("Link does not connect to Node")
176             
177         my_ip = self.get_virt_ip(remote)
178         remote_ip = remote.get_virt_ip(self)
179         net = self.get_virt_net(remote)
180         bw = format_tc_rate(link.bps)
181         return (remote.id, remote.ipaddr, bw, my_ip, remote_ip, net)
182         
183     def add_link(self, link):
184         self.links.add(link)
185         
186     def add_tag(self, sites):
187         s = self.get_site(sites)
188         words = self.hostname.split(".")
189         index = words[0].replace("node", "")
190         if index.isdigit():
191             self.tag = s.tag + index
192         else:
193             self.tag = None
194
195     # Assumes there is at most one Link between two sites
196     def get_sitelink(self, node, sites):
197         site1 = sites[self.site_id]
198         site2 = sites[node.site_id]
199         sl = site1.links.intersection(site2.links)
200         if len(sl):
201             return sl.pop()
202         return None
203     
204
205 class Link:
206     def __init__(self, end1, end2, bps = 1000 * 1000000):
207         self.end1 = end1
208         self.end2 = end2
209         self.bps = bps
210         
211         end1.add_link(self)
212         end2.add_link(self)
213         
214         
215 class Site:
216     def __init__(self, site):
217         self.id = site['site_id']
218         self.node_ids = site['node_ids']
219         self.name = site['abbreviated_name'].replace(" ", "_")
220         self.tag = site['login_base']
221         self.public = site['is_public']
222         self.enabled = site['enabled']
223         self.links = set()
224
225     def get_sitenodes(self, nodes):
226         n = []
227         for i in self.node_ids:
228             n.append(nodes[i])
229         return n
230     
231     def add_link(self, link):
232         self.links.add(link)
233     
234     
235 class Slice:
236     def __init__(self, slice):
237         self.id = slice['slice_id']
238         self.name = slice['name']
239         self.node_ids = set(slice['node_ids'])
240         self.slice_tag_ids = slice['slice_tag_ids']
241     
242     def get_tag(self, tagname, slicetags, node = None):
243         for i in self.slice_tag_ids:
244             tag = slicetags[i]
245             if tag.tagname == tagname:
246                 if (not node) or (node.id == tag.node_id):
247                     return tag
248         else:
249             return None
250         
251     def get_nodes(self, nodes):
252         n = []
253         for id in self.node_ids:
254             n.append(nodes[id])
255         return n
256              
257     
258     # Add a new slice tag   
259     def add_tag(self, tagname, value, slicetags, node = None):
260         record = {'slice_tag_id':None, 'slice_id':self.id, 'tagname':tagname, 'value':value}
261         if node:
262             record['node_id'] = node.id
263         else:
264             record['node_id'] = None
265         tag = Slicetag(record)
266         slicetags[tag.id] = tag
267         self.slice_tag_ids.append(tag.id)
268         tag.changed = True       
269         tag.updated = True
270         return tag
271     
272     # Update a slice tag if it exists, else add it             
273     def update_tag(self, tagname, value, slicetags, node = None):
274         tag = self.get_tag(tagname, slicetags, node)
275         if tag and tag.value == value:
276             value = "no change"
277         elif tag:
278             tag.value = value
279             tag.changed = True
280         else:
281             tag = self.add_tag(tagname, value, slicetags, node)
282         tag.updated = True
283             
284     def assign_egre_key(self, slicetags):
285         if not self.get_tag('egre_key', slicetags):
286             try:
287                 key = free_egre_key(slicetags)
288                 self.update_tag('egre_key', key, slicetags)
289             except:
290                 # Should handle this case...
291                 pass
292         return
293             
294     def turn_on_netns(self, slicetags):
295         tag = self.get_tag('netns', slicetags)
296         if (not tag) or (tag.value != '1'):
297             self.update_tag('netns', '1', slicetags)
298         return
299    
300     def turn_off_netns(self, slicetags):
301         tag = self.get_tag('netns', slicetags)
302         if tag and (tag.value != '0'):
303             tag.delete()
304         return
305     
306     def add_cap_net_admin(self, slicetags):
307         tag = self.get_tag('capabilities', slicetags)
308         if tag:
309             caps = tag.value.split(',')
310             for cap in caps:
311                 if cap == "CAP_NET_ADMIN":
312                     return
313             else:
314                 newcaps = "CAP_NET_ADMIN," + tag.value
315                 self.update_tag('capabilities', newcaps, slicetags)
316         else:
317             self.add_tag('capabilities', 'CAP_NET_ADMIN', slicetags)
318         return
319     
320     def remove_cap_net_admin(self, slicetags):
321         tag = self.get_tag('capabilities', slicetags)
322         if tag:
323             caps = tag.value.split(',')
324             newcaps = []
325             for cap in caps:
326                 if cap != "CAP_NET_ADMIN":
327                     newcaps.append(cap)
328             if newcaps:
329                 value = ','.join(newcaps)
330                 self.update_tag('capabilities', value, slicetags)
331             else:
332                 tag.delete()
333         return
334
335     # Update the vsys/setup-link and vsys/setup-nat slice tags.
336     def add_vsys_tags(self, slicetags):
337         link = nat = False
338         for i in self.slice_tag_ids:
339             tag = slicetags[i]
340             if tag.tagname == 'vsys':
341                 if tag.value == 'setup-link':
342                     link = True
343                 elif tag.value == 'setup-nat':
344                     nat = True
345         if not link:
346             self.add_tag('vsys', 'setup-link', slicetags)
347         if not nat:
348             self.add_tag('vsys', 'setup-nat', slicetags)
349         return
350
351
352 class Slicetag:
353     newid = -1 
354     def __init__(self, tag):
355         self.id = tag['slice_tag_id']
356         if not self.id:
357             # Make one up for the time being...
358             self.id = Slicetag.newid
359             Slicetag.newid -= 1
360         self.slice_id = tag['slice_id']
361         self.tagname = tag['tagname']
362         self.value = tag['value']
363         self.node_id = tag['node_id']
364         self.updated = False
365         self.changed = False
366         self.deleted = False
367     
368     # Mark a tag as deleted
369     def delete(self):
370         self.deleted = True
371         self.updated = True
372     
373     def write(self, api):
374         if self.changed:
375             if int(self.id) > 0:
376                 api.plshell.UpdateSliceTag(api.plauth, self.id, self.value)
377             else:
378                 api.plshell.AddSliceTag(api.plauth, self.slice_id, 
379                                         self.tagname, self.value, self.node_id)
380         elif self.deleted and int(self.id) > 0:
381             api.plshell.DeleteSliceTag(api.plauth, self.id)
382
383
384 """
385 A topology is a compound object consisting of:
386 * a dictionary mapping site IDs to Site objects
387 * a dictionary mapping node IDs to Node objects
388 * the Site objects are connected via SiteLink objects representing
389   the physical topology and available bandwidth
390 * the Node objects are connected via Link objects representing
391   the requested or assigned virtual topology of a slice
392 """
393 class Topology:
394     def __init__(self, api):
395         self.api = api
396         self.sites = get_sites(api)
397         self.nodes = get_nodes(api)
398         self.tags = get_slice_tags(api)
399         self.sitelinks = []
400         self.nodelinks = []
401     
402         for (s1, s2) in PhysicalLinks:
403             self.sitelinks.append(Link(self.sites[s1], self.sites[s2]))
404         
405         for id in self.nodes:
406             self.nodes[id].add_tag(self.sites)
407         
408         for t in self.tags:
409             tag = self.tags[t]
410             if tag.tagname == 'topo_rspec':
411                 node1 = self.nodes[tag.node_id]
412                 l = eval(tag.value)
413                 for (id, realip, bw, lvip, rvip, vnet) in l:
414                     allocbps = get_tc_rate(bw)
415                     node1.bps -= allocbps
416                     try:
417                         node2 = self.nodes[id]
418                         if node1.id < node2.id:
419                             sl = node1.get_sitelink(node2, self.sites)
420                             sl.bps -= allocbps
421                     except:
422                         pass
423
424     
425     def lookupSite(self, id):
426         val = None
427         try:
428             val = self.sites[id]
429         except:
430             raise KeyError("site ID %s not found" % id)
431         return val
432     
433     def getSites(self):
434         sites = []
435         for s in self.sites:
436             sites.append(self.sites[s])
437         return sites
438         
439     def lookupNode(self, id):
440         val = None
441         try:
442             val = self.nodes[id]
443         except:
444             raise KeyError("node ID %s not found" % id)
445         return val
446     
447     def getNodes(self):
448         nodes = []
449         for n in self.nodes:
450             nodes.append(self.nodes[n])
451         return nodes
452     
453     def nodesInTopo(self):
454         nodes = []
455         for n in self.nodes:
456             if self.nodes[n].links:
457                 nodes.append(self.nodes[n])
458         return nodes
459             
460     def lookupSliceTag(self, id):
461         val = None
462         try:
463             val = self.tags[id]
464         except:
465             raise KeyError("slicetag ID %s not found" % id)
466         return val
467     
468     def getSliceTags(self):
469         tags = []
470         for t in self.tags:
471             tags.append(self.tags[t])
472         return tags
473     
474     def lookupSiteLink(self, node1, node2):
475         site1 = self.sites[node1.site_id]
476         site2 = self.sites[node2.site_id]
477         for link in self.sitelinks:
478             if site1 == link.end1 and site2 == link.end2:
479                 return link
480             if site2 == link.end1 and site1 == link.end2:
481                 return link
482         return None
483     
484     def nodeTopoFromRSpec(self, rspec):
485         if self.nodelinks:
486             raise Error("virtual topology already present")
487             
488         rspecdict = rspec.toDict()
489         nodedict = {}
490         for node in self.getNodes():
491             nodedict[node.tag] = node
492             
493         linkspecs = rspecdict['RSpec']['Request'][0]['NetSpec'][0]['LinkSpec']    
494         for l in linkspecs:
495             n1 = nodedict[l['endpoint'][0]]
496             n2 = nodedict[l['endpoint'][1]]
497             bps = int(l['kbps'][0]) * 1000
498             self.nodelinks.append(Link(n1, n2, bps))
499  
500     def nodeTopoFromSliceTags(self, slice):
501         if self.nodelinks:
502             raise Error("virtual topology already present")
503             
504         for node in slice.get_nodes(self.nodes):
505             linktag = slice.get_tag('topo_rspec', self.tags, node)
506             if linktag:
507                 l = eval(linktag.value)
508                 for (id, realip, bw, lvip, rvip, vnet) in l:
509                     if node.id < id:
510                         bps = get_tc_rate(bw)
511                         remote = self.lookupNode(id)
512                         self.nodelinks.append(Link(node, remote, bps))
513
514     def updateSliceTags(self, slice):
515         if not self.nodelinks:
516             return
517  
518         slice.update_tag('vini_topo', 'manual', self.tags)
519         slice.assign_egre_key(self.tags)
520         slice.turn_on_netns(self.tags)
521         slice.add_cap_net_admin(self.tags)
522
523         for node in slice.get_nodes(self.nodes):
524             linkdesc = []
525             for link in node.links:
526                 linkdesc.append(node.get_topo_rspec(link))
527             if linkdesc:
528                 topo_str = "%s" % linkdesc
529                 slice.update_tag('topo_rspec', topo_str, self.tags, node)
530
531         # Update slice tags in database
532         for tag in self.getSliceTags():
533             if tag.slice_id == slice.id:
534                 if tag.tagname == 'topo_rspec' and not tag.updated:
535                     tag.delete()
536                 tag.write(self.api)
537                 
538     """
539     Check the requested topology against the available topology and capacity
540     """
541     def verifyNodeTopo(self, hrn, topo, maxbw):
542         maxbps = get_tc_rate(maxbw)
543         for link in self.nodelinks:
544             if link.bps <= 0:
545                 raise SfaInvalidArgument(bw, "BW")
546             if link.bps > maxbps:
547                 raise PermissionError(" %s requested %s but max BW is %s" % 
548                                       (hrn, format_tc_rate(link.bps), maxbw))
549                 
550             n1 = link.end1
551             n2 = link.end2
552             sitelink = self.lookupSiteLink(n1, n2)
553             if not sitelink:
554                 raise PermissionError("%s: nodes %s and %s not adjacent" % (hrn, n1.tag, n2.tag))
555             if sitelink.bps < link.bps:
556                 raise PermissionError("%s: insufficient capacity between %s and %s" % (hrn, n1.tag, n2.tag))
557                 
558     """
559     Produce XML directly from the topology specification.
560     """
561     def toxml(self, hrn = None):
562         xml = """<?xml version="1.0"?>
563 <RSpec name="vini">
564     <Capacity>
565         <NetSpec name="physical_topology">"""
566
567         for site in self.getSites():
568             if not (site.public and site.enabled and site.node_ids):
569                 continue
570             
571             xml += """
572             <SiteSpec name="%s"> """ % site.name
573
574             for node in site.get_sitenodes(self.nodes):
575                 if not node.tag:
576                     continue
577                 
578                 xml += """
579                 <NodeSpec name="%s">
580                     <hostname>%s</hostname>
581                     <kbps>%s</kbps>
582                 </NodeSpec>""" % (node.tag, node.hostname, int(node.bps/1000))
583             xml += """
584             </SiteSpec>"""
585             
586         for link in self.sitelinks:
587             xml += """
588             <SiteLinkSpec>
589                 <endpoint>%s</endpoint>
590                 <endpoint>%s</endpoint> 
591                 <kbps>%s</kbps>
592             </SiteLinkSpec>""" % (link.end1.name, link.end2.name, int(link.bps/1000))
593             
594         
595         if hrn:
596             name = hrn
597         else:
598             name = 'default_topology'
599         xml += """
600         </NetSpec>
601     </Capacity>
602     <Request>
603         <NetSpec name="%s">""" % name
604         
605         if hrn:
606             for link in self.nodelinks:
607                 xml += """
608             <LinkSpec>
609                 <endpoint>%s</endpoint>
610                 <endpoint>%s</endpoint> 
611                 <kbps>%s</kbps>
612             </LinkSpec>""" % (link.end1.tag, link.end2.tag, int(link.bps/1000))
613         else:
614             xml += default_topo_xml
615             
616         xml += """
617         </NetSpec>
618     </Request>
619 </RSpec>"""
620
621         # Remove all leading whitespace and newlines
622         lines = xml.split("\n")
623         noblanks = ""
624         for line in lines:
625             noblanks += line.strip()
626         return noblanks
627
628
629 """
630 Create a dictionary of site objects keyed by site ID
631 """
632 def get_sites(api):
633     tmp = []
634     for site in api.plshell.GetSites(api.plauth):
635         t = site['site_id'], Site(site)
636         tmp.append(t)
637     return dict(tmp)
638
639
640 """
641 Create a dictionary of node objects keyed by node ID
642 """
643 def get_nodes(api):
644     tmp = []
645     for node in api.plshell.GetNodes(api.plauth):
646         t = node['node_id'], Node(node)
647         tmp.append(t)
648     return dict(tmp)
649
650 """
651 Create a dictionary of slice objects keyed by slice ID
652 """
653 def get_slice(api, slicename):
654     slice = api.plshell.GetSlices(api.plauth, [slicename])
655     if slice:
656         return Slice(slice[0])
657     else:
658         return None
659
660 """
661 Create a dictionary of slicetag objects keyed by slice tag ID
662 """
663 def get_slice_tags(api):
664     tmp = []
665     for tag in api.plshell.GetSliceTags(api.plauth):
666         t = tag['slice_tag_id'], Slicetag(tag)
667         tmp.append(t)
668     return dict(tmp)
669     
670 """
671 Find a free EGRE key
672 """
673 def free_egre_key(slicetags):
674     used = set()
675     for i in slicetags:
676         tag = slicetags[i]
677         if tag.tagname == 'egre_key':
678             used.add(int(tag.value))
679                 
680     for i in range(1, 256):
681         if i not in used:
682             key = i
683             break
684     else:
685         raise KeyError("No more EGRE keys available")
686         
687     return "%s" % key
688