1 from __future__ import with_statement
4 from sfa.util.faults import *
5 from sfa.managers.vini.topology import PhysicalLinks
6 from xmlbuilder import XMLBuilder
9 from StringIO import StringIO
11 VINI_RELAXNG_SCHEMA = "/var/www/html/schemas/vini.rng"
13 # Taken from bwlimit.py
15 # See tc_util.c and http://physics.nist.gov/cuu/Units/binary.html. Be
16 # warned that older versions of tc interpret "kbps", "mbps", "mbit",
17 # and "kbit" to mean (in this system) "kibps", "mibps", "mibit", and
18 # "kibit" and that if an older version is installed, all rates will
19 # be off by a small fraction.
27 "gibit": 1024*1024*1024,
29 "tibit": 1024*1024*1024*1024,
30 "tbit": 1000000000000,
36 "gibps": 8*1024*1024*1024,
38 "tibps": 8*1024*1024*1024*1024,
45 Parses an integer or a tc rate string (e.g., 1.5mbit) into bits/second
50 m = re.match(r"([0-9.]+)(\D*)", s)
53 suffix = m.group(2).lower()
54 if suffixes.has_key(suffix):
55 return int(float(m.group(1)) * suffixes[suffix])
59 def format_tc_rate(rate):
61 Formats a bits/second rate into a tc rate string
64 if rate >= 1000000000 and (rate % 1000000000) == 0:
65 return "%.0fgbit" % (rate / 1000000000.)
66 elif rate >= 1000000 and (rate % 1000000) == 0:
67 return "%.0fmbit" % (rate / 1000000.)
69 return "%.0fkbit" % (rate / 1000.)
71 return "%.0fbit" % rate
75 def __init__(self, node, bps = 1000 * 1000000):
76 self.id = node['node_id']
77 self.idtag = "n%s" % self.id
78 self.hostname = node['hostname']
79 self.name = self.shortname = self.hostname.replace('.vini-veritas.net', '')
80 self.site_id = node['site_id']
81 self.ipaddr = socket.gethostbyname(self.hostname)
86 def get_link_id(self, remote):
87 if self.id < remote.id:
88 link = (self.id<<7) + remote.id
90 link = (remote.id<<7) + self.id
93 def get_iface_id(self, remote):
94 if self.id < remote.id:
100 def get_virt_ip(self, remote):
101 link = self.get_link_id(remote)
102 iface = self.get_iface_id(remote)
104 second = ((link & 0x3f)<<2) + iface
105 return "192.168.%d.%d" % (first, second)
107 def get_virt_net(self, remote):
108 link = self.get_link_id(remote)
110 second = (link & 0x3f)<<2
111 return "192.168.%d.%d/30" % (first, second)
113 def get_site(self, sites):
114 return sites[self.site_id]
116 def get_topo_rspec(self, link):
117 if link.end1 == self:
119 elif link.end2 == self:
122 raise Error("Link does not connect to Node")
124 my_ip = self.get_virt_ip(remote)
125 remote_ip = remote.get_virt_ip(self)
126 net = self.get_virt_net(remote)
127 bw = format_tc_rate(link.bps)
128 return (remote.id, remote.ipaddr, bw, my_ip, remote_ip, net)
130 def add_link(self, link):
133 def add_tag(self, sites):
134 s = self.get_site(sites)
135 words = self.hostname.split(".")
136 index = words[0].replace("node", "")
138 self.tag = s.tag + index
142 # Assumes there is at most one Link between two sites
143 def get_sitelink(self, node, sites):
144 site1 = sites[self.site_id]
145 site2 = sites[node.site_id]
146 sl = site1.links.intersection(site2.links)
151 def add_sliver(self):
154 def toxml(self, xml, hrn):
157 with xml.node(id = self.idtag):
161 xml << str(int(self.bps/1000))
168 def __init__(self, end1, end2, bps = 1000 * 1000000, parent = None):
179 self.parent.children.append(self)
181 def toxml(self, xml):
182 end_ids = "%s %s" % (self.end1.idtag, self.end2.idtag)
185 element = xml.vlink(endpoints=end_ids)
187 element = xml.link(endpoints=end_ids)
190 with xml.description:
191 xml << "%s -- %s" % (self.end1.name, self.end2.name)
193 xml << str(int(self.bps/1000))
194 for child in self.children:
199 def __init__(self, site):
200 self.id = site['site_id']
201 self.idtag = "s%s" % self.id
202 self.node_ids = site['node_ids']
203 self.name = site['abbreviated_name'].replace(" ", "_")
204 self.tag = site['login_base']
205 self.public = site['is_public']
206 self.enabled = site['enabled']
209 def get_sitenodes(self, nodes):
211 for i in self.node_ids:
215 def add_link(self, link):
218 def toxml(self, xml, hrn, nodes):
219 if not (self.public and self.enabled and self.node_ids):
221 with xml.site(id = self.idtag):
225 for node in self.get_sitenodes(nodes):
230 def __init__(self, slice):
231 self.id = slice['slice_id']
232 self.name = slice['name']
233 self.node_ids = set(slice['node_ids'])
234 self.slice_tag_ids = slice['slice_tag_ids']
236 def get_tag(self, tagname, slicetags, node = None):
237 for i in self.slice_tag_ids:
239 if tag.tagname == tagname:
240 if (not node) or (node.id == tag.node_id):
245 def get_nodes(self, nodes):
247 for id in self.node_ids:
252 # Add a new slice tag
253 def add_tag(self, tagname, value, slicetags, node = None):
254 record = {'slice_tag_id':None, 'slice_id':self.id, 'tagname':tagname, 'value':value}
256 record['node_id'] = node.id
258 record['node_id'] = None
259 tag = Slicetag(record)
260 slicetags[tag.id] = tag
261 self.slice_tag_ids.append(tag.id)
266 # Update a slice tag if it exists, else add it
267 def update_tag(self, tagname, value, slicetags, node = None):
268 tag = self.get_tag(tagname, slicetags, node)
269 if tag and tag.value == value:
275 tag = self.add_tag(tagname, value, slicetags, node)
278 def assign_egre_key(self, slicetags):
279 if not self.get_tag('egre_key', slicetags):
281 key = free_egre_key(slicetags)
282 self.update_tag('egre_key', key, slicetags)
284 # Should handle this case...
288 def turn_on_netns(self, slicetags):
289 tag = self.get_tag('netns', slicetags)
290 if (not tag) or (tag.value != '1'):
291 self.update_tag('netns', '1', slicetags)
294 def turn_off_netns(self, slicetags):
295 tag = self.get_tag('netns', slicetags)
296 if tag and (tag.value != '0'):
300 def add_cap_net_admin(self, slicetags):
301 tag = self.get_tag('capabilities', slicetags)
303 caps = tag.value.split(',')
305 if cap == "CAP_NET_ADMIN":
308 newcaps = "CAP_NET_ADMIN," + tag.value
309 self.update_tag('capabilities', newcaps, slicetags)
311 self.add_tag('capabilities', 'CAP_NET_ADMIN', slicetags)
314 def remove_cap_net_admin(self, slicetags):
315 tag = self.get_tag('capabilities', slicetags)
317 caps = tag.value.split(',')
320 if cap != "CAP_NET_ADMIN":
323 value = ','.join(newcaps)
324 self.update_tag('capabilities', value, slicetags)
329 # Update the vsys/setup-link and vsys/setup-nat slice tags.
330 def add_vsys_tags(self, slicetags):
332 for i in self.slice_tag_ids:
334 if tag.tagname == 'vsys':
335 if tag.value == 'setup-link':
337 elif tag.value == 'setup-nat':
340 self.add_tag('vsys', 'setup-link', slicetags)
342 self.add_tag('vsys', 'setup-nat', slicetags)
348 def __init__(self, tag):
349 self.id = tag['slice_tag_id']
351 # Make one up for the time being...
352 self.id = Slicetag.newid
354 self.slice_id = tag['slice_id']
355 self.tagname = tag['tagname']
356 self.value = tag['value']
357 self.node_id = tag['node_id']
362 # Mark a tag as deleted
367 def write(self, api):
370 api.plshell.UpdateSliceTag(api.plauth, self.id, self.value)
372 api.plshell.AddSliceTag(api.plauth, self.slice_id,
373 self.tagname, self.value, self.node_id)
374 elif self.deleted and int(self.id) > 0:
375 api.plshell.DeleteSliceTag(api.plauth, self.id)
379 A topology is a compound object consisting of:
380 * a dictionary mapping site IDs to Site objects
381 * a dictionary mapping node IDs to Node objects
382 * the Site objects are connected via SiteLink objects representing
383 the physical topology and available bandwidth
384 * the Node objects are connected via Link objects representing
385 the requested or assigned virtual topology of a slice
388 def __init__(self, api):
390 self.sites = get_sites(api)
391 self.nodes = get_nodes(api)
392 self.tags = get_slice_tags(api)
396 for (s1, s2) in PhysicalLinks:
397 self.sitelinks.append(Link(self.sites[s1], self.sites[s2]))
399 for id in self.nodes:
400 self.nodes[id].add_tag(self.sites)
404 if tag.tagname == 'topo_rspec':
405 node1 = self.nodes[tag.node_id]
407 for (id, realip, bw, lvip, rvip, vnet) in l:
408 allocbps = get_tc_rate(bw)
409 node1.bps -= allocbps
411 node2 = self.nodes[id]
412 if node1.id < node2.id:
413 sl = node1.get_sitelink(node2, self.sites)
419 """ Lookup site based on id or idtag value """
420 def lookupSite(self, id):
422 if isinstance(id, basestring):
423 id = int(id.lstrip('s'))
427 raise KeyError("site ID %s not found" % id)
433 sites.append(self.sites[s])
436 """ Lookup node based on id or idtag value """
437 def lookupNode(self, id):
439 if isinstance(id, basestring):
440 id = int(id.lstrip('n'))
444 raise KeyError("node ID %s not found" % id)
450 nodes.append(self.nodes[n])
453 def nodesInTopo(self):
461 def lookupSliceTag(self, id):
466 raise KeyError("slicetag ID %s not found" % id)
469 def getSliceTags(self):
472 tags.append(self.tags[t])
475 def lookupSiteLink(self, node1, node2):
476 site1 = self.sites[node1.site_id]
477 site2 = self.sites[node2.site_id]
478 for link in self.sitelinks:
479 if site1 == link.end1 and site2 == link.end2:
481 if site2 == link.end1 and site1 == link.end2:
486 def __add_vlink(self, vlink, slicenodes, parent = None):
488 endpoints = vlink.get("endpoints")
490 (end1, end2) = endpoints.split()
491 n1 = self.lookupNode(end1)
492 n2 = self.lookupNode(end2)
494 """ Try to infer the endpoints for the virtual link """
495 site_endpoints = parent.get("endpoints")
496 (n1, n2) = self.__infer_endpoints(site_endpoints, slicenodes)
498 raise Error("no endpoints given")
500 #print "Added virtual link: %s -- %s" % (n1.tag, n2.tag)
501 bps = int(vlink.findtext("kbps")) * 1000
502 sitelink = self.lookupSiteLink(n1, n2)
504 raise PermissionError("nodes %s and %s not adjacent" %
505 (n1.idtag, n2.idtag))
506 self.nodelinks.append(Link(n1, n2, bps, sitelink))
510 Infer the endpoints of the virtual link. If the slice exists on
511 only a single node at each end of the physical link, we'll assume that
512 the user wants the virtual link to terminate at these nodes.
514 def __infer_endpoints(self, endpoints, slicenodes):
516 ends = endpoints.split()
519 site = self.lookupSite(end)
520 for id in site.node_ids:
522 n.append(slicenodes[id])
525 raise Error("could not infer endpoint for site %s" % site.id)
526 #print "Inferred endpoints: %s %s" % (n[0].idtag, n[1].idtag)
529 def nodeTopoFromRSpec(self, xml):
531 raise Error("virtual topology already present")
534 for node in self.getNodes():
535 nodedict[node.idtag] = node
539 tree = etree.parse(StringIO(xml))
541 # Validate the incoming request against the RelaxNG schema
542 relaxng_doc = etree.parse(VINI_RELAXNG_SCHEMA)
543 relaxng = etree.RelaxNG(relaxng_doc)
545 if not relaxng(tree):
546 error = relaxng.error_log.last_error
547 message = "%s (line %s)" % (error.message, error.line)
548 raise InvalidRSpec(message)
550 rspec = tree.getroot()
553 Handle requests where the user has annotated a description of the
554 physical resources (nodes and links) with virtual ones (slivers
557 # Find slivers under node elements
558 for sliver in rspec.iterfind("./network/site/node/sliver"):
559 elem = sliver.getparent()
560 node = nodedict[elem.get("id")]
561 slicenodes[node.id] = node
564 # Find vlinks under link elements
565 for vlink in rspec.iterfind("./network/link/vlink"):
566 link = vlink.getparent()
567 self.__add_vlink(vlink, slicenodes, link)
570 Handle requests where the user has listed the virtual resources only
572 # Find slivers that specify nodeid
573 for sliver in rspec.iterfind("./request/sliver[@nodeid]"):
574 node = nodedict[sliver.get("nodeid")]
575 slicenodes[node.id] = node
578 # Find vlinks that specify endpoints
579 for vlink in rspec.iterfind("./request/vlink[@endpoints]"):
580 self.__add_vlink(vlink, slicenodes)
584 def nodeTopoFromSliceTags(self, slice):
586 raise Error("virtual topology already present")
588 for node in slice.get_nodes(self.nodes):
590 linktag = slice.get_tag('topo_rspec', self.tags, node)
592 l = eval(linktag.value)
593 for (id, realip, bw, lvip, rvip, vnet) in l:
595 bps = get_tc_rate(bw)
596 remote = self.lookupNode(id)
597 sitelink = self.lookupSiteLink(node, remote)
598 self.nodelinks.append(Link(node,remote,bps,sitelink))
600 def updateSliceTags(self, slice):
601 if not self.nodelinks:
604 slice.update_tag('vini_topo', 'manual', self.tags)
605 slice.assign_egre_key(self.tags)
606 slice.turn_on_netns(self.tags)
607 slice.add_cap_net_admin(self.tags)
609 for node in slice.get_nodes(self.nodes):
611 for link in node.links:
612 linkdesc.append(node.get_topo_rspec(link))
614 topo_str = "%s" % linkdesc
615 slice.update_tag('topo_rspec', topo_str, self.tags, node)
617 # Update slice tags in database
618 for tag in self.getSliceTags():
619 if tag.slice_id == slice.id:
620 if tag.tagname == 'topo_rspec' and not tag.updated:
625 Check the requested topology against the available topology and capacity
627 def verifyNodeTopo(self, hrn, topo):
628 for link in self.nodelinks:
630 raise GeniInvalidArgument(bw, "BW")
634 sitelink = self.lookupSiteLink(n1, n2)
636 raise PermissionError("%s: nodes %s and %s not adjacent" % (hrn, n1.tag, n2.tag))
637 if sitelink.bps < link.bps:
638 raise PermissionError("%s: insufficient capacity between %s and %s" % (hrn, n1.tag, n2.tag))
641 Produce XML directly from the topology specification.
643 def toxml(self, hrn = None):
644 xml = XMLBuilder(format = True, tab_step = " ")
645 with xml.RSpec(type="VINI"):
647 element = xml.network(name="Public_VINI", slice=hrn)
649 element = xml.network(name="Public_VINI")
652 for site in self.getSites():
653 site.toxml(xml, hrn, self.nodes)
654 for link in self.sitelinks:
657 header = '<?xml version="1.0"?>\n'
658 return header + str(xml)
661 Create a dictionary of site objects keyed by site ID
665 for site in api.plshell.GetSites(api.plauth):
666 t = site['site_id'], Site(site)
672 Create a dictionary of node objects keyed by node ID
676 for node in api.plshell.GetNodes(api.plauth):
677 t = node['node_id'], Node(node)
682 Create a dictionary of slice objects keyed by slice ID
684 def get_slice(api, slicename):
685 slice = api.plshell.GetSlices(api.plauth, [slicename])
687 return Slice(slice[0])
692 Create a dictionary of slicetag objects keyed by slice tag ID
694 def get_slice_tags(api):
696 for tag in api.plshell.GetSliceTags(api.plauth):
697 t = tag['slice_tag_id'], Slicetag(tag)
704 def free_egre_key(slicetags):
708 if tag.tagname == 'egre_key':
709 used.add(int(tag.value))
711 for i in range(1, 256):
716 raise KeyError("No more EGRE keys available")