2 File containing the IotlabShell, used to interact with nodes, users,
3 slices, leases and keys, as well as the dedicated iotlab database and table,
4 holding information about which slice is running which job.
5 TODO: Remove interactons with the SFA DB and put it in the driver iotlabdriver
9 from datetime import datetime
11 from sfa.util.sfalogging import logger
13 from sqlalchemy.orm import joinedload
14 from sfa.storage.model import RegRecord, RegUser, RegSlice, RegKey
15 from sfa.iotlab.iotlabpostgres import TestbedAdditionalSfaDB, LeaseTableXP
16 from sfa.iotlab.OARrestapi import OARrestapi
17 from sfa.iotlab.LDAPapi import LDAPapi
19 from sfa.util.xrn import Xrn, hrn_to_urn, get_authority
21 from sfa.trust.certificate import Keypair, convert_public_key
22 from sfa.trust.gid import create_uuid
23 from sfa.trust.hierarchy import Hierarchy
25 from sfa.iotlab.iotlabxrn import xrn_object
28 """ Class enabled to use LDAP and OAR api calls. """
30 _MINIMUM_DURATION = 10 # 10 units of granularity 60 s, 10 mins
32 def __init__(self, api):
33 """Creates an instance of OARrestapi and LDAPapi which will be used to
34 issue calls to OAR or LDAP methods.
35 Set the time format and the testbed granularity used for OAR
36 reservation and leases.
38 :param config: configuration object from sfa.util.config
39 :type config: Config object
43 self.leases_db = TestbedAdditionalSfaDB(config)
44 self.oar = OARrestapi()
46 self.time_format = "%Y-%m-%d %H:%M:%S"
47 self.root_auth = config.SFA_REGISTRY_ROOT_AUTH
48 self.grain = 60 # 10 mins lease minimum, 60 sec granularity
49 #import logging, logging.handlers
50 #from sfa.util.sfalogging import _SfaLogger
51 #sql_logger = _SfaLogger(loggername = 'sqlalchemy.engine', \
56 def GetMinExperimentDurationInGranularity():
57 """ Returns the minimum allowed duration for an experiment on the
61 return IotlabShell._MINIMUM_DURATION
64 def GetPeers(self, peer_filter=None ):
65 """ Gathers registered authorities in SFA DB and looks for specific peer
66 if peer_filter is specified.
67 :param peer_filter: name of the site authority looked for.
68 :type peer_filter: string
69 :returns: list of records.
74 existing_hrns_by_types = {}
75 logger.debug("IOTLAB_API \tGetPeers peer_filter %s " % (peer_filter))
76 all_records = self.api.dbsession().query(RegRecord).filter(RegRecord.type.like('%authority%')).all()
78 for record in all_records:
79 existing_records[(record.hrn, record.type)] = record
80 if record.type not in existing_hrns_by_types:
81 existing_hrns_by_types[record.type] = [record.hrn]
83 existing_hrns_by_types[record.type].append(record.hrn)
85 logger.debug("IOTLAB_API \tGetPeer\texisting_hrns_by_types %s "
86 % (existing_hrns_by_types))
91 records_list.append(existing_records[(peer_filter,
94 for hrn in existing_hrns_by_types['authority']:
95 records_list.append(existing_records[(hrn, 'authority')])
97 logger.debug("IOTLAB_API \tGetPeer \trecords_list %s "
103 return_records = records_list
104 logger.debug("IOTLAB_API \tGetPeer return_records %s "
106 return return_records
108 #TODO : Handling OR request in make_ldap_filters_from_records
109 #instead of the for loop
110 #over the records' list
111 def GetPersons(self, person_filter=None):
113 Get the enabled users and their properties from Iotlab LDAP.
114 If a filter is specified, looks for the user whose properties match
115 the filter, otherwise returns the whole enabled users'list.
117 :param person_filter: Must be a list of dictionnaries with users
118 properties when not set to None.
119 :type person_filter: list of dict
121 :returns: Returns a list of users whose accounts are enabled
123 :rtype: list of dicts
126 logger.debug("IOTLAB_API \tGetPersons person_filter %s"
129 if person_filter and isinstance(person_filter, list):
130 #If we are looking for a list of users (list of dict records)
131 #Usually the list contains only one user record
132 for searched_attributes in person_filter:
134 #Get only enabled user accounts in iotlab LDAP :
135 #add a filter for make_ldap_filters_from_record
136 person = self.ldap.LdapFindUser(searched_attributes,
137 is_user_enabled=True)
138 #If a person was found, append it to the list
140 person_list.append(person)
142 #If the list is empty, return None
143 if len(person_list) is 0:
147 #Get only enabled user accounts in iotlab LDAP :
148 #add a filter for make_ldap_filters_from_record
149 person_list = self.ldap.LdapFindUser(is_user_enabled=True)
154 #def GetTimezone(self):
155 #""" Returns the OAR server time and timezone.
156 #Unused SA 30/05/13"""
157 #server_timestamp, server_tz = self.oar.parser.\
158 #SendRequest("GET_timezone")
159 #return server_timestamp, server_tz
161 def DeleteJobs(self, job_id, username):
164 Deletes the job with the specified job_id and username on OAR by
165 posting a delete request to OAR.
167 :param job_id: job id in OAR.
168 :param username: user's iotlab login in LDAP.
169 :type job_id: integer
170 :type username: string
172 :returns: dictionary with the job id and if delete has been successful
177 logger.debug("IOTLAB_API \tDeleteJobs jobid %s username %s "
178 % (job_id, username))
179 if not job_id or job_id is -1:
183 reqdict['method'] = "delete"
184 reqdict['strval'] = str(job_id)
186 answer = self.oar.POSTRequestToOARRestAPI('DELETE_jobs_id',
188 if answer['status'] == 'Delete request registered':
191 ret = {job_id: False}
192 logger.debug("IOTLAB_API \tDeleteJobs jobid %s \r\n answer %s \
193 username %s" % (job_id, answer, username))
198 ##TODO : Unused GetJobsId ? SA 05/07/12
199 #def GetJobsId(self, job_id, username = None ):
201 #Details about a specific job.
202 #Includes details about submission time, jot type, state, events,
203 #owner, assigned ressources, walltime etc...
207 #node_list_k = 'assigned_network_address'
208 ##Get job info from OAR
209 #job_info = self.oar.parser.SendRequest(req, job_id, username)
211 #logger.debug("IOTLAB_API \t GetJobsId %s " %(job_info))
213 #if job_info['state'] == 'Terminated':
214 #logger.debug("IOTLAB_API \t GetJobsId job %s TERMINATED"\
217 #if job_info['state'] == 'Error':
218 #logger.debug("IOTLAB_API \t GetJobsId ERROR message %s "\
223 #logger.error("IOTLAB_API \tGetJobsId KeyError")
226 #parsed_job_info = self.get_info_on_reserved_nodes(job_info, \
228 ##Replaces the previous entry
229 ##"assigned_network_address" / "reserved_resources"
231 #job_info.update({'node_ids':parsed_job_info[node_list_k]})
232 #del job_info[node_list_k]
233 #logger.debug(" \r\nIOTLAB_API \t GetJobsId job_info %s " %(job_info))
237 def GetJobsResources(self, job_id, username = None):
238 """ Gets the list of nodes associated with the job_id and username
241 Transforms the iotlab hostnames to the corresponding SFA nodes hrns.
242 Returns dict key :'node_ids' , value : hostnames list.
244 :param username: user's LDAP login
245 :paran job_id: job's OAR identifier.
246 :type username: string
247 :type job_id: integer
249 :returns: dicionary with nodes' hostnames belonging to the job.
252 .. warning:: Unused. SA 16/10/13
255 req = "GET_jobs_id_resources"
258 #Get job resources list from OAR
259 node_id_list = self.oar.parser.SendRequest(req, job_id, username)
260 logger.debug("IOTLAB_API \t GetJobsResources %s " %(node_id_list))
263 self.__get_hostnames_from_oar_node_ids(node_id_list)
266 #Replaces the previous entry "assigned_network_address" /
267 #"reserved_resources" with "node_ids"
268 job_info = {'node_ids': hostname_list}
273 def GetNodesCurrentlyInUse(self):
274 """Returns a list of all the nodes already involved in an oar running
276 :rtype: list of nodes hostnames.
278 return self.oar.parser.SendRequest("GET_running_jobs")
280 def __get_hostnames_from_oar_node_ids(self, oar_id_node_dict,
282 """Get the hostnames of the nodes from their OAR identifiers.
283 Get the list of nodes dict using GetNodes and find the hostname
284 associated with the identifier.
285 :param oar_id_node_dict: full node dictionary list keyed by oar node id
286 :param resource_id_list: list of nodes identifiers
287 :returns: list of node hostnames.
291 for resource_id in resource_id_list:
292 #Because jobs requested "asap" do not have defined resources
293 if resource_id is not "Undefined":
294 hostname_list.append(\
295 oar_id_node_dict[resource_id]['hostname'])
299 def GetReservedNodes(self, username=None):
300 """ Get list of leases. Get the leases for the username if specified,
301 otherwise get all the leases. Finds the nodes hostnames for each
303 :param username: user's LDAP login
304 :type username: string
305 :returns: list of reservations dict
309 #Get the nodes in use and the reserved nodes
310 reservation_dict_list = \
311 self.oar.parser.SendRequest("GET_reserved_nodes", \
314 # Get the full node dict list once for all
315 # so that we can get the hostnames given their oar node id afterwards
316 # when the reservations are checked.
317 full_nodes_dict_list = self.GetNodes()
318 #Put the full node list into a dictionary keyed by oar node id
319 oar_id_node_dict = {}
320 for node in full_nodes_dict_list:
321 oar_id_node_dict[node['oar_id']] = node
323 for resa in reservation_dict_list:
324 logger.debug ("GetReservedNodes resa %s"%(resa))
325 #dict list of hostnames and their site
326 resa['reserved_nodes'] = \
327 self.__get_hostnames_from_oar_node_ids(oar_id_node_dict,
328 resa['resource_ids'])
330 #del resa['resource_ids']
331 return reservation_dict_list
333 def GetNodes(self, node_filter_dict=None, return_fields_list=None):
336 Make a list of iotlab nodes and their properties from information
337 given by OAR. Search for specific nodes if some filters are
338 specified. Nodes properties returned if no return_fields_list given:
339 'hrn','archi','mobile','hostname','site','boot_state','node_id',
340 'radio','posx','posy','oar_id','posz'.
342 :param node_filter_dict: dictionnary of lists with node properties. For
343 instance, if you want to look for a specific node with its hrn,
344 the node_filter_dict should be {'hrn': [hrn_of_the_node]}
345 :type node_filter_dict: dict
346 :param return_fields_list: list of specific fields the user wants to be
348 :type return_fields_list: list
349 :returns: list of dictionaries with node properties
353 node_dict_by_id = self.oar.parser.SendRequest("GET_resources_full")
354 node_dict_list = node_dict_by_id.values()
355 logger.debug (" IOTLAB_API GetNodes node_filter_dict %s \
356 return_fields_list %s " % (node_filter_dict, return_fields_list))
357 #No filtering needed return the list directly
358 if not (node_filter_dict or return_fields_list):
359 return node_dict_list
361 return_node_list = []
363 for filter_key in node_filter_dict:
365 #Filter the node_dict_list by each value contained in the
366 #list node_filter_dict[filter_key]
367 for value in node_filter_dict[filter_key]:
368 for node in node_dict_list:
369 if node[filter_key] == value:
370 if return_fields_list:
372 for k in return_fields_list:
374 return_node_list.append(tmp)
376 return_node_list.append(node)
378 logger.log_exc("GetNodes KeyError")
382 return return_node_list
385 def AddSlice(self, slice_record, user_record):
388 Add slice to the local iotlab sfa tables if the slice comes
389 from a federated site and is not yet in the iotlab sfa DB,
390 although the user has already a LDAP login.
391 Called by verify_slice during lease/sliver creation.
393 :param slice_record: record of slice, must contain hrn, gid, slice_id
394 and authority of the slice.
395 :type slice_record: dictionary
396 :param user_record: record of the user
397 :type user_record: RegUser
401 sfa_record = RegSlice(hrn=slice_record['hrn'],
402 gid=slice_record['gid'],
403 pointer=slice_record['slice_id'],
404 authority=slice_record['authority'])
405 logger.debug("IOTLAB_API.PY AddSlice sfa_record %s user_record %s"
406 % (sfa_record, user_record))
407 sfa_record.just_created()
408 self.api.dbsession().add(sfa_record)
409 self.api.dbsession().commit()
410 #Update the reg-researcher dependance table
411 sfa_record.reg_researchers = [user_record]
412 self.api.dbsession().commit()
417 def GetSites(self, site_filter_name_list=None, return_fields_list=None):
418 """Returns the list of Iotlab's sites with the associated nodes and
419 the sites' properties as dictionaries.
422 ['address_ids', 'slice_ids', 'name', 'node_ids', 'url', 'person_ids',
423 'site_tag_ids', 'enabled', 'site', 'longitude', 'pcu_ids',
424 'max_slivers', 'max_slices', 'ext_consortium_id', 'date_created',
425 'latitude', 'is_public', 'peer_site_id', 'peer_id', 'abbreviated_name']
426 Uses the OAR request GET_sites to find the Iotlab's sites.
428 :param site_filter_name_list: used to specify specific sites
429 :param return_fields_list: field that has to be returned
430 :type site_filter_name_list: list
431 :type return_fields_list: list
435 site_dict = self.oar.parser.SendRequest("GET_sites")
436 #site_dict : dict where the key is the sit ename
437 return_site_list = []
438 if not (site_filter_name_list or return_fields_list):
439 return_site_list = site_dict.values()
440 return return_site_list
442 for site_filter_name in site_filter_name_list:
443 if site_filter_name in site_dict:
444 if return_fields_list:
445 for field in return_fields_list:
448 tmp[field] = site_dict[site_filter_name][field]
450 logger.error("GetSites KeyError %s " % (field))
452 return_site_list.append(tmp)
454 return_site_list.append(site_dict[site_filter_name])
456 return return_site_list
459 #TODO : Check rights to delete person
460 def DeletePerson(self, person_record):
461 """Disable an existing account in iotlab LDAP.
463 Users and techs can only delete themselves. PIs can only
464 delete themselves and other non-PIs at their sites.
465 ins can delete anyone.
467 :param person_record: user's record
468 :type person_record: dict
469 :returns: True if successful, False otherwise.
472 .. todo:: CHECK THAT ONLY THE USER OR ADMIN CAN DEL HIMSELF.
474 #Disable user account in iotlab LDAP
475 ret = self.ldap.LdapMarkUserAsDeleted(person_record)
476 logger.warning("IOTLAB_API DeletePerson %s " % (person_record))
479 def DeleteSlice(self, slice_record):
480 """Deletes the specified slice and kills the jobs associated with
481 the slice if any, using DeleteSliceFromNodes.
483 :param slice_record: record of the slice, must contain oar_job_id, user
484 :type slice_record: dict
485 :returns: True if all the jobs in the slice have been deleted,
486 or the list of jobs that could not be deleted otherwise.
487 :rtype: list or boolean
489 .. seealso:: DeleteSliceFromNodes
492 ret = self.DeleteSliceFromNodes(slice_record)
495 if False in ret[job_id]:
496 if delete_failed is None:
498 delete_failed.append(job_id)
500 logger.info("IOTLAB_API DeleteSlice %s answer %s"%(slice_record, \
502 return delete_failed or True
505 def __add_person_to_db(self, user_dict):
507 Add a federated user straight to db when the user issues a lease
508 request with iotlab nodes and that he has not registered with iotlab
509 yet (that is he does not have a LDAP entry yet).
510 Uses parts of the routines in IotlabImport when importing user from
511 LDAP. Called by AddPerson, right after LdapAddUser.
512 :param user_dict: Must contain email, hrn and pkey to get a GID
513 and be added to the SFA db.
514 :type user_dict: dict
517 query = self.api.dbsession().query(RegUser)
518 check_if_exists = query.filter_by(email = user_dict['email']).first()
520 if not check_if_exists:
521 logger.debug("__add_person_to_db \t Adding %s \r\n \r\n \
523 hrn = user_dict['hrn']
524 person_urn = hrn_to_urn(hrn, 'user')
525 pubkey = user_dict['pkey']
527 pkey = convert_public_key(pubkey)
529 #key not good. create another pkey
530 logger.warn('__add_person_to_db: unable to convert public \
532 pkey = Keypair(create=True)
535 if pubkey is not None and pkey is not None :
536 hierarchy = Hierarchy()
537 person_gid = hierarchy.create_gid(person_urn, create_uuid(), \
539 if user_dict['email']:
540 logger.debug("__add_person_to_db \r\n \r\n \
541 IOTLAB IMPORTER PERSON EMAIL OK email %s "\
542 %(user_dict['email']))
543 person_gid.set_email(user_dict['email'])
545 user_record = RegUser(hrn=hrn , pointer= '-1', \
546 authority=get_authority(hrn), \
547 email=user_dict['email'], gid = person_gid)
548 user_record.reg_keys = [RegKey(user_dict['pkey'])]
549 user_record.just_created()
550 self.api.dbsession().add (user_record)
551 self.api.dbsession().commit()
555 def AddPerson(self, record):
558 Adds a new account. Any fields specified in records are used,
559 otherwise defaults are used. Creates an appropriate login by calling
562 :param record: dictionary with the sfa user's properties.
563 :returns: a dicitonary with the status. If successful, the dictionary
564 boolean is set to True and there is a 'uid' key with the new login
565 added to LDAP, otherwise the bool is set to False and a key
566 'message' is in the dictionary, with the error message.
570 ret = self.ldap.LdapAddUser(record)
572 if ret['bool'] is True:
573 record['hrn'] = self.root_auth + '.' + ret['uid']
574 logger.debug("IOTLAB_API AddPerson return code %s record %s "
576 self.__add_person_to_db(record)
583 #TODO AddPersonKey 04/07/2012 SA
584 def AddPersonKey(self, person_uid, old_attributes_dict, new_key_dict):
585 """Adds a new key to the specified account. Adds the key to the
586 iotlab ldap, provided that the person_uid is valid.
588 Non-admins can only modify their own keys.
590 :param person_uid: user's iotlab login in LDAP
591 :param old_attributes_dict: dict with the user's old sshPublicKey
592 :param new_key_dict: dict with the user's new sshPublicKey
593 :type person_uid: string
597 :returns: True if the key has been modified, False otherwise.
600 ret = self.ldap.LdapModify(person_uid, old_attributes_dict, \
602 logger.warning("IOTLAB_API AddPersonKey EMPTY - DO NOTHING \r\n ")
605 def DeleteLeases(self, leases_id_list, slice_hrn):
608 Deletes several leases, based on their job ids and the slice
609 they are associated with. Uses DeleteJobs to delete the jobs
610 on OAR. Note that one slice can contain multiple jobs, and in this
611 case all the jobs in the leases_id_list MUST belong to ONE slice,
612 since there is only one slice hrn provided here.
614 :param leases_id_list: list of job ids that belong to the slice whose
615 slice hrn is provided.
616 :param slice_hrn: the slice hrn.
617 :type slice_hrn: string
619 .. warning:: Does not have a return value since there was no easy
620 way to handle failure when dealing with multiple job delete. Plus,
621 there was no easy way to report it to the user.
624 logger.debug("IOTLAB_API DeleteLeases leases_id_list %s slice_hrn %s \
625 \r\n " %(leases_id_list, slice_hrn))
626 for job_id in leases_id_list:
627 self.DeleteJobs(job_id, slice_hrn)
632 def _process_walltime(duration):
633 """ Calculates the walltime in seconds from the duration in H:M:S
634 specified in the RSpec.
638 # Fixing the walltime by adding a few delays.
639 # First put the walltime in seconds oarAdditionalDelay = 20;
640 # additional delay for /bin/sleep command to
641 # take in account prologue and epilogue scripts execution
642 # int walltimeAdditionalDelay = 240; additional delay
643 #for prologue/epilogue execution = $SERVER_PROLOGUE_EPILOGUE_TIMEOUT
645 # Put the duration in seconds first
646 #desired_walltime = duration * 60
647 desired_walltime = duration
648 total_walltime = desired_walltime + 240 #+4 min Update SA 23/10/12
649 sleep_walltime = desired_walltime # 0 sec added Update SA 23/10/12
651 #Put the walltime back in str form
653 walltime.append(str(total_walltime / 3600))
654 total_walltime = total_walltime - 3600 * int(walltime[0])
655 #Get the remaining minutes
656 walltime.append(str(total_walltime / 60))
657 total_walltime = total_walltime - 60 * int(walltime[1])
659 walltime.append(str(total_walltime))
662 logger.log_exc(" __process_walltime duration null")
664 return walltime, sleep_walltime
667 def _create_job_structure_request_for_OAR(lease_dict):
668 """ Creates the structure needed for a correct POST on OAR.
669 Makes the timestamp transformation into the appropriate format.
670 Sends the POST request to create the job with the resources in
679 reqdict['workdir'] = '/tmp'
680 reqdict['resource'] = "{network_address in ("
682 for node in lease_dict['added_nodes']:
683 logger.debug("\r\n \r\n OARrestapi \t \
684 __create_job_structure_request_for_OAR node %s" %(node))
686 # Get the ID of the node
688 reqdict['resource'] += "'" + nodeid + "', "
689 nodeid_list.append(nodeid)
691 custom_length = len(reqdict['resource'])- 2
692 reqdict['resource'] = reqdict['resource'][0:custom_length] + \
693 ")}/nodes=" + str(len(nodeid_list))
696 walltime, sleep_walltime = \
697 IotlabShell._process_walltime(\
698 int(lease_dict['lease_duration']))
701 reqdict['resource'] += ",walltime=" + str(walltime[0]) + \
702 ":" + str(walltime[1]) + ":" + str(walltime[2])
703 reqdict['script_path'] = "/bin/sleep " + str(sleep_walltime)
705 #In case of a scheduled experiment (not immediate)
706 #To run an XP immediately, don't specify date and time in RSpec
707 #They will be set to None.
708 if lease_dict['lease_start_time'] is not '0':
709 #Readable time accepted by OAR
710 start_time = datetime.fromtimestamp( \
711 int(lease_dict['lease_start_time'])).\
712 strftime(lease_dict['time_format'])
713 reqdict['reservation'] = start_time
714 #If there is not start time, Immediate XP. No need to add special
718 reqdict['type'] = "deploy"
719 reqdict['directory'] = ""
720 reqdict['name'] = "SFA_" + lease_dict['slice_user']
725 def LaunchExperimentOnOAR(self, added_nodes, slice_name, \
726 lease_start_time, lease_duration, slice_user=None):
729 Create a job request structure based on the information provided
730 and post the job on OAR.
731 :param added_nodes: list of nodes that belong to the described lease.
732 :param slice_name: the slice hrn associated to the lease.
733 :param lease_start_time: timestamp of the lease startting time.
734 :param lease_duration: lease durationin minutes
738 lease_dict['lease_start_time'] = lease_start_time
739 lease_dict['lease_duration'] = lease_duration
740 lease_dict['added_nodes'] = added_nodes
741 lease_dict['slice_name'] = slice_name
742 lease_dict['slice_user'] = slice_user
743 lease_dict['grain'] = self.GetLeaseGranularity()
744 lease_dict['time_format'] = self.time_format
747 logger.debug("IOTLAB_API.PY \tLaunchExperimentOnOAR slice_user %s\
748 \r\n " %(slice_user))
749 #Create the request for OAR
750 reqdict = self._create_job_structure_request_for_OAR(lease_dict)
751 # first step : start the OAR job and update the job
752 logger.debug("IOTLAB_API.PY \tLaunchExperimentOnOAR reqdict %s\
755 answer = self.oar.POSTRequestToOARRestAPI('POST_job', \
757 logger.debug("IOTLAB_API \tLaunchExperimentOnOAR jobid %s " %(answer))
761 logger.log_exc("IOTLAB_API \tLaunchExperimentOnOAR \
762 Impossible to create job %s " %(answer))
769 logger.debug("IOTLAB_API \tLaunchExperimentOnOAR jobid %s \
770 added_nodes %s slice_user %s" %(jobid, added_nodes, \
777 def AddLeases(self, hostname_list, slice_record,
778 lease_start_time, lease_duration):
780 """Creates a job in OAR corresponding to the information provided
781 as parameters. Adds the job id and the slice hrn in the iotlab
782 database so that we are able to know which slice has which nodes.
784 :param hostname_list: list of nodes' OAR hostnames.
785 :param slice_record: sfa slice record, must contain login and hrn.
786 :param lease_start_time: starting time , unix timestamp format
787 :param lease_duration: duration in minutes
789 :type hostname_list: list
790 :type slice_record: dict
791 :type lease_start_time: integer
792 :type lease_duration: integer
793 :returns: job_id, can be None if the job request failed.
796 logger.debug("IOTLAB_API \r\n \r\n \t AddLeases hostname_list %s \
797 slice_record %s lease_start_time %s lease_duration %s "\
798 %( hostname_list, slice_record , lease_start_time, \
801 #tmp = slice_record['reg-researchers'][0].split(".")
802 username = slice_record['login']
803 #username = tmp[(len(tmp)-1)]
804 job_id = self.LaunchExperimentOnOAR(hostname_list, \
805 slice_record['hrn'], \
806 lease_start_time, lease_duration, \
808 if job_id is not None:
810 datetime.fromtimestamp(int(lease_start_time)).\
811 strftime(self.time_format)
812 end_time = lease_start_time + lease_duration
815 logger.debug("IOTLAB_API \r\n \r\n \t AddLeases TURN ON LOGGING SQL \
816 %s %s %s "%(slice_record['hrn'], job_id, end_time))
819 logger.debug("IOTLAB_API \r\n \r\n \t AddLeases %s %s %s " \
820 %(type(slice_record['hrn']), type(job_id), type(end_time)))
822 iotlab_ex_row = LeaseTableXP(slice_hrn = slice_record['hrn'],
823 experiment_id=job_id,
826 logger.debug("IOTLAB_API \r\n \r\n \t AddLeases iotlab_ex_row %s" \
828 self.leases_db.testbed_session.add(iotlab_ex_row)
829 self.leases_db.testbed_session.commit()
831 logger.debug("IOTLAB_API \t AddLeases hostname_list start_time %s "
837 #Delete the jobs from job_iotlab table
838 def DeleteSliceFromNodes(self, slice_record):
841 Deletes all the running or scheduled jobs of a given slice
844 :param slice_record: record of the slice, must contain oar_job_id, user
845 :type slice_record: dict
847 :returns: dict of the jobs'deletion status. Success= True, Failure=
848 False, for each job id.
852 logger.debug("IOTLAB_API \t DeleteSliceFromNodes %s "
855 if isinstance(slice_record['oar_job_id'], list):
857 for job_id in slice_record['oar_job_id']:
858 ret = self.DeleteJobs(job_id, slice_record['user'])
860 oar_bool_answer.update(ret)
863 oar_bool_answer = self.DeleteJobs(slice_record['oar_job_id'],
864 slice_record['user'])
866 return oar_bool_answer
870 def GetLeaseGranularity(self):
871 """ Returns the granularity of an experiment in the Iotlab testbed.
872 OAR uses seconds for experiments duration , the granulaity is also
874 Experiments which last less than 10 min (600 sec) are invalid"""
880 def filter_lease_name(reservation_list, filter_value):
881 filtered_reservation_list = list(reservation_list)
882 logger.debug("IOTLAB_API \t filter_lease_name reservation_list %s" \
883 % (reservation_list))
884 for reservation in reservation_list:
885 if 'slice_hrn' in reservation and \
886 reservation['slice_hrn'] != filter_value:
887 filtered_reservation_list.remove(reservation)
889 logger.debug("IOTLAB_API \t filter_lease_name filtered_reservation_list\
890 %s" % (filtered_reservation_list))
891 return filtered_reservation_list
894 def filter_lease_start_time(reservation_list, filter_value):
895 filtered_reservation_list = list(reservation_list)
897 for reservation in reservation_list:
898 if 't_from' in reservation and \
899 reservation['t_from'] > filter_value:
900 filtered_reservation_list.remove(reservation)
902 return filtered_reservation_list
905 def GetLeases(self, lease_filter_dict=None, login=None):
908 Get the list of leases from OAR with complete information
909 about which slice owns which jobs and nodes.
911 -Fetch all the jobs from OAR (running, waiting..)
912 complete the reservation information with slice hrn
913 found in testbed_xp table. If not available in the table,
914 assume it is a iotlab slice.
915 -Updates the iotlab table, deleting jobs when necessary.
917 :returns: reservation_list, list of dictionaries with 'lease_id',
918 'reserved_nodes','slice_id', 'state', 'user', 'component_id_list',
919 'slice_hrn', 'resource_ids', 't_from', 't_until'
924 unfiltered_reservation_list = self.GetReservedNodes(login)
926 reservation_list = []
927 #Find the slice associated with this user iotlab ldap uid
928 logger.debug(" IOTLAB_API.PY \tGetLeases login %s\
929 unfiltered_reservation_list %s "
930 % (login, unfiltered_reservation_list))
931 #Create user dict first to avoid looking several times for
932 #the same user in LDAP SA 27/07/12
934 jobs_psql_query = self.leases_db.testbed_session.query(LeaseTableXP).all()
935 jobs_psql_dict = dict([(row.experiment_id, row.__dict__)
936 for row in jobs_psql_query])
937 #jobs_psql_dict = jobs_psql_dict)
938 logger.debug("IOTLAB_API \tGetLeases jobs_psql_dict %s"
940 jobs_psql_id_list = [row.experiment_id for row in jobs_psql_query]
942 for resa in unfiltered_reservation_list:
943 logger.debug("IOTLAB_API \tGetLeases USER %s"
945 #Construct list of jobs (runing, waiting..) in oar
946 job_oar_list.append(resa['lease_id'])
947 #If there is information on the job in IOTLAB DB ]
948 #(slice used and job id)
949 if resa['lease_id'] in jobs_psql_dict:
950 job_info = jobs_psql_dict[resa['lease_id']]
951 logger.debug("IOTLAB_API \tGetLeases job_info %s"
953 resa['slice_hrn'] = job_info['slice_hrn']
954 resa['slice_id'] = hrn_to_urn(resa['slice_hrn'], 'slice')
956 #otherwise, assume it is a iotlab slice:
958 resa['slice_id'] = hrn_to_urn(self.root_auth + '.' +
959 resa['user'] + "_slice", 'slice')
960 resa['slice_hrn'] = Xrn(resa['slice_id']).get_hrn()
962 resa['component_id_list'] = []
963 #Transform the hostnames into urns (component ids)
964 for node in resa['reserved_nodes']:
966 iotlab_xrn = xrn_object(self.root_auth, node)
967 resa['component_id_list'].append(iotlab_xrn.urn)
969 if lease_filter_dict:
970 logger.debug("IOTLAB_API \tGetLeases \
971 \r\n leasefilter %s" % ( lease_filter_dict))
973 filter_dict_functions = {
974 'slice_hrn' : IotlabShell.filter_lease_name,
975 't_from' : IotlabShell.filter_lease_start_time
977 reservation_list = list(unfiltered_reservation_list)
978 for filter_type in lease_filter_dict:
979 logger.debug("IOTLAB_API \tGetLeases reservation_list %s" \
980 % (reservation_list))
981 reservation_list = filter_dict_functions[filter_type](\
982 reservation_list,lease_filter_dict[filter_type] )
984 # Filter the reservation list with a maximum timespan so that the
985 # leases and jobs running after this timestamp do not appear
986 # in the result leases.
987 # if 'start_time' in :
988 # if resa['start_time'] < lease_filter_dict['start_time']:
989 # reservation_list.append(resa)
992 # if 'name' in lease_filter_dict and \
993 # lease_filter_dict['name'] == resa['slice_hrn']:
994 # reservation_list.append(resa)
997 if lease_filter_dict is None:
998 reservation_list = unfiltered_reservation_list
1000 self.leases_db.update_experiments_in_additional_sfa_db(job_oar_list, jobs_psql_id_list)
1002 logger.debug(" IOTLAB_API.PY \tGetLeases reservation_list %s"
1003 % (reservation_list))
1004 return reservation_list
1009 #TODO FUNCTIONS SECTION 04/07/2012 SA
1011 ##TODO : Is UnBindObjectFromPeer still necessary ? Currently does nothing
1014 #def UnBindObjectFromPeer( auth, object_type, object_id, shortname):
1015 #""" This method is a hopefully temporary hack to let the sfa correctly
1016 #detach the objects it creates from a remote peer object. This is
1017 #needed so that the sfa federation link can work in parallel with
1018 #RefreshPeer, as RefreshPeer depends on remote objects being correctly
1021 #auth : struct, API authentication structure
1022 #AuthMethod : string, Authentication method to use
1023 #object_type : string, Object type, among 'site','person','slice',
1025 #object_id : int, object_id
1026 #shortname : string, peer shortname
1030 #logger.warning("IOTLAB_API \tUnBindObjectFromPeer EMPTY-\
1034 ##TODO Is BindObjectToPeer still necessary ? Currently does nothing
1036 #|| Commented out 28/05/13 SA
1037 #def BindObjectToPeer(self, auth, object_type, object_id, shortname=None, \
1038 #remote_object_id=None):
1039 #"""This method is a hopefully temporary hack to let the sfa correctly
1040 #attach the objects it creates to a remote peer object. This is needed
1041 #so that the sfa federation link can work in parallel with RefreshPeer,
1042 #as RefreshPeer depends on remote objects being correctly marked.
1044 #shortname : string, peer shortname
1045 #remote_object_id : int, remote object_id, set to 0 if unknown
1049 #logger.warning("IOTLAB_API \tBindObjectToPeer EMPTY - DO NOTHING \r\n ")
1052 ##TODO UpdateSlice 04/07/2012 SA || Commented out 28/05/13 SA
1053 ##Funciton should delete and create another job since oin iotlab slice=job
1054 #def UpdateSlice(self, auth, slice_id_or_name, slice_fields=None):
1055 #"""Updates the parameters of an existing slice with the values in
1057 #Users may only update slices of which they are members.
1058 #PIs may update any of the slices at their sites, or any slices of
1059 #which they are members. Admins may update any slice.
1060 #Only PIs and admins may update max_nodes. Slices cannot be renewed
1061 #(by updating the expires parameter) more than 8 weeks into the future.
1062 #Returns 1 if successful, faults otherwise.
1066 #logger.warning("IOTLAB_API UpdateSlice EMPTY - DO NOTHING \r\n ")
1069 #Unused SA 30/05/13, we only update the user's key or we delete it.
1070 ##TODO UpdatePerson 04/07/2012 SA
1071 #def UpdatePerson(self, iotlab_hrn, federated_hrn, person_fields=None):
1072 #"""Updates a person. Only the fields specified in person_fields
1073 #are updated, all other fields are left untouched.
1074 #Users and techs can only update themselves. PIs can only update
1075 #themselves and other non-PIs at their sites.
1076 #Returns 1 if successful, faults otherwise.
1080 ##new_row = FederatedToIotlab(iotlab_hrn, federated_hrn)
1081 ##self.leases_db.testbed_session.add(new_row)
1082 ##self.leases_db.testbed_session.commit()
1084 #logger.debug("IOTLAB_API UpdatePerson EMPTY - DO NOTHING \r\n ")
1088 def GetKeys(self, key_filter=None):
1089 """Returns a dict of dict based on the key string. Each dict entry
1090 contains the key id, the ssh key, the user's email and the
1092 If key_filter is specified and is an array of key identifiers,
1093 only keys matching the filter will be returned.
1095 Admin may query all keys. Non-admins may only query their own keys.
1098 :returns: dict with ssh key as key and dicts as value.
1101 if key_filter is None:
1102 keys = self.api.dbsession().query(RegKey).options(joinedload('reg_user')).all()
1104 keys = self.api.dbsession().query(RegKey).options(joinedload('reg_user')).filter(RegKey.key.in_(key_filter)).all()
1108 key_dict[key.key] = {'key_id': key.key_id, 'key': key.key,
1109 'email': key.reg_user.email,
1110 'hrn': key.reg_user.hrn}
1112 #ldap_rslt = self.ldap.LdapSearch({'enabled']=True})
1113 #user_by_email = dict((user[1]['mail'][0], user[1]['sshPublicKey']) \
1114 #for user in ldap_rslt)
1116 logger.debug("IOTLAB_API GetKeys -key_dict %s \r\n " % (key_dict))
1120 def DeleteKey(self, user_record, key_string):
1121 """Deletes a key in the LDAP entry of the specified user.
1123 Removes the key_string from the user's key list and updates the LDAP
1124 user's entry with the new key attributes.
1126 :param key_string: The ssh key to remove
1127 :param user_record: User's record
1128 :type key_string: string
1129 :type user_record: dict
1130 :returns: True if sucessful, False if not.
1135 all_user_keys = user_record['keys']
1136 all_user_keys.remove(key_string)
1137 new_attributes = {'sshPublicKey':all_user_keys}
1138 ret = self.ldap.LdapModifyUser(user_record, new_attributes)
1139 logger.debug("IOTLAB_API DeleteKey %s- " % (ret))
1143 def _sql_get_slice_info(self, slice_filter):
1145 Get the slice record based on the slice hrn. Fetch the record of the
1146 user associated with the slice by using joinedload based on the
1147 reg_researcher relationship.
1149 :param slice_filter: the slice hrn we are looking for
1150 :type slice_filter: string
1151 :returns: the slice record enhanced with the user's information if the
1152 slice was found, None it wasn't.
1154 :rtype: dict or None.
1156 #DO NOT USE RegSlice - reg_researchers to get the hrn
1157 #of the user otherwise will mess up the RegRecord in
1158 #Resolve, don't know why - SA 08/08/2012
1160 #Only one entry for one user = one slice in testbed_xp table
1161 #slicerec = dbsession.query(RegRecord).filter_by(hrn = slice_filter).first()
1162 raw_slicerec = self.api.dbsession().query(RegSlice).options(joinedload('reg_researchers')).filter_by(hrn=slice_filter).first()
1163 #raw_slicerec = self.api.dbsession().query(RegRecord).filter_by(hrn = slice_filter).first()
1165 #load_reg_researcher
1166 #raw_slicerec.reg_researchers
1167 raw_slicerec = raw_slicerec.__dict__
1168 logger.debug(" IOTLAB_API \t _sql_get_slice_info slice_filter %s \
1169 raw_slicerec %s" % (slice_filter, raw_slicerec))
1170 slicerec = raw_slicerec
1171 #only one researcher per slice so take the first one
1172 #slicerec['reg_researchers'] = raw_slicerec['reg_researchers']
1173 #del slicerec['reg_researchers']['_sa_instance_state']
1179 def _sql_get_slice_info_from_user(self, slice_filter):
1181 Get the slice record based on the user recordid by using a joinedload
1182 on the relationship reg_slices_as_researcher. Format the sql record
1183 into a dict with the mandatory fields for user and slice.
1184 :returns: dict with slice record and user record if the record was found
1185 based on the user's id, None if not..
1186 :rtype:dict or None..
1188 #slicerec = dbsession.query(RegRecord).filter_by(record_id = slice_filter).first()
1189 raw_slicerec = self.api.dbsession().query(RegUser).options(joinedload('reg_slices_as_researcher')).filter_by(record_id=slice_filter).first()
1190 #raw_slicerec = self.api.dbsession().query(RegRecord).filter_by(record_id = slice_filter).first()
1191 #Put it in correct order
1192 user_needed_fields = ['peer_authority', 'hrn', 'last_updated',
1193 'classtype', 'authority', 'gid', 'record_id',
1194 'date_created', 'type', 'email', 'pointer']
1195 slice_needed_fields = ['peer_authority', 'hrn', 'last_updated',
1196 'classtype', 'authority', 'gid', 'record_id',
1197 'date_created', 'type', 'pointer']
1199 #raw_slicerec.reg_slices_as_researcher
1200 raw_slicerec = raw_slicerec.__dict__
1203 dict([(k, raw_slicerec[
1204 'reg_slices_as_researcher'][0].__dict__[k])
1205 for k in slice_needed_fields])
1206 slicerec['reg_researchers'] = dict([(k, raw_slicerec[k])
1207 for k in user_needed_fields])
1208 #TODO Handle multiple slices for one user SA 10/12/12
1209 #for now only take the first slice record associated to the rec user
1210 ##slicerec = raw_slicerec['reg_slices_as_researcher'][0].__dict__
1211 #del raw_slicerec['reg_slices_as_researcher']
1212 #slicerec['reg_researchers'] = raw_slicerec
1213 ##del slicerec['_sa_instance_state']
1220 def _get_slice_records(self, slice_filter=None,
1221 slice_filter_type=None):
1223 Get the slice record depending on the slice filter and its type.
1224 :param slice_filter: Can be either the slice hrn or the user's record
1226 :type slice_filter: string
1227 :param slice_filter_type: describes the slice filter type used, can be
1228 slice_hrn or record_id_user
1230 :returns: the slice record
1232 .. seealso::_sql_get_slice_info_from_user
1233 .. seealso:: _sql_get_slice_info
1236 #Get list of slices based on the slice hrn
1237 if slice_filter_type == 'slice_hrn':
1239 #if get_authority(slice_filter) == self.root_auth:
1240 #login = slice_filter.split(".")[1].split("_")[0]
1242 slicerec = self._sql_get_slice_info(slice_filter)
1244 if slicerec is None:
1248 #Get slice based on user id
1249 if slice_filter_type == 'record_id_user':
1251 slicerec = self._sql_get_slice_info_from_user(slice_filter)
1254 fixed_slicerec_dict = slicerec
1255 #At this point if there is no login it means
1256 #record_id_user filter has been used for filtering
1258 ##If theslice record is from iotlab
1259 #if fixed_slicerec_dict['peer_authority'] is None:
1260 #login = fixed_slicerec_dict['hrn'].split(".")[1].split("_")[0]
1261 #return login, fixed_slicerec_dict
1262 return fixed_slicerec_dict
1267 def GetSlices(self, slice_filter=None, slice_filter_type=None,
1269 """Get the slice records from the iotlab db and add lease information
1272 :param slice_filter: can be the slice hrn or slice record id in the db
1273 depending on the slice_filter_type.
1274 :param slice_filter_type: defines the type of the filtering used, Can be
1275 either 'slice_hrn' or "record_id'.
1276 :type slice_filter: string
1277 :type slice_filter_type: string
1278 :returns: a slice dict if slice_filter and slice_filter_type
1279 are specified and a matching entry is found in the db. The result
1280 is put into a list.Or a list of slice dictionnaries if no filters
1287 authorized_filter_types_list = ['slice_hrn', 'record_id_user']
1288 return_slicerec_dictlist = []
1290 #First try to get information on the slice based on the filter provided
1291 if slice_filter_type in authorized_filter_types_list:
1292 fixed_slicerec_dict = self._get_slice_records(slice_filter,
1294 # if the slice was not found in the sfa db
1295 if fixed_slicerec_dict is None:
1296 return return_slicerec_dictlist
1298 slice_hrn = fixed_slicerec_dict['hrn']
1300 logger.debug(" IOTLAB_API \tGetSlices login %s \
1301 slice record %s slice_filter %s \
1302 slice_filter_type %s " % (login,
1303 fixed_slicerec_dict, slice_filter,
1307 #Now we have the slice record fixed_slicerec_dict, get the
1308 #jobs associated to this slice
1311 leases_list = self.GetLeases(login=login)
1312 #If no job is running or no job scheduled
1313 #return only the slice record
1314 if leases_list == [] and fixed_slicerec_dict:
1315 return_slicerec_dictlist.append(fixed_slicerec_dict)
1317 # if the jobs running don't belong to the user/slice we are looking
1319 leases_hrn = [lease['slice_hrn'] for lease in leases_list]
1320 if slice_hrn not in leases_hrn:
1321 return_slicerec_dictlist.append(fixed_slicerec_dict)
1322 #If several jobs for one slice , put the slice record into
1323 # each lease information dict
1324 for lease in leases_list:
1326 logger.debug("IOTLAB_API.PY \tGetSlices slice_filter %s \
1327 \t lease['slice_hrn'] %s"
1328 % (slice_filter, lease['slice_hrn']))
1329 if lease['slice_hrn'] == slice_hrn:
1330 slicerec_dict['oar_job_id'] = lease['lease_id']
1331 #Update lease dict with the slice record
1332 if fixed_slicerec_dict:
1333 fixed_slicerec_dict['oar_job_id'] = []
1334 fixed_slicerec_dict['oar_job_id'].append(
1335 slicerec_dict['oar_job_id'])
1336 slicerec_dict.update(fixed_slicerec_dict)
1337 #slicerec_dict.update({'hrn':\
1338 #str(fixed_slicerec_dict['slice_hrn'])})
1339 slicerec_dict['slice_hrn'] = lease['slice_hrn']
1340 slicerec_dict['hrn'] = lease['slice_hrn']
1341 slicerec_dict['user'] = lease['user']
1342 slicerec_dict.update(
1344 {'hostname': lease['reserved_nodes']}})
1345 slicerec_dict.update({'node_ids': lease['reserved_nodes']})
1349 return_slicerec_dictlist.append(slicerec_dict)
1350 logger.debug("IOTLAB_API.PY \tGetSlices \
1351 OHOHOHOH %s" %(return_slicerec_dictlist))
1353 logger.debug("IOTLAB_API.PY \tGetSlices \
1354 slicerec_dict %s return_slicerec_dictlist %s \
1355 lease['reserved_nodes'] \
1356 %s" % (slicerec_dict, return_slicerec_dictlist,
1357 lease['reserved_nodes']))
1359 logger.debug("IOTLAB_API.PY \tGetSlices RETURN \
1360 return_slicerec_dictlist %s"
1361 % (return_slicerec_dictlist))
1363 return return_slicerec_dictlist
1367 #Get all slices from the iotlab sfa database ,
1368 #put them in dict format
1369 #query_slice_list = dbsession.query(RegRecord).all()
1370 query_slice_list = \
1371 self.api.dbsession().query(RegSlice).options(joinedload('reg_researchers')).all()
1373 for record in query_slice_list:
1374 tmp = record.__dict__
1375 tmp['reg_researchers'] = tmp['reg_researchers'][0].__dict__
1376 #del tmp['reg_researchers']['_sa_instance_state']
1377 return_slicerec_dictlist.append(tmp)
1378 #return_slicerec_dictlist.append(record.__dict__)
1380 #Get all the jobs reserved nodes
1381 leases_list = self.GetReservedNodes()
1383 for fixed_slicerec_dict in return_slicerec_dictlist:
1385 #Check if the slice belongs to a iotlab user
1386 if fixed_slicerec_dict['peer_authority'] is None:
1387 owner = fixed_slicerec_dict['hrn'].split(
1388 ".")[1].split("_")[0]
1391 for lease in leases_list:
1392 if owner == lease['user']:
1393 slicerec_dict['oar_job_id'] = lease['lease_id']
1395 #for reserved_node in lease['reserved_nodes']:
1396 logger.debug("IOTLAB_API.PY \tGetSlices lease %s "
1398 slicerec_dict.update(fixed_slicerec_dict)
1399 slicerec_dict.update({'node_ids':
1400 lease['reserved_nodes']})
1401 slicerec_dict.update({'list_node_ids':
1403 lease['reserved_nodes']}})
1405 #slicerec_dict.update({'hrn':\
1406 #str(fixed_slicerec_dict['slice_hrn'])})
1407 #return_slicerec_dictlist.append(slicerec_dict)
1408 fixed_slicerec_dict.update(slicerec_dict)
1410 logger.debug("IOTLAB_API.PY \tGetSlices RETURN \
1411 return_slicerec_dictlist %s \slice_filter %s " \
1412 %(return_slicerec_dictlist, slice_filter))
1414 return return_slicerec_dictlist
1418 #Update slice unused, therefore sfa_fields_to_iotlab_fields unused
1421 #def sfa_fields_to_iotlab_fields(sfa_type, hrn, record):
1426 ##for field in record:
1427 ## iotlab_record[field] = record[field]
1429 #if sfa_type == "slice":
1430 ##instantion used in get_slivers ?
1431 #if not "instantiation" in iotlab_record:
1432 #iotlab_record["instantiation"] = "iotlab-instantiated"
1433 ##iotlab_record["hrn"] = hrn_to_pl_slicename(hrn)
1434 ##Unused hrn_to_pl_slicename because Iotlab's hrn already
1435 ##in the appropriate form SA 23/07/12
1436 #iotlab_record["hrn"] = hrn
1437 #logger.debug("IOTLAB_API.PY sfa_fields_to_iotlab_fields \
1438 #iotlab_record %s " %(iotlab_record['hrn']))
1439 #if "url" in record:
1440 #iotlab_record["url"] = record["url"]
1441 #if "description" in record:
1442 #iotlab_record["description"] = record["description"]
1443 #if "expires" in record:
1444 #iotlab_record["expires"] = int(record["expires"])
1446 ##nodes added by OAR only and then imported to SFA
1447 ##elif type == "node":
1448 ##if not "hostname" in iotlab_record:
1449 ##if not "hostname" in record:
1450 ##raise MissingSfaInfo("hostname")
1451 ##iotlab_record["hostname"] = record["hostname"]
1452 ##if not "model" in iotlab_record:
1453 ##iotlab_record["model"] = "geni"
1455 ##One authority only
1456 ##elif type == "authority":
1457 ##iotlab_record["login_base"] = hrn_to_iotlab_login_base(hrn)
1459 ##if not "name" in iotlab_record:
1460 ##iotlab_record["name"] = hrn
1462 ##if not "abbreviated_name" in iotlab_record:
1463 ##iotlab_record["abbreviated_name"] = hrn
1465 ##if not "enabled" in iotlab_record:
1466 ##iotlab_record["enabled"] = True
1468 ##if not "is_public" in iotlab_record:
1469 ##iotlab_record["is_public"] = True
1471 #return iotlab_record