Verify request against capacity
[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                 <bw>1Mbit</bw>
11             </LinkSpec>
12             <LinkSpec>
13                 <endpoint>i2atla1</endpoint>
14                 <endpoint>i2hous1</endpoint>
15                 <bw>1Mbit</bw>
16             </LinkSpec>
17             <LinkSpec>
18                 <endpoint>i2atla1</endpoint>
19                 <endpoint>i2wash1</endpoint>
20                 <bw>1Mbit</bw>
21             </LinkSpec>
22             <LinkSpec>
23                 <endpoint>i2chic1</endpoint>
24                 <endpoint>i2kans1</endpoint>
25                 <bw>1Mbit</bw>
26             </LinkSpec>
27             <LinkSpec>
28                 <endpoint>i2chic1</endpoint>
29                 <endpoint>i2wash1</endpoint>
30                 <bw>1Mbit</bw>
31             </LinkSpec>
32             <LinkSpec>
33                 <endpoint>i2hous1</endpoint>
34                 <endpoint>i2kans1</endpoint>
35                 <bw>1Mbit</bw>
36             </LinkSpec>
37             <LinkSpec>
38                 <endpoint>i2hous1</endpoint>
39                 <endpoint>i2losa1</endpoint>
40                 <bw>1Mbit</bw>
41             </LinkSpec>
42             <LinkSpec>
43                 <endpoint>i2kans1</endpoint>
44                 <endpoint>i2salt1</endpoint>
45                 <bw>1Mbit</bw>
46             </LinkSpec>
47             <LinkSpec>
48                 <endpoint>i2losa1</endpoint>
49                 <endpoint>i2salt1</endpoint>
50                 <bw>1Mbit</bw>
51             </LinkSpec>
52             <LinkSpec>
53                 <endpoint>i2losa1</endpoint>
54                 <endpoint>i2seat1</endpoint>
55                 <bw>1Mbit</bw>
56             </LinkSpec>
57             <LinkSpec>
58                 <endpoint>i2newy1</endpoint>
59                 <endpoint>i2wash1</endpoint>
60                 <bw>1Mbit</bw>
61             </LinkSpec>
62             <LinkSpec>
63                 <endpoint>i2salt1</endpoint>
64                 <endpoint>i2seat1</endpoint>
65                 <bw>1Mbit</bw>
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.links = set()
223
224     def get_sitenodes(self, nodes):
225         n = []
226         for i in self.node_ids:
227             n.append(nodes[i])
228         return n
229     
230     def add_link(self, link):
231         self.links.add(link)
232     
233     
234 class Slice:
235     def __init__(self, slice):
236         self.id = slice['slice_id']
237         self.name = slice['name']
238         self.node_ids = set(slice['node_ids'])
239         self.slice_tag_ids = slice['slice_tag_ids']
240     
241     def get_tag(self, tagname, slicetags, node = None):
242         for i in self.slice_tag_ids:
243             tag = slicetags[i]
244             if tag.tagname == tagname:
245                 if (not node) or (node.id == tag.node_id):
246                     return tag
247         else:
248             return None
249         
250     def get_nodes(self, nodes):
251         n = []
252         for id in self.node_ids:
253             n.append(nodes[id])
254         return n
255              
256     
257     # Add a new slice tag   
258     def add_tag(self, tagname, value, slicetags, node = None):
259         record = {'slice_tag_id':None, 'slice_id':self.id, 'tagname':tagname, 'value':value}
260         if node:
261             record['node_id'] = node.id
262         else:
263             record['node_id'] = None
264         tag = Slicetag(record)
265         slicetags[tag.id] = tag
266         self.slice_tag_ids.append(tag.id)
267         tag.changed = True       
268         tag.updated = True
269         return tag
270     
271     # Update a slice tag if it exists, else add it             
272     def update_tag(self, tagname, value, slicetags, node = None):
273         tag = self.get_tag(tagname, slicetags, node)
274         if tag and tag.value == value:
275             value = "no change"
276         elif tag:
277             tag.value = value
278             tag.changed = True
279         else:
280             tag = self.add_tag(tagname, value, slicetags, node)
281         tag.updated = True
282             
283     def assign_egre_key(self, slicetags):
284         if not self.get_tag('egre_key', slicetags):
285             try:
286                 key = free_egre_key(slicetags)
287                 self.update_tag('egre_key', key, slicetags)
288             except:
289                 # Should handle this case...
290                 pass
291         return
292             
293     def turn_on_netns(self, slicetags):
294         tag = self.get_tag('netns', slicetags)
295         if (not tag) or (tag.value != '1'):
296             self.update_tag('netns', '1', slicetags)
297         return
298    
299     def turn_off_netns(self, slicetags):
300         tag = self.get_tag('netns', slicetags)
301         if tag and (tag.value != '0'):
302             tag.delete()
303         return
304     
305     def add_cap_net_admin(self, slicetags):
306         tag = self.get_tag('capabilities', slicetags)
307         if tag:
308             caps = tag.value.split(',')
309             for cap in caps:
310                 if cap == "CAP_NET_ADMIN":
311                     return
312             else:
313                 newcaps = "CAP_NET_ADMIN," + tag.value
314                 self.update_tag('capabilities', newcaps, slicetags)
315         else:
316             self.add_tag('capabilities', 'CAP_NET_ADMIN', slicetags)
317         return
318     
319     def remove_cap_net_admin(self, slicetags):
320         tag = self.get_tag('capabilities', slicetags)
321         if tag:
322             caps = tag.value.split(',')
323             newcaps = []
324             for cap in caps:
325                 if cap != "CAP_NET_ADMIN":
326                     newcaps.append(cap)
327             if newcaps:
328                 value = ','.join(newcaps)
329                 self.update_tag('capabilities', value, slicetags)
330             else:
331                 tag.delete()
332         return
333
334     # Update the vsys/setup-link and vsys/setup-nat slice tags.
335     def add_vsys_tags(self, slicetags):
336         link = nat = False
337         for i in self.slice_tag_ids:
338             tag = slicetags[i]
339             if tag.tagname == 'vsys':
340                 if tag.value == 'setup-link':
341                     link = True
342                 elif tag.value == 'setup-nat':
343                     nat = True
344         if not link:
345             self.add_tag('vsys', 'setup-link', slicetags)
346         if not nat:
347             self.add_tag('vsys', 'setup-nat', slicetags)
348         return
349
350
351 class Slicetag:
352     newid = -1 
353     def __init__(self, tag):
354         self.id = tag['slice_tag_id']
355         if not self.id:
356             # Make one up for the time being...
357             self.id = Slicetag.newid
358             Slicetag.newid -= 1
359         self.slice_id = tag['slice_id']
360         self.tagname = tag['tagname']
361         self.value = tag['value']
362         self.node_id = tag['node_id']
363         self.updated = False
364         self.changed = False
365         self.deleted = False
366     
367     # Mark a tag as deleted
368     def delete(self):
369         self.deleted = True
370         self.updated = True
371     
372     def write(self, api):
373         if self.changed:
374             if int(self.id) > 0:
375                 api.plshell.UpdateSliceTag(api.plauth, self.id, self.value)
376             else:
377                 api.plshell.AddSliceTag(api.plauth, self.slice_id, 
378                                         self.tagname, self.value, self.node_id)
379         elif self.deleted and int(self.id) > 0:
380             api.plshell.DeleteSliceTag(api.plauth, self.id)
381
382
383 """
384 A topology is a compound object consisting of:
385 * a dictionary mapping site IDs to Site objects
386 * a dictionary mapping node IDs to Node objects
387 * the Site objects are connected via SiteLink objects representing
388   the physical topology and available bandwidth
389 * the Node objects are connected via Link objects representing
390   the requested or assigned virtual topology of a slice
391 """
392 class Topology:
393     def __init__(self, api):
394         self.api = api
395         self.sites = get_sites(api)
396         self.nodes = get_nodes(api)
397         self.tags = get_slice_tags(api)
398         self.sitelinks = []
399         self.nodelinks = []
400     
401         for (s1, s2) in PhysicalLinks:
402             self.sitelinks.append(Link(self.sites[s1], self.sites[s2]))
403         
404         for id in self.nodes:
405             self.nodes[id].add_tag(self.sites)
406         
407         for t in self.tags:
408             tag = self.tags[t]
409             if tag.tagname == 'topo_rspec':
410                 node1 = self.nodes[tag.node_id]
411                 l = eval(tag.value)
412                 for (id, realip, bw, lvip, rvip, vnet) in l:
413                     allocbps = get_tc_rate(bw)
414                     node1.bps -= allocbps
415                     try:
416                         node2 = self.nodes[id]
417                         if node1.id < node2.id:
418                             sl = node1.get_sitelink(node2, self.sites)
419                             sl.bps -= allocbps
420                     except:
421                         pass
422
423     
424     def lookupSite(self, id):
425         val = None
426         try:
427             val = self.sites[id]
428         except:
429             raise KeyError("site ID %s not found" % id)
430         return val
431     
432     def getSites(self):
433         sites = []
434         for s in self.sites:
435             sites.append(self.sites[s])
436         return sites
437         
438     def lookupNode(self, id):
439         val = None
440         try:
441             val = self.nodes[id]
442         except:
443             raise KeyError("node ID %s not found" % id)
444         return val
445     
446     def getNodes(self):
447         nodes = []
448         for n in self.nodes:
449             nodes.append(self.nodes[n])
450         return nodes
451     
452     def nodesInTopo(self):
453         nodes = []
454         for n in self.nodes:
455             if self.nodes[n].links:
456                 nodes.append(self.nodes[n])
457         return nodes
458             
459     def lookupSliceTag(self, id):
460         val = None
461         try:
462             val = self.tags[id]
463         except:
464             raise KeyError("slicetag ID %s not found" % id)
465         return val
466     
467     def getSliceTags(self):
468         tags = []
469         for t in self.tags:
470             tags.append(self.tags[t])
471         return tags
472     
473     def lookupSiteLink(self, node1, node2):
474         site1 = self.sites[node1.site_id]
475         site2 = self.sites[node2.site_id]
476         for link in self.sitelinks:
477             if site1 == link.end1 and site2 == link.end2:
478                 return link
479             if site2 == link.end1 and site1 == link.end2:
480                 return link
481         return None
482     
483     def nodeTopoFromRspec(self, rspec):
484         if self.nodelinks:
485             raise Error("virtual topology already present")
486             
487         rspecdict = rspec.toDict()
488         nodedict = {}
489         for node in self.getNodes():
490             nodedict[node.tag] = node
491             
492         linkspecs = rspecdict['Rspec']['Request'][0]['NetSpec'][0]['LinkSpec']    
493         for l in linkspecs:
494             n1 = nodedict[l['endpoint'][0]]
495             n2 = nodedict[l['endpoint'][1]]
496             bps = get_tc_rate(l['bw'][0])
497             self.nodelinks.append(Link(n1, n2, bps))
498  
499     def nodeTopoFromSliceTags(self, slice):
500         if self.nodelinks:
501             raise Error("virtual topology already present")
502             
503         for node in slice.get_nodes(self.nodes):
504             linktag = slice.get_tag('topo_rspec', self.tags, node)
505             if linktag:
506                 l = eval(linktag.value)
507                 for (id, realip, bw, lvip, rvip, vnet) in l:
508                     if node.id < id:
509                         bps = get_tc_rate(bw)
510                         remote = self.lookupNode(id)
511                         self.nodelinks.append(Link(node, remote, bps))
512
513     def updateSliceTags(self, slice):
514         if not self.nodelinks:
515             return
516  
517         slice.update_tag('vini_topo', 'manual', self.tags)
518         slice.assign_egre_key(self.tags)
519         slice.turn_on_netns(self.tags)
520         slice.add_cap_net_admin(self.tags)
521
522         for node in slice.get_nodes(self.nodes):
523             linkdesc = []
524             for link in node.links:
525                 linkdesc.append(node.get_topo_rspec(link))
526             if linkdesc:
527                 topo_str = "%s" % linkdesc
528                 slice.update_tag('topo_rspec', topo_str, self.tags, node)
529
530         # Update slice tags in database
531         for tag in self.getSliceTags():
532             if tag.slice_id == slice.id:
533                 if tag.tagname == 'topo_rspec' and not tag.updated:
534                     tag.delete()
535                 tag.write(self.api)
536                 
537     """
538     Check the requested topology against the available topology and capacity
539     """
540     def verifyNodeTopo(self, hrn, topo, maxbw):
541         maxbps = get_tc_rate(maxbw)
542         for link in self.nodelinks:
543             if link.bps <= 0:
544                 raise GeniInvalidArgument(bw, "BW")
545             if link.bps > maxbps:
546                 raise PermissionError(" %s requested %s but max BW is %s" % 
547                                       (hrn, format_tc_rate(link.bps), maxbw))
548                 
549             n1 = link.end1
550             n2 = link.end2
551             sitelink = self.lookupSiteLink(n1, n2)
552             if not sitelink:
553                 raise PermissionError("%s: nodes %s and %s not adjacent" % (hrn, n1.tag, n2.tag))
554             if sitelink.bps < link.bps:
555                 raise PermissionError("%s: insufficient capacity between %s and %s" % (hrn, n1.tag, n2.tag))
556                 
557     """
558     Produce XML directly from the topology specification.
559     """
560     def toxml(self, hrn = None):
561         xml = """<?xml version="1.0"?>
562 <Rspec xmlns="http://www.planet-lab.org/sfa/rspec/" name="vini">
563     <Capacity>
564         <NetSpec name="physical_topology">"""
565
566         for site in self.getSites():
567             if not site.public:
568                 continue
569             
570             xml += """
571             <SiteSpec name="%s"> """ % site.name
572
573             for node in site.get_sitenodes(self.nodes):
574                 if not node.tag:
575                     continue
576                 
577                 xml += """
578                 <NodeSpec name="%s">
579                     <hostname>%s</hostname>
580                     <bw>%s</bw>
581                 </NodeSpec>""" % (node.tag, node.hostname, format_tc_rate(node.bps))
582             xml += """
583             </SiteSpec>"""
584             
585         for link in self.sitelinks:
586             xml += """
587             <SiteLinkSpec>
588                 <endpoint>%s</endpoint>
589                 <endpoint>%s</endpoint> 
590                 <bw>%s</bw>
591             </SiteLinkSpec>""" % (link.end1.name, link.end2.name, format_tc_rate(link.bps))
592             
593         
594         if hrn:
595             name = hrn
596         else:
597             name = 'default_topology'
598         xml += """
599         </NetSpec>
600     </Capacity>
601     <Request>
602         <NetSpec name="%s">""" % name
603         
604         if hrn:
605             for link in self.nodelinks:
606                 xml += """
607             <LinkSpec>
608                 <endpoint>%s</endpoint>
609                 <endpoint>%s</endpoint> 
610                 <bw>%s</bw>
611             </LinkSpec>""" % (link.end1.tag, link.end2.tag, format_tc_rate(link.bps))
612         else:
613             xml += default_topo_xml
614             
615         xml += """
616         </NetSpec>
617     </Request>
618 </Rspec>"""
619
620         # Remove all leading whitespace and newlines
621         lines = xml.split("\n")
622         noblanks = ""
623         for line in lines:
624             noblanks += line.strip()
625         return noblanks
626
627
628 """
629 Create a dictionary of site objects keyed by site ID
630 """
631 def get_sites(api):
632     tmp = []
633     for site in api.plshell.GetSites(api.plauth):
634         t = site['site_id'], Site(site)
635         tmp.append(t)
636     return dict(tmp)
637
638
639 """
640 Create a dictionary of node objects keyed by node ID
641 """
642 def get_nodes(api):
643     tmp = []
644     for node in api.plshell.GetNodes(api.plauth):
645         t = node['node_id'], Node(node)
646         tmp.append(t)
647     return dict(tmp)
648
649 """
650 Create a dictionary of slice objects keyed by slice ID
651 """
652 def get_slice(api, slicename):
653     slice = api.plshell.GetSlices(api.plauth, [slicename])
654     if slice:
655         return Slice(slice[0])
656     else:
657         return None
658
659 """
660 Create a dictionary of slicetag objects keyed by slice tag ID
661 """
662 def get_slice_tags(api):
663     tmp = []
664     for tag in api.plshell.GetSliceTags(api.plauth):
665         t = tag['slice_tag_id'], Slicetag(tag)
666         tmp.append(t)
667     return dict(tmp)
668     
669 """
670 Find a free EGRE key
671 """
672 def free_egre_key(slicetags):
673     used = set()
674     for i in slicetags:
675         tag = slicetags[i]
676         if tag.tagname == 'egre_key':
677             used.add(int(tag.value))
678                 
679     for i in range(1, 256):
680         if i not in used:
681             key = i
682             break
683     else:
684         raise KeyError("No more EGRE keys available")
685         
686     return "%s" % key
687