3 from datetime import datetime
4 from dateutil import tz
5 from time import strftime, gmtime
7 from sfa.util.faults import SliverDoesNotExist, UnknownSfaType
8 from sfa.util.sfalogging import logger
10 from sfa.storage.alchemy import dbsession
11 from sfa.storage.model import RegRecord, RegUser
13 from sfa.trust.credential import Credential
16 from sfa.managers.driver import Driver
17 from sfa.rspecs.version_manager import VersionManager
18 from sfa.rspecs.rspec import RSpec
20 from sfa.util.xrn import hrn_to_urn, urn_to_sliver_id, get_leaf
23 ## thierry: everything that is API-related (i.e. handling incoming requests)
25 # SlabDriver should be really only about talking to the senslab testbed
28 from sfa.senslab.OARrestapi import OARrestapi
29 from sfa.senslab.LDAPapi import LDAPapi
31 from sfa.senslab.slabpostgres import SlabDB, slab_dbsession, SliceSenslab
32 from sfa.senslab.slabaggregate import SlabAggregate, slab_xrn_to_hostname, slab_xrn_object
33 from sfa.senslab.slabslices import SlabSlices
40 # this inheritance scheme is so that the driver object can receive
41 # GetNodes or GetSites sorts of calls directly
42 # and thus minimize the differences in the managers with the pl version
43 class SlabDriver(Driver):
45 def __init__(self, config):
46 Driver.__init__ (self, config)
48 self.hrn = config.SFA_INTERFACE_HRN
50 self.root_auth = config.SFA_REGISTRY_ROOT_AUTH
52 self.oar = OARrestapi()
54 self.time_format = "%Y-%m-%d %H:%M:%S"
55 self.db = SlabDB(config,debug = True)
59 def sliver_status(self, slice_urn, slice_hrn):
60 """Receive a status request for slice named urn/hrn
61 urn:publicid:IDN+senslab+nturro_slice hrn senslab.nturro_slice
62 shall return a structure as described in
63 http://groups.geni.net/geni/wiki/GAPI_AM_API_V2#SliverStatus
64 NT : not sure if we should implement this or not, but used by sface.
68 #First get the slice with the slice hrn
69 sl = self.GetSlices(slice_filter = slice_hrn, \
70 slice_filter_type = 'slice_hrn')
72 raise SliverDoesNotExist("%s slice_hrn" % (slice_hrn))
74 top_level_status = 'unknown'
75 nodes_in_slice = sl['node_ids']
77 if len(nodes_in_slice) is 0:
78 raise SliverDoesNotExist("No slivers allocated ")
80 top_level_status = 'ready'
82 logger.debug("Slabdriver - sliver_status Sliver status urn %s hrn %s sl\
83 %s \r\n " %(slice_urn, slice_hrn, sl))
85 if sl['oar_job_id'] is not -1:
86 #A job is running on Senslab for this slice
87 # report about the local nodes that are in the slice only
89 nodes_all = self.GetNodes({'hostname':nodes_in_slice},
90 ['node_id', 'hostname','site','boot_state'])
91 nodeall_byhostname = dict([(n['hostname'], n) for n in nodes_all])
95 result['geni_urn'] = slice_urn
96 result['pl_login'] = sl['job_user'] #For compatibility
99 timestamp = float(sl['startTime']) + float(sl['walltime'])
100 result['pl_expires'] = strftime(self.time_format, \
101 gmtime(float(timestamp)))
102 #result['slab_expires'] = strftime(self.time_format,\
103 #gmtime(float(timestamp)))
106 for node in nodeall_byhostname:
108 #res['slab_hostname'] = node['hostname']
109 #res['slab_boot_state'] = node['boot_state']
111 res['pl_hostname'] = nodeall_byhostname[node]['hostname']
112 res['pl_boot_state'] = nodeall_byhostname[node]['boot_state']
113 res['pl_last_contact'] = strftime(self.time_format, \
114 gmtime(float(timestamp)))
115 sliver_id = urn_to_sliver_id(slice_urn, sl['record_id_slice'], \
116 nodeall_byhostname[node]['node_id'])
117 res['geni_urn'] = sliver_id
118 if nodeall_byhostname[node]['boot_state'] == 'Alive':
120 res['geni_status'] = 'ready'
122 res['geni_status'] = 'failed'
123 top_level_status = 'failed'
125 res['geni_error'] = ''
127 resources.append(res)
129 result['geni_status'] = top_level_status
130 result['geni_resources'] = resources
131 logger.debug("SLABDRIVER \tsliver_statusresources %s res %s "\
136 def create_sliver (self, slice_urn, slice_hrn, creds, rspec_string, \
138 logger.debug("SLABDRIVER.PY \tcreate_sliver ")
139 aggregate = SlabAggregate(self)
141 slices = SlabSlices(self)
142 peer = slices.get_peer(slice_hrn)
143 sfa_peer = slices.get_sfa_peer(slice_hrn)
146 if not isinstance(creds, list):
150 slice_record = users[0].get('slice_record', {})
153 rspec = RSpec(rspec_string)
154 logger.debug("SLABDRIVER.PY \tcreate_sliver \trspec.version %s " \
158 # ensure site record exists?
159 # ensure slice record exists
160 sfa_slice = slices.verify_slice(slice_hrn, slice_record, peer, \
161 sfa_peer, options=options)
162 requested_attributes = rspec.version.get_slice_attributes()
164 if requested_attributes:
165 for attrib_dict in requested_attributes:
166 if 'timeslot' in attrib_dict and attrib_dict['timeslot'] \
168 sfa_slice.update({'timeslot':attrib_dict['timeslot']})
169 logger.debug("SLABDRIVER.PY create_sliver slice %s " %(sfa_slice))
171 # ensure person records exists
172 persons = slices.verify_persons(slice_hrn, sfa_slice, users, peer, \
173 sfa_peer, options=options)
175 # ensure slice attributes exists?
178 # add/remove slice from nodes
180 requested_slivers = [node.get('component_name') \
181 for node in rspec.version.get_nodes_with_slivers()]
182 logger.debug("SLADRIVER \tcreate_sliver requested_slivers \
183 requested_slivers %s " %(requested_slivers))
185 nodes = slices.verify_slice_nodes(sfa_slice, requested_slivers, peer)
188 requested_leases = []
190 for lease in rspec.version.get_leases():
192 if not lease.get('lease_id'):
193 requested_lease['hostname'] = \
194 slab_xrn_to_hostname(lease.get('component_id').strip())
195 requested_lease['start_time'] = lease.get('start_time')
196 requested_lease['duration'] = lease.get('duration')
198 kept_leases.append(int(lease['lease_id']))
199 if requested_lease.get('hostname'):
200 requested_leases.append(requested_lease)
202 leases = slices.verify_slice_leases(sfa_slice, \
203 requested_leases, kept_leases, peer)
205 return aggregate.get_rspec(slice_xrn=slice_urn, version=rspec.version)
208 def delete_sliver (self, slice_urn, slice_hrn, creds, options):
210 sfa_slice = self.GetSlices(slice_filter = slice_hrn, \
211 slice_filter_type = 'slice_hrn')
212 logger.debug("SLABDRIVER.PY delete_sliver slice %s" %(sfa_slice))
216 slices = SlabSlices(self)
217 # determine if this is a peer slice
219 peer = slices.get_peer(slice_hrn)
222 self.UnBindObjectFromPeer('slice', \
223 sfa_slice['record_id_slice'], peer)
224 self.DeleteSliceFromNodes(sfa_slice)
227 self.BindObjectToPeer('slice', sfa_slice['slice_id'], \
228 peer, sfa_slice['peer_slice_id'])
232 def AddSlice(self, slice_record):
233 slab_slice = SliceSenslab( slice_hrn = slice_record['slice_hrn'], \
234 record_id_slice= slice_record['record_id_slice'] , \
235 record_id_user= slice_record['record_id_user'], \
236 peer_authority = slice_record['peer_authority'])
237 logger.debug("SLABDRIVER.PY \tAddSlice slice_record %s slab_slice %s" \
238 %(slice_record,slab_slice))
239 slab_dbsession.add(slab_slice)
240 slab_dbsession.commit()
243 # first 2 args are None in case of resource discovery
244 def list_resources (self, slice_urn, slice_hrn, creds, options):
245 #cached_requested = options.get('cached', True)
247 version_manager = VersionManager()
248 # get the rspec's return format from options
250 version_manager.get_version(options.get('geni_rspec_version'))
251 version_string = "rspec_%s" % (rspec_version)
253 #panos adding the info option to the caching key (can be improved)
254 if options.get('info'):
255 version_string = version_string + "_" + \
256 options.get('info', 'default')
258 # look in cache first
259 #if cached_requested and self.cache and not slice_hrn:
260 #rspec = self.cache.get(version_string)
262 #logger.debug("SlabDriver.ListResources: \
263 #returning cached advertisement")
266 #panos: passing user-defined options
267 aggregate = SlabAggregate(self)
268 origin_hrn = Credential(string=creds[0]).get_gid_caller().get_hrn()
269 options.update({'origin_hrn':origin_hrn})
270 rspec = aggregate.get_rspec(slice_xrn=slice_urn, \
271 version=rspec_version, options=options)
274 #if self.cache and not slice_hrn:
275 #logger.debug("Slab.ListResources: stores advertisement in cache")
276 #self.cache.add(version_string, rspec)
281 def list_slices (self, creds, options):
282 # look in cache first
284 #slices = self.cache.get('slices')
286 #logger.debug("PlDriver.list_slices returns from cache")
291 slices = self.GetSlices()
292 logger.debug("SLABDRIVER.PY \tlist_slices hrn %s \r\n \r\n" %(slices))
293 slice_hrns = [slab_slice['slice_hrn'] for slab_slice in slices]
294 #slice_hrns = [slicename_to_hrn(self.hrn, slab_slice['slice_hrn']) \
295 #for slab_slice in slices]
296 slice_urns = [hrn_to_urn(slice_hrn, 'slice') \
297 for slice_hrn in slice_hrns]
301 #logger.debug ("SlabDriver.list_slices stores value in cache")
302 #self.cache.add('slices', slice_urns)
306 #No site or node register supported
307 def register (self, sfa_record, hrn, pub_key):
308 record_type = sfa_record['type']
309 slab_record = self.sfa_fields_to_slab_fields(record_type, hrn, \
313 if record_type == 'slice':
314 acceptable_fields = ['url', 'instantiation', 'name', 'description']
315 for key in slab_record.keys():
316 if key not in acceptable_fields:
318 logger.debug("SLABDRIVER.PY register")
319 slices = self.GetSlices(slice_filter =slab_record['hrn'], \
320 slice_filter_type = 'slice_hrn')
322 pointer = self.AddSlice(slab_record)
324 pointer = slices[0]['slice_id']
326 elif record_type == 'user':
327 persons = self.GetPersons([sfa_record])
328 #persons = self.GetPersons([sfa_record['hrn']])
330 pointer = self.AddPerson(dict(sfa_record))
333 pointer = persons[0]['person_id']
335 #Does this make sense to senslab ?
336 #if 'enabled' in sfa_record and sfa_record['enabled']:
337 #self.UpdatePerson(pointer, \
338 #{'enabled': sfa_record['enabled']})
340 #TODO register Change this AddPersonToSite stuff 05/07/2012 SA
341 # add this person to the site only if
342 # she is being added for the first
343 # time by sfa and doesnt already exist in plc
344 if not persons or not persons[0]['site_ids']:
345 login_base = get_leaf(sfa_record['authority'])
346 self.AddPersonToSite(pointer, login_base)
348 # What roles should this user have?
349 #TODO : DElete this AddRoleToPerson 04/07/2012 SA
350 #Function prototype is :
351 #AddRoleToPerson(self, auth, role_id_or_name, person_id_or_email)
352 #what's the pointer doing here?
353 self.AddRoleToPerson('user', pointer)
356 self.AddPersonKey(pointer, {'key_type' : 'ssh', \
359 #No node adding outside OAR
363 #No site or node record update allowed
364 def update (self, old_sfa_record, new_sfa_record, hrn, new_key):
365 pointer = old_sfa_record['pointer']
366 old_sfa_record_type = old_sfa_record['type']
368 # new_key implemented for users only
369 if new_key and old_sfa_record_type not in [ 'user' ]:
370 raise UnknownSfaType(old_sfa_record_type)
372 #if (type == "authority"):
373 #self.shell.UpdateSite(pointer, new_sfa_record)
375 if old_sfa_record_type == "slice":
376 slab_record = self.sfa_fields_to_slab_fields(old_sfa_record_type, \
378 if 'name' in slab_record:
379 slab_record.pop('name')
380 #Prototype should be UpdateSlice(self,
381 #auth, slice_id_or_name, slice_fields)
382 #Senslab cannot update slice since slice = job
383 #so we must delete and create another job
384 self.UpdateSlice(pointer, slab_record)
386 elif old_sfa_record_type == "user":
388 all_fields = new_sfa_record
389 for key in all_fields.keys():
390 if key in ['first_name', 'last_name', 'title', 'email',
391 'password', 'phone', 'url', 'bio', 'accepted_aup',
393 update_fields[key] = all_fields[key]
394 self.UpdatePerson(pointer, update_fields)
397 # must check this key against the previous one if it exists
398 persons = self.GetPersons([pointer], ['key_ids'])
400 keys = person['key_ids']
401 keys = self.GetKeys(person['key_ids'])
403 # Delete all stale keys
406 if new_key != key['key']:
407 self.DeleteKey(key['key_id'])
411 self.AddPersonKey(pointer, {'key_type': 'ssh', \
418 def remove (self, sfa_record):
419 sfa_record_type = sfa_record['type']
420 hrn = sfa_record['hrn']
421 record_id = sfa_record['record_id']
422 if sfa_record_type == 'user':
424 #get user from senslab ldap
425 person = self.GetPersons(sfa_record)
426 #No registering at a given site in Senslab.
427 #Once registered to the LDAP, all senslab sites are
430 #Mark account as disabled in ldap
431 self.DeletePerson(sfa_record)
432 elif sfa_record_type == 'slice':
433 if self.GetSlices(slice_filter = hrn, \
434 slice_filter_type = 'slice_hrn'):
435 self.DeleteSlice(sfa_record_type)
437 #elif type == 'authority':
438 #if self.GetSites(pointer):
439 #self.DeleteSite(pointer)
445 #TODO clean GetPeers. 05/07/12SA
446 def GetPeers (self, auth = None, peer_filter=None, return_fields_list=None):
448 existing_records = {}
449 existing_hrns_by_types = {}
450 logger.debug("SLABDRIVER \tGetPeers auth = %s, peer_filter %s, \
451 return_field %s " %(auth , peer_filter, return_fields_list))
452 all_records = dbsession.query(RegRecord).filter(RegRecord.type.like('%authority%')).all()
453 for record in all_records:
454 existing_records[(record.hrn, record.type)] = record
455 if record.type not in existing_hrns_by_types:
456 existing_hrns_by_types[record.type] = [record.hrn]
457 logger.debug("SLABDRIVER \tGetPeer\t NOT IN \
458 existing_hrns_by_types %s " %( existing_hrns_by_types))
461 logger.debug("SLABDRIVER \tGetPeer\t \INNN type %s hrn %s " \
462 %(record.type,record.hrn))
463 existing_hrns_by_types[record.type].append(record.hrn)
466 logger.debug("SLABDRIVER \tGetPeer\texisting_hrns_by_types %s "\
467 %( existing_hrns_by_types))
472 records_list.append(existing_records[(peer_filter,'authority')])
474 for hrn in existing_hrns_by_types['authority']:
475 records_list.append(existing_records[(hrn,'authority')])
477 logger.debug("SLABDRIVER \tGetPeer \trecords_list %s " \
483 return_records = records_list
484 if not peer_filter and not return_fields_list:
488 logger.debug("SLABDRIVER \tGetPeer return_records %s " \
490 return return_records
493 #TODO : Handling OR request in make_ldap_filters_from_records
494 #instead of the for loop
495 #over the records' list
496 def GetPersons(self, person_filter=None, return_fields_list=None):
498 person_filter should be a list of dictionnaries when not set to None.
499 Returns a list of users whose accounts are enabled found in ldap.
502 logger.debug("SLABDRIVER \tGetPersons person_filter %s" \
505 if person_filter and isinstance(person_filter, list):
506 #If we are looking for a list of users (list of dict records)
507 #Usually the list contains only one user record
508 for searched_attributes in person_filter:
510 #Get only enabled user accounts in senslab LDAP :
511 #add a filter for make_ldap_filters_from_record
512 person = self.ldap.LdapFindUser(searched_attributes, \
513 is_user_enabled=True)
514 person_list.append(person)
517 #Get only enabled user accounts in senslab LDAP :
518 #add a filter for make_ldap_filters_from_record
519 person_list = self.ldap.LdapFindUser(is_user_enabled=True)
523 def GetTimezone(self):
524 server_timestamp, server_tz = self.oar.parser.\
525 SendRequest("GET_timezone")
526 return server_timestamp, server_tz
529 def DeleteJobs(self, job_id, slice_hrn):
530 if not job_id or job_id is -1:
532 username = slice_hrn.split(".")[-1].rstrip("_slice")
534 reqdict['method'] = "delete"
535 reqdict['strval'] = str(job_id)
537 answer = self.oar.POSTRequestToOARRestAPI('DELETE_jobs_id', \
539 logger.debug("SLABDRIVER \tDeleteJobs jobid %s \r\n answer %s username %s" \
540 %(job_id,answer, username))
545 ##TODO : Unused GetJobsId ? SA 05/07/12
546 #def GetJobsId(self, job_id, username = None ):
548 #Details about a specific job.
549 #Includes details about submission time, jot type, state, events,
550 #owner, assigned ressources, walltime etc...
554 #node_list_k = 'assigned_network_address'
555 ##Get job info from OAR
556 #job_info = self.oar.parser.SendRequest(req, job_id, username)
558 #logger.debug("SLABDRIVER \t GetJobsId %s " %(job_info))
560 #if job_info['state'] == 'Terminated':
561 #logger.debug("SLABDRIVER \t GetJobsId job %s TERMINATED"\
564 #if job_info['state'] == 'Error':
565 #logger.debug("SLABDRIVER \t GetJobsId ERROR message %s "\
570 #logger.error("SLABDRIVER \tGetJobsId KeyError")
573 #parsed_job_info = self.get_info_on_reserved_nodes(job_info, \
575 ##Replaces the previous entry
576 ##"assigned_network_address" / "reserved_resources"
578 #job_info.update({'node_ids':parsed_job_info[node_list_k]})
579 #del job_info[node_list_k]
580 #logger.debug(" \r\nSLABDRIVER \t GetJobsId job_info %s " %(job_info))
584 def GetJobsResources(self, job_id, username = None):
585 #job_resources=['reserved_resources', 'assigned_resources',\
586 #'job_id', 'job_uri', 'assigned_nodes',\
588 #assigned_res = ['resource_id', 'resource_uri']
589 #assigned_n = ['node', 'node_uri']
591 req = "GET_jobs_id_resources"
592 node_list_k = 'reserved_resources'
594 #Get job resources list from OAR
595 node_id_list = self.oar.parser.SendRequest(req, job_id, username)
596 logger.debug("SLABDRIVER \t GetJobsResources %s " %(node_id_list))
599 self.__get_hostnames_from_oar_node_ids(node_id_list)
601 #parsed_job_info = self.get_info_on_reserved_nodes(job_info, \
603 #Replaces the previous entry "assigned_network_address" /
604 #"reserved_resources"
606 job_info = {'node_ids': hostname_list}
611 def get_info_on_reserved_nodes(self, job_info, node_list_name):
612 #Get the list of the testbed nodes records and make a
613 #dictionnary keyed on the hostname out of it
614 node_list_dict = self.GetNodes()
615 #node_hostname_list = []
616 node_hostname_list = [node['hostname'] for node in node_list_dict]
617 #for node in node_list_dict:
618 #node_hostname_list.append(node['hostname'])
619 node_dict = dict(zip(node_hostname_list, node_list_dict))
621 reserved_node_hostname_list = []
622 for index in range(len(job_info[node_list_name])):
623 #job_info[node_list_name][k] =
624 reserved_node_hostname_list[index] = \
625 node_dict[job_info[node_list_name][index]]['hostname']
627 logger.debug("SLABDRIVER \t get_info_on_reserved_nodes \
628 reserved_node_hostname_list %s" \
629 %(reserved_node_hostname_list))
631 logger.error("SLABDRIVER \t get_info_on_reserved_nodes KEYERROR " )
633 return reserved_node_hostname_list
635 def GetNodesCurrentlyInUse(self):
636 """Returns a list of all the nodes already involved in an oar job"""
637 return self.oar.parser.SendRequest("GET_running_jobs")
639 def __get_hostnames_from_oar_node_ids(self, resource_id_list ):
640 full_nodes_dict_list = self.GetNodes()
641 #Put the full node list into a dictionary keyed by oar node id
642 oar_id_node_dict = {}
643 for node in full_nodes_dict_list:
644 oar_id_node_dict[node['oar_id']] = node
646 logger.debug("SLABDRIVER \t __get_hostnames_from_oar_node_ids\
647 oar_id_node_dict %s" %(oar_id_node_dict))
649 hostname_dict_list = []
650 for resource_id in resource_id_list:
651 #Because jobs requested "asap" do not have defined resources
652 if resource_id is not "Undefined":
653 hostname_dict_list.append({'hostname' : \
654 oar_id_node_dict[resource_id]['hostname'],
655 'site_id' : oar_id_node_dict[resource_id]['site']})
657 #hostname_list.append(oar_id_node_dict[resource_id]['hostname'])
658 return hostname_dict_list
660 def GetReservedNodes(self):
661 #Get the nodes in use and the reserved nodes
662 reservation_dict_list = \
663 self.oar.parser.SendRequest("GET_reserved_nodes")
666 for resa in reservation_dict_list:
667 logger.debug ("GetReservedNodes resa %s"%(resa))
668 #dict list of hostnames and their site
669 resa['reserved_nodes'] = \
670 self.__get_hostnames_from_oar_node_ids(resa['resource_ids'])
672 #del resa['resource_ids']
673 return reservation_dict_list
675 def GetNodes(self, node_filter_dict = None, return_fields_list = None):
677 node_filter_dict : dictionnary of lists
680 node_dict_by_id = self.oar.parser.SendRequest("GET_resources_full")
681 node_dict_list = node_dict_by_id.values()
683 #No filtering needed return the list directly
684 if not (node_filter_dict or return_fields_list):
685 return node_dict_list
687 return_node_list = []
689 for filter_key in node_filter_dict:
691 #Filter the node_dict_list by each value contained in the
692 #list node_filter_dict[filter_key]
693 for value in node_filter_dict[filter_key]:
694 for node in node_dict_list:
695 if node[filter_key] == value:
696 if return_fields_list :
698 for k in return_fields_list:
700 return_node_list.append(tmp)
702 return_node_list.append(node)
704 logger.log_exc("GetNodes KeyError")
708 return return_node_list
711 def GetSites(self, site_filter_name_list = None, return_fields_list = None):
712 site_dict = self.oar.parser.SendRequest("GET_sites")
713 #site_dict : dict where the key is the sit ename
714 return_site_list = []
715 if not ( site_filter_name_list or return_fields_list):
716 return_site_list = site_dict.values()
717 return return_site_list
719 for site_filter_name in site_filter_name_list:
720 if site_filter_name in site_dict:
721 if return_fields_list:
722 for field in return_fields_list:
725 tmp[field] = site_dict[site_filter_name][field]
727 logger.error("GetSites KeyError %s "%(field))
729 return_site_list.append(tmp)
731 return_site_list.append( site_dict[site_filter_name])
734 return return_site_list
735 #warning return_fields_list paramr emoved (Not used)
736 def GetSlices(self, slice_filter = None, slice_filter_type = None):
737 #def GetSlices(self, slice_filter = None, slice_filter_type = None, \
738 #return_fields_list = None):
739 """ Get the slice records from the slab db.
740 Returns a slice ditc if slice_filter and slice_filter_type
742 Returns a list of slice dictionnaries if there are no filters
746 return_slice_list = []
749 authorized_filter_types_list = ['slice_hrn', 'record_id_user']
750 logger.debug("SLABDRIVER \tGetSlices authorized_filter_types_list %s"\
751 %(authorized_filter_types_list))
752 if slice_filter_type in authorized_filter_types_list:
753 if slice_filter_type == 'slice_hrn':
754 slicerec = slab_dbsession.query(SliceSenslab).filter_by(slice_hrn = slice_filter).first()
756 if slice_filter_type == 'record_id_user':
757 slicerec = slab_dbsession.query(SliceSenslab).filter_by(record_id_user = slice_filter).first()
761 slicerec_dict = slicerec.dump_sqlalchemyobj_to_dict()
762 logger.debug("SLABDRIVER \tGetSlices slicerec_dict %s" \
765 login = slicerec_dict['slice_hrn'].split(".")[1].split("_")[0]
766 logger.debug("\r\n SLABDRIVER \tGetSlices login %s \
768 %(login, slicerec_dict))
769 if slicerec_dict['oar_job_id'] is not -1:
770 #Check with OAR the status of the job if a job id is in
772 rslt = self.GetJobsResources(slicerec_dict['oar_job_id'], \
776 slicerec_dict.update(rslt)
777 slicerec_dict.update({'hrn':\
778 str(slicerec_dict['slice_hrn'])})
779 #If GetJobsResources is empty, this means the job is
780 #now in the 'Terminated' state
781 #Update the slice record
783 self.db.update_job(slice_filter, job_id = -1)
784 slicerec_dict['oar_job_id'] = -1
786 update({'hrn':str(slicerec_dict['slice_hrn'])})
789 slicerec_dict['node_ids'] = slicerec_dict['node_list']
793 logger.debug("SLABDRIVER.PY \tGetSlices slicerec_dict %s"\
800 slice_list = slab_dbsession.query(SliceSenslab).all()
801 return_slice_list = []
802 for record in slice_list:
803 return_slice_list.append(record.dump_sqlalchemyobj_to_dict())
805 logger.debug("SLABDRIVER.PY \tGetSlices slices %s \
806 slice_filter %s " %(return_slice_list, slice_filter))
808 #if return_fields_list:
809 #return_slice_list = parse_filter(sliceslist, \
810 #slice_filter,'slice', return_fields_list)
812 return return_slice_list
818 def testbed_name (self): return self.hrn
820 # 'geni_request_rspec_versions' and 'geni_ad_rspec_versions' are mandatory
821 def aggregate_version (self):
822 version_manager = VersionManager()
823 ad_rspec_versions = []
824 request_rspec_versions = []
825 for rspec_version in version_manager.versions:
826 if rspec_version.content_type in ['*', 'ad']:
827 ad_rspec_versions.append(rspec_version.to_dict())
828 if rspec_version.content_type in ['*', 'request']:
829 request_rspec_versions.append(rspec_version.to_dict())
831 'testbed':self.testbed_name(),
832 'geni_request_rspec_versions': request_rspec_versions,
833 'geni_ad_rspec_versions': ad_rspec_versions,
842 # Convert SFA fields to PLC fields for use when registering up updating
843 # registry record in the PLC database
845 # @param type type of record (user, slice, ...)
846 # @param hrn human readable name
847 # @param sfa_fields dictionary of SFA fields
848 # @param slab_fields dictionary of PLC fields (output)
850 def sfa_fields_to_slab_fields(self, sfa_type, hrn, record):
852 def convert_ints(tmpdict, int_fields):
853 for field in int_fields:
855 tmpdict[field] = int(tmpdict[field])
858 #for field in record:
859 # slab_record[field] = record[field]
861 if sfa_type == "slice":
862 #instantion used in get_slivers ?
863 if not "instantiation" in slab_record:
864 slab_record["instantiation"] = "senslab-instantiated"
865 #slab_record["hrn"] = hrn_to_pl_slicename(hrn)
866 #Unused hrn_to_pl_slicename because Slab's hrn already in the appropriate form SA 23/07/12
867 slab_record["hrn"] = hrn
868 logger.debug("SLABDRIVER.PY sfa_fields_to_slab_fields \
869 slab_record %s " %(slab_record['hrn']))
871 slab_record["url"] = record["url"]
872 if "description" in record:
873 slab_record["description"] = record["description"]
874 if "expires" in record:
875 slab_record["expires"] = int(record["expires"])
877 #nodes added by OAR only and then imported to SFA
878 #elif type == "node":
879 #if not "hostname" in slab_record:
880 #if not "hostname" in record:
881 #raise MissingSfaInfo("hostname")
882 #slab_record["hostname"] = record["hostname"]
883 #if not "model" in slab_record:
884 #slab_record["model"] = "geni"
887 #elif type == "authority":
888 #slab_record["login_base"] = hrn_to_slab_login_base(hrn)
890 #if not "name" in slab_record:
891 #slab_record["name"] = hrn
893 #if not "abbreviated_name" in slab_record:
894 #slab_record["abbreviated_name"] = hrn
896 #if not "enabled" in slab_record:
897 #slab_record["enabled"] = True
899 #if not "is_public" in slab_record:
900 #slab_record["is_public"] = True
907 def __transforms_timestamp_into_date(self, xp_utc_timestamp = None):
908 """ Transforms unix timestamp into valid OAR date format """
910 #Used in case of a scheduled experiment (not immediate)
911 #To run an XP immediately, don't specify date and time in RSpec
912 #They will be set to None.
914 #transform the xp_utc_timestamp into server readable time
915 xp_server_readable_date = datetime.fromtimestamp(int(\
916 xp_utc_timestamp)).strftime(self.time_format)
918 return xp_server_readable_date
923 def LaunchExperimentOnOAR(self, slice_dict, added_nodes, slice_user=None):
924 """ Creates the structure needed for a correct POST on OAR.
925 Makes the timestamp transformation into the appropriate format.
926 Sends the POST request to create the job with the resources in
934 slice_name = slice_dict['name']
936 slot = slice_dict['timeslot']
937 logger.debug("SLABDRIVER.PY \tLaunchExperimentOnOAR \
940 #Running on default parameters
941 #XP immediate , 10 mins
942 slot = { 'date':None, 'start_time':None,
943 'timezone':None, 'duration':None }#10 min
945 reqdict['workdir'] = '/tmp'
946 reqdict['resource'] = "{network_address in ("
948 for node in added_nodes:
949 logger.debug("OARrestapi \tLaunchExperimentOnOAR \
952 #Get the ID of the node : remove the root auth and put
953 # the site in a separate list.
954 # NT: it's not clear for me if the nodenames will have the senslab
955 #prefix so lets take the last part only, for now.
957 # Again here it's not clear if nodes will be prefixed with <site>_,
958 #lets split and tanke the last part for now.
959 #s=lastpart.split("_")
962 reqdict['resource'] += "'" + nodeid + "', "
963 nodeid_list.append(nodeid)
965 custom_length = len(reqdict['resource'])- 2
966 reqdict['resource'] = reqdict['resource'][0:custom_length] + \
967 ")}/nodes=" + str(len(nodeid_list))
969 def __process_walltime(duration=None):
970 """ Calculates the walltime in seconds from the duration in H:M:S
971 specified in the RSpec.
975 walltime = duration.split(":")
976 # Fixing the walltime by adding a few delays. First put the walltime
977 # in seconds oarAdditionalDelay = 20; additional delay for
978 # /bin/sleep command to
979 # take in account prologue and epilogue scripts execution
980 # int walltimeAdditionalDelay = 120; additional delay
982 desired_walltime = int(walltime[0])*3600 + int(walltime[1]) * 60 +\
984 total_walltime = desired_walltime + 140 #+2 min 20
985 sleep_walltime = desired_walltime + 20 #+20 sec
986 logger.debug("SLABDRIVER \t__process_walltime desired_walltime %s\
987 total_walltime %s sleep_walltime %s "\
988 %(desired_walltime, total_walltime, \
990 #Put the walltime back in str form
992 walltime[0] = str(total_walltime / 3600)
993 total_walltime = total_walltime - 3600 * int(walltime[0])
994 #Get the remaining minutes
995 walltime[1] = str(total_walltime / 60)
996 total_walltime = total_walltime - 60 * int(walltime[1])
998 walltime[2] = str(total_walltime)
999 logger.debug("SLABDRIVER \t__process_walltime walltime %s "\
1002 #automatically set 10min +2 min 20
1006 sleep_walltime = '620'
1008 return walltime, sleep_walltime
1010 #if slot['duration']:
1011 walltime, sleep_walltime = __process_walltime(duration = \
1014 #walltime, sleep_walltime = self.__process_walltime(duration = None)
1016 reqdict['resource'] += ",walltime=" + str(walltime[0]) + \
1017 ":" + str(walltime[1]) + ":" + str(walltime[2])
1018 reqdict['script_path'] = "/bin/sleep " + str(sleep_walltime)
1022 #In case of a scheduled experiment (not immediate)
1023 #To run an XP immediately, don't specify date and time in RSpec
1024 #They will be set to None.
1025 server_timestamp, server_tz = self.GetTimezone()
1026 if slot['date'] and slot['start_time']:
1027 if slot['timezone'] is '' or slot['timezone'] is None:
1028 #assume it is server timezone
1029 from_zone = tz.gettz(server_tz)
1030 logger.warning("SLABDRIVER \tLaunchExperimentOnOAR timezone \
1031 not specified server_tz %s from_zone %s" \
1032 %(server_tz, from_zone))
1034 #Get zone of the user from the reservation time given
1036 from_zone = tz.gettz(slot['timezone'])
1038 date = str(slot['date']) + " " + str(slot['start_time'])
1039 user_datetime = datetime.strptime(date, self.time_format)
1040 user_datetime = user_datetime.replace(tzinfo = from_zone)
1042 #Convert to server zone
1044 to_zone = tz.gettz(server_tz)
1045 reservation_date = user_datetime.astimezone(to_zone)
1046 #Readable time accpeted by OAR
1047 reqdict['reservation'] = reservation_date.strftime(self.time_format)
1049 logger.debug("SLABDRIVER \tLaunchExperimentOnOAR \
1050 reqdict['reservation'] %s " %(reqdict['reservation']))
1053 # Immediate XP. Not need to add special parameters.
1054 # normally not used in SFA
1059 reqdict['type'] = "deploy"
1060 reqdict['directory'] = ""
1061 reqdict['name'] = "TestSandrine"
1064 # first step : start the OAR job and update the job
1065 logger.debug("SLABDRIVER.PY \tLaunchExperimentOnOAR reqdict %s\
1066 \r\n site_list %s" %(reqdict, site_list))
1068 answer = self.oar.POSTRequestToOARRestAPI('POST_job', \
1069 reqdict, slice_user)
1070 logger.debug("SLABDRIVER \tLaunchExperimentOnOAR jobid %s " %(answer))
1072 jobid = answer['id']
1074 logger.log_exc("SLABDRIVER \tLaunchExperimentOnOAR \
1075 Impossible to create job %s " %(answer))
1078 logger.debug("SLABDRIVER \tLaunchExperimentOnOAR jobid %s \
1079 added_nodes %s slice_user %s" %(jobid, added_nodes, slice_user))
1080 self.db.update_job( slice_name, jobid, added_nodes)
1083 # second step : configure the experiment
1084 # we need to store the nodes in a yaml (well...) file like this :
1085 # [1,56,23,14,45,75] with name /tmp/sfa<jobid>.json
1086 job_file = open('/tmp/sfa/'+ str(jobid) + '.json', 'w')
1088 job_file.write(str(added_nodes[0].strip('node')))
1089 for node in added_nodes[1:len(added_nodes)] :
1090 job_file.write(', '+ node.strip('node'))
1094 # third step : call the senslab-experiment wrapper
1095 #command= "java -jar target/sfa-1.0-jar-with-dependencies.jar
1096 # "+str(jobid)+" "+slice_user
1097 javacmdline = "/usr/bin/java"
1099 "/opt/senslabexperimentwrapper/sfa-1.0-jar-with-dependencies.jar"
1100 #ret=subprocess.check_output(["/usr/bin/java", "-jar", ", \
1101 #str(jobid), slice_user])
1102 output = subprocess.Popen([javacmdline, "-jar", jarname, str(jobid), \
1103 slice_user],stdout=subprocess.PIPE).communicate()[0]
1105 logger.debug("SLABDRIVER \tLaunchExperimentOnOAR wrapper returns%s " \
1110 #Delete the jobs and updates the job id in the senslab table
1112 #Does not clear the node list
1113 def DeleteSliceFromNodes(self, slice_record):
1114 # Get user information
1116 self.DeleteJobs(slice_record['oar_job_id'], slice_record['hrn'])
1117 self.db.update_job(slice_record['hrn'], job_id = -1)
1121 def GetLeaseGranularity(self):
1122 """ Returns the granularity of Senslab testbed.
1123 Defined in seconds. """
1128 def GetLeases(self, lease_filter_dict=None, return_fields_list=None):
1129 unfiltered_reservation_list = self.GetReservedNodes()
1130 reservation_list = []
1131 #Find the slice associated with this user senslab ldap uid
1132 logger.debug(" SLABDRIVER.PY \tGetLeases ")
1133 for resa in unfiltered_reservation_list:
1134 ldap_info = self.ldap.LdapSearch('(uid='+resa['user']+')')
1135 ldap_info = ldap_info[0][1]
1137 user = dbsession.query(RegUser).filter_by(email = \
1138 ldap_info['mail'][0]).first()
1139 #Separated in case user not in database : record_id not defined SA 17/07//12
1140 query_slice_info = slab_dbsession.query(SliceSenslab).filter_by(record_id_user = user.record_id)
1141 if query_slice_info:
1142 slice_info = query_slice_info.first()
1145 resa['slice_id'] = hrn_to_urn(slice_info.slice_hrn, 'slice')
1146 resa['component_id_list'] = []
1147 #Transform the hostnames into urns (component ids)
1148 for node in resa['reserved_nodes']:
1149 #resa['component_id_list'].append(hostname_to_urn(self.hrn, \
1150 #self.root_auth, node['hostname']))
1151 slab_xrn = slab_xrn_object(self.root_auth, node['hostname'])
1152 resa['component_id_list'].append(slab_xrn.urn)
1154 #Filter the reservation list if necessary
1155 #Returns all the leases associated with a given slice
1156 if lease_filter_dict:
1157 logger.debug("SLABDRIVER \tGetLeases lease_filter_dict %s"\
1158 %(lease_filter_dict))
1159 for resa in unfiltered_reservation_list:
1160 if lease_filter_dict['name'] == resa['slice_id']:
1161 reservation_list.append(resa)
1163 reservation_list = unfiltered_reservation_list
1165 logger.debug(" SLABDRIVER.PY \tGetLeases reservation_list %s"\
1166 %(reservation_list))
1167 return reservation_list
1169 def augment_records_with_testbed_info (self, sfa_records):
1170 return self.fill_record_info (sfa_records)
1172 def fill_record_info(self, record_list):
1174 Given a SFA record, fill in the senslab specific and SFA specific
1175 fields in the record.
1178 logger.debug("SLABDRIVER \tfill_record_info records %s " %(record_list))
1179 if not isinstance(record_list, list):
1180 record_list = [record_list]
1183 for record in record_list:
1184 #If the record is a SFA slice record, then add information
1185 #about the user of this slice. This kind of
1186 #information is in the Senslab's DB.
1187 if str(record['type']) == 'slice':
1188 #Get slab slice record.
1189 recslice = self.GetSlices(slice_filter = \
1190 str(record['hrn']),\
1191 slice_filter_type = 'slice_hrn')
1192 recuser = dbsession.query(RegRecord).filter_by(record_id = \
1193 recslice['record_id_user']).first()
1194 logger.debug( "SLABDRIVER.PY \t fill_record_info SLICE \
1195 rec %s \r\n \r\n" %(recslice))
1196 record.update({'PI':[recuser.hrn],
1197 'researcher': [recuser.hrn],
1198 'name':record['hrn'],
1199 'oar_job_id':recslice['oar_job_id'],
1201 'person_ids':[recslice['record_id_user']],
1202 'geni_urn':'', #For client_helper.py compatibility
1203 'keys':'', #For client_helper.py compatibility
1204 'key_ids':''}) #For client_helper.py compatibility
1206 elif str(record['type']) == 'user':
1207 #The record is a SFA user record.
1208 #Get the information about his slice from Senslab's DB
1209 #and add it to the user record.
1210 recslice = self.GetSlices(\
1211 slice_filter = record['record_id'],\
1212 slice_filter_type = 'record_id_user')
1214 logger.debug( "SLABDRIVER.PY \t fill_record_info user \
1215 rec %s \r\n \r\n" %(recslice))
1216 #Append slice record in records list,
1217 #therefore fetches user and slice info again(one more loop)
1218 #Will update PIs and researcher for the slice
1219 recuser = dbsession.query(RegRecord).filter_by(record_id = \
1220 recslice['record_id_user']).first()
1221 recslice.update({'PI':[recuser.hrn],
1222 'researcher': [recuser.hrn],
1223 'name':record['hrn'],
1224 'oar_job_id':recslice['oar_job_id'],
1226 'person_ids':[recslice['record_id_user']]})
1228 #GetPersons takes [] as filters
1229 #user_slab = self.GetPersons([{'hrn':recuser.hrn}])
1230 user_slab = self.GetPersons([record])
1232 recslice.update({'type':'slice', \
1233 'hrn':recslice['slice_hrn']})
1234 record.update(user_slab[0])
1235 #For client_helper.py compatibility
1236 record.update( { 'geni_urn':'',
1239 record_list.append(recslice)
1241 logger.debug("SLABDRIVER.PY \tfill_record_info ADDING SLICE\
1242 INFO TO USER records %s" %(record_list))
1245 except TypeError, error:
1246 logger.log_exc("SLABDRIVER \t fill_record_info EXCEPTION %s"\
1251 #self.fill_record_slab_info(records)
1257 #TODO Update membership? update_membership_list SA 05/07/12
1258 #def update_membership_list(self, oldRecord, record, listName, addFunc, \
1260 ## get a list of the HRNs tht are members of the old and new records
1262 #oldList = oldRecord.get(listName, [])
1265 #newList = record.get(listName, [])
1267 ## if the lists are the same, then we don't have to update anything
1268 #if (oldList == newList):
1271 ## build a list of the new person ids, by looking up each person to get
1275 #records = table.find({'type': 'user', 'hrn': newList})
1276 #for rec in records:
1277 #newIdList.append(rec['pointer'])
1279 ## build a list of the old person ids from the person_ids field
1281 #oldIdList = oldRecord.get("person_ids", [])
1282 #containerId = oldRecord.get_pointer()
1284 ## if oldRecord==None, then we are doing a Register, instead of an
1287 #containerId = record.get_pointer()
1289 ## add people who are in the new list, but not the oldList
1290 #for personId in newIdList:
1291 #if not (personId in oldIdList):
1292 #addFunc(self.plauth, personId, containerId)
1294 ## remove people who are in the old list, but not the new list
1295 #for personId in oldIdList:
1296 #if not (personId in newIdList):
1297 #delFunc(self.plauth, personId, containerId)
1299 #def update_membership(self, oldRecord, record):
1301 #if record.type == "slice":
1302 #self.update_membership_list(oldRecord, record, 'researcher',
1303 #self.users.AddPersonToSlice,
1304 #self.users.DeletePersonFromSlice)
1305 #elif record.type == "authority":
1310 # I don't think you plan on running a component manager at this point
1311 # let me clean up the mess of ComponentAPI that is deprecated anyways
1314 #TODO FUNCTIONS SECTION 04/07/2012 SA
1316 #TODO : Is UnBindObjectFromPeer still necessary ? Currently does nothing
1318 def UnBindObjectFromPeer(self, auth, object_type, object_id, shortname):
1319 """ This method is a hopefully temporary hack to let the sfa correctly
1320 detach the objects it creates from a remote peer object. This is
1321 needed so that the sfa federation link can work in parallel with
1322 RefreshPeer, as RefreshPeer depends on remote objects being correctly
1325 auth : struct, API authentication structure
1326 AuthMethod : string, Authentication method to use
1327 object_type : string, Object type, among 'site','person','slice',
1329 object_id : int, object_id
1330 shortname : string, peer shortname
1334 logger.warning("SLABDRIVER \tUnBindObjectFromPeer EMPTY-\
1338 #TODO Is BindObjectToPeer still necessary ? Currently does nothing
1340 def BindObjectToPeer(self, auth, object_type, object_id, shortname=None, \
1341 remote_object_id=None):
1342 """This method is a hopefully temporary hack to let the sfa correctly
1343 attach the objects it creates to a remote peer object. This is needed
1344 so that the sfa federation link can work in parallel with RefreshPeer,
1345 as RefreshPeer depends on remote objects being correctly marked.
1347 shortname : string, peer shortname
1348 remote_object_id : int, remote object_id, set to 0 if unknown
1352 logger.warning("SLABDRIVER \tBindObjectToPeer EMPTY - DO NOTHING \r\n ")
1355 #TODO UpdateSlice 04/07/2012 SA
1356 #Funciton should delete and create another job since oin senslab slice=job
1357 def UpdateSlice(self, auth, slice_id_or_name, slice_fields=None):
1358 """Updates the parameters of an existing slice with the values in
1360 Users may only update slices of which they are members.
1361 PIs may update any of the slices at their sites, or any slices of
1362 which they are members. Admins may update any slice.
1363 Only PIs and admins may update max_nodes. Slices cannot be renewed
1364 (by updating the expires parameter) more than 8 weeks into the future.
1365 Returns 1 if successful, faults otherwise.
1369 logger.warning("SLABDRIVER UpdateSlice EMPTY - DO NOTHING \r\n ")
1372 #TODO UpdatePerson 04/07/2012 SA
1373 def UpdatePerson(self, auth, person_id_or_email, person_fields=None):
1374 """Updates a person. Only the fields specified in person_fields
1375 are updated, all other fields are left untouched.
1376 Users and techs can only update themselves. PIs can only update
1377 themselves and other non-PIs at their sites.
1378 Returns 1 if successful, faults otherwise.
1382 logger.warning("SLABDRIVER UpdatePerson EMPTY - DO NOTHING \r\n ")
1385 #TODO GetKeys 04/07/2012 SA
1386 def GetKeys(self, auth, key_filter=None, return_fields=None):
1387 """Returns an array of structs containing details about keys.
1388 If key_filter is specified and is an array of key identifiers,
1389 or a struct of key attributes, only keys matching the filter
1390 will be returned. If return_fields is specified, only the
1391 specified details will be returned.
1393 Admin may query all keys. Non-admins may only query their own keys.
1397 logger.warning("SLABDRIVER GetKeys EMPTY - DO NOTHING \r\n ")
1400 #TODO DeleteKey 04/07/2012 SA
1401 def DeleteKey(self, auth, key_id):
1403 Non-admins may only delete their own keys.
1404 Returns 1 if successful, faults otherwise.
1408 logger.warning("SLABDRIVER DeleteKey EMPTY - DO NOTHING \r\n ")
1412 #TODO : Check rights to delete person
1413 def DeletePerson(self, auth, person_record):
1414 """ Disable an existing account in senslab LDAP.
1415 Users and techs can only delete themselves. PIs can only
1416 delete themselves and other non-PIs at their sites.
1417 ins can delete anyone.
1418 Returns 1 if successful, faults otherwise.
1422 #Disable user account in senslab LDAP
1423 ret = self.ldap.LdapMarkUserAsDeleted(person_record)
1424 logger.warning("SLABDRIVER DeletePerson %s " %(person_record))
1427 #TODO Check DeleteSlice, check rights 05/07/2012 SA
1428 def DeleteSlice(self, auth, slice_record):
1429 """ Deletes the specified slice.
1430 Senslab : Kill the job associated with the slice if there is one
1431 using DeleteSliceFromNodes.
1432 Updates the slice record in slab db to remove the slice nodes.
1434 Users may only delete slices of which they are members. PIs may
1435 delete any of the slices at their sites, or any slices of which
1436 they are members. Admins may delete any slice.
1437 Returns 1 if successful, faults otherwise.
1441 self.DeleteSliceFromNodes(slice_record)
1442 self.db.update_job(slice_record['hrn'], job_id = -1, nodes = [])
1443 logger.warning("SLABDRIVER DeleteSlice %s "%(slice_record))
1446 #TODO AddPerson 04/07/2012 SA
1447 def AddPerson(self, auth, person_fields=None):
1448 """Adds a new account. Any fields specified in person_fields are used,
1449 otherwise defaults are used.
1450 Accounts are disabled by default. To enable an account,
1452 Returns the new person_id (> 0) if successful, faults otherwise.
1456 logger.warning("SLABDRIVER AddPerson EMPTY - DO NOTHING \r\n ")
1459 #TODO AddPersonToSite 04/07/2012 SA
1460 def AddPersonToSite (self, auth, person_id_or_email, \
1461 site_id_or_login_base=None):
1462 """ Adds the specified person to the specified site. If the person is
1463 already a member of the site, no errors are returned. Does not change
1464 the person's primary site.
1465 Returns 1 if successful, faults otherwise.
1469 logger.warning("SLABDRIVER AddPersonToSite EMPTY - DO NOTHING \r\n ")
1472 #TODO AddRoleToPerson : Not sure if needed in senslab 04/07/2012 SA
1473 def AddRoleToPerson(self, auth, role_id_or_name, person_id_or_email):
1474 """Grants the specified role to the person.
1475 PIs can only grant the tech and user roles to users and techs at their
1476 sites. Admins can grant any role to any user.
1477 Returns 1 if successful, faults otherwise.
1481 logger.warning("SLABDRIVER AddRoleToPerson EMPTY - DO NOTHING \r\n ")
1484 #TODO AddPersonKey 04/07/2012 SA
1485 def AddPersonKey(self, auth, person_id_or_email, key_fields=None):
1486 """Adds a new key to the specified account.
1487 Non-admins can only modify their own keys.
1488 Returns the new key_id (> 0) if successful, faults otherwise.
1492 logger.warning("SLABDRIVER AddPersonKey EMPTY - DO NOTHING \r\n ")