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