Adding more error check in OARrestapi.
[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()
217             body = resp.read()
218         except Exception as error:
219             logger.log_exc("GET_OAR_SRVR : Connection error: %s "
220                 % (error))
221             raise Exception ("GET_OAR_SRVR : Connection error %s " %(error))
222
223         finally:
224             conn.close()
225
226         # except HTTPException, error:
227         #     logger.log_exc("GET_OAR_SRVR : Problem with OAR server : %s "
228         #                    % (error))
229             #raise ServerError("GET_OAR_SRVR : Could not reach OARserver")
230         if resp.status >= 400:
231             raise ValueError ("Response Error %s, %s" %(resp.status,
232                 resp.reason))
233         try:
234             js_dict = json.loads(body)
235             #print "\r\n \t\t\t js_dict keys" , js_dict.keys(), " \r\n", js_dict
236             return js_dict
237
238         except ValueError, error:
239             logger.log_exc("Failed to parse Server Response: %s ERROR %s"
240                            % (body, error))
241             #raise ServerError("Failed to parse Server Response:" + js)
242
243
244     def POSTRequestToOARRestAPI(self, request, datadict, username=None):
245         """ Used to post a job on OAR , along with data associated
246         with the job.
247
248         """
249
250         #first check that all params for are OK
251         try:
252             self.oarserver['uri'] = \
253                 self.OAR_REQUEST_POST_URI_DICT[request]['uri']
254
255         except KeyError:
256             logger.log_exc("OARrestapi \tPOSTRequestToOARRestAPI request not \
257                              valid")
258             return
259         if datadict and 'strval' in datadict:
260             self.oarserver['uri'] = self.oarserver['uri'].replace("id", \
261                                                 str(datadict['strval']))
262             del datadict['strval']
263
264         data = json.dumps(datadict)
265         headers = {'X-REMOTE_IDENT':username, \
266                 'content-type': self.POST_FORMAT['json']['content'], \
267                 'content-length':str(len(data))}
268         try :
269
270             conn = HTTPConnection(self.oarserver['ip'], \
271                                         self.oarserver['port'])
272             conn.request("POST", self.oarserver['uri'], data, headers)
273             resp = conn.getresponse()
274             body = resp.read()
275
276         except NotConnected:
277             logger.log_exc("POSTRequestToOARRestAPI NotConnected ERROR: \
278                             data %s \r\n \t\n \t\t headers %s uri %s" \
279                             %(data,headers,self.oarserver['uri']))
280         except Exception as error:
281             logger.log_exc("POST_OAR_SERVER : Connection error: %s "
282                 % (error))
283             raise Exception ("POST_OAR_SERVER : Connection error %s " %(error))
284
285         finally:
286             conn.close()
287
288         if resp.status >= 400:
289             raise ValueError ("Response Error %s, %s" %(resp.status,
290                 resp.reason))
291
292
293         try:
294             answer = json.loads(body)
295             logger.debug("POSTRequestToOARRestAPI : answer %s" % (answer))
296             return answer
297
298         except ValueError, error:
299             logger.log_exc("Failed to parse Server Response: error %s  \
300                             %s" %(error))
301             #raise ServerError("Failed to parse Server Response:" + answer)
302
303
304 class ParsingResourcesFull():
305     """
306     Class dedicated to parse the json response from a GET_resources_full from
307     OAR.
308
309     """
310     def __init__(self):
311         """
312         Set the parsing dictionary. Works like a switch case, if the key is
313         found in the dictionary, then the associated function is called.
314         This is used in ParseNodes to create an usable dictionary from
315         the Json returned by OAR when issuing a GET resources full request.
316
317         .. seealso:: ParseNodes
318
319         """
320         self.resources_fulljson_dict = {
321         'network_address': self.AddNodeNetworkAddr,
322         'site':  self.AddNodeSite,
323         # 'radio':  self.AddNodeRadio,
324         'mobile':  self.AddMobility,
325         'x':  self.AddPosX,
326         'y':  self.AddPosY,
327         'z': self.AddPosZ,
328         'archi': self.AddHardwareType,
329         'state': self.AddBootState,
330         'id': self.AddOarNodeId,
331         'mobility_type': self.AddMobilityType,
332         }
333
334
335
336     def AddOarNodeId(self, tuplelist, value):
337         """Adds Oar internal node id to the nodes' attributes.
338
339         Appends tuple ('oar_id', node_id) to the tuplelist. Used by ParseNodes.
340
341         .. seealso:: ParseNodes
342
343         """
344
345         tuplelist.append(('oar_id', int(value)))
346
347
348     def AddNodeNetworkAddr(self, dictnode, value):
349         """First parsing function to be called to parse the json returned by OAR
350         answering a GET_resources (/oarapi/resources.json) request.
351
352         When a new node is found in the json, this function is responsible for
353         creating a new entry in the dictionary for storing information on this
354         specific node. The key is the node network address, which is also the
355         node's hostname.
356         The value associated with the key is a tuple list.It contains all
357         the nodes attributes. The tuplelist will later be turned into a dict.
358
359         :param dictnode: should be set to the OARGETParser atribute
360             node_dictlist. It will store the information on the nodes.
361         :param value: the node_id is the network_address in the raw json.
362         :type value: string
363         :type dictnode: dictionary
364
365         .. seealso: ParseResources, ParseNodes
366         """
367
368         node_id = value
369         dictnode[node_id] = [('node_id', node_id),('hostname', node_id) ]
370
371         return node_id
372
373     def AddNodeSite(self, tuplelist, value):
374         """Add the site's node to the dictionary.
375
376
377         :param tuplelist: tuple list on which to add the node's site.
378             Contains the other node attributes as well.
379         :param value: value to add to the tuple list, in this case the node's
380             site.
381         :type tuplelist: list
382         :type value: string
383
384         .. seealso:: AddNodeNetworkAddr
385
386         """
387         tuplelist.append(('site', str(value)))
388
389     # def AddNodeRadio(tuplelist, value):
390     #     """Add thenode's radio chipset type to the tuple list.
391
392     #     :param tuplelist: tuple list on which to add the node's mobility
393                 # status. The tuplelist is the value associated with the node's
394                 # id in the OARGETParser
395     #          's dictionary node_dictlist.
396     #     :param value: name of the radio chipset on the node.
397     #     :type tuplelist: list
398     #     :type value: string
399
400     #     .. seealso:: AddNodeNetworkAddr
401
402     #     """
403     #     tuplelist.append(('radio', str(value)))
404
405     def AddMobilityType(self, tuplelist, value):
406         """Adds  which kind of mobility it is, train or roomba robot.
407
408         :param tuplelist: tuple list on which to add the node's mobility status.
409             The tuplelist is the value associated with the node's id in the
410             OARGETParser's dictionary node_dictlist.
411         :param value: tells if a node is a mobile node or not. The value is
412             found in the json.
413
414         :type tuplelist: list
415         :type value: integer
416
417         """
418         tuplelist.append(('mobility_type', str(value)))
419
420
421     def AddMobility(self, tuplelist, value):
422         """Add if the node is a mobile node or not to the tuple list.
423
424         :param tuplelist: tuple list on which to add the node's mobility status.
425             The tuplelist is the value associated with the node's id in the
426             OARGETParser's dictionary node_dictlist.
427         :param value: tells if a node is a mobile node or not. The value is
428             found in the json.
429
430         :type tuplelist: list
431         :type value: integer
432
433         .. seealso:: AddNodeNetworkAddr
434
435         """
436         # future support of mobility type
437
438         if value is 0:
439             tuplelist.append(('mobile', 'False'))
440         else:
441             tuplelist.append(('mobile', 'True'))
442
443
444     def AddPosX(self, tuplelist, value):
445         """Add the node's position on the x axis.
446
447         :param tuplelist: tuple list on which to add the node's position . The
448             tuplelist is the value associated with the node's id in the
449             OARGETParser's dictionary node_dictlist.
450         :param value: the position x.
451
452         :type tuplelist: list
453         :type value: integer
454
455          .. seealso:: AddNodeNetworkAddr
456
457         """
458         tuplelist.append(('posx', value))
459
460
461
462     def AddPosY(self, tuplelist, value):
463         """Add the node's position on the y axis.
464
465         :param tuplelist: tuple list on which to add the node's position . The
466             tuplelist is the value associated with the node's id in the
467             OARGETParser's dictionary node_dictlist.
468         :param value: the position y.
469
470         :type tuplelist: list
471         :type value: integer
472
473          .. seealso:: AddNodeNetworkAddr
474
475         """
476         tuplelist.append(('posy', value))
477
478
479
480     def AddPosZ(self, tuplelist, value):
481         """Add the node's position on the z axis.
482
483         :param tuplelist: tuple list on which to add the node's position . The
484             tuplelist is the value associated with the node's id in the
485             OARGETParser's dictionary node_dictlist.
486         :param value: the position z.
487
488         :type tuplelist: list
489         :type value: integer
490
491          .. seealso:: AddNodeNetworkAddr
492
493         """
494
495         tuplelist.append(('posz', value))
496
497
498
499     def AddBootState(tself, tuplelist, value):
500         """Add the node's state, Alive or Suspected.
501
502         :param tuplelist: tuple list on which to add the node's state . The
503             tuplelist is the value associated with the node's id in the
504             OARGETParser 's dictionary node_dictlist.
505         :param value: node's state.
506
507         :type tuplelist: list
508         :type value: string
509
510          .. seealso:: AddNodeNetworkAddr
511
512         """
513         tuplelist.append(('boot_state', str(value)))
514
515
516     def AddHardwareType(self, tuplelist, value):
517         """Add the node's hardware model and radio chipset type to the tuple
518         list.
519
520         :param tuplelist: tuple list on which to add the node's architecture
521             and radio chipset type.
522         :param value: hardware type: radio chipset. The value contains both the
523             architecture and the radio chipset, separated by a colon.
524         :type tuplelist: list
525         :type value: string
526
527         .. seealso:: AddNodeNetworkAddr
528
529         """
530
531         value_list = value.split(':')
532         tuplelist.append(('archi', value_list[0]))
533         tuplelist.append(('radio', value_list[1]))
534
535
536 class OARGETParser:
537     """Class providing parsing methods associated to specific GET requests.
538
539     """
540
541     def __init__(self, srv):
542         self.version_json_dict = {
543             'api_version': None, 'apilib_version': None,
544             'api_timezone': None, 'api_timestamp': None, 'oar_version': None}
545         self.config = Config()
546         self.interface_hrn = self.config.SFA_INTERFACE_HRN
547         self.timezone_json_dict = {
548             'timezone': None, 'api_timestamp': None, }
549         #self.jobs_json_dict = {
550             #'total' : None, 'links' : [],\
551             #'offset':None , 'items' : [], }
552         #self.jobs_table_json_dict = self.jobs_json_dict
553         #self.jobs_details_json_dict = self.jobs_json_dict
554         self.server = srv
555         self.node_dictlist = {}
556
557         self.json_page = JsonPage()
558         self.parsing_resourcesfull = ParsingResourcesFull()
559         self.site_dict = {}
560         self.jobs_list = []
561         self.SendRequest("GET_version")
562
563
564     def ParseVersion(self):
565         """Parses the OAR answer to the GET_version ( /oarapi/version.json.)
566
567         Finds the OAR apilib version currently used. Has an impact on the json
568         structure returned by OAR, so the version has to be known before trying
569         to parse the jsons returned after a get request has been issued.
570         Updates the attribute version_json_dict.
571
572         """
573
574         if 'oar_version' in self.json_page.raw_json:
575             self.version_json_dict.update(
576                 api_version=self.json_page.raw_json['api_version'],
577                 apilib_version=self.json_page.raw_json['apilib_version'],
578                 api_timezone=self.json_page.raw_json['api_timezone'],
579                 api_timestamp=self.json_page.raw_json['api_timestamp'],
580                 oar_version=self.json_page.raw_json['oar_version'])
581         else:
582             self.version_json_dict.update(
583                 api_version=self.json_page.raw_json['api'],
584                 apilib_version=self.json_page.raw_json['apilib'],
585                 api_timezone=self.json_page.raw_json['api_timezone'],
586                 api_timestamp=self.json_page.raw_json['api_timestamp'],
587                 oar_version=self.json_page.raw_json['oar'])
588
589         print self.version_json_dict['apilib_version']
590
591
592     def ParseTimezone(self):
593         """Get the timezone used by OAR.
594
595         Get the timezone from the answer to the GET_timezone request.
596         :return: api_timestamp and api timezone.
597         :rype: integer, integer
598
599         .. warning:: unused.
600         """
601         api_timestamp = self.json_page.raw_json['api_timestamp']
602         api_tz = self.json_page.raw_json['timezone']
603         return api_timestamp, api_tz
604
605     def ParseJobs(self):
606         """Called when a GET_jobs request has been issued to OAR.
607
608         Corresponds to /oarapi/jobs.json uri. Currently returns the raw json
609         information dict.
610         :returns: json_page.raw_json
611         :rtype: dictionary
612
613         .. warning:: Does not actually parse the information in the json. SA
614             15/07/13.
615
616         """
617         self.jobs_list = []
618         print " ParseJobs "
619         return self.json_page.raw_json
620
621     def ParseJobsTable(self):
622         """In case we need to use the job table in the future.
623
624         Associated with the GET_jobs_table : '/oarapi/jobs/table.json uri.
625         .. warning:: NOT USED. DOES NOTHING.
626         """
627         print "ParseJobsTable"
628
629     def ParseJobsDetails(self):
630         """Currently only returns the same json in self.json_page.raw_json.
631
632         .. todo:: actually parse the json
633         .. warning:: currently, this function is not used a lot, so I have no
634             idea what could  be useful to parse, returning the full json. NT
635         """
636
637         #logger.debug("ParseJobsDetails %s " %(self.json_page.raw_json))
638         return self.json_page.raw_json
639
640
641     def ParseJobsIds(self):
642         """Associated with the GET_jobs_id OAR request.
643
644         Parses the json dict (OAR answer) to the GET_jobs_id request
645         /oarapi/jobs/id.json.
646
647
648         :returns: dictionary whose keys are listed in the local variable
649             job_resources and values that are in the json dictionary returned
650             by OAR with the job information.
651         :rtype: dict
652
653         """
654         job_resources = ['wanted_resources', 'name', 'id', 'start_time',
655                          'state', 'owner', 'walltime', 'message']
656
657         # Unused variable providing the contents of the json dict returned from
658         # get job resources full request
659         job_resources_full = [
660             'launching_directory', 'links',
661             'resubmit_job_id', 'owner', 'events', 'message',
662             'scheduled_start', 'id', 'array_id', 'exit_code',
663             'properties', 'state', 'array_index', 'walltime',
664             'type', 'initial_request', 'stop_time', 'project',
665             'start_time',  'dependencies', 'api_timestamp', 'submission_time',
666             'reservation', 'stdout_file', 'types', 'cpuset_name',
667             'name', 'wanted_resources', 'queue', 'stderr_file', 'command']
668
669
670         job_info = self.json_page.raw_json
671         #logger.debug("OARESTAPI ParseJobsIds %s" %(self.json_page.raw_json))
672         values = []
673         try:
674             for k in job_resources:
675                 values.append(job_info[k])
676             return dict(zip(job_resources, values))
677
678         except KeyError:
679             logger.log_exc("ParseJobsIds KeyError ")
680
681
682     def ParseJobsIdResources(self):
683         """ Parses the json produced by the request
684         /oarapi/jobs/id/resources.json.
685         Returns a list of oar node ids that are scheduled for the
686         given job id.
687
688         """
689         job_resources = []
690         for resource in self.json_page.raw_json['items']:
691             job_resources.append(resource['id'])
692
693         return job_resources
694
695     def ParseResources(self):
696         """ Parses the json produced by a get_resources request on oar."""
697
698         #logger.debug("OARESTAPI \tParseResources " )
699         #resources are listed inside the 'items' list from the json
700         self.json_page.raw_json = self.json_page.raw_json['items']
701         self.ParseNodes()
702
703     def ParseReservedNodes(self):
704         """  Returns an array containing the list of the jobs scheduled
705         with the reserved nodes if available.
706
707         :returns: list of job dicts, each dict containing the following keys:
708             t_from, t_until, resources_ids (of the reserved nodes for this job).
709             If the information is not available, default values will be set for
710             these keys. The other keys are : state, lease_id and user.
711         :rtype: list
712
713         """
714
715         #resources are listed inside the 'items' list from the json
716         reservation_list = []
717         job = {}
718         #Parse resources info
719         for json_element in self.json_page.raw_json['items']:
720             #In case it is a real reservation (not asap case)
721             if json_element['scheduled_start']:
722                 job['t_from'] = json_element['scheduled_start']
723                 job['t_until'] = int(json_element['scheduled_start']) + \
724                     int(json_element['walltime'])
725                 #Get resources id list for the job
726                 job['resource_ids'] = [node_dict['id'] for node_dict
727                                        in json_element['resources']]
728             else:
729                 job['t_from'] = "As soon as possible"
730                 job['t_until'] = "As soon as possible"
731                 job['resource_ids'] = ["Undefined"]
732
733             job['state'] = json_element['state']
734             job['lease_id'] = json_element['id']
735
736             job['user'] = json_element['owner']
737             #logger.debug("OARRestapi \tParseReservedNodes job %s" %(job))
738             reservation_list.append(job)
739             #reset dict
740             job = {}
741         return reservation_list
742
743     def ParseRunningJobs(self):
744         """ Gets the list of nodes currently in use from the attributes of the
745         running jobs.
746
747         :returns: list of hostnames, the nodes that are currently involved in
748             running jobs.
749         :rtype: list
750
751
752         """
753         logger.debug("OARESTAPI \tParseRunningJobs_________________ ")
754         #resources are listed inside the 'items' list from the json
755         nodes = []
756         for job in self.json_page.raw_json['items']:
757             for node in job['nodes']:
758                 nodes.append(node['network_address'])
759         return nodes
760
761     def ChangeRawJsonDependingOnApilibVersion(self):
762         """
763         Check if the OAR apilib version is different from 0.2.10, in which case
764         the Json answer is also dict instead as a plain list.
765
766         .. warning:: the whole code is assuming the json contains a 'items' key
767         .. seealso:: ConcatenateJsonPages, ParseJobs, ParseReservedNodes,
768             ParseJobsIdResources, ParseResources, ParseRunningJobs
769         .. todo:: Clean the whole code. Either suppose the  apilib will always
770             provide the 'items' key, or handle different options.
771         """
772
773         if self.version_json_dict['apilib_version'] != "0.2.10":
774             self.json_page.raw_json = self.json_page.raw_json['items']
775
776     def ParseDeleteJobs(self):
777         """ No need to parse anything in this function.A POST
778         is done to delete the job.
779
780         """
781         return
782
783     def ParseResourcesFull(self):
784         """ This method is responsible for parsing all the attributes
785         of all the nodes returned by OAR when issuing a get resources full.
786         The information from the nodes and the sites are separated.
787         Updates the node_dictlist so that the dictionnary of the platform's
788         nodes is available afterwards.
789
790         :returns: node_dictlist, a list of dictionaries about the nodes and
791             their properties.
792         :rtype: list
793
794         """
795         logger.debug("OARRESTAPI ParseResourcesFull___________ ")
796         #print self.json_page.raw_json[1]
797         #resources are listed inside the 'items' list from the json
798         self.ChangeRawJsonDependingOnApilibVersion()
799         self.ParseNodes()
800         self.ParseSites()
801         return self.node_dictlist
802
803     def ParseResourcesFullSites(self):
804         """ Called by GetSites which is unused.
805         Originally used to get information from the sites, with for each site
806         the list of nodes it has, along with their properties.
807
808         :return: site_dict, dictionary of sites
809         :rtype: dict
810
811         .. warning:: unused
812         .. seealso:: GetSites (IotlabTestbedAPI)
813
814         """
815         self.ChangeRawJsonDependingOnApilibVersion()
816         self.ParseNodes()
817         self.ParseSites()
818         return self.site_dict
819
820
821     def ParseNodes(self):
822         """ Parse nodes properties from OAR
823         Put them into a dictionary with key = node id and value is a dictionary
824         of the node properties and properties'values.
825
826         """
827         node_id = None
828         _resources_fulljson_dict = \
829             self.parsing_resourcesfull.resources_fulljson_dict
830         keys = _resources_fulljson_dict.keys()
831         keys.sort()
832
833         for dictline in self.json_page.raw_json:
834             node_id = None
835             # dictionary is empty and/or a new node has to be inserted
836             node_id = _resources_fulljson_dict['network_address'](
837                 self.node_dictlist, dictline['network_address'])
838             for k in keys:
839                 if k in dictline:
840                     if k == 'network_address':
841                         continue
842
843                     _resources_fulljson_dict[k](
844                         self.node_dictlist[node_id], dictline[k])
845
846             #The last property has been inserted in the property tuple list,
847             #reset node_id
848             #Turn the property tuple list (=dict value) into a dictionary
849             self.node_dictlist[node_id] = dict(self.node_dictlist[node_id])
850             node_id = None
851
852     @staticmethod
853     def iotlab_hostname_to_hrn(root_auth,  hostname):
854         """
855         Transforms a node hostname into a SFA hrn.
856
857         :param root_auth: Name of the root authority of the SFA server. In
858             our case, it is set to iotlab.
859         :param hostname: node's hotname, given by OAR.
860         :type root_auth: string
861         :type hostname: string
862         :returns: inserts the root_auth and '.' before the hostname.
863         :rtype: string
864
865         """
866         return root_auth + '.' + hostname
867
868     def ParseSites(self):
869         """ Returns a list of dictionnaries containing the sites' attributes."""
870
871         nodes_per_site = {}
872         config = Config()
873         #logger.debug(" OARrestapi.py \tParseSites  self.node_dictlist %s"\
874                                                         #%(self.node_dictlist))
875         # Create a list of nodes per site_id
876         for node_id in self.node_dictlist:
877             node = self.node_dictlist[node_id]
878
879             if node['site'] not in nodes_per_site:
880                 nodes_per_site[node['site']] = []
881                 nodes_per_site[node['site']].append(node['node_id'])
882             else:
883                 if node['node_id'] not in nodes_per_site[node['site']]:
884                     nodes_per_site[node['site']].append(node['node_id'])
885
886         #Create a site dictionary whose key is site_login_base
887         # (name of the site) and value is a dictionary of properties,
888         # including the list of the node_ids
889         for node_id in self.node_dictlist:
890             node = self.node_dictlist[node_id]
891             node.update({'hrn': self.iotlab_hostname_to_hrn(self.interface_hrn,
892                                                             node['hostname'])})
893             self.node_dictlist.update({node_id: node})
894
895             if node['site'] not in self.site_dict:
896                 self.site_dict[node['site']] = {
897                     'site': node['site'],
898                     'node_ids': nodes_per_site[node['site']],
899                     'latitude': "48.83726",
900                     'longitude': "- 2.10336",
901                     'name': config.SFA_REGISTRY_ROOT_AUTH,
902                     'pcu_ids': [], 'max_slices': None,
903                     'ext_consortium_id': None,
904                     'max_slivers': None, 'is_public': True,
905                     'peer_site_id': None,
906                     'abbreviated_name': "iotlab", 'address_ids': [],
907                     'url': "https://portal.senslab.info", 'person_ids': [],
908                     'site_tag_ids': [], 'enabled': True,  'slice_ids': [],
909                     'date_created': None, 'peer_id': None
910                 }
911
912     OARrequests_uri_dict = {
913         'GET_version':
914         {'uri': '/oarapi/version.json', 'parse_func': ParseVersion},
915
916         'GET_timezone':
917         {'uri': '/oarapi/timezone.json', 'parse_func': ParseTimezone},
918
919         'GET_jobs':
920         {'uri': '/oarapi/jobs.json', 'parse_func': ParseJobs},
921
922         'GET_jobs_id':
923         {'uri': '/oarapi/jobs/id.json', 'parse_func': ParseJobsIds},
924
925         'GET_jobs_id_resources':
926         {'uri': '/oarapi/jobs/id/resources.json',
927         'parse_func': ParseJobsIdResources},
928
929         'GET_jobs_table':
930         {'uri': '/oarapi/jobs/table.json', 'parse_func': ParseJobsTable},
931
932         'GET_jobs_details':
933         {'uri': '/oarapi/jobs/details.json', 'parse_func': ParseJobsDetails},
934
935         'GET_reserved_nodes':
936         {'uri':
937         '/oarapi/jobs/details.json?state=Running,Waiting,Launching',
938         'owner': '&user=', 'parse_func': ParseReservedNodes},
939
940         'GET_running_jobs':
941         {'uri': '/oarapi/jobs/details.json?state=Running',
942         'parse_func': ParseRunningJobs},
943
944         'GET_resources_full':
945         {'uri': '/oarapi/resources/full.json',
946         'parse_func': ParseResourcesFull},
947
948         'GET_sites':
949         {'uri': '/oarapi/resources/full.json',
950         'parse_func': ParseResourcesFullSites},
951
952         'GET_resources':
953         {'uri': '/oarapi/resources.json', 'parse_func': ParseResources},
954
955         'DELETE_jobs_id':
956         {'uri': '/oarapi/jobs/id.json', 'parse_func': ParseDeleteJobs}}
957
958
959     def SendRequest(self, request, strval=None, username=None):
960         """ Connects to OAR , sends the valid GET requests and uses
961         the appropriate json parsing functions.
962
963         :returns: calls to the appropriate parsing function, associated with the
964             GET request
965         :rtype: depends on the parsing function called.
966
967         .. seealso:: OARrequests_uri_dict
968         """
969         save_json = None
970
971         self.json_page.ResetNextPage()
972         save_json = []
973
974         if request in self.OARrequests_uri_dict:
975             while self.json_page.next_page:
976                 self.json_page.raw_json = self.server.GETRequestToOARRestAPI(
977                     request,
978                     strval,
979                     self.json_page.next_offset,
980                     username)
981                 self.json_page.FindNextPage()
982                 if self.json_page.concatenate:
983                     save_json.append(self.json_page.raw_json)
984
985             if self.json_page.concatenate and self.json_page.end:
986                 self.json_page.raw_json = \
987                     self.json_page.ConcatenateJsonPages(save_json)
988
989             return self.OARrequests_uri_dict[request]['parse_func'](self)
990         else:
991             logger.error("OARRESTAPI OARGetParse __init__ : ERROR_REQUEST "
992                          % (request))