36d8bd04ac43cf58484edd8906b8203d829390be
[sfa.git] / sfa / planetlab / pldriver.py
1 import datetime
2 #
3 from sfa.util.faults import MissingSfaInfo, UnknownSfaType, \
4     RecordNotFound, SfaNotImplemented, SliverDoesNotExist, SearchFailed, \
5     UnsupportedOperation, Forbidden 
6 from sfa.util.sfalogging import logger
7 from sfa.util.defaultdict import defaultdict
8 from sfa.util.sfatime import utcparse, datetime_to_string, datetime_to_epoch
9 from sfa.util.xrn import Xrn, hrn_to_urn, get_leaf
10 from sfa.util.cache import Cache
11
12 # one would think the driver should not need to mess with the SFA db, but..
13 from sfa.storage.model import RegRecord, SliverAllocation
14 from sfa.trust.credential import Credential
15
16 # used to be used in get_ticket
17 #from sfa.trust.sfaticket import SfaTicket
18 from sfa.rspecs.version_manager import VersionManager
19 from sfa.rspecs.rspec import RSpec
20
21 # the driver interface, mostly provides default behaviours
22 from sfa.managers.driver import Driver
23 from sfa.planetlab.plshell import PlShell
24 from sfa.planetlab.plaggregate import PlAggregate
25 from sfa.planetlab.plslices import PlSlices
26 from sfa.planetlab.plxrn import PlXrn, slicename_to_hrn, hostname_to_hrn, hrn_to_pl_slicename, top_auth, hash_loginbase
27
28
29 def list_to_dict(recs, key):
30     """
31     convert a list of dictionaries into a dictionary keyed on the 
32     specified dictionary key 
33     """
34     return { rec[key] : rec for rec in recs }
35
36 #
37 # PlShell is just an xmlrpc serverproxy where methods
38 # can be sent as-is; it takes care of authentication
39 # from the global config
40
41 class PlDriver (Driver):
42
43     # the cache instance is a class member so it survives across incoming requests
44     cache = None
45
46     def __init__ (self, api):
47         Driver.__init__ (self, api)
48         config = api.config
49         self.shell = PlShell (config)
50         self.cache = None
51         if config.SFA_AGGREGATE_CACHING:
52             if PlDriver.cache is None:
53                 PlDriver.cache = Cache()
54             self.cache = PlDriver.cache
55
56     def sliver_to_slice_xrn(self, xrn):
57         sliver_id_parts = Xrn(xrn).get_sliver_id_parts()
58         filter = {'peer_id': None}
59         try:
60             filter['slice_id'] = int(sliver_id_parts[0])
61         except ValueError:
62             filter['name'] = sliver_id_parts[0] 
63         slices = self.shell.GetSlices(filter, ['hrn'])
64         if not slices:
65             raise Forbidden("Unable to locate slice record for sliver:  {}".format(xrn))
66         slice = slices[0]
67         slice_xrn = slice['hrn']
68         return slice_xrn 
69  
70     def check_sliver_credentials(self, creds, urns):
71         # build list of cred object hrns
72         slice_cred_names = []
73         for cred in creds:
74             slice_cred_hrn = Credential(cred=cred).get_gid_object().get_hrn() 
75             top_auth_hrn = top_auth(slice_cred_hrn)
76             site_hrn = '.'.join(slice_cred_hrn.split('.')[:-1])
77             slice_part = slice_cred_hrn.split('.')[-1]
78             if top_auth_hrn == self.hrn:
79                 login_base = slice_cred_hrn.split('.')[-2][:12]
80             else:
81                 login_base = hash_loginbase(site_hrn)
82
83             slicename = '_'.join([login_base, slice_part])   
84             slice_cred_names.append(slicename) 
85
86         # look up slice name of slivers listed in urns arg
87         slice_ids = []
88         for urn in urns:
89             sliver_id_parts = Xrn(xrn=urn).get_sliver_id_parts()
90             try:
91                 slice_ids.append(int(sliver_id_parts[0]))
92             except ValueError: 
93                 pass
94
95         if not slice_ids:
96              raise Forbidden("sliver urn not provided")
97
98         slices = self.shell.GetSlices(slice_ids)
99         sliver_names = [slice['name'] for slice in slices]
100
101         # make sure we have a credential for every specified sliver ierd
102         for sliver_name in sliver_names:
103             if sliver_name not in slice_cred_names:
104                 msg = "Valid credential not found for target: {}".format(sliver_name)
105                 raise Forbidden(msg)
106
107     ########################################
108     ########## registry oriented
109     ########################################
110
111     def augment_records_with_testbed_info (self, sfa_records):
112         return self.fill_record_info (sfa_records)
113
114     ########## 
115     def register (self, sfa_record, hrn, pub_key):
116         type = sfa_record['type']
117         pl_record = self.sfa_fields_to_pl_fields(type, hrn, sfa_record)
118
119         if type == 'authority':
120             sites = self.shell.GetSites({'peer_id': None, 'login_base': pl_record['login_base']})
121             if not sites:
122                 # xxx when a site gets registered through SFA we need to set its max_slices
123                 if 'max_slices' not in pl_record:
124                     pl_record['max_slices'] = 2
125                 pointer = self.shell.AddSite(pl_record)
126                 self.shell.SetSiteHrn(int(pointer), hrn)
127             else:
128                 pointer = sites[0]['site_id']
129
130         elif type == 'slice':
131             acceptable_fields = ['url', 'instantiation', 'name', 'description']
132             for key in pl_record.keys():
133                 if key not in acceptable_fields:
134                     pl_record.pop(key)
135             slices = self.shell.GetSlices({'peer_id': None, 'name': pl_record['name']})
136             if not slices:
137                  if not pl_record.get('url', None) or not pl_record.get('description', None):
138                      pl_record['url'] = hrn
139                      pl_record['description'] = hrn
140
141                  pointer = self.shell.AddSlice(pl_record)
142                  self.shell.SetSliceHrn(int(pointer), hrn)
143             else:
144                  pointer = slices[0]['slice_id']
145
146         elif type == 'user':
147             persons = self.shell.GetPersons({'peer_id': None, 'email': sfa_record['email']})
148             if not persons:
149                 for key in ['first_name','last_name']:
150                     if key not in sfa_record:
151                         sfa_record[key] = '*from*sfa*'
152                 # AddPerson does not allow everything to be set
153                 can_add = ['first_name', 'last_name', 'title','email', 'password', 'phone', 'url', 'bio']
154                 add_person_dict = { k : sfa_record[k] for k in sfa_record if k in can_add }
155                 pointer = self.shell.AddPerson(add_person_dict)
156                 self.shell.SetPersonHrn(int(pointer), hrn)
157             else:
158                 pointer = persons[0]['person_id']
159     
160             # enable the person's account
161             self.shell.UpdatePerson(pointer, {'enabled': True})
162             # add this person to the site
163             login_base = get_leaf(sfa_record['authority'])
164             self.shell.AddPersonToSite(pointer, login_base)
165     
166             # What roles should this user have?
167             roles = []
168             if 'roles' in sfa_record: 
169                 # if specified in xml, but only low-level roles
170                 roles = [ role for role in sfa_record['roles'] if role in ['user','tech'] ]
171             # at least user if no other cluse could be found
172             if not roles:
173                 roles = ['user']
174             for role in roles:
175                 self.shell.AddRoleToPerson(role, pointer)
176             # Add the user's key
177             if pub_key:
178                 self.shell.AddPersonKey(pointer, {'key_type' : 'ssh', 'key' : pub_key})
179
180         elif type == 'node':
181             login_base = PlXrn(xrn=sfa_record['authority'], type='authority').pl_login_base()
182             nodes = self.shell.GetNodes({'peer_id': None, 'hostname': pl_record['hostname']})
183             if not nodes:
184                 pointer = self.shell.AddNode(login_base, pl_record)
185                 self.shell.SetNodeHrn(int(pointer), hrn)
186             else:
187                 pointer = nodes[0]['node_id']
188     
189         return pointer
190         
191     ##########
192     # xxx actually old_sfa_record comes filled with plc stuff as well in the original code
193     def update (self, old_sfa_record, new_sfa_record, hrn, new_key):
194         pointer = old_sfa_record['pointer']
195         type = old_sfa_record['type']
196         new_key_pointer = None
197
198         # new_key implemented for users only
199         if new_key and type not in [ 'user' ]:
200             raise UnknownSfaType(type)
201
202         if (type == "authority"):
203             logger.debug("pldriver.update: calling UpdateSite with {}".format(new_sfa_record))
204             self.shell.UpdateSite(pointer, new_sfa_record)
205             self.shell.SetSiteHrn(pointer, hrn)
206     
207         elif type == "slice":
208             pl_record = self.sfa_fields_to_pl_fields(type, hrn, new_sfa_record)
209             if 'name' in pl_record:
210                 pl_record.pop('name')
211                 self.shell.UpdateSlice(pointer, pl_record)
212                 self.shell.SetSliceHrn(pointer, hrn)
213     
214         elif type == "user":
215             # SMBAKER: UpdatePerson only allows a limited set of fields to be
216             #    updated. Ideally we should have a more generic way of doing
217             #    this. I copied the field names from UpdatePerson.py...
218             update_fields = {}
219             all_fields = new_sfa_record
220             for key in all_fields.keys():
221                 if key in ['first_name', 'last_name', 'title', 'email',
222                            'password', 'phone', 'url', 'bio', 'accepted_aup',
223                            'enabled']:
224                     update_fields[key] = all_fields[key]
225             # when updating a user, we always get a 'email' field at this point
226             # this is because 'email' is a native field in the RegUser object...
227             if 'email' in update_fields and not update_fields['email']:
228                 del update_fields['email']
229             self.shell.UpdatePerson(pointer, update_fields)
230             self.shell.SetPersonHrn(pointer, hrn)
231     
232             if new_key:
233                 # must check this key against the previous one if it exists
234                 persons = self.shell.GetPersons({'peer_id': None, 'person_id': pointer}, ['key_ids'])
235                 person = persons[0]
236                 keys = person['key_ids']
237                 keys = self.shell.GetKeys(person['key_ids'])
238                 
239                 key_exists = False
240                 for key in keys:
241                     if new_key == key['key']:
242                         key_exists = True
243                         new_key_pointer = key['key_id']
244                         break
245                 if not key_exists:
246                     new_key_pointer = self.shell.AddPersonKey(pointer, {'key_type': 'ssh', 'key': new_key})
247     
248         elif type == "node":
249             self.shell.UpdateNode(pointer, new_sfa_record)
250
251         return (pointer, new_key_pointer)
252         
253
254     ##########
255     def remove (self, sfa_record):
256         type = sfa_record['type']
257         pointer = sfa_record['pointer']
258         if type == 'user':
259             persons = self.shell.GetPersons({'peer_id': None, 'person_id': pointer})
260             # only delete this person if he has site ids. if he doesnt, it probably means
261             # he was just removed from a site, not actually deleted
262             if persons and persons[0]['site_ids']:
263                 self.shell.DeletePerson(pointer)
264         elif type == 'slice':
265             if self.shell.GetSlices({'peer_id': None, 'slice_id': pointer}):
266                 self.shell.DeleteSlice(pointer)
267         elif type == 'node':
268             if self.shell.GetNodes({'peer_id': None, 'node_id': pointer}):
269                 self.shell.DeleteNode(pointer)
270         elif type == 'authority':
271             if self.shell.GetSites({'peer_id': None, 'site_id': pointer}):
272                 self.shell.DeleteSite(pointer)
273
274         return True
275
276
277     ##
278     # Convert SFA fields to PLC fields for use when registering or updating
279     # registry record in the PLC database
280     #
281
282     def sfa_fields_to_pl_fields(self, type, hrn, sfa_record):
283
284         pl_record = {}
285  
286         if type == "slice":
287             pl_record["name"] = hrn_to_pl_slicename(hrn)
288             if "instantiation" in sfa_record:
289                 pl_record['instantiation'] = sfa_record['instantiation']
290             else:
291                 pl_record["instantiation"] = "plc-instantiated"
292             if "url" in sfa_record:
293                pl_record["url"] = sfa_record["url"]
294             if "description" in sfa_record:
295                 pl_record["description"] = sfa_record["description"]
296             if "expires" in sfa_record:
297                 date = utcparse(sfa_record['expires'])
298                 expires = datetime_to_epoch(date)
299                 pl_record["expires"] = expires
300
301         elif type == "node":
302             if not "hostname" in pl_record:
303                 # fetch from sfa_record
304                 if "hostname" not in sfa_record:
305                     raise MissingSfaInfo("hostname")
306                 pl_record["hostname"] = sfa_record["hostname"]
307             if "model" in sfa_record: 
308                 pl_record["model"] = sfa_record["model"]
309             else:
310                 pl_record["model"] = "geni"
311
312         elif type == "authority":
313             pl_record["login_base"] = PlXrn(xrn=hrn,type='authority').pl_login_base()
314             if "name" not in sfa_record or not sfa_record['name']:
315                 pl_record["name"] = hrn
316             if "abbreviated_name" not in sfa_record:
317                 pl_record["abbreviated_name"] = hrn
318             if "enabled" not in sfa_record:
319                 pl_record["enabled"] = True
320             if "is_public" not in sfa_record:
321                 pl_record["is_public"] = True
322
323         return pl_record
324
325     ####################
326     def fill_record_info(self, records):
327         """
328         Given a (list of) SFA record, fill in the PLC specific 
329         and SFA specific fields in the record. 
330         """
331         if not isinstance(records, list):
332             records = [records]
333
334         self.fill_record_pl_info(records)
335         self.fill_record_hrns(records)
336         self.fill_record_sfa_info(records)
337         return records
338
339     def fill_record_pl_info(self, records):
340         """
341         Fill in the planetlab specific fields of a SFA record. This
342         involves calling the appropriate PLC method to retrieve the 
343         database record for the object.
344             
345         @param record: record to fill in field (in/out param)     
346         """
347         # get ids by type
348         node_ids, site_ids, slice_ids = [], [], [] 
349         person_ids, key_ids = [], []
350         type_map = {'node': node_ids, 'authority': site_ids,
351                     'slice': slice_ids, 'user': person_ids}
352                   
353         for record in records:
354             for type in type_map:
355                 if type == record['type']:
356                     type_map[type].append(record['pointer'])
357
358         # get pl records
359         nodes, sites, slices, persons, keys = {}, {}, {}, {}, {}
360         if node_ids:
361             node_list = self.shell.GetNodes({'peer_id': None, 'node_id': node_ids})
362             nodes = list_to_dict(node_list, 'node_id')
363         if site_ids:
364             site_list = self.shell.GetSites({'peer_id': None, 'site_id': site_ids})
365             sites = list_to_dict(site_list, 'site_id')
366         if slice_ids:
367             slice_list = self.shell.GetSlices({'peer_id': None, 'slice_id': slice_ids})
368             slices = list_to_dict(slice_list, 'slice_id')
369         if person_ids:
370             person_list = self.shell.GetPersons({'peer_id': None, 'person_id': person_ids})
371             persons = list_to_dict(person_list, 'person_id')
372             for person in persons:
373                 key_ids.extend(persons[person]['key_ids'])
374
375         pl_records = {'node': nodes, 'authority': sites,
376                       'slice': slices, 'user': persons}
377
378         if key_ids:
379             key_list = self.shell.GetKeys(key_ids)
380             keys = list_to_dict(key_list, 'key_id')
381
382         # fill record info
383         for record in records:
384             # records with pointer==-1 do not have plc info.
385             # for example, the top level authority records which are
386             # authorities, but not PL "sites"
387             if record['pointer'] == -1:
388                 continue
389            
390             for type in pl_records:
391                 if record['type'] == type:
392                     if record['pointer'] in pl_records[type]:
393                         record.update(pl_records[type][record['pointer']])
394                         break
395             # fill in key info
396             if record['type'] == 'user':
397                 if 'key_ids' not in record:
398                     logger.info("user record has no 'key_ids' - need to import from myplc ?")
399                 else:
400                     pubkeys = [keys[key_id]['key'] for key_id in record['key_ids'] if key_id in keys] 
401                     record['keys'] = pubkeys
402
403         return records
404
405     def fill_record_hrns(self, records):
406         """
407         convert pl ids to hrns
408         """
409
410         # get ids
411         slice_ids, person_ids, site_ids, node_ids = [], [], [], []
412         for record in records:
413             if 'site_id' in record:
414                 site_ids.append(record['site_id'])
415             if 'site_ids' in record:
416                 site_ids.extend(record['site_ids'])
417             if 'person_ids' in record:
418                 person_ids.extend(record['person_ids'])
419             if 'slice_ids' in record:
420                 slice_ids.extend(record['slice_ids'])
421             if 'node_ids' in record:
422                 node_ids.extend(record['node_ids'])
423
424         # get pl records
425         slices, persons, sites, nodes = {}, {}, {}, {}
426         if site_ids:
427             site_list = self.shell.GetSites({'peer_id': None, 'site_id': site_ids}, ['site_id', 'login_base'])
428             sites = list_to_dict(site_list, 'site_id')
429         if person_ids:
430             person_list = self.shell.GetPersons({'peer_id': None, 'person_id': person_ids}, ['person_id', 'email'])
431             persons = list_to_dict(person_list, 'person_id')
432         if slice_ids:
433             slice_list = self.shell.GetSlices({'peer_id': None, 'slice_id': slice_ids}, ['slice_id', 'name'])
434             slices = list_to_dict(slice_list, 'slice_id')       
435         if node_ids:
436             node_list = self.shell.GetNodes({'peer_id': None, 'node_id': node_ids}, ['node_id', 'hostname'])
437             nodes = list_to_dict(node_list, 'node_id')
438        
439         # convert ids to hrns
440         for record in records:
441             # get all relevant data
442             type = record['type']
443             pointer = record['pointer']
444             auth_hrn = self.hrn
445             login_base = ''
446             if pointer == -1:
447                 continue
448
449             if 'site_id' in record:
450                 site = sites[record['site_id']]
451                 login_base = site['login_base']
452                 record['site'] = ".".join([auth_hrn, login_base])
453             if 'person_ids' in record:
454                 emails = [persons[person_id]['email'] for person_id in record['person_ids'] \
455                           if person_id in  persons]
456                 usernames = [email.split('@')[0] for email in emails]
457                 person_hrns = [".".join([auth_hrn, login_base, username]) for username in usernames]
458                 record['persons'] = person_hrns 
459             if 'slice_ids' in record:
460                 slicenames = [slices[slice_id]['name'] for slice_id in record['slice_ids'] \
461                               if slice_id in slices]
462                 slice_hrns = [slicename_to_hrn(auth_hrn, slicename) for slicename in slicenames]
463                 record['slices'] = slice_hrns
464             if 'node_ids' in record:
465                 hostnames = [nodes[node_id]['hostname'] for node_id in record['node_ids'] \
466                              if node_id in nodes]
467                 node_hrns = [hostname_to_hrn(auth_hrn, login_base, hostname) for hostname in hostnames]
468                 record['nodes'] = node_hrns
469             if 'site_ids' in record:
470                 login_bases = [sites[site_id]['login_base'] for site_id in record['site_ids'] \
471                                if site_id in sites]
472                 site_hrns = [".".join([auth_hrn, lbase]) for lbase in login_bases]
473                 record['sites'] = site_hrns
474
475             if 'expires' in record:
476                 date = utcparse(record['expires'])
477                 datestring = datetime_to_string(date)
478                 record['expires'] = datestring 
479             
480         return records   
481
482     def fill_record_sfa_info(self, records):
483
484         def startswith(prefix, values):
485             return [value for value in values if value.startswith(prefix)]
486
487         # get person ids
488         person_ids = []
489         site_ids = []
490         for record in records:
491             person_ids.extend(record.get("person_ids", []))
492             site_ids.extend(record.get("site_ids", [])) 
493             if 'site_id' in record:
494                 site_ids.append(record['site_id']) 
495         
496         # get all pis from the sites we've encountered
497         # and store them in a dictionary keyed on site_id 
498         site_pis = {}
499         if site_ids:
500             pi_filter = {'peer_id': None, '|roles': ['pi'], '|site_ids': site_ids} 
501             pi_list = self.shell.GetPersons(pi_filter, ['person_id', 'site_ids'])
502             for pi in pi_list:
503                 # we will need the pi's hrns also
504                 person_ids.append(pi['person_id'])
505                 
506                 # we also need to keep track of the sites these pis
507                 # belong to
508                 for site_id in pi['site_ids']:
509                     if site_id in site_pis:
510                         site_pis[site_id].append(pi)
511                     else:
512                         site_pis[site_id] = [pi]
513                  
514         # get sfa records for all records associated with these records.   
515         # we'll replace pl ids (person_ids) with hrns from the sfa records
516         # we obtain
517         
518         # get the registry records
519         person_list, persons = [], {}
520         person_list = self.api.dbsession().query (RegRecord).filter(RegRecord.pointer.in_(person_ids))
521         # create a hrns keyed on the sfa record's pointer.
522         # Its possible for multiple records to have the same pointer so
523         # the dict's value will be a list of hrns.
524         persons = defaultdict(list)
525         for person in person_list:
526             persons[person.pointer].append(person)
527
528         # get the pl records
529         pl_person_list, pl_persons = [], {}
530         pl_person_list = self.shell.GetPersons(person_ids, ['person_id', 'roles'])
531         pl_persons = list_to_dict(pl_person_list, 'person_id')
532
533         # fill sfa info
534         for record in records:
535             # skip records with no pl info (top level authorities)
536             #if record['pointer'] == -1:
537             #    continue 
538             sfa_info = {}
539             type = record['type']
540             logger.info("fill_record_sfa_info - incoming record typed {}".format(type))
541             if (type == "slice"):
542                 # all slice users are researchers
543                 record['geni_urn'] = hrn_to_urn(record['hrn'], 'slice')
544                 record['PI'] = []
545                 record['researcher'] = []
546                 for person_id in record.get('person_ids', []):
547                     hrns = [person.hrn for person in persons[person_id]]
548                     record['researcher'].extend(hrns)                
549
550                 # pis at the slice's site
551                 if 'site_id' in record and record['site_id'] in site_pis:
552                     pl_pis = site_pis[record['site_id']]
553                     pi_ids = [pi['person_id'] for pi in pl_pis]
554                     for person_id in pi_ids:
555                         hrns = [person.hrn for person in persons[person_id]]
556                         record['PI'].extend(hrns)
557                         record['geni_creator'] = record['PI'] 
558                 
559             elif (type.startswith("authority")):
560                 record['url'] = None
561                 logger.info("fill_record_sfa_info - authority xherex")
562                 if record['pointer'] != -1:
563                     record['PI'] = []
564                     record['operator'] = []
565                     record['owner'] = []
566                     for pointer in record.get('person_ids', []):
567                         if pointer not in persons or pointer not in pl_persons:
568                             # this means there is not sfa or pl record for this user
569                             continue   
570                         hrns = [person.hrn for person in persons[pointer]] 
571                         roles = pl_persons[pointer]['roles']   
572                         if 'pi' in roles:
573                             record['PI'].extend(hrns)
574                         if 'tech' in roles:
575                             record['operator'].extend(hrns)
576                         if 'admin' in roles:
577                             record['owner'].extend(hrns)
578                         # xxx TODO: OrganizationName
579             elif (type == "node"):
580                 sfa_info['dns'] = record.get("hostname", "")
581                 # xxx TODO: URI, LatLong, IP, DNS
582     
583             elif (type == "user"):
584                 logger.info('setting user.email')
585                 sfa_info['email'] = record.get("email", "")
586                 sfa_info['geni_urn'] = hrn_to_urn(record['hrn'], 'user')
587                 sfa_info['geni_certificate'] = record['gid'] 
588                 # xxx TODO: PostalAddress, Phone
589             record.update(sfa_info)
590
591
592     ####################
593     # plcapi works by changes, compute what needs to be added/deleted
594     def update_relation (self, subject_type, target_type, relation_name, subject_id, target_ids):
595         # hard-wire the code for slice/user for now, could be smarter if needed
596         if subject_type == 'slice' and target_type == 'user' and relation_name == 'researcher':
597             subject = self.shell.GetSlices (subject_id)[0]
598             current_target_ids = subject['person_ids']
599             add_target_ids = list ( set (target_ids).difference(current_target_ids))
600             del_target_ids = list ( set (current_target_ids).difference(target_ids))
601             logger.debug ("subject_id = {} (type={})".format(subject_id, type(subject_id)))
602             for target_id in add_target_ids:
603                 self.shell.AddPersonToSlice (target_id,subject_id)
604                 logger.debug ("add_target_id = {} (type={})".format(target_id, type(target_id)))
605             for target_id in del_target_ids:
606                 logger.debug ("del_target_id = {} (type={})".format(target_id, type(target_id)))
607                 self.shell.DeletePersonFromSlice (target_id, subject_id)
608         elif subject_type == 'authority' and target_type == 'user' and relation_name == 'pi':
609             # due to the plcapi limitations this means essentially adding pi role to all people in the list
610             # it's tricky to remove any pi role here, although it might be desirable
611             persons = self.shell.GetPersons ({'peer_id': None, 'person_id': target_ids})
612             for person in persons: 
613                 if 'pi' not in person['roles']:
614                     self.shell.AddRoleToPerson('pi',person['person_id'])
615         else:
616             logger.info('unexpected relation {} to maintain, {} -> {}'\
617                         .format(relation_name, subject_type, target_type))
618
619         
620     ########################################
621     ########## aggregate oriented
622     ########################################
623
624     def testbed_name (self): return "myplc"
625
626     def aggregate_version (self):
627         return {}
628
629     # first 2 args are None in case of resource discovery
630     def list_resources (self, version=None, options=None):
631         if options is None: options={}
632         aggregate = PlAggregate(self)
633         rspec =  aggregate.list_resources(version=version, options=options)
634         return rspec
635
636     def describe(self, urns, version, options=None):
637         if options is None: options={}
638         aggregate = PlAggregate(self)
639         return aggregate.describe(urns, version=version, options=options)
640     
641     def status (self, urns, options=None):
642         if options is None: options={}
643         aggregate = PlAggregate(self)
644         desc =  aggregate.describe(urns, version='GENI 3')
645         status = {'geni_urn': desc['geni_urn'],
646                   'geni_slivers': desc['geni_slivers']}
647         return status
648
649     def allocate (self, urn, rspec_string, expiration, options=None):
650         """
651         Allocate a PL slice
652
653         Supported options:
654         (*) geni_users
655         (*) append : if set to True, provided attributes are appended 
656             to the current list of tags for the slice
657             otherwise, the set of provided attributes are meant to be the 
658             the exact set of tags at the end of the call, meaning pre-existing tags
659             are deleted if not repeated in the incoming request
660         """
661         if options is None: options={}
662         xrn = Xrn(urn)
663         aggregate = PlAggregate(self)
664         slices = PlSlices(self)
665         sfa_peer = slices.get_sfa_peer(xrn.get_hrn())
666         slice_record = None    
667         users = options.get('geni_users', [])
668
669         if users:
670             slice_record = users[0].get('slice_record', {})
671     
672         # parse rspec
673         rspec = RSpec(rspec_string)
674         requested_attributes = rspec.version.get_slice_attributes()
675         
676         # ensure site record exists
677         site = slices.verify_site(xrn.hrn, slice_record, sfa_peer, options=options)
678         # ensure slice record exists
679         slice = slices.verify_slice(xrn.hrn, slice_record, sfa_peer, expiration=expiration, options=options)
680         # ensure person records exists
681         persons = slices.verify_persons(xrn.hrn, slice, users, sfa_peer, options=options)
682         # ensure slice attributes exists
683         slices.verify_slice_tags(slice, requested_attributes, options=options)
684        
685         # add/remove slice from nodes
686         request_nodes = rspec.version.get_nodes_with_slivers()
687         nodes = slices.verify_slice_nodes(urn, slice, request_nodes)
688          
689         # add/remove links links 
690         slices.verify_slice_links(slice, rspec.version.get_link_requests(), nodes)
691
692         # add/remove leases
693         rspec_requested_leases = rspec.version.get_leases()
694         leases = slices.verify_slice_leases(slice, rspec_requested_leases)
695
696         return aggregate.describe([xrn.get_urn()], version=rspec.version)
697
698     def provision(self, urns, options=None):
699         if options is None: options={}
700         # update users
701         slices = PlSlices(self)
702         aggregate = PlAggregate(self)
703         slivers = aggregate.get_slivers(urns)
704         if not slivers:
705             sliver_id_parts = Xrn(urns[0]).get_sliver_id_parts()
706             # allow to be called with an empty rspec, meaning flush reservations
707             if sliver_id_parts:
708                 filter = {}
709                 try:
710                     filter['slice_id'] = int(sliver_id_parts[0])
711                 except ValueError:
712                     filter['name'] = sliver_id_parts[0]
713                 slices = self.shell.GetSlices(filter,['hrn'])
714                 if not slices:
715                     raise Forbidden("Unable to locate slice record for sliver:  {}".format(xrn))
716                 slice = slices[0]
717                 slice_urn = hrn_to_urn(slice['hrn'], type='slice')
718                 urns = [slice_urn]          
719         else:    
720             slice_id = slivers[0]['slice_id']
721             slice_hrn = self.shell.GetSliceHrn(slice_id)
722             slice = self.shell.GetSlices({'slice_id': slice_id})[0]
723             slice['hrn'] = slice_hrn
724             sfa_peer = slices.get_sfa_peer(slice['hrn'])
725             users = options.get('geni_users', [])
726             persons = slices.verify_persons(slice['hrn'], slice, users, sfa_peer, options=options)
727             # update sliver allocation states and set them to geni_provisioned
728             sliver_ids = [sliver['sliver_id'] for sliver in slivers]
729             dbsession = self.api.dbsession()
730             SliverAllocation.set_allocations(sliver_ids, 'geni_provisioned',dbsession)
731
732         version_manager = VersionManager()
733         rspec_version = version_manager.get_version(options['geni_rspec_version']) 
734         return self.describe(urns, rspec_version, options=options)
735
736     def delete(self, urns, options=None):
737         if options is None: options={}
738         # collect sliver ids so we can update sliver allocation states after
739         # we remove the slivers.
740         aggregate = PlAggregate(self)
741         slivers = aggregate.get_slivers(urns)
742         if slivers:
743             slice_id = slivers[0]['slice_id'] 
744             slice_name = slivers[0]['name']
745             node_ids = []
746             sliver_ids = []
747             for sliver in slivers:
748                 node_ids.append(sliver['node_id'])
749                 sliver_ids.append(sliver['sliver_id']) 
750
751             # leases
752             leases = self.shell.GetLeases({'name': slice_name, 'node_id': node_ids})
753             leases_ids = [lease['lease_id'] for lease in leases ]
754
755             slice_hrn = self.shell.GetSliceHrn(int(slice_id))
756             try:
757                 self.shell.DeleteSliceFromNodes(slice_id, node_ids)
758                 if len(leases_ids) > 0:
759                     self.shell.DeleteLeases(leases_ids)
760      
761                 # delete sliver allocation states
762                 dbsession = self.api.dbsession()
763                 SliverAllocation.delete_allocations(sliver_ids, dbsession)
764             finally:
765                 pass
766
767         # prepare return struct
768         geni_slivers = []
769         for sliver in slivers:
770             geni_slivers.append(
771                 {'geni_sliver_urn': sliver['sliver_id'],
772                  'geni_allocation_status': 'geni_unallocated',
773                  'geni_expires': datetime_to_string(utcparse(sliver['expires']))})  
774         return geni_slivers
775
776     def renew (self, urns, expiration_time, options=None):
777         if options is None: options={}
778         aggregate = PlAggregate(self)
779         slivers = aggregate.get_slivers(urns)
780         if not slivers:
781             raise SearchFailed(urns)
782         slice = slivers[0]
783         requested_time = utcparse(expiration_time)
784         record = {'expires': int(datetime_to_epoch(requested_time))}
785         self.shell.UpdateSlice(slice['slice_id'], record)
786         description = self.describe(urns, 'GENI 3', options)
787         return description['geni_slivers']
788             
789
790     def perform_operational_action (self, urns, action, options=None):
791         if options is None: options={}
792         # MyPLC doesn't support operational actions. Lets pretend like it
793         # supports start, but reject everything else.
794         action = action.lower()
795         if action not in ['geni_start']:
796             raise UnsupportedOperation(action)
797
798         # fault if sliver is not full allocated (operational status is geni_pending_allocation)
799         description = self.describe(urns, 'GENI 3', options)
800         for sliver in description['geni_slivers']:
801             if sliver['geni_operational_status'] == 'geni_pending_allocation':
802                 raise UnsupportedOperation\
803                     (action, "Sliver must be fully allocated (operational status is not geni_pending_allocation)")
804         #
805         # Perform Operational Action Here
806         #
807
808         geni_slivers = self.describe(urns, 'GENI 3', options)['geni_slivers']
809         return geni_slivers
810
811     # set the 'enabled' tag to 0
812     def shutdown (self, xrn, options=None):
813         if options is None: options={}
814         hrn, _ = urn_to_hrn(xrn)
815         top_auth_hrn = top_auth(hrn)
816         site_hrn = '.'.join(hrn.split('.')[:-1])
817         slice_part = hrn.split('.')[-1]
818         if top_auth_hrn == self.hrn:
819             login_base = slice_hrn.split('.')[-2][:12]
820         else:
821             login_base = hash_loginbase(site_hrn)
822
823         slicename = '_'.join([login_base, slice_part])
824
825         slices = self.shell.GetSlices({'peer_id': None, 'name': slicename}, ['slice_id'])
826         if not slices:
827             raise RecordNotFound(slice_hrn)
828         slice_id = slices[0]['slice_id']
829         slice_tags = self.shell.GetSliceTags({'slice_id': slice_id, 'tagname': 'enabled'})
830         if not slice_tags:
831             self.shell.AddSliceTag(slice_id, 'enabled', '0')
832         elif slice_tags[0]['value'] != "0":
833             tag_id = slice_tags[0]['slice_tag_id']
834             self.shell.UpdateSliceTag(tag_id, '0')
835         return 1