2 Implements what a driver should provide for SFA to work.
4 from sfa.util.faults import SliverDoesNotExist, UnknownSfaType
5 from sfa.util.sfalogging import logger
6 from sfa.storage.model import RegRecord
8 from sfa.managers.driver import Driver
9 from sfa.rspecs.version_manager import VersionManager
10 from sfa.rspecs.rspec import RSpec
12 from sfa.util.xrn import Xrn, hrn_to_urn, get_authority
14 from sfa.iotlab.iotlabaggregate import IotlabAggregate, iotlab_xrn_to_hostname
15 from sfa.iotlab.iotlabslices import IotlabSlices
18 from sfa.iotlab.iotlabshell import IotlabShell
21 class IotlabDriver(Driver):
22 """ Iotlab Driver class inherited from Driver generic class.
24 Contains methods compliant with the SFA standard and the testbed
25 infrastructure (calls to LDAP and OAR).
27 .. seealso::: Driver class
30 def __init__(self, api):
33 Sets the iotlab SFA config parameters,
34 instanciates the testbed api and the iotlab database.
36 :param config: iotlab SFA configuration object
37 :type config: Config object
40 Driver.__init__(self, api)
43 self.testbed_shell = IotlabShell(api)
46 def augment_records_with_testbed_info(self, record_list):
49 Adds specific testbed info to the records.
51 :param record_list: list of sfa dictionaries records
52 :type record_list: list
53 :returns: list of records with extended information in each record
57 return self.fill_record_info(record_list)
59 def fill_record_info(self, record_list):
62 For each SFA record, fill in the iotlab specific and SFA specific
65 :param record_list: list of sfa dictionaries records
66 :type record_list: list
67 :returns: list of records with extended information in each record
70 .. warning:: Should not be modifying record_list directly because modi
71 fication are kept outside the method's scope. Howerver, there is no
72 other way to do it given the way it's called in registry manager.
76 logger.debug("IOTLABDRIVER \tfill_record_info records %s "
78 if not isinstance(record_list, list):
79 record_list = [record_list]
82 for record in record_list:
84 if str(record['type']) == 'node':
85 # look for node info using GetNodes
86 # the record is about one node only
87 filter_dict = {'hrn': [record['hrn']]}
88 node_info = self.testbed_shell.GetNodes(filter_dict)
89 # the node_info is about one node only, but it is formatted
91 record.update(node_info[0])
92 logger.debug("IOTLABDRIVER.PY \t \
93 fill_record_info NODE" % (record))
95 #If the record is a SFA slice record, then add information
96 #about the user of this slice. This kind of
97 #information is in the Iotlab's DB.
98 if str(record['type']) == 'slice':
99 if 'reg_researchers' in record and isinstance(record
102 record['reg_researchers'] = \
103 record['reg_researchers'][0].__dict__
105 {'PI': [record['reg_researchers']['hrn']],
106 'researcher': [record['reg_researchers']['hrn']],
107 'name': record['hrn'],
110 'person_ids': [record['reg_researchers']
112 # For client_helper.py compatibility
114 # For client_helper.py compatibility
116 # For client_helper.py compatibility
119 #Get iotlab slice record and oar job id if any.
120 recslice_list = self.testbed_shell.GetSlices(
121 slice_filter=str(record['hrn']),
122 slice_filter_type='slice_hrn')
124 logger.debug("IOTLABDRIVER \tfill_record_info \
125 TYPE SLICE RECUSER record['hrn'] %s record['oar_job_id']\
126 %s " % (record['hrn'], record['oar_job_id']))
127 del record['reg_researchers']
129 for rec in recslice_list:
130 logger.debug("IOTLABDRIVER\r\n \t \
131 fill_record_info oar_job_id %s "
132 % (rec['oar_job_id']))
134 record['node_ids'] = [self.testbed_shell.root_auth +
135 '.' + hostname for hostname
140 logger.debug("IOTLABDRIVER.PY \t fill_record_info SLICE \
141 recslice_list %s \r\n \t RECORD %s \r\n \
142 \r\n" % (recslice_list, record))
144 if str(record['type']) == 'user':
145 #The record is a SFA user record.
146 #Get the information about his slice from Iotlab's DB
147 #and add it to the user record.
148 recslice_list = self.testbed_shell.GetSlices(
149 slice_filter=record['record_id'],
150 slice_filter_type='record_id_user')
152 logger.debug("IOTLABDRIVER.PY \t fill_record_info \
153 TYPE USER recslice_list %s \r\n \t RECORD %s \r\n"
154 % (recslice_list, record))
155 #Append slice record in records list,
156 #therefore fetches user and slice info again(one more loop)
157 #Will update PIs and researcher for the slice
159 recuser = recslice_list[0]['reg_researchers']
160 logger.debug("IOTLABDRIVER.PY \t fill_record_info USER \
161 recuser %s \r\n \r\n" % (recuser))
163 recslice = recslice_list[0]
165 {'PI': [recuser['hrn']],
166 'researcher': [recuser['hrn']],
167 'name': record['hrn'],
170 'person_ids': [recuser['record_id']]})
172 for rec in recslice_list:
173 recslice['oar_job_id'].append(rec['oar_job_id'])
177 recslice.update({'type': 'slice',
178 'hrn': recslice_list[0]['hrn']})
180 #GetPersons takes [] as filters
181 user_iotlab = self.testbed_shell.GetPersons([record])
183 record.update(user_iotlab[0])
184 #For client_helper.py compatibility
189 record_list.append(recslice)
191 logger.debug("IOTLABDRIVER.PY \t \
192 fill_record_info ADDING SLICE\
193 INFO TO USER records %s" % (record_list))
195 except TypeError, error:
196 logger.log_exc("IOTLABDRIVER \t fill_record_info EXCEPTION %s"
201 def sliver_status(self, slice_urn, slice_hrn):
203 Receive a status request for slice named urn/hrn
204 urn:publicid:IDN+iotlab+nturro_slice hrn iotlab.nturro_slice
205 shall return a structure as described in
206 http://groups.geni.net/geni/wiki/GAPI_AM_API_V2#SliverStatus
207 NT : not sure if we should implement this or not, but used by sface.
209 :param slice_urn: slice urn
210 :type slice_urn: string
211 :param slice_hrn: slice hrn
212 :type slice_hrn: string
216 #First get the slice with the slice hrn
217 slice_list = self.testbed_shell.GetSlices(slice_filter=slice_hrn,
218 slice_filter_type='slice_hrn')
220 if len(slice_list) == 0:
221 raise SliverDoesNotExist("%s slice_hrn" % (slice_hrn))
223 #Used for fetching the user info witch comes along the slice info
224 one_slice = slice_list[0]
226 #Make a list of all the nodes hostnames in use for this slice
227 slice_nodes_list = []
228 slice_nodes_list = one_slice['node_ids']
229 #Get all the corresponding nodes details
230 nodes_all = self.testbed_shell.GetNodes(
231 {'hostname': slice_nodes_list},
232 ['node_id', 'hostname', 'site', 'boot_state'])
233 nodeall_byhostname = dict([(one_node['hostname'], one_node)
234 for one_node in nodes_all])
236 for single_slice in slice_list:
238 top_level_status = 'empty'
241 ['geni_urn', 'geni_error', 'iotlab_login', 'geni_status',
242 'geni_resources'], None)
244 # ['geni_urn','geni_error', 'pl_login','geni_status',
245 # 'geni_resources'], None)
246 # result['pl_login'] = one_slice['reg_researchers'][0].hrn
247 result['iotlab_login'] = one_slice['user']
248 logger.debug("Slabdriver - sliver_status Sliver status \
249 urn %s hrn %s single_slice %s \r\n "
250 % (slice_urn, slice_hrn, single_slice))
252 if 'node_ids' not in single_slice:
254 result['geni_status'] = top_level_status
255 result['geni_resources'] = []
258 top_level_status = 'ready'
260 #A job is running on Iotlab for this slice
261 # report about the local nodes that are in the slice only
263 result['geni_urn'] = slice_urn
266 for node_hostname in single_slice['node_ids']:
268 res['iotlab_hostname'] = node_hostname
269 res['iotlab_boot_state'] = \
270 nodeall_byhostname[node_hostname]['boot_state']
272 #res['pl_hostname'] = node['hostname']
273 #res['pl_boot_state'] = \
274 #nodeall_byhostname[node['hostname']]['boot_state']
275 #res['pl_last_contact'] = strftime(self.time_format, \
276 #gmtime(float(timestamp)))
278 slice_urn, type='slice',
279 id=nodeall_byhostname[node_hostname]['node_id']).urn
281 res['geni_urn'] = sliver_id
282 #node_name = node['hostname']
283 if nodeall_byhostname[node_hostname]['boot_state'] == 'Alive':
285 res['geni_status'] = 'ready'
287 res['geni_status'] = 'failed'
288 top_level_status = 'failed'
290 res['geni_error'] = ''
292 resources.append(res)
294 result['geni_status'] = top_level_status
295 result['geni_resources'] = resources
296 logger.debug("IOTLABDRIVER \tsliver_statusresources %s res %s "
301 def get_user_record(hrn):
304 Returns the user record based on the hrn from the SFA DB .
306 :param hrn: user's hrn
308 :returns: user record from SFA database
312 # xxx this method should not be static
313 from sfa.storage.alchemy import global_dbsession
314 return global_dbsession.query(RegRecord).filter_by(hrn=hrn).first()
315 # code should read instead (if this method was not static, that is)
316 #return self.api.dbsession().query(RegRecord).filter_by(hrn=hrn).first()
318 def testbed_name(self):
321 Returns testbed's name.
322 :returns: testbed authority name.
328 # 'geni_request_rspec_versions' and 'geni_ad_rspec_versions' are mandatory
329 def aggregate_version(self):
332 Returns the testbed's supported rspec advertisement and request
334 :returns: rspec versions supported ad a dictionary.
338 version_manager = VersionManager()
339 ad_rspec_versions = []
340 request_rspec_versions = []
341 for rspec_version in version_manager.versions:
342 if rspec_version.content_type in ['*', 'ad']:
343 ad_rspec_versions.append(rspec_version.to_dict())
344 if rspec_version.content_type in ['*', 'request']:
345 request_rspec_versions.append(rspec_version.to_dict())
347 'testbed': self.testbed_name(),
348 'geni_request_rspec_versions': request_rspec_versions,
349 'geni_ad_rspec_versions': ad_rspec_versions}
351 def _get_requested_leases_list(self, rspec):
353 Process leases in rspec depending on the rspec version (format)
354 type. Find the lease requests in the rspec and creates
355 a lease request list with the mandatory information ( nodes,
356 start time and duration) of the valid leases (duration above or
357 equal to the iotlab experiment minimum duration).
359 :param rspec: rspec request received.
361 :returns: list of lease requests found in the rspec
364 requested_lease_list = []
365 for lease in rspec.version.get_leases():
366 single_requested_lease = {}
367 logger.debug("IOTLABDRIVER.PY \t \
368 _get_requested_leases_list lease %s " % (lease))
370 if not lease.get('lease_id'):
371 if get_authority(lease['component_id']) == \
372 self.testbed_shell.root_auth:
373 single_requested_lease['hostname'] = \
374 iotlab_xrn_to_hostname(\
375 lease.get('component_id').strip())
376 single_requested_lease['start_time'] = \
377 lease.get('start_time')
378 single_requested_lease['duration'] = lease.get('duration')
379 #Check the experiment's duration is valid before adding
380 #the lease to the requested leases list
381 duration_in_seconds = \
382 int(single_requested_lease['duration'])
383 if duration_in_seconds >= self.testbed_shell.GetMinExperimentDurationInGranularity():
384 requested_lease_list.append(single_requested_lease)
386 return requested_lease_list
389 def _group_leases_by_start_time(requested_lease_list):
391 Create dict of leases by start_time, regrouping nodes reserved
392 at the same time, for the same amount of time so as to
393 define one job on OAR.
395 :param requested_lease_list: list of leases
396 :type requested_lease_list: list
397 :returns: Dictionary with key = start time, value = list of leases
398 with the same start time.
403 requested_xp_dict = {}
404 for lease in requested_lease_list:
406 #In case it is an asap experiment start_time is empty
407 if lease['start_time'] == '':
408 lease['start_time'] = '0'
410 if lease['start_time'] not in requested_xp_dict:
411 if isinstance(lease['hostname'], str):
412 lease['hostname'] = [lease['hostname']]
414 requested_xp_dict[lease['start_time']] = lease
417 job_lease = requested_xp_dict[lease['start_time']]
418 if lease['duration'] == job_lease['duration']:
419 job_lease['hostname'].append(lease['hostname'])
421 return requested_xp_dict
423 def _process_requested_xp_dict(self, rspec):
425 Turns the requested leases and information into a dictionary
426 of requested jobs, grouped by starting time.
428 :param rspec: RSpec received
433 requested_lease_list = self._get_requested_leases_list(rspec)
434 logger.debug("IOTLABDRIVER _process_requested_xp_dict \
435 requested_lease_list %s" % (requested_lease_list))
436 xp_dict = self._group_leases_by_start_time(requested_lease_list)
437 logger.debug("IOTLABDRIVER _process_requested_xp_dict xp_dict\
442 def create_sliver(self, slice_urn, slice_hrn, creds, rspec_string,
444 """Answer to CreateSliver.
446 Creates the leases and slivers for the users from the information
447 found in the rspec string.
448 Launch experiment on OAR if the requested leases is valid. Delete
449 no longer requested leases.
452 :param creds: user's credentials
454 :param users: user record list
459 :returns: a valid Rspec for the slice which has just been
465 aggregate = IotlabAggregate(self)
467 slices = IotlabSlices(self)
468 peer = slices.get_peer(slice_hrn)
469 sfa_peer = slices.get_sfa_peer(slice_hrn)
472 if not isinstance(creds, list):
476 slice_record = users[0].get('slice_record', {})
477 logger.debug("IOTLABDRIVER.PY \t ===============create_sliver \t\
478 creds %s \r\n \r\n users %s"
480 slice_record['user'] = {'keys': users[0]['keys'],
481 'email': users[0]['email'],
482 'hrn': slice_record['reg-researchers'][0]}
484 rspec = RSpec(rspec_string)
485 logger.debug("IOTLABDRIVER.PY \t create_sliver \trspec.version \
486 %s slice_record %s users %s"
487 % (rspec.version, slice_record, users))
489 # ensure site record exists?
490 # ensure slice record exists
491 #Removed options in verify_slice SA 14/08/12
492 #Removed peer record in verify_slice SA 18/07/13
493 sfa_slice = slices.verify_slice(slice_hrn, slice_record, sfa_peer)
495 # ensure person records exists
496 #verify_persons returns added persons but the return value
498 #Removed peer record and sfa_peer in verify_persons SA 18/07/13
499 slices.verify_persons(slice_hrn, sfa_slice, users, options=options)
500 #requested_attributes returned by rspec.version.get_slice_attributes()
501 #unused, removed SA 13/08/12
502 #rspec.version.get_slice_attributes()
504 logger.debug("IOTLABDRIVER.PY create_sliver slice %s " % (sfa_slice))
506 # add/remove slice from nodes
508 #requested_slivers = [node.get('component_id') \
509 #for node in rspec.version.get_nodes_with_slivers()\
510 #if node.get('authority_id') is self.testbed_shell.root_auth]
511 #l = [ node for node in rspec.version.get_nodes_with_slivers() ]
512 #logger.debug("SLADRIVER \tcreate_sliver requested_slivers \
513 #requested_slivers %s listnodes %s" \
514 #%(requested_slivers,l))
515 #verify_slice_nodes returns nodes, but unused here. Removed SA 13/08/12.
516 #slices.verify_slice_nodes(sfa_slice, requested_slivers, peer)
518 requested_xp_dict = self._process_requested_xp_dict(rspec)
520 logger.debug("IOTLABDRIVER.PY \tcreate_sliver requested_xp_dict %s "
521 % (requested_xp_dict))
522 #verify_slice_leases returns the leases , but the return value is unused
523 #here. Removed SA 13/08/12
524 slices.verify_slice_leases(sfa_slice,
525 requested_xp_dict, peer)
527 return aggregate.get_rspec(slice_xrn=slice_urn,
528 login=sfa_slice['login'],
529 version=rspec.version)
531 def delete_sliver(self, slice_urn, slice_hrn, creds, options):
533 Deletes the lease associated with the slice hrn and the credentials
534 if the slice belongs to iotlab. Answer to DeleteSliver.
536 :param slice_urn: urn of the slice
537 :param slice_hrn: name of the slice
538 :param creds: slice credenials
539 :type slice_urn: string
540 :type slice_hrn: string
541 :type creds: ? unused
543 :returns: 1 if the slice to delete was not found on iotlab,
544 True if the deletion was successful, False otherwise otherwise.
546 .. note:: Should really be named delete_leases because iotlab does
547 not have any slivers, but only deals with leases. However,
548 SFA api only have delete_sliver define so far. SA 13/05/2013
549 .. note:: creds are unused, and are not used either in the dummy driver
553 sfa_slice_list = self.testbed_shell.GetSlices(
554 slice_filter=slice_hrn,
555 slice_filter_type='slice_hrn')
557 if not sfa_slice_list:
560 #Delete all leases in the slice
561 for sfa_slice in sfa_slice_list:
562 logger.debug("IOTLABDRIVER.PY delete_sliver slice %s" % (sfa_slice))
563 slices = IotlabSlices(self)
564 # determine if this is a peer slice
566 peer = slices.get_peer(slice_hrn)
568 logger.debug("IOTLABDRIVER.PY delete_sliver peer %s \
569 \r\n \t sfa_slice %s " % (peer, sfa_slice))
571 self.testbed_shell.DeleteSliceFromNodes(sfa_slice)
576 def list_resources (self, slice_urn, slice_hrn, creds, options):
579 List resources from the iotlab aggregate and returns a Rspec
580 advertisement with resources found when slice_urn and slice_hrn are
581 None (in case of resource discovery).
582 If a slice hrn and urn are provided, list experiment's slice
583 nodes in a rspec format. Answer to ListResources.
586 :param slice_urn: urn of the slice
587 :param slice_hrn: name of the slice
588 :param creds: slice credenials
589 :type slice_urn: string
590 :type slice_hrn: string
591 :type creds: ? unused
592 :param options: options used when listing resources (list_leases, info,
594 :returns: rspec string in xml
597 .. note:: creds are unused
600 #cached_requested = options.get('cached', True)
602 version_manager = VersionManager()
603 # get the rspec's return format from options
605 version_manager.get_version(options.get('geni_rspec_version'))
606 version_string = "rspec_%s" % (rspec_version)
608 #panos adding the info option to the caching key (can be improved)
609 if options.get('info'):
610 version_string = version_string + "_" + \
611 options.get('info', 'default')
613 # Adding the list_leases option to the caching key
614 if options.get('list_leases'):
615 version_string = version_string + "_" + \
616 options.get('list_leases', 'default')
618 # Adding geni_available to caching key
619 if options.get('geni_available'):
620 version_string = version_string + "_" + \
621 str(options.get('geni_available'))
623 # look in cache first
624 #if cached_requested and self.cache and not slice_hrn:
625 #rspec = self.cache.get(version_string)
627 #logger.debug("IotlabDriver.ListResources: \
628 #returning cached advertisement")
631 #panos: passing user-defined options
632 aggregate = IotlabAggregate(self)
634 rspec = aggregate.get_rspec(slice_xrn=slice_urn,
635 version=rspec_version, options=options)
638 #if self.cache and not slice_hrn:
639 #logger.debug("Iotlab.ListResources: stores advertisement in cache")
640 #self.cache.add(version_string, rspec)
645 def list_slices(self, creds, options):
646 """Answer to ListSlices.
648 List slices belonging to iotlab, returns slice urns list.
649 No caching used. Options unused but are defined in the SFA method
652 :returns: slice urns list
655 .. note:: creds are unused
657 # look in cache first
659 #slices = self.cache.get('slices')
661 #logger.debug("PlDriver.list_slices returns from cache")
666 slices = self.testbed_shell.GetSlices()
667 logger.debug("IOTLABDRIVER.PY \tlist_slices hrn %s \r\n \r\n"
669 slice_hrns = [iotlab_slice['hrn'] for iotlab_slice in slices]
671 slice_urns = [hrn_to_urn(slice_hrn, 'slice')
672 for slice_hrn in slice_hrns]
676 #logger.debug ("IotlabDriver.list_slices stores value in cache")
677 #self.cache.add('slices', slice_urns)
682 def register(self, sfa_record, hrn, pub_key):
684 Adding new user, slice, node or site should not be handled
687 ..warnings:: should not be used. Different components are in charge of
688 doing this task. Adding nodes = OAR
689 Adding users = LDAP Iotlab
690 Adding slice = Import from LDAP users
693 :param sfa_record: record provided by the client of the
695 :type sfa_record: dict
696 :param pub_key: public key of the user
697 :type pub_key: string
699 .. note:: DOES NOTHING. Returns -1.
705 def update(self, old_sfa_record, new_sfa_record, hrn, new_key):
707 No site or node record update allowed in Iotlab. The only modifications
708 authorized here are key deletion/addition on an existing user and
709 password change. On an existing user, CAN NOT BE MODIFIED: 'first_name',
710 'last_name', 'email'. DOES NOT EXIST IN SENSLAB: 'phone', 'url', 'bio',
711 'title', 'accepted_aup'. A slice is bound to its user, so modifying the
712 user's ssh key should nmodify the slice's GID after an import procedure.
714 :param old_sfa_record: what is in the db for this hrn
715 :param new_sfa_record: what was passed to the update call
716 :param new_key: the new user's public key
717 :param hrn: the user's sfa hrn
718 :type old_sfa_record: dict
719 :type new_sfa_record: dict
720 :type new_key: string
724 .. seealso:: update in driver.py.
727 pointer = old_sfa_record['pointer']
728 old_sfa_record_type = old_sfa_record['type']
730 # new_key implemented for users only
731 if new_key and old_sfa_record_type not in ['user']:
732 raise UnknownSfaType(old_sfa_record_type)
734 if old_sfa_record_type == "user":
736 all_fields = new_sfa_record
737 for key in all_fields.keys():
738 if key in ['key', 'password']:
739 update_fields[key] = all_fields[key]
742 # must check this key against the previous one if it exists
743 persons = self.testbed_shell.GetPersons([old_sfa_record])
745 keys = [person['pkey']]
746 #Get all the person's keys
747 keys_dict = self.testbed_shell.GetKeys(keys)
749 # Delete all stale keys, meaning the user has only one key
751 #TODO: do we really want to delete all the other keys?
752 #Is this a problem with the GID generation to have multiple
758 #remove all the other keys
759 for key in keys_dict:
760 self.testbed_shell.DeleteKey(person, key)
761 self.testbed_shell.AddPersonKey(
762 person, {'sshPublicKey': person['pkey']},
763 {'sshPublicKey': new_key})
766 def remove(self, sfa_record):
769 Removes users only. Mark the user as disabled in LDAP. The user and his
770 slice are then deleted from the db by running an import on the registry.
772 :param sfa_record: record is the existing sfa record in the db
773 :type sfa_record: dict
775 ..warning::As fas as the slice is concerned, here only the leases are
776 removed from the slice. The slice is record itself is not removed
781 TODO : REMOVE SLICE FROM THE DB AS WELL? SA 14/05/2013,
783 TODO: return boolean for the slice part
785 sfa_record_type = sfa_record['type']
786 hrn = sfa_record['hrn']
787 if sfa_record_type == 'user':
789 #get user from iotlab ldap
790 person = self.testbed_shell.GetPersons(sfa_record)
791 #No registering at a given site in Iotlab.
792 #Once registered to the LDAP, all iotlab sites are
795 #Mark account as disabled in ldap
796 return self.testbed_shell.DeletePerson(sfa_record)
798 elif sfa_record_type == 'slice':
799 if self.testbed_shell.GetSlices(slice_filter=hrn,
800 slice_filter_type='slice_hrn'):
801 ret = self.testbed_shell.DeleteSlice(sfa_record)