Adding a new field in 'mobile' in Iotlab Rspec type.
[sfa.git] / sfa / iotlab / OARrestapi.py
1 """
2 File used to handle issuing request to OAR and parse OAR's JSON responses.
3 Contains the following classes:
4 - JsonPage : handles multiple pages OAR answers.
5 - OARRestapi : handles issuing POST or GET requests to OAR.
6 - ParsingResourcesFull : dedicated to parsing OAR's answer to a get resources
7 full request.
8 - OARGETParser : handles parsing the Json answers to different GET requests.
9
10 """
11 from httplib import HTTPConnection, HTTPException, NotConnected
12 import json
13 from sfa.util.config import Config
14 from sfa.util.sfalogging import logger
15 import os.path
16
17
18 class JsonPage:
19
20     """Class used to manipulate json pages given by OAR.
21
22     In case the json answer from a GET request is too big to fit in one json
23     page, this class provides helper methods to retrieve all the pages and
24     store them in a list before putting them into one single json dictionary,
25     facilitating the parsing.
26
27     """
28
29     def __init__(self):
30         """Defines attributes to manipulate and parse the json pages.
31
32         """
33         #All are boolean variables
34         self.concatenate = False
35         #Indicates end of data, no more pages to be loaded.
36         self.end = False
37         self.next_page = False
38         #Next query address
39         self.next_offset = None
40         #Json page
41         self.raw_json = None
42
43     def FindNextPage(self):
44         """
45         Gets next data page from OAR when the query's results are too big to
46         be transmitted in a single page. Uses the "links' item in the json
47         returned to check if an additionnal page has to be loaded. Updates
48         object attributes next_page, next_offset, and end.
49
50         """
51         if "links" in self.raw_json:
52             for page in self.raw_json['links']:
53                 if page['rel'] == 'next':
54                     self.concatenate = True
55                     self.next_page = True
56                     self.next_offset = "?" + page['href'].split("?")[1]
57                     return
58
59         if self.concatenate:
60             self.end = True
61             self.next_page = False
62             self.next_offset = None
63
64             return
65
66         #Otherwise, no next page and no concatenate, must be a single page
67         #Concatenate the single page and get out of here.
68         else:
69             self.next_page = False
70             self.concatenate = True
71             self.next_offset = None
72             return
73
74     @staticmethod
75     def ConcatenateJsonPages(saved_json_list):
76         """
77         If the json answer is too big to be contained in a single page,
78         all the pages have to be loaded and saved before being appended to the
79         first page.
80
81         :param saved_json_list: list of all the stored pages, including the
82             first page.
83         :type saved_json_list: list
84         :returns: Returns a dictionary with all the pages saved in the
85             saved_json_list. The key of the dictionary is 'items'.
86         :rtype: dict
87
88
89         .. seealso:: SendRequest
90         .. warning:: Assumes the apilib is 0.2.10 (with the 'items' key in the
91             raw json dictionary)
92
93         """
94         #reset items list
95
96         tmp = {}
97         tmp['items'] = []
98
99         for page in saved_json_list:
100             tmp['items'].extend(page['items'])
101         return tmp
102
103     def ResetNextPage(self):
104         """
105         Resets all the Json page attributes (next_page, next_offset,
106         concatenate, end). Has to be done before getting another json answer
107         so that the previous page status does not affect the new json load.
108
109         """
110         self.next_page = True
111         self.next_offset = None
112         self.concatenate = False
113         self.end = False
114
115
116 class OARrestapi:
117     """Class used to connect to the OAR server and to send GET and POST
118     requests.
119
120     """
121
122     # classes attributes
123
124     OAR_REQUEST_POST_URI_DICT = {'POST_job': {'uri': '/oarapi/jobs.json'},
125                                  'DELETE_jobs_id':
126                                  {'uri': '/oarapi/jobs/id.json'},
127                                  }
128
129     POST_FORMAT = {'json': {'content': "application/json", 'object': json}}
130
131     #OARpostdatareqfields = {'resource' :"/nodes=", 'command':"sleep", \
132                             #'workdir':"/home/", 'walltime':""}
133
134     def __init__(self, config_file='/etc/sfa/oar_config.py'):
135         self.oarserver = {}
136         self.oarserver['uri'] = None
137         self.oarserver['postformat'] = 'json'
138
139         try:
140             execfile(config_file, self.__dict__)
141
142             self.config_file = config_file
143             # path to configuration data
144             self.config_path = os.path.dirname(config_file)
145
146         except IOError:
147             raise IOError, "Could not find or load the configuration file: %s" \
148                 % config_file
149         #logger.setLevelDebug()
150         self.oarserver['ip'] = self.OAR_IP
151         self.oarserver['port'] = self.OAR_PORT
152         self.jobstates = ['Terminated', 'Hold', 'Waiting', 'toLaunch',
153                           'toError', 'toAckReservation', 'Launching',
154                           'Finishing', 'Running', 'Suspended', 'Resuming',
155                           'Error']
156
157         self.parser = OARGETParser(self)
158
159
160     def GETRequestToOARRestAPI(self, request, strval=None,
161                                next_page=None, username=None):
162
163         """Makes a GET request to OAR.
164
165         Fetch the uri associated with the resquest stored in
166         OARrequests_uri_dict, adds the username if needed and if available, adds
167         strval to the request uri if needed, connects to OAR and issues the GET
168         request. Gets the json reply.
169
170         :param request: One of the known get requests that are keys in the
171             OARrequests_uri_dict.
172         :param strval: used when a job id has to be specified.
173         :param next_page: used to tell OAR to send the next page for this
174             Get request. Is appended to the GET uri.
175         :param username: used when a username has to be specified, when looking
176             for jobs scheduled by a particular user  for instance.
177
178         :type request: string
179         :type strval: integer
180         :type next_page: boolean
181         :type username: string
182         :returns: a json dictionary if OAR successfully processed the GET
183             request.
184
185         .. seealso:: OARrequests_uri_dict
186         """
187         self.oarserver['uri'] = \
188             OARGETParser.OARrequests_uri_dict[request]['uri']
189         #Get job details with username
190         if 'owner' in OARGETParser.OARrequests_uri_dict[request] and username:
191             self.oarserver['uri'] += \
192                 OARGETParser.OARrequests_uri_dict[request]['owner'] + username
193         headers = {}
194         data = json.dumps({})
195         logger.debug("OARrestapi \tGETRequestToOARRestAPI %s" % (request))
196         if strval:
197             self.oarserver['uri'] = self.oarserver['uri'].\
198                 replace("id", str(strval))
199
200         if next_page:
201             self.oarserver['uri'] += next_page
202
203         if username:
204             headers['X-REMOTE_IDENT'] = username
205
206         logger.debug("OARrestapi: \t  GETRequestToOARRestAPI  \
207                         self.oarserver['uri'] %s strval %s"
208                      % (self.oarserver['uri'], strval))
209         try:
210             #seems that it does not work if we don't add this
211             headers['content-length'] = '0'
212
213             conn = HTTPConnection(self.oarserver['ip'],
214                                   self.oarserver['port'])
215             conn.request("GET", self.oarserver['uri'], data, headers)
216             resp = (conn.getresponse()).read()
217             conn.close()
218
219         except HTTPException, error:
220             logger.log_exc("GET_OAR_SRVR : Problem with OAR server : %s "
221                            % (error))
222             #raise ServerError("GET_OAR_SRVR : Could not reach OARserver")
223         try:
224             js_dict = json.loads(resp)
225             #print "\r\n \t\t\t js_dict keys" , js_dict.keys(), " \r\n", js_dict
226             return js_dict
227
228         except ValueError, error:
229             logger.log_exc("Failed to parse Server Response: %s ERROR %s"
230                            % (js_dict, error))
231             #raise ServerError("Failed to parse Server Response:" + js)
232
233
234     def POSTRequestToOARRestAPI(self, request, datadict, username=None):
235         """ Used to post a job on OAR , along with data associated
236         with the job.
237
238         """
239
240         #first check that all params for are OK
241         try:
242             self.oarserver['uri'] = \
243                 self.OAR_REQUEST_POST_URI_DICT[request]['uri']
244
245         except KeyError:
246             logger.log_exc("OARrestapi \tPOSTRequestToOARRestAPI request not \
247                              valid")
248             return
249         if datadict and 'strval' in datadict:
250             self.oarserver['uri'] = self.oarserver['uri'].replace("id", \
251                                                 str(datadict['strval']))
252             del datadict['strval']
253
254         data = json.dumps(datadict)
255         headers = {'X-REMOTE_IDENT':username, \
256                 'content-type': self.POST_FORMAT['json']['content'], \
257                 'content-length':str(len(data))}
258         try :
259
260             conn = HTTPConnection(self.oarserver['ip'], \
261                                         self.oarserver['port'])
262             conn.request("POST", self.oarserver['uri'], data, headers)
263             resp = (conn.getresponse()).read()
264             conn.close()
265         except NotConnected:
266             logger.log_exc("POSTRequestToOARRestAPI NotConnected ERROR: \
267                             data %s \r\n \t\n \t\t headers %s uri %s" \
268                             %(data,headers,self.oarserver['uri']))
269
270             #raise ServerError("POST_OAR_SRVR : error")
271
272         try:
273             answer = json.loads(resp)
274             logger.debug("POSTRequestToOARRestAPI : answer %s" % (answer))
275             return answer
276
277         except ValueError, error:
278             logger.log_exc("Failed to parse Server Response: error %s  \
279                             %s" %(error))
280             #raise ServerError("Failed to parse Server Response:" + answer)
281
282
283 class ParsingResourcesFull():
284     """
285     Class dedicated to parse the json response from a GET_resources_full from
286     OAR.
287
288     """
289     def __init__(self):
290         """
291         Set the parsing dictionary. Works like a switch case, if the key is
292         found in the dictionary, then the associated function is called.
293         This is used in ParseNodes to create an usable dictionary from
294         the Json returned by OAR when issuing a GET resources full request.
295
296         .. seealso:: ParseNodes
297
298         """
299         self.resources_fulljson_dict = {
300         'network_address': self.AddNodeNetworkAddr,
301         'site':  self.AddNodeSite,
302         # 'radio':  self.AddNodeRadio,
303         'mobile':  self.AddMobility,
304         'x':  self.AddPosX,
305         'y':  self.AddPosY,
306         'z': self.AddPosZ,
307         'archi': self.AddHardwareType,
308         'state': self.AddBootState,
309         'id': self.AddOarNodeId,
310         }
311
312
313
314     def AddOarNodeId(self, tuplelist, value):
315         """Adds Oar internal node id to the nodes' attributes.
316
317         Appends tuple ('oar_id', node_id) to the tuplelist. Used by ParseNodes.
318
319         .. seealso:: ParseNodes
320
321         """
322
323         tuplelist.append(('oar_id', int(value)))
324
325
326     def AddNodeNetworkAddr(self, dictnode, value):
327         """First parsing function to be called to parse the json returned by OAR
328         answering a GET_resources (/oarapi/resources.json) request.
329
330         When a new node is found in the json, this function is responsible for
331         creating a new entry in the dictionary for storing information on this
332         specific node. The key is the node network address, which is also the
333         node's hostname.
334         The value associated with the key is a tuple list.It contains all
335         the nodes attributes. The tuplelist will later be turned into a dict.
336
337         :param dictnode: should be set to the OARGETParser atribute
338             node_dictlist. It will store the information on the nodes.
339         :param value: the node_id is the network_address in the raw json.
340         :type value: string
341         :type dictnode: dictionary
342
343         .. seealso: ParseResources, ParseNodes
344         """
345
346         node_id = value
347         dictnode[node_id] = [('node_id', node_id),('hostname', node_id) ]
348
349         return node_id
350
351     def AddNodeSite(self, tuplelist, value):
352         """Add the site's node to the dictionary.
353
354
355         :param tuplelist: tuple list on which to add the node's site.
356             Contains the other node attributes as well.
357         :param value: value to add to the tuple list, in this case the node's
358             site.
359         :type tuplelist: list
360         :type value: string
361
362         .. seealso:: AddNodeNetworkAddr
363
364         """
365         tuplelist.append(('site', str(value)))
366
367     # def AddNodeRadio(tuplelist, value):
368     #     """Add thenode's radio chipset type to the tuple list.
369
370     #     :param tuplelist: tuple list on which to add the node's mobility
371                 # status. The tuplelist is the value associated with the node's
372                 # id in the OARGETParser
373     #          's dictionary node_dictlist.
374     #     :param value: name of the radio chipset on the node.
375     #     :type tuplelist: list
376     #     :type value: string
377
378     #     .. seealso:: AddNodeNetworkAddr
379
380     #     """
381     #     tuplelist.append(('radio', str(value)))
382
383
384     def AddMobility(self, tuplelist, value):
385         """Add if the node is a mobile node or not to the tuple list.
386
387         will also add which kind of mobility it is if the information
388         is available, supposing the information is formatted as a string
389         with the 2 information separated by a colon.
390
391         :param tuplelist: tuple list on which to add the node's mobility status.
392             The tuplelist is the value associated with the node's id in the
393             OARGETParser's dictionary node_dictlist.
394         :param value: tells if a node is a mobile node or not. The value is
395             found in the json.
396
397         :type tuplelist: list
398         :type value: integer
399
400         .. seealso:: AddNodeNetworkAddr
401
402         """
403         # future support of mobility type
404         if ":" in value:
405             value_list = value.split(':')
406             if value_list[0] is 0:
407                 tuplelist.append(('mobile', 'False'))
408             else:
409                tuplelist.append(('mobile', 'True'))
410             tuplelist.append(('type', value_list[1]))
411
412         if value is 0:
413             tuplelist.append(('mobile', 'False'))
414         else:
415             tuplelist.append(('mobile', 'True'))
416
417
418     def AddPosX(self, tuplelist, value):
419         """Add the node's position on the x axis.
420
421         :param tuplelist: tuple list on which to add the node's position . The
422             tuplelist is the value associated with the node's id in the
423             OARGETParser's dictionary node_dictlist.
424         :param value: the position x.
425
426         :type tuplelist: list
427         :type value: integer
428
429          .. seealso:: AddNodeNetworkAddr
430
431         """
432         tuplelist.append(('posx', value))
433
434
435
436     def AddPosY(self, tuplelist, value):
437         """Add the node's position on the y axis.
438
439         :param tuplelist: tuple list on which to add the node's position . The
440             tuplelist is the value associated with the node's id in the
441             OARGETParser's dictionary node_dictlist.
442         :param value: the position y.
443
444         :type tuplelist: list
445         :type value: integer
446
447          .. seealso:: AddNodeNetworkAddr
448
449         """
450         tuplelist.append(('posy', value))
451
452
453
454     def AddPosZ(self, tuplelist, value):
455         """Add the node's position on the z axis.
456
457         :param tuplelist: tuple list on which to add the node's position . The
458             tuplelist is the value associated with the node's id in the
459             OARGETParser's dictionary node_dictlist.
460         :param value: the position z.
461
462         :type tuplelist: list
463         :type value: integer
464
465          .. seealso:: AddNodeNetworkAddr
466
467         """
468
469         tuplelist.append(('posz', value))
470
471
472
473     def AddBootState(tself, tuplelist, value):
474         """Add the node's state, Alive or Suspected.
475
476         :param tuplelist: tuple list on which to add the node's state . The
477             tuplelist is the value associated with the node's id in the
478             OARGETParser 's dictionary node_dictlist.
479         :param value: node's state.
480
481         :type tuplelist: list
482         :type value: string
483
484          .. seealso:: AddNodeNetworkAddr
485
486         """
487         tuplelist.append(('boot_state', str(value)))
488
489
490     def AddHardwareType(self, tuplelist, value):
491         """Add the node's hardware model and radio chipset type to the tuple
492         list.
493
494         :param tuplelist: tuple list on which to add the node's architecture
495             and radio chipset type.
496         :param value: hardware type: radio chipset. The value contains both the
497             architecture and the radio chipset, separated by a colon.
498         :type tuplelist: list
499         :type value: string
500
501         .. seealso:: AddNodeNetworkAddr
502
503         """
504
505         value_list = value.split(':')
506         tuplelist.append(('archi', value_list[0]))
507         tuplelist.append(('radio', value_list[1]))
508
509
510 class OARGETParser:
511     """Class providing parsing methods associated to specific GET requests.
512
513     """
514
515     def __init__(self, srv):
516         self.version_json_dict = {
517             'api_version': None, 'apilib_version': None,
518             'api_timezone': None, 'api_timestamp': None, 'oar_version': None}
519         self.config = Config()
520         self.interface_hrn = self.config.SFA_INTERFACE_HRN
521         self.timezone_json_dict = {
522             'timezone': None, 'api_timestamp': None, }
523         #self.jobs_json_dict = {
524             #'total' : None, 'links' : [],\
525             #'offset':None , 'items' : [], }
526         #self.jobs_table_json_dict = self.jobs_json_dict
527         #self.jobs_details_json_dict = self.jobs_json_dict
528         self.server = srv
529         self.node_dictlist = {}
530
531         self.json_page = JsonPage()
532         self.parsing_resourcesfull = ParsingResourcesFull()
533         self.site_dict = {}
534         self.jobs_list = []
535         self.SendRequest("GET_version")
536
537
538     def ParseVersion(self):
539         """Parses the OAR answer to the GET_version ( /oarapi/version.json.)
540
541         Finds the OAR apilib version currently used. Has an impact on the json
542         structure returned by OAR, so the version has to be known before trying
543         to parse the jsons returned after a get request has been issued.
544         Updates the attribute version_json_dict.
545
546         """
547
548         if 'oar_version' in self.json_page.raw_json:
549             self.version_json_dict.update(
550                 api_version=self.json_page.raw_json['api_version'],
551                 apilib_version=self.json_page.raw_json['apilib_version'],
552                 api_timezone=self.json_page.raw_json['api_timezone'],
553                 api_timestamp=self.json_page.raw_json['api_timestamp'],
554                 oar_version=self.json_page.raw_json['oar_version'])
555         else:
556             self.version_json_dict.update(
557                 api_version=self.json_page.raw_json['api'],
558                 apilib_version=self.json_page.raw_json['apilib'],
559                 api_timezone=self.json_page.raw_json['api_timezone'],
560                 api_timestamp=self.json_page.raw_json['api_timestamp'],
561                 oar_version=self.json_page.raw_json['oar'])
562
563         print self.version_json_dict['apilib_version']
564
565
566     def ParseTimezone(self):
567         """Get the timezone used by OAR.
568
569         Get the timezone from the answer to the GET_timezone request.
570         :return: api_timestamp and api timezone.
571         :rype: integer, integer
572
573         .. warning:: unused.
574         """
575         api_timestamp = self.json_page.raw_json['api_timestamp']
576         api_tz = self.json_page.raw_json['timezone']
577         return api_timestamp, api_tz
578
579     def ParseJobs(self):
580         """Called when a GET_jobs request has been issued to OAR.
581
582         Corresponds to /oarapi/jobs.json uri. Currently returns the raw json
583         information dict.
584         :returns: json_page.raw_json
585         :rtype: dictionary
586
587         .. warning:: Does not actually parse the information in the json. SA
588             15/07/13.
589
590         """
591         self.jobs_list = []
592         print " ParseJobs "
593         return self.json_page.raw_json
594
595     def ParseJobsTable(self):
596         """In case we need to use the job table in the future.
597
598         Associated with the GET_jobs_table : '/oarapi/jobs/table.json uri.
599         .. warning:: NOT USED. DOES NOTHING.
600         """
601         print "ParseJobsTable"
602
603     def ParseJobsDetails(self):
604         """Currently only returns the same json in self.json_page.raw_json.
605
606         .. todo:: actually parse the json
607         .. warning:: currently, this function is not used a lot, so I have no
608             idea what could  be useful to parse, returning the full json. NT
609         """
610
611         #logger.debug("ParseJobsDetails %s " %(self.json_page.raw_json))
612         return self.json_page.raw_json
613
614
615     def ParseJobsIds(self):
616         """Associated with the GET_jobs_id OAR request.
617
618         Parses the json dict (OAR answer) to the GET_jobs_id request
619         /oarapi/jobs/id.json.
620
621
622         :returns: dictionary whose keys are listed in the local variable
623             job_resources and values that are in the json dictionary returned
624             by OAR with the job information.
625         :rtype: dict
626
627         """
628         job_resources = ['wanted_resources', 'name', 'id', 'start_time',
629                          'state', 'owner', 'walltime', 'message']
630
631         # Unused variable providing the contents of the json dict returned from
632         # get job resources full request
633         job_resources_full = [
634             'launching_directory', 'links',
635             'resubmit_job_id', 'owner', 'events', 'message',
636             'scheduled_start', 'id', 'array_id', 'exit_code',
637             'properties', 'state', 'array_index', 'walltime',
638             'type', 'initial_request', 'stop_time', 'project',
639             'start_time',  'dependencies', 'api_timestamp', 'submission_time',
640             'reservation', 'stdout_file', 'types', 'cpuset_name',
641             'name', 'wanted_resources', 'queue', 'stderr_file', 'command']
642
643
644         job_info = self.json_page.raw_json
645         #logger.debug("OARESTAPI ParseJobsIds %s" %(self.json_page.raw_json))
646         values = []
647         try:
648             for k in job_resources:
649                 values.append(job_info[k])
650             return dict(zip(job_resources, values))
651
652         except KeyError:
653             logger.log_exc("ParseJobsIds KeyError ")
654
655
656     def ParseJobsIdResources(self):
657         """ Parses the json produced by the request
658         /oarapi/jobs/id/resources.json.
659         Returns a list of oar node ids that are scheduled for the
660         given job id.
661
662         """
663         job_resources = []
664         for resource in self.json_page.raw_json['items']:
665             job_resources.append(resource['id'])
666
667         return job_resources
668
669     def ParseResources(self):
670         """ Parses the json produced by a get_resources request on oar."""
671
672         #logger.debug("OARESTAPI \tParseResources " )
673         #resources are listed inside the 'items' list from the json
674         self.json_page.raw_json = self.json_page.raw_json['items']
675         self.ParseNodes()
676
677     def ParseReservedNodes(self):
678         """  Returns an array containing the list of the jobs scheduled
679         with the reserved nodes if available.
680
681         :returns: list of job dicts, each dict containing the following keys:
682             t_from, t_until, resources_ids (of the reserved nodes for this job).
683             If the information is not available, default values will be set for
684             these keys. The other keys are : state, lease_id and user.
685         :rtype: list
686
687         """
688
689         #resources are listed inside the 'items' list from the json
690         reservation_list = []
691         job = {}
692         #Parse resources info
693         for json_element in self.json_page.raw_json['items']:
694             #In case it is a real reservation (not asap case)
695             if json_element['scheduled_start']:
696                 job['t_from'] = json_element['scheduled_start']
697                 job['t_until'] = int(json_element['scheduled_start']) + \
698                     int(json_element['walltime'])
699                 #Get resources id list for the job
700                 job['resource_ids'] = [node_dict['id'] for node_dict
701                                        in json_element['resources']]
702             else:
703                 job['t_from'] = "As soon as possible"
704                 job['t_until'] = "As soon as possible"
705                 job['resource_ids'] = ["Undefined"]
706
707             job['state'] = json_element['state']
708             job['lease_id'] = json_element['id']
709
710             job['user'] = json_element['owner']
711             #logger.debug("OARRestapi \tParseReservedNodes job %s" %(job))
712             reservation_list.append(job)
713             #reset dict
714             job = {}
715         return reservation_list
716
717     def ParseRunningJobs(self):
718         """ Gets the list of nodes currently in use from the attributes of the
719         running jobs.
720
721         :returns: list of hostnames, the nodes that are currently involved in
722             running jobs.
723         :rtype: list
724
725
726         """
727         logger.debug("OARESTAPI \tParseRunningJobs_________________ ")
728         #resources are listed inside the 'items' list from the json
729         nodes = []
730         for job in self.json_page.raw_json['items']:
731             for node in job['nodes']:
732                 nodes.append(node['network_address'])
733         return nodes
734
735     def ChangeRawJsonDependingOnApilibVersion(self):
736         """
737         Check if the OAR apilib version is different from 0.2.10, in which case
738         the Json answer is also dict instead as a plain list.
739
740         .. warning:: the whole code is assuming the json contains a 'items' key
741         .. seealso:: ConcatenateJsonPages, ParseJobs, ParseReservedNodes,
742             ParseJobsIdResources, ParseResources, ParseRunningJobs
743         .. todo:: Clean the whole code. Either suppose the  apilib will always
744             provide the 'items' key, or handle different options.
745         """
746
747         if self.version_json_dict['apilib_version'] != "0.2.10":
748             self.json_page.raw_json = self.json_page.raw_json['items']
749
750     def ParseDeleteJobs(self):
751         """ No need to parse anything in this function.A POST
752         is done to delete the job.
753
754         """
755         return
756
757     def ParseResourcesFull(self):
758         """ This method is responsible for parsing all the attributes
759         of all the nodes returned by OAR when issuing a get resources full.
760         The information from the nodes and the sites are separated.
761         Updates the node_dictlist so that the dictionnary of the platform's
762         nodes is available afterwards.
763
764         :returns: node_dictlist, a list of dictionaries about the nodes and
765             their properties.
766         :rtype: list
767
768         """
769         logger.debug("OARRESTAPI ParseResourcesFull___________ ")
770         #print self.json_page.raw_json[1]
771         #resources are listed inside the 'items' list from the json
772         self.ChangeRawJsonDependingOnApilibVersion()
773         self.ParseNodes()
774         self.ParseSites()
775         return self.node_dictlist
776
777     def ParseResourcesFullSites(self):
778         """ Called by GetSites which is unused.
779         Originally used to get information from the sites, with for each site
780         the list of nodes it has, along with their properties.
781
782         :return: site_dict, dictionary of sites
783         :rtype: dict
784
785         .. warning:: unused
786         .. seealso:: GetSites (IotlabTestbedAPI)
787
788         """
789         self.ChangeRawJsonDependingOnApilibVersion()
790         self.ParseNodes()
791         self.ParseSites()
792         return self.site_dict
793
794
795     def ParseNodes(self):
796         """ Parse nodes properties from OAR
797         Put them into a dictionary with key = node id and value is a dictionary
798         of the node properties and properties'values.
799
800         """
801         node_id = None
802         _resources_fulljson_dict = \
803             self.parsing_resourcesfull.resources_fulljson_dict
804         keys = _resources_fulljson_dict.keys()
805         keys.sort()
806
807         for dictline in self.json_page.raw_json:
808             node_id = None
809             # dictionary is empty and/or a new node has to be inserted
810             node_id = _resources_fulljson_dict['network_address'](
811                 self.node_dictlist, dictline['network_address'])
812             for k in keys:
813                 if k in dictline:
814                     if k == 'network_address':
815                         continue
816
817                     _resources_fulljson_dict[k](
818                         self.node_dictlist[node_id], dictline[k])
819
820             #The last property has been inserted in the property tuple list,
821             #reset node_id
822             #Turn the property tuple list (=dict value) into a dictionary
823             self.node_dictlist[node_id] = dict(self.node_dictlist[node_id])
824             node_id = None
825
826     @staticmethod
827     def iotlab_hostname_to_hrn(root_auth,  hostname):
828         """
829         Transforms a node hostname into a SFA hrn.
830
831         :param root_auth: Name of the root authority of the SFA server. In
832             our case, it is set to iotlab.
833         :param hostname: node's hotname, given by OAR.
834         :type root_auth: string
835         :type hostname: string
836         :returns: inserts the root_auth and '.' before the hostname.
837         :rtype: string
838
839         """
840         return root_auth + '.' + hostname
841
842     def ParseSites(self):
843         """ Returns a list of dictionnaries containing the sites' attributes."""
844
845         nodes_per_site = {}
846         config = Config()
847         #logger.debug(" OARrestapi.py \tParseSites  self.node_dictlist %s"\
848                                                         #%(self.node_dictlist))
849         # Create a list of nodes per site_id
850         for node_id in self.node_dictlist:
851             node = self.node_dictlist[node_id]
852
853             if node['site'] not in nodes_per_site:
854                 nodes_per_site[node['site']] = []
855                 nodes_per_site[node['site']].append(node['node_id'])
856             else:
857                 if node['node_id'] not in nodes_per_site[node['site']]:
858                     nodes_per_site[node['site']].append(node['node_id'])
859
860         #Create a site dictionary whose key is site_login_base
861         # (name of the site) and value is a dictionary of properties,
862         # including the list of the node_ids
863         for node_id in self.node_dictlist:
864             node = self.node_dictlist[node_id]
865             node.update({'hrn': self.iotlab_hostname_to_hrn(self.interface_hrn,
866                                                             node['hostname'])})
867             self.node_dictlist.update({node_id: node})
868
869             if node['site'] not in self.site_dict:
870                 self.site_dict[node['site']] = {
871                     'site': node['site'],
872                     'node_ids': nodes_per_site[node['site']],
873                     'latitude': "48.83726",
874                     'longitude': "- 2.10336",
875                     'name': config.SFA_REGISTRY_ROOT_AUTH,
876                     'pcu_ids': [], 'max_slices': None,
877                     'ext_consortium_id': None,
878                     'max_slivers': None, 'is_public': True,
879                     'peer_site_id': None,
880                     'abbreviated_name': "iotlab", 'address_ids': [],
881                     'url': "https://portal.senslab.info", 'person_ids': [],
882                     'site_tag_ids': [], 'enabled': True,  'slice_ids': [],
883                     'date_created': None, 'peer_id': None
884                 }
885
886     OARrequests_uri_dict = {
887         'GET_version':
888         {'uri': '/oarapi/version.json', 'parse_func': ParseVersion},
889
890         'GET_timezone':
891         {'uri': '/oarapi/timezone.json', 'parse_func': ParseTimezone},
892
893         'GET_jobs':
894         {'uri': '/oarapi/jobs.json', 'parse_func': ParseJobs},
895
896         'GET_jobs_id':
897         {'uri': '/oarapi/jobs/id.json', 'parse_func': ParseJobsIds},
898
899         'GET_jobs_id_resources':
900         {'uri': '/oarapi/jobs/id/resources.json',
901         'parse_func': ParseJobsIdResources},
902
903         'GET_jobs_table':
904         {'uri': '/oarapi/jobs/table.json', 'parse_func': ParseJobsTable},
905
906         'GET_jobs_details':
907         {'uri': '/oarapi/jobs/details.json', 'parse_func': ParseJobsDetails},
908
909         'GET_reserved_nodes':
910         {'uri':
911         '/oarapi/jobs/details.json?state=Running,Waiting,Launching',
912         'owner': '&user=', 'parse_func': ParseReservedNodes},
913
914         'GET_running_jobs':
915         {'uri': '/oarapi/jobs/details.json?state=Running',
916         'parse_func': ParseRunningJobs},
917
918         'GET_resources_full':
919         {'uri': '/oarapi/resources/full.json',
920         'parse_func': ParseResourcesFull},
921
922         'GET_sites':
923         {'uri': '/oarapi/resources/full.json',
924         'parse_func': ParseResourcesFullSites},
925
926         'GET_resources':
927         {'uri': '/oarapi/resources.json', 'parse_func': ParseResources},
928
929         'DELETE_jobs_id':
930         {'uri': '/oarapi/jobs/id.json', 'parse_func': ParseDeleteJobs}}
931
932
933     def SendRequest(self, request, strval=None, username=None):
934         """ Connects to OAR , sends the valid GET requests and uses
935         the appropriate json parsing functions.
936
937         :returns: calls to the appropriate parsing function, associated with the
938             GET request
939         :rtype: depends on the parsing function called.
940
941         .. seealso:: OARrequests_uri_dict
942         """
943         save_json = None
944
945         self.json_page.ResetNextPage()
946         save_json = []
947
948         if request in self.OARrequests_uri_dict:
949             while self.json_page.next_page:
950                 self.json_page.raw_json = self.server.GETRequestToOARRestAPI(
951                     request,
952                     strval,
953                     self.json_page.next_offset,
954                     username)
955                 self.json_page.FindNextPage()
956                 if self.json_page.concatenate:
957                     save_json.append(self.json_page.raw_json)
958
959             if self.json_page.concatenate and self.json_page.end:
960                 self.json_page.raw_json = \
961                     self.json_page.ConcatenateJsonPages(save_json)
962
963             return self.OARrequests_uri_dict[request]['parse_func'](self)
964         else:
965             logger.error("OARRESTAPI OARGetParse __init__ : ERROR_REQUEST "
966                          % (request))