Improved error handling when Energy recorder API is unavailable.
[functest.git] / functest / energy / energy.py
1 #!/usr/bin/env python
2 # -*- coding: UTF-8 -*-
3
4 # Copyright (c) 2017 Orange and others.
5 #
6 # All rights reserved. This program and the accompanying materials
7 # are made available under the terms of the Apache License, Version 2.0
8 # which accompanies this distribution, and is available at
9 # http://www.apache.org/licenses/LICENSE-2.0
10
11 """This module manages calls to Energy recording API."""
12
13 import json
14 import logging
15 import urllib
16
17 from functools import wraps
18 import requests
19
20 import functest.utils.functest_utils as ft_utils
21
22
23 def finish_session(current_scenario):
24     """Finish a recording session."""
25     if current_scenario is None:
26         EnergyRecorder.stop()
27     else:
28         EnergyRecorder.submit_scenario(
29             current_scenario["scenario"],
30             current_scenario["step"]
31         )
32
33
34 def enable_recording(method):
35     """
36     Record energy during method execution.
37
38     Decorator to record energy during "method" exection.
39
40         param method: Method to suround with start and stop
41         :type method: function
42
43         .. note:: "method" should belong to a class having a "case_name"
44                   attribute
45     """
46     @wraps(method)
47     def wrapper(*args):
48         """
49         Record energy during method execution (implementation).
50
51         Wrapper for decorator to handle method arguments.
52         """
53         current_scenario = EnergyRecorder.get_current_scenario()
54         EnergyRecorder.start(args[0].case_name)
55         try:
56             return_value = method(*args)
57             finish_session(current_scenario)
58         except Exception as exc:  # pylint: disable=broad-except
59             EnergyRecorder.logger.exception(exc)
60             finish_session(current_scenario)
61             raise exc
62         return return_value
63     return wrapper
64
65
66 # Class to manage energy recording sessions
67 class EnergyRecorder(object):
68     """Manage Energy recording session."""
69
70     logger = logging.getLogger(__name__)
71     # Energy recording API connectivity settings
72     # see load_config method
73     energy_recorder_api = None
74
75     # Default initial step
76     INITIAL_STEP = "running"
77
78     # Default connection timeout
79     CONNECTION_TIMOUT = 1
80
81     @staticmethod
82     def load_config():
83         """
84         Load connectivity settings from yaml.
85
86         Load connectivity settings to Energy recording API
87         Use functest global config yaml file
88         (see functest_utils.get_functest_config)
89         """
90         # Singleton pattern for energy_recorder_api static member
91         # Load only if not previouly done
92         if EnergyRecorder.energy_recorder_api is None:
93             environment = ft_utils.get_pod_name()
94
95             # API URL
96             energy_recorder_uri = ft_utils.get_functest_config(
97                 "energy_recorder.api_url")
98             assert energy_recorder_uri
99             assert environment
100
101             uri_comp = "/recorders/environment/"
102             uri_comp += urllib.quote_plus(environment)
103             EnergyRecorder.logger.debug(
104                 "API recorder at: " + energy_recorder_uri + uri_comp)
105
106             # Creds
107             user = ft_utils.get_functest_config(
108                 "energy_recorder.api_user")
109             password = ft_utils.get_functest_config(
110                 "energy_recorder.api_password")
111
112             if user != "" and password != "":
113                 energy_recorder_api_auth = (user, password)
114             else:
115                 energy_recorder_api_auth = None
116
117             try:
118                 resp = requests.get(energy_recorder_uri + "/monitoring/ping",
119                                     auth=energy_recorder_api_auth,
120                                     headers={
121                                         'content-type': 'application/json'
122                                     },
123                                     timeout=EnergyRecorder.CONNECTION_TIMOUT)
124                 api_available = json.loads(resp.text)["status"] == "OK"
125             except Exception:  # pylint: disable=broad-except
126                 EnergyRecorder.logger.error(
127                     "Energy recorder API is not available")
128                 api_available = False
129             # Final config
130             EnergyRecorder.energy_recorder_api = {
131                 "uri": energy_recorder_uri + uri_comp,
132                 "auth": energy_recorder_api_auth,
133                 "available": api_available
134             }
135         return EnergyRecorder.energy_recorder_api["available"]
136
137     @staticmethod
138     def submit_scenario(scenario, step):
139         """
140         Submit a complet scenario definition to Energy recorder API.
141
142             param scenario: Scenario name
143             :type scenario: string
144             param step: Step name
145             :type step: string
146         """
147         try:
148             return_status = True
149             # Ensure that connectyvity settings are loaded
150             if EnergyRecorder.load_config():
151                 EnergyRecorder.logger.debug("Submitting scenario")
152
153                 # Create API payload
154                 payload = {
155                     "step": step,
156                     "scenario": scenario
157                 }
158                 # Call API to start energy recording
159                 response = requests.post(
160                     EnergyRecorder.energy_recorder_api["uri"],
161                     data=json.dumps(payload),
162                     auth=EnergyRecorder.energy_recorder_api["auth"],
163                     headers={
164                         'content-type': 'application/json'
165                     },
166                     timeout=EnergyRecorder.CONNECTION_TIMOUT
167                 )
168                 if response.status_code != 200:
169                     EnergyRecorder.logger.error(
170                         "Error while submitting scenario\n%s",
171                         response.text)
172                     return_status = False
173         except requests.exceptions.ConnectionError:
174             EnergyRecorder.logger.warning(
175                 "submit_scenario: Unable to connect energy recorder API")
176             return_status = False
177         except Exception:  # pylint: disable=broad-except
178             # Default exception handler to ensure that method
179             # is safe for caller
180             EnergyRecorder.logger.exception(
181                 "Error while submitting scenarion to energy recorder API"
182             )
183             return_status = False
184         return return_status
185
186     @staticmethod
187     def start(scenario):
188         """
189         Start a recording session for scenario.
190
191             param scenario: Starting scenario
192             :type scenario: string
193         """
194         return_status = True
195         try:
196             if EnergyRecorder.load_config():
197                 EnergyRecorder.logger.debug("Starting recording")
198                 return_status = EnergyRecorder.submit_scenario(
199                     scenario,
200                     EnergyRecorder.INITIAL_STEP
201                 )
202
203         except Exception:  # pylint: disable=broad-except
204             # Default exception handler to ensure that method
205             # is safe for caller
206             EnergyRecorder.logger.exception(
207                 "Error while starting energy recorder API"
208             )
209             return_status = False
210         return return_status
211
212     @staticmethod
213     def stop():
214         """Stop current recording session."""
215         return_status = True
216         try:
217             # Ensure that connectyvity settings are loaded
218             if EnergyRecorder.load_config():
219                 EnergyRecorder.logger.debug("Stopping recording")
220
221                 # Call API to stop energy recording
222                 response = requests.delete(
223                     EnergyRecorder.energy_recorder_api["uri"],
224                     auth=EnergyRecorder.energy_recorder_api["auth"],
225                     headers={
226                         'content-type': 'application/json'
227                     },
228                     timeout=EnergyRecorder.CONNECTION_TIMOUT
229                 )
230                 if response.status_code != 200:
231                     EnergyRecorder.logger.error(
232                         "Error while stating energy recording session\n%s",
233                         response.text)
234                     return_status = False
235         except requests.exceptions.ConnectionError:
236             EnergyRecorder.logger.warning(
237                 "stop: Unable to connect energy recorder API")
238             return_status = False
239         except Exception:  # pylint: disable=broad-except
240             # Default exception handler to ensure that method
241             # is safe for caller
242             EnergyRecorder.logger.exception(
243                 "Error while stoping energy recorder API"
244             )
245             return_status = False
246         return return_status
247
248     @staticmethod
249     def set_step(step):
250         """Notify energy recording service of current step of the testcase."""
251         return_status = True
252         try:
253             # Ensure that connectyvity settings are loaded
254             if EnergyRecorder.load_config():
255                 EnergyRecorder.logger.debug("Setting step")
256
257                 # Create API payload
258                 payload = {
259                     "step": step,
260                 }
261
262                 # Call API to define step
263                 response = requests.post(
264                     EnergyRecorder.energy_recorder_api["uri"] + "/step",
265                     data=json.dumps(payload),
266                     auth=EnergyRecorder.energy_recorder_api["auth"],
267                     headers={
268                         'content-type': 'application/json'
269                     },
270                     timeout=EnergyRecorder.CONNECTION_TIMOUT
271                 )
272                 if response.status_code != 200:
273                     EnergyRecorder.logger.error(
274                         "Error while setting current step of testcase\n%s",
275                         response.text)
276                     return_status = False
277         except requests.exceptions.ConnectionError:
278             EnergyRecorder.logger.warning(
279                 "set_step: Unable to connect energy recorder API")
280             return_status = False
281         except Exception:  # pylint: disable=broad-except
282             # Default exception handler to ensure that method
283             # is safe for caller
284             EnergyRecorder.logger.exception(
285                 "Error while setting step on energy recorder API"
286             )
287             return_status = False
288         return return_status
289
290     @staticmethod
291     def get_current_scenario():
292         """Get current running scenario (if any, None else)."""
293         return_value = None
294         try:
295             # Ensure that connectyvity settings are loaded
296             if EnergyRecorder.load_config():
297                 EnergyRecorder.logger.debug("Getting current scenario")
298
299                 # Call API get running scenario
300                 response = requests.get(
301                     EnergyRecorder.energy_recorder_api["uri"],
302                     auth=EnergyRecorder.energy_recorder_api["auth"],
303                     timeout=EnergyRecorder.CONNECTION_TIMOUT
304                 )
305                 if response.status_code == 200:
306                     return_value = json.loads(response.text)
307                 elif response.status_code == 404:
308                     EnergyRecorder.logger.info(
309                         "No current running scenario at %s",
310                         EnergyRecorder.energy_recorder_api["uri"])
311                     return_value = None
312                 else:
313                     EnergyRecorder.logger.error(
314                         "Error while getting current scenario\n%s",
315                         response.text)
316                     return_value = None
317         except requests.exceptions.ConnectionError:
318             EnergyRecorder.logger.warning(
319                 "get_currernt_sceario: Unable to connect energy recorder API")
320             return_value = None
321         except Exception:  # pylint: disable=broad-except
322             # Default exception handler to ensure that method
323             # is safe for caller
324             EnergyRecorder.logger.exception(
325                 "Error while getting current scenario from energy recorder API"
326             )
327             return_value = None
328         return return_value