7300fd70435a41cfde5a11f2d573db9ca16eb3e7
[sfa.git] / sfa / plc / network.py
1 from __future__ import with_statement
2 import re
3 import socket
4 from sfa.util.namespace import *
5 from sfa.util.faults import *
6 from xmlbuilder import XMLBuilder
7 from lxml import etree
8 import sys
9 from StringIO import StringIO
10
11 # Taken from bwlimit.py
12 #
13 # See tc_util.c and http://physics.nist.gov/cuu/Units/binary.html. Be
14 # warned that older versions of tc interpret "kbps", "mbps", "mbit",
15 # and "kbit" to mean (in this system) "kibps", "mibps", "mibit", and
16 # "kibit" and that if an older version is installed, all rates will
17 # be off by a small fraction.
18 suffixes = {
19     "":         1,
20     "bit":      1,
21     "kibit":    1024,
22     "kbit":     1000,
23     "mibit":    1024*1024,
24     "mbit":     1000000,
25     "gibit":    1024*1024*1024,
26     "gbit":     1000000000,
27     "tibit":    1024*1024*1024*1024,
28     "tbit":     1000000000000,
29     "bps":      8,
30     "kibps":    8*1024,
31     "kbps":     8000,
32     "mibps":    8*1024*1024,
33     "mbps":     8000000,
34     "gibps":    8*1024*1024*1024,
35     "gbps":     8000000000,
36     "tibps":    8*1024*1024*1024*1024,
37     "tbps":     8000000000000
38 }
39
40
41 def get_tc_rate(s):
42     """
43     Parses an integer or a tc rate string (e.g., 1.5mbit) into bits/second
44     """
45
46     if type(s) == int:
47         return s
48     m = re.match(r"([0-9.]+)(\D*)", s)
49     if m is None:
50         return -1
51     suffix = m.group(2).lower()
52     if suffixes.has_key(suffix):
53         return int(float(m.group(1)) * suffixes[suffix])
54     else:
55         return -1
56
57 def format_tc_rate(rate):
58     """
59     Formats a bits/second rate into a tc rate string
60     """
61
62     if rate >= 1000000000 and (rate % 1000000000) == 0:
63         return "%.0fgbit" % (rate / 1000000000.)
64     elif rate >= 1000000 and (rate % 1000000) == 0:
65         return "%.0fmbit" % (rate / 1000000.)
66     elif rate >= 1000:
67         return "%.0fkbit" % (rate / 1000.)
68     else:
69         return "%.0fbit" % rate 
70
71 class Sliver:
72     def __init__(self, node):
73         self.node = node
74         self.network = node.network
75         self.slice = node.network.slice
76         self.vsys_tags = []
77         
78     def read_from_tags(self):
79         self.vsys_tags = self.slice.get_tags("vsys", self.node)
80
81     def write_to_tags(self):
82         pass
83         
84     def toxml(self, xml):
85         with xml.sliver:
86             for tag in self.vsys_tags:
87                 with xml.vsys:
88                     xml << tag.value
89
90         
91 class Iface:
92     def __init__(self, network, iface):
93         self.network = network
94         self.id = iface['interface_id']
95         self.idtag = "i%s" % self.id
96         self.ipv4 = iface['ip']
97         self.bwlimit = iface['bwlimit']
98         self.hostname = iface['hostname']
99
100     """
101     Just print out bwlimit right now
102     """
103     def toxml(self, xml):
104         if self.bwlimit:
105             with xml.bw_limit:
106                 xml << format_tc_rate(self.bwlimit)
107
108
109 class Node:
110     def __init__(self, network, node, bps = 1000 * 1000000):
111         self.network = network
112         self.id = node['node_id']
113         self.idtag = "n%s" % self.id
114         self.hostname = node['hostname']
115         self.name = self.shortname = self.hostname.replace('.vini-veritas.net', '')
116         self.site_id = node['site_id']
117         #self.ipaddr = socket.gethostbyname(self.hostname)
118         self.bps = bps
119         self.links = set()
120         self.iface_ids = node['interface_ids']
121         self.iface_ids.sort()
122         self.sliver = None
123         self.whitelist = node['slice_ids_whitelist']
124
125     def get_link_id(self, remote):
126         if self.id < remote.id:
127             link = (self.id<<7) + remote.id
128         else:
129             link = (remote.id<<7) + self.id
130         return link
131         
132     def get_iface_id(self, remote):
133         if self.id < remote.id:
134             iface = 1
135         else:
136             iface = 2
137         return iface
138     
139     def get_ifaces(self):
140         i = []
141         for id in self.iface_ids:
142             i.append(self.network.lookupIface(id))
143             # Only return the first interface
144             break
145         return i
146         
147     def get_virt_ip(self, remote):
148         link = self.get_link_id(remote)
149         iface = self.get_iface_id(remote)
150         first = link >> 6
151         second = ((link & 0x3f)<<2) + iface
152         return "192.168.%d.%d" % (first, second)
153
154     def get_virt_net(self, remote):
155         link = self.get_link_id(remote)
156         first = link >> 6
157         second = (link & 0x3f)<<2
158         return "192.168.%d.%d/30" % (first, second)
159         
160     def get_site(self):
161         return self.network.lookupSite(self.site_id)
162     
163     def get_topo_rspec(self, link):
164         if link.end1 == self:
165             remote = link.end2
166         elif link.end2 == self:
167             remote = link.end1
168         else:
169             raise Error("Link does not connect to Node")
170             
171         my_ip = self.get_virt_ip(remote)
172         remote_ip = remote.get_virt_ip(self)
173         net = self.get_virt_net(remote)
174         bw = format_tc_rate(link.bps)
175         return (remote.id, remote.ipaddr, bw, my_ip, remote_ip, net)
176         
177     def add_link(self, link):
178         self.links.add(link)
179         
180     # Assumes there is at most one Link between two sites
181     def get_sitelink(self, node, sites):
182         site1 = sites[self.site_id]
183         site2 = sites[node.site_id]
184         sl = site1.links.intersection(site2.links)
185         if len(sl):
186             return sl.pop()
187         return None
188
189     def add_sliver(self):
190         self.sliver = Sliver(self)
191
192     def toxml(self, xml):
193         slice = self.network.slice
194         if self.whitelist and not self.sliver:
195             if not slice or slice.id not in self.whitelist:
196                 return
197
198         with xml.node(id = self.idtag):
199             with xml.hostname:
200                 xml << self.hostname
201             if self.network.type == "VINI":
202                 with xml.kbps:
203                     xml << str(int(self.bps/1000))
204             for iface in self.get_ifaces():
205                 iface.toxml(xml)
206             if self.sliver:
207                 self.sliver.toxml(xml)
208     
209
210 class Link:
211     def __init__(self, end1, end2, bps = 1000000000, parent = None):
212         self.end1 = end1
213         self.end2 = end2
214         self.bps = bps
215         self.parent = parent
216         self.children = []
217
218         end1.add_link(self)
219         end2.add_link(self)
220         
221         if self.parent:
222             self.parent.children.append(self)
223             
224     def toxml(self, xml):
225         end_ids = "%s %s" % (self.end1.idtag, self.end2.idtag)
226
227         if self.parent:
228             element = xml.vlink(endpoints=end_ids)
229         else:
230             element = xml.link(endpoints=end_ids)
231
232         with element:
233             with xml.description:
234                 xml << "%s -- %s" % (self.end1.name, self.end2.name)
235             with xml.kbps:
236                 xml << str(int(self.bps/1000))
237             for child in self.children:
238                 child.toxml(xml)
239         
240
241 class Site:
242     def __init__(self, network, site):
243         self.network = network
244         self.id = site['site_id']
245         self.idtag = "s%s" % self.id
246         self.node_ids = site['node_ids']
247         self.node_ids.sort()
248         self.name = site['abbreviated_name']
249         self.tag = site['login_base']
250         self.public = site['is_public']
251         self.enabled = site['enabled']
252         self.links = set()
253         self.whitelist = False
254
255     def get_sitenodes(self):
256         n = []
257         for i in self.node_ids:
258             n.append(self.network.lookupNode(i))
259         return n
260     
261     def add_link(self, link):
262         self.links.add(link)
263
264     def toxml(self, xml):
265         if not (self.public and self.enabled and self.node_ids):
266             return
267         with xml.site(id = self.idtag):
268             with xml.name:
269                 xml << self.name
270                 
271             for node in self.get_sitenodes():
272                 node.toxml(xml)
273    
274     
275 class Slice:
276     def __init__(self, network, hrn, slice):
277         self.hrn = hrn
278         self.network = network
279         self.id = slice['slice_id']
280         self.name = slice['name']
281         self.node_ids = set(slice['node_ids'])
282         self.slice_tag_ids = slice['slice_tag_ids']
283         self.peer_id = slice['peer_slice_id']
284     
285     """
286     Use with tags that can have more than one instance
287     """
288     def get_tags(self, tagname, node = None):
289         tags = []
290         for i in self.slice_tag_ids:
291             tag = self.network.lookupSliceTag(i)
292             if tag.tagname == tagname:
293                 if not (tag.node_id and node and node.id != tag.node_id):
294                     tags.append(tag)
295         return tags
296         
297     """
298     Use with tags that have only one instance
299     """
300     def get_tag(self, tagname, node = None):
301         for i in self.slice_tag_ids:
302             tag = self.network.lookupSliceTag(i)
303             if tag.tagname == tagname:
304                 if (not node) or (node.id == tag.node_id):
305                     return tag
306         return None
307         
308     def get_nodes(self):
309         n = []
310         for id in self.node_ids:
311             n.append(self.network.nodes[id])
312         return n
313   
314     # Add a new slice tag   
315     def add_tag(self, tagname, value, node = None):
316         record = {'slice_tag_id':None, 'slice_id':self.id, 'tagname':tagname, 'value':value}
317         if node:
318             record['node_id'] = node.id
319         else:
320             record['node_id'] = None
321         tag = Slicetag(record)
322         self.network.slicetags[tag.id] = tag
323         self.slice_tag_ids.append(tag.id)
324         tag.changed = True       
325         tag.updated = True
326         return tag
327     
328     # Update a slice tag if it exists, else add it             
329     def update_tag(self, tagname, value, node = None):
330         tag = self.get_tag(tagname, node)
331         if tag and tag.value == value:
332             value = "no change"
333         elif tag:
334             tag.value = value
335             tag.changed = True
336         else:
337             tag = self.add_tag(tagname, value, node)
338         tag.updated = True
339             
340     """
341     Find a free EGRE key
342     """
343     def new_egre_key():
344         slicetags = self.network.slicetags
345         used = set()
346         for i in slicetags:
347             tag = slicetags[i]
348             if tag.tagname == 'egre_key':
349                 used.add(int(tag.value))
350                 
351         for i in range(1, 256):
352             if i not in used:
353                 key = i
354                 break
355         else:
356             raise KeyError("No more EGRE keys available")
357         
358         return "%s" % key
359    
360
361     def assign_egre_key(self):
362         if not self.get_tag('egre_key'):
363             try:
364                 key = self.new_egre_key()
365                 self.update_tag('egre_key', key)
366             except:
367                 # Should handle this case...
368                 pass
369         return
370             
371     def turn_on_netns(self):
372         tag = self.get_tag('netns')
373         if (not tag) or (tag.value != '1'):
374             self.update_tag('netns', '1')
375         return
376    
377     def turn_off_netns(self):
378         tag = self.get_tag('netns')
379         if tag and (tag.value != '0'):
380             tag.delete()
381         return
382     
383     def add_cap_net_admin(self):
384         tag = self.get_tag('capabilities')
385         if tag:
386             caps = tag.value.split(',')
387             for cap in caps:
388                 if cap == "CAP_NET_ADMIN":
389                     return
390             else:
391                 newcaps = "CAP_NET_ADMIN," + tag.value
392                 self.update_tag('capabilities', newcaps)
393         else:
394             self.add_tag('capabilities', 'CAP_NET_ADMIN')
395         return
396     
397     def remove_cap_net_admin(self):
398         tag = self.get_tag('capabilities')
399         if tag:
400             caps = tag.value.split(',')
401             newcaps = []
402             for cap in caps:
403                 if cap != "CAP_NET_ADMIN":
404                     newcaps.append(cap)
405             if newcaps:
406                 value = ','.join(newcaps)
407                 self.update_tag('capabilities', value)
408             else:
409                 tag.delete()
410         return
411
412     # Update the vsys/setup-link and vsys/setup-nat slice tags.
413     def add_vsys_tags(self):
414         link = nat = False
415         for i in self.slice_tag_ids:
416             tag = self.network.lookupSliceTag(i)
417             if tag.tagname == 'vsys':
418                 if tag.value == 'setup-link':
419                     link = True
420                 elif tag.value == 'setup-nat':
421                     nat = True
422         if not link:
423             self.add_tag('vsys', 'setup-link')
424         if not nat:
425             self.add_tag('vsys', 'setup-nat')
426         return
427
428
429 class Slicetag:
430     newid = -1 
431     def __init__(self, tag):
432         self.id = tag['slice_tag_id']
433         if not self.id:
434             # Make one up for the time being...
435             self.id = Slicetag.newid
436             Slicetag.newid -= 1
437         self.slice_id = tag['slice_id']
438         self.tagname = tag['tagname']
439         self.value = tag['value']
440         self.node_id = tag['node_id']
441         self.updated = False
442         self.changed = False
443         self.deleted = False
444     
445     # Mark a tag as deleted
446     def delete(self):
447         self.deleted = True
448         self.updated = True
449     
450     def write(self, api):
451         if self.changed:
452             if int(self.id) > 0:
453                 api.plshell.UpdateSliceTag(api.plauth, self.id, self.value)
454             else:
455                 api.plshell.AddSliceTag(api.plauth, self.slice_id, 
456                                         self.tagname, self.value, self.node_id)
457         elif self.deleted and int(self.id) > 0:
458             api.plshell.DeleteSliceTag(api.plauth, self.id)
459
460
461 """
462 A Network is a compound object consisting of:
463 * a dictionary mapping site IDs to Site objects
464 * a dictionary mapping node IDs to Node objects
465 * a dictionary mapping interface IDs to Iface objects
466 * the Site objects are connected via Link objects representing
467   the physical topology and available bandwidth
468 * the Node objects are connected via Link objects representing
469   the requested or assigned virtual topology of a slice
470 """
471 class Network:
472     def __init__(self, api, type = "PlanetLab", physical_links = [], 
473                  schema = None):
474         self.api = api
475         self.type = type
476         self.sites = self.get_sites(api)
477         self.nodes = self.get_nodes(api)
478         self.ifaces = self.get_ifaces(api)
479         self.tags = self.get_slice_tags(api)
480         self.slice = None
481         self.sitelinks = []
482         self.nodelinks = []
483         self.schema = schema
484     
485         for (s1, s2) in physical_links:
486             self.sitelinks.append(Link(self.sites[s1], self.sites[s2]))
487         
488         for t in self.tags:
489             tag = self.tags[t]
490             if tag.tagname == 'topo_rspec':
491                 node1 = self.nodes[tag.node_id]
492                 l = eval(tag.value)
493                 for (id, realip, bw, lvip, rvip, vnet) in l:
494                     allocbps = get_tc_rate(bw)
495                     node1.bps -= allocbps
496                     try:
497                         node2 = self.nodes[id]
498                         if node1.id < node2.id:
499                             sl = node1.get_sitelink(node2, self.sites)
500                             sl.bps -= allocbps
501                     except:
502                         pass
503
504     
505     """ Lookup site based on id or idtag value """
506     def lookupSite(self, id):
507         val = None
508         if isinstance(id, basestring):
509             id = int(id.lstrip('s'))
510         try:
511             val = self.sites[id]
512         except:
513             raise KeyError("site ID %s not found" % id)
514         return val
515     
516     def getSites(self):
517         sites = []
518         for s in self.sites:
519             sites.append(self.sites[s])
520         return sites
521         
522     """ Lookup node based on id or idtag value """
523     def lookupNode(self, id):
524         val = None
525         if isinstance(id, basestring):
526             id = int(id.lstrip('n'))
527         try:
528             val = self.nodes[id]
529         except:
530             raise KeyError("node ID %s not found" % id)
531         return val
532     
533     def getNodes(self):
534         nodes = []
535         for n in self.nodes:
536             nodes.append(self.nodes[n])
537         return nodes
538     
539     """ Lookup iface based on id or idtag value """
540     def lookupIface(self, id):
541         val = None
542         if isinstance(id, basestring):
543             id = int(id.lstrip('i'))
544         try:
545             val = self.ifaces[id]
546         except:
547             raise KeyError("interface ID %s not found" % id)
548         return val
549     
550     def getIfaces(self):
551         ifaces = []
552         for i in self.ifaces:
553             ifaces.append(self.ifaces[i])
554         return ifaces
555     
556     def nodesWithSlivers(self):
557         nodes = []
558         for n in self.nodes:
559             node = self.nodes[n]
560             if node.sliver:
561                 nodes.append(node)
562         return nodes
563             
564     def lookupSliceTag(self, id):
565         val = None
566         try:
567             val = self.tags[id]
568         except:
569             raise KeyError("slicetag ID %s not found" % id)
570         return val
571     
572     def getSliceTags(self):
573         tags = []
574         for t in self.tags:
575             tags.append(self.tags[t])
576         return tags
577     
578     def lookupSiteLink(self, node1, node2):
579         site1 = self.sites[node1.site_id]
580         site2 = self.sites[node2.site_id]
581         for link in self.sitelinks:
582             if site1 == link.end1 and site2 == link.end2:
583                 return link
584             if site2 == link.end1 and site1 == link.end2:
585                 return link
586         return None
587     
588
589     def __add_vlink(self, vlink, slicenodes, parent = None):
590         n1 = n2 = None
591         endpoints = vlink.get("endpoints")
592         if endpoints:
593             (end1, end2) = endpoints.split()
594             n1 = self.lookupNode(end1)
595             n2 = self.lookupNode(end2)
596         elif parent:
597             """ Try to infer the endpoints for the virtual link """
598             site_endpoints = parent.get("endpoints")
599             (n1, n2) = self.__infer_endpoints(site_endpoints, slicenodes)
600         else:
601             raise Error("no endpoints given")
602
603         #print "Added virtual link: %s -- %s" % (n1.tag, n2.tag)
604         bps = int(vlink.findtext("kbps")) * 1000
605         sitelink = self.lookupSiteLink(n1, n2)
606         if not sitelink:
607             raise PermissionError("nodes %s and %s not adjacent" % 
608                                   (n1.idtag, n2.idtag))
609         self.nodelinks.append(Link(n1, n2, bps, sitelink))
610         return
611
612     """ 
613     Infer the endpoints of the virtual link.  If the slice exists on 
614     only a single node at each end of the physical link, we'll assume that
615     the user wants the virtual link to terminate at these nodes.
616     """
617     def __infer_endpoints(self, endpoints, slicenodes):
618         n = []
619         ends = endpoints.split()
620         for end in ends:
621             found = 0
622             site = self.lookupSite(end)
623             for id in site.node_ids:
624                 if id in slicenodes:
625                     n.append(slicenodes[id])
626                     found += 1
627             if found != 1:
628                 raise Error("could not infer endpoint for site %s" % site.id)
629         #print "Inferred endpoints: %s %s" % (n[0].idtag, n[1].idtag)
630         return n
631         
632     def annotateFromRSpec(self, xml):
633         if self.nodelinks:
634             raise Error("virtual topology already present")
635             
636         nodedict = {}
637         for node in self.getNodes():
638             nodedict[node.idtag] = node
639             
640         slicenodes = {}
641
642         tree = etree.parse(StringIO(xml))
643
644         if self.schema:
645             # Validate the incoming request against the RelaxNG schema
646             relaxng_doc = etree.parse(self.schema)
647             relaxng = etree.RelaxNG(relaxng_doc)
648         
649             if not relaxng(tree):
650                 error = relaxng.error_log.last_error
651                 message = "%s (line %s)" % (error.message, error.line)
652                 raise InvalidRSpec(message)
653
654         rspec = tree.getroot()
655
656         """
657         Handle requests where the user has annotated a description of the
658         physical resources (nodes and links) with virtual ones (slivers
659         and vlinks).
660         """
661         # Find slivers under node elements
662         for sliver in rspec.iterfind("./network/site/node/sliver"):
663             elem = sliver.getparent()
664             node = nodedict[elem.get("id")]
665             slicenodes[node.id] = node
666             node.add_sliver()
667
668         # Find vlinks under link elements
669         for vlink in rspec.iterfind("./network/link/vlink"):
670             link = vlink.getparent()
671             self.__add_vlink(vlink, slicenodes, link)
672
673         """
674         Handle requests where the user has listed the virtual resources only
675         """
676         # Find slivers that specify nodeid
677         for sliver in rspec.iterfind("./request/sliver[@nodeid]"):
678             node = nodedict[sliver.get("nodeid")]
679             slicenodes[node.id] = node
680             node.add_sliver()
681
682         # Find vlinks that specify endpoints
683         for vlink in rspec.iterfind("./request/vlink[@endpoints]"):
684             self.__add_vlink(vlink, slicenodes)
685
686         return
687
688     def annotateFromSliceTags(self):
689         slice = self.slice
690         if not slice:
691             raise Error("no slice associated with network")
692
693         if self.nodelinks:
694             raise Error("virtual topology already present")
695             
696         for node in slice.get_nodes():
697             node.add_sliver()
698             node.sliver.read_from_tags()
699
700             linktag = slice.get_tag('topo_rspec', node)
701             if linktag:
702                 l = eval(linktag.value)
703                 for (id, realip, bw, lvip, rvip, vnet) in l:
704                     if node.id < id:
705                         bps = get_tc_rate(bw)
706                         remote = self.lookupNode(id)
707                         sitelink = self.lookupSiteLink(node, remote)
708                         self.nodelinks.append(Link(node,remote,bps,sitelink))
709
710     def updateSliceTags(self, slice):
711         if not self.nodelinks:
712             return
713
714         """  Comment this out for right now
715         slice.update_tag('vini_topo', 'manual', self.tags)
716         slice.assign_egre_key(self.tags)
717         slice.turn_on_netns(self.tags)
718         slice.add_cap_net_admin(self.tags)
719
720         for node in slice.get_nodes(self.nodes):
721             linkdesc = []
722             for link in node.links:
723                 linkdesc.append(node.get_topo_rspec(link))
724             if linkdesc:
725                 topo_str = "%s" % linkdesc
726                 slice.update_tag('topo_rspec', topo_str, self.tags, node)
727
728         # Update slice tags in database
729         for tag in self.getSliceTags():
730             if tag.slice_id == slice.id:
731                 if tag.tagname == 'topo_rspec' and not tag.updated:
732                     tag.delete()
733                 tag.write(self.api)
734         """
735
736                 
737     """
738     Check the requested topology against the available topology and capacity
739     """
740     def verifyNodeNetwork(self, hrn, topo):
741         for link in self.nodelinks:
742             if link.bps <= 0:
743                 raise GeniInvalidArgument(bw, "BW")
744                 
745             n1 = link.end1
746             n2 = link.end2
747             sitelink = self.lookupSiteLink(n1, n2)
748             if not sitelink:
749                 raise PermissionError("%s: nodes %s and %s not adjacent" % (hrn, n1.tag, n2.tag))
750             if sitelink.bps < link.bps:
751                 raise PermissionError("%s: insufficient capacity between %s and %s" % (hrn, n1.tag, n2.tag))
752                 
753     """
754     Produce XML directly from the topology specification.
755     """
756     def toxml(self):
757         xml = XMLBuilder(format = True, tab_step = "  ")
758         with xml.RSpec(type=self.type):
759             name = "Public_" + self.type
760             if self.slice:
761                 element = xml.network(name=name, slice=self.slice.hrn)
762             else:
763                 element = xml.network(name=name)
764                 
765             with element:
766                 for site in self.getSites():
767                     site.toxml(xml)
768                 for link in self.sitelinks:
769                     link.toxml(xml)
770
771         header = '<?xml version="1.0"?>\n'
772         return header + str(xml)
773
774     """
775     Create a dictionary of site objects keyed by site ID
776     """
777     def get_sites(self, api):
778         tmp = []
779         for site in api.plshell.GetSites(api.plauth):
780             t = site['site_id'], Site(self, site)
781             tmp.append(t)
782         return dict(tmp)
783
784
785     """
786     Create a dictionary of node objects keyed by node ID
787     """
788     def get_nodes(self, api):
789         tmp = []
790         for node in api.plshell.GetNodes(api.plauth):
791             t = node['node_id'], Node(self, node)
792             tmp.append(t)
793         return dict(tmp)
794
795     """
796     Create a dictionary of node objects keyed by node ID
797     """
798     def get_ifaces(self, api):
799         tmp = []
800         for iface in api.plshell.GetInterfaces(api.plauth):
801             t = iface['interface_id'], Iface(self, iface)
802             tmp.append(t)
803         return dict(tmp)
804
805     """
806     Create a dictionary of slicetag objects keyed by slice tag ID
807     """
808     def get_slice_tags(self, api):
809         tmp = []
810         for tag in api.plshell.GetSliceTags(api.plauth):
811             t = tag['slice_tag_id'], Slicetag(tag)
812             tmp.append(t)
813         return dict(tmp)
814     
815     """
816     Return a Slice object for a single slice
817     """
818     def get_slice(self, api, hrn):
819         slicename = hrn_to_pl_slicename(hrn)
820         slice = api.plshell.GetSlices(api.plauth, [slicename])
821         if slice:
822             self.slice = Slice(self, slicename, slice[0])
823             return self.slice
824         else:
825             return None
826     
827