From 57b3a9e6b836b33201cf4e2630fd228032e657e4 Mon Sep 17 00:00:00 2001 From: Benoit HERARD Date: Thu, 27 Apr 2017 11:23:00 +0200 Subject: Add Energy recording support It adds helpers to send notifications to Energy recording API and related unit tests. It requires a dedicated section in functest config file to set connectivity parameters to Energy recording API. It is using shared API Recording at http://161.105.253.100:8888 Change-Id: Idcb74d1bf7341ccce7cc1c3926f22338ce24f714 Signed-off-by: Benoit HERARD --- functest/ci/config_functest.yaml | 5 + functest/ci/logging.ini | 7 +- functest/energy/__init__.py | 0 functest/energy/energy.py | 203 +++++++++++++++ functest/tests/unit/energy/__init__.py | 0 functest/tests/unit/energy/test_functest_energy.py | 277 +++++++++++++++++++++ 6 files changed, 491 insertions(+), 1 deletion(-) create mode 100644 functest/energy/__init__.py create mode 100644 functest/energy/energy.py create mode 100644 functest/tests/unit/energy/__init__.py create mode 100644 functest/tests/unit/energy/test_functest_energy.py diff --git a/functest/ci/config_functest.yaml b/functest/ci/config_functest.yaml index fd663abca..677c4856e 100644 --- a/functest/ci/config_functest.yaml +++ b/functest/ci/config_functest.yaml @@ -204,3 +204,8 @@ results: # you can also set a file (e.g. /home/opnfv/functest/results/dump.txt) to dump results # test_db_url: file:///home/opnfv/functest/results/dump.txt test_db_url: http://testresults.opnfv.org/test/api/v1/results + +energy_recorder: + api_url: http://161.105.253.100:8888/resources + api_user: "" + api_password: "" diff --git a/functest/ci/logging.ini b/functest/ci/logging.ini index 8036ed292..210c8f5f4 100644 --- a/functest/ci/logging.ini +++ b/functest/ci/logging.ini @@ -1,5 +1,5 @@ [loggers] -keys=root,functest,ci,cli,core,opnfv_tests,utils +keys=root,functest,ci,cli,core,energy,opnfv_tests,utils [handlers] keys=console,wconsole,file,null @@ -31,6 +31,11 @@ level=NOTSET handlers=console qualname=functest.core +[logger_energy] +level=NOTSET +handlers=wconsole +qualname=functest.energy + [logger_opnfv_tests] level=NOTSET handlers=wconsole diff --git a/functest/energy/__init__.py b/functest/energy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/functest/energy/energy.py b/functest/energy/energy.py new file mode 100644 index 000000000..a20c79926 --- /dev/null +++ b/functest/energy/energy.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- + +# Copyright (c) 2017 Orange and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 + +"""This module manages calls to Energy recording API.""" + +import json +import logging +import urllib +import requests + +import functest.utils.functest_utils as ft_utils + + +def enable_recording(method): + """ + Decorator to record energy during "method" exection. + + param method: Method to suround with start and stop + :type method: function + + .. note:: "method" should belong to a class having a "case_name" + attribute + """ + def wrapper(*args): + """Wrapper for decorator to handle method arguments.""" + EnergyRecorder.start(args[0].case_name) + return_value = method(*args) + EnergyRecorder.stop() + return return_value + return wrapper + + +# Class to manage energy recording sessions +class EnergyRecorder(object): + """Manage Energy recording session.""" + + logger = logging.getLogger(__name__) + # Energy recording API connectivity settings + # see load_config method + energy_recorder_api = None + + # Default initial step + INITIAL_STEP = "starting" + + @staticmethod + def load_config(): + """ + Load connectivity settings from yaml. + + Load connectivity settings to Energy recording API + Use functest global config yaml file + (see functest_utils.get_functest_config) + """ + # Singleton pattern for energy_recorder_api static member + # Load only if not previouly done + if EnergyRecorder.energy_recorder_api is None: + environment = ft_utils.get_pod_name() + + # API URL + energy_recorder_uri = ft_utils.get_functest_config( + "energy_recorder.api_url") + assert energy_recorder_uri + assert environment + + energy_recorder_uri += "/recorders/environment/" + energy_recorder_uri += urllib.quote_plus(environment) + EnergyRecorder.logger.debug( + "API recorder at: " + energy_recorder_uri) + + # Creds + user = ft_utils.get_functest_config( + "energy_recorder.api_user") + password = ft_utils.get_functest_config( + "energy_recorder.api_password") + + if user != "" and password != "": + energy_recorder_api_auth = (user, password) + else: + energy_recorder_api_auth = None + + # Final config + EnergyRecorder.energy_recorder_api = { + "uri": energy_recorder_uri, + "auth": energy_recorder_api_auth + } + + @staticmethod + def start(scenario): + """ + Start a recording session for scenario. + + param scenario: Starting scenario + :type scenario: string + """ + return_status = True + try: + EnergyRecorder.logger.debug("Starting recording") + # Ensure that connectyvity settings are loaded + EnergyRecorder.load_config() + + # Create API payload + payload = { + "step": EnergyRecorder.INITIAL_STEP, + "scenario": scenario + } + # Call API to start energy recording + response = requests.post( + EnergyRecorder.energy_recorder_api["uri"], + data=json.dumps(payload), + auth=EnergyRecorder.energy_recorder_api["auth"], + headers={ + 'content-type': 'application/json' + } + ) + if response.status_code != 200: + log_msg = "Error while starting energy recording session\n{}" + log_msg = log_msg.format(response.text) + EnergyRecorder.logger.info(log_msg) + return_status = False + + except Exception: # pylint: disable=broad-except + # Default exception handler to ensure that method + # is safe for caller + EnergyRecorder.logger.exception( + "Error while starting energy recorder API" + ) + return_status = False + return return_status + + @staticmethod + def stop(): + """Stop current recording session.""" + EnergyRecorder.logger.debug("Stopping recording") + return_status = True + try: + # Ensure that connectyvity settings are loaded + EnergyRecorder.load_config() + + # Call API to stop energy recording + response = requests.delete( + EnergyRecorder.energy_recorder_api["uri"], + auth=EnergyRecorder.energy_recorder_api["auth"], + headers={ + 'content-type': 'application/json' + } + ) + if response.status_code != 200: + log_msg = "Error while stating energy recording session\n{}" + log_msg = log_msg.format(response.text) + EnergyRecorder.logger.error(log_msg) + return_status = False + except Exception: # pylint: disable=broad-except + # Default exception handler to ensure that method + # is safe for caller + EnergyRecorder.logger.exception( + "Error while stoping energy recorder API" + ) + return_status = False + return return_status + + @staticmethod + def set_step(step): + """Notify energy recording service of current step of the testcase.""" + EnergyRecorder.logger.debug("Setting step") + return_status = True + try: + # Ensure that connectyvity settings are loaded + EnergyRecorder.load_config() + + # Create API payload + payload = { + "step": step, + } + + # Call API to define step + response = requests.post( + EnergyRecorder.energy_recorder_api["uri"] + "/step", + data=json.dumps(payload), + auth=EnergyRecorder.energy_recorder_api["auth"], + headers={ + 'content-type': 'application/json' + } + ) + if response.status_code != 200: + log_msg = "Error while setting current step of testcase\n{}" + log_msg = log_msg.format(response.text) + EnergyRecorder.logger.error(log_msg) + return_status = False + except Exception: # pylint: disable=broad-except + # Default exception handler to ensure that method + # is safe for caller + EnergyRecorder.logger.exception( + "Error while setting step on energy recorder API" + ) + return_status = False + return return_status diff --git a/functest/tests/unit/energy/__init__.py b/functest/tests/unit/energy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/functest/tests/unit/energy/test_functest_energy.py b/functest/tests/unit/energy/test_functest_energy.py new file mode 100644 index 000000000..ffe044bc2 --- /dev/null +++ b/functest/tests/unit/energy/test_functest_energy.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python + +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 + +"""Unitary test for energy module.""" +# pylint: disable=unused-argument +import logging +import unittest + +import mock + +from functest.energy.energy import EnergyRecorder +import functest.energy.energy as energy + + +CASE_NAME = "UNIT_test_CASE" +STEP_NAME = "UNIT_test_STEP" + +logging.disable(logging.CRITICAL) + + +class MockHttpResponse(object): # pylint: disable=too-few-public-methods + """Mock response for Energy recorder API.""" + + def __init__(self, text, status_code): + """Create an instance of MockHttpResponse.""" + self.text = text + self.status_code = status_code + + +RECORDER_OK = MockHttpResponse( + '{"environment": "UNIT_TEST",' + ' "step": "string",' + ' "scenario": "' + CASE_NAME + '"}', + 200 +) +RECORDER_KO = MockHttpResponse( + '{"message": "An unhandled API exception occurred (MOCK)"}', + 500 +) + + +def config_loader_mock(config_key): + """Return mocked config values.""" + if config_key == "energy_recorder.api_url": + return "http://pod-uri:8888" + elif config_key == "energy_recorder.api_user": + return "user" + elif config_key == "energy_recorder.api_password": + return "password" + else: + raise Exception("Config not mocked") + + +def config_loader_mock_no_creds(config_key): + """Return mocked config values.""" + if config_key == "energy_recorder.api_url": + return "http://pod-uri:8888" + elif config_key == "energy_recorder.api_user": + return "" + elif config_key == "energy_recorder.api_password": + return "" + else: + raise Exception("Config not mocked:" + config_key) + + +class EnergyRecorderTest(unittest.TestCase): + """Energy module unitary test suite.""" + + case_name = CASE_NAME + request_headers = {'content-type': 'application/json'} + returned_value_to_preserve = "value" + exception_message_to_preserve = "exception_message" + + @mock.patch('functest.energy.energy.requests.post', + return_value=RECORDER_OK) + def test_start(self, post_mock=None): + """EnergyRecorder.start method (regular case).""" + self.test_load_config() + self.assertTrue(EnergyRecorder.start(self.case_name)) + post_mock.assert_called_once_with( + EnergyRecorder.energy_recorder_api["uri"], + auth=EnergyRecorder.energy_recorder_api["auth"], + data=mock.ANY, + headers=self.request_headers + ) + + @mock.patch('functest.energy.energy.requests.post', + side_effect=Exception("Internal execution error (MOCK)")) + def test_start_error(self, post_mock=None): + """EnergyRecorder.start method (error in method).""" + self.test_load_config() + self.assertFalse(EnergyRecorder.start(self.case_name)) + post_mock.assert_called_once_with( + EnergyRecorder.energy_recorder_api["uri"], + auth=EnergyRecorder.energy_recorder_api["auth"], + data=mock.ANY, + headers=self.request_headers + ) + + @mock.patch('functest.energy.energy.requests.post', + return_value=RECORDER_KO) + def test_start_api_error(self, post_mock=None): + """EnergyRecorder.start method (API error).""" + self.test_load_config() + self.assertFalse(EnergyRecorder.start(self.case_name)) + post_mock.assert_called_once_with( + EnergyRecorder.energy_recorder_api["uri"], + auth=EnergyRecorder.energy_recorder_api["auth"], + data=mock.ANY, + headers=self.request_headers + ) + + @mock.patch('functest.energy.energy.requests.post', + return_value=RECORDER_OK) + def test_set_step(self, post_mock=None): + """EnergyRecorder.set_step method (regular case).""" + self.test_load_config() + self.assertTrue(EnergyRecorder.set_step(STEP_NAME)) + post_mock.assert_called_once_with( + EnergyRecorder.energy_recorder_api["uri"] + "/step", + auth=EnergyRecorder.energy_recorder_api["auth"], + data=mock.ANY, + headers=self.request_headers + ) + + @mock.patch('functest.energy.energy.requests.post', + return_value=RECORDER_KO) + def test_set_step_api_error(self, post_mock=None): + """EnergyRecorder.set_step method (API error).""" + self.test_load_config() + self.assertFalse(EnergyRecorder.set_step(STEP_NAME)) + post_mock.assert_called_once_with( + EnergyRecorder.energy_recorder_api["uri"] + "/step", + auth=EnergyRecorder.energy_recorder_api["auth"], + data=mock.ANY, + headers=self.request_headers + ) + + @mock.patch('functest.energy.energy.requests.post', + side_effect=Exception("Internal execution error (MOCK)")) + def test_set_step_error(self, post_mock=None): + """EnergyRecorder.set_step method (method error).""" + self.test_load_config() + self.assertFalse(EnergyRecorder.set_step(STEP_NAME)) + post_mock.assert_called_once_with( + EnergyRecorder.energy_recorder_api["uri"] + "/step", + auth=EnergyRecorder.energy_recorder_api["auth"], + data=mock.ANY, + headers=self.request_headers + ) + + @mock.patch('functest.energy.energy.requests.delete', + return_value=RECORDER_OK) + def test_stop(self, delete_mock=None): + """EnergyRecorder.stop method (regular case).""" + self.test_load_config() + self.assertTrue(EnergyRecorder.stop()) + delete_mock.assert_called_once_with( + EnergyRecorder.energy_recorder_api["uri"], + auth=EnergyRecorder.energy_recorder_api["auth"], + headers=self.request_headers + ) + + @mock.patch('functest.energy.energy.requests.delete', + return_value=RECORDER_KO) + def test_stop_api_error(self, delete_mock=None): + """EnergyRecorder.stop method (API Error).""" + self.test_load_config() + self.assertFalse(EnergyRecorder.stop()) + delete_mock.assert_called_once_with( + EnergyRecorder.energy_recorder_api["uri"], + auth=EnergyRecorder.energy_recorder_api["auth"], + headers=self.request_headers + ) + + @mock.patch('functest.energy.energy.requests.delete', + side_effect=Exception("Internal execution error (MOCK)")) + def test_stop_error(self, delete_mock=None): + """EnergyRecorder.stop method (method error).""" + self.test_load_config() + self.assertFalse(EnergyRecorder.stop()) + delete_mock.assert_called_once_with( + EnergyRecorder.energy_recorder_api["uri"], + auth=EnergyRecorder.energy_recorder_api["auth"], + headers=self.request_headers + ) + + @energy.enable_recording + def __decorated_method(self): + """Call with to energy recorder decorators.""" + return self.returned_value_to_preserve + + @energy.enable_recording + def __decorated_method_with_ex(self): + """Call with to energy recorder decorators.""" + raise Exception(self.exception_message_to_preserve) + + @mock.patch("functest.energy.energy.EnergyRecorder") + @mock.patch("functest.utils.functest_utils.get_pod_name", + return_value="MOCK_POD") + @mock.patch("functest.utils.functest_utils.get_functest_config", + side_effect=config_loader_mock) + def test_decorators(self, + loader_mock=None, + pod_mock=None, + recorder_mock=None): + """Test energy module decorators.""" + self.__decorated_method() + calls = [mock.call.start(self.case_name), + mock.call.stop()] + recorder_mock.assert_has_calls(calls) + + def test_decorator_preserve_return(self): + """Test that decorator preserve method returned value.""" + self.test_load_config() + self.assertTrue( + self.__decorated_method() == self.returned_value_to_preserve + ) + + def test_decorator_preserve_ex(self): + """Test that decorator preserve method exceptions.""" + self.test_load_config() + with self.assertRaises(Exception) as context: + self.__decorated_method_with_ex() + self.assertTrue( + self.exception_message_to_preserve in context.exception + ) + + @mock.patch("functest.utils.functest_utils.get_functest_config", + side_effect=config_loader_mock) + @mock.patch("functest.utils.functest_utils.get_pod_name", + return_value="MOCK_POD") + def test_load_config(self, loader_mock=None, pod_mock=None): + """Test load config.""" + EnergyRecorder.energy_recorder_api = None + EnergyRecorder.load_config() + self.assertEquals( + EnergyRecorder.energy_recorder_api["auth"], + ("user", "password") + ) + self.assertEquals( + EnergyRecorder.energy_recorder_api["uri"], + "http://pod-uri:8888/recorders/environment/MOCK_POD" + ) + + @mock.patch("functest.utils.functest_utils.get_functest_config", + side_effect=config_loader_mock_no_creds) + @mock.patch("functest.utils.functest_utils.get_pod_name", + return_value="MOCK_POD") + def test_load_config_no_creds(self, loader_mock=None, pod_mock=None): + """Test load config without creds.""" + EnergyRecorder.energy_recorder_api = None + EnergyRecorder.load_config() + self.assertEquals(EnergyRecorder.energy_recorder_api["auth"], None) + self.assertEquals( + EnergyRecorder.energy_recorder_api["uri"], + "http://pod-uri:8888/recorders/environment/MOCK_POD" + ) + + @mock.patch("functest.utils.functest_utils.get_functest_config", + return_value=None) + @mock.patch("functest.utils.functest_utils.get_pod_name", + return_value="MOCK_POD") + def test_load_config_ex(self, loader_mock=None, pod_mock=None): + """Test load config with exception.""" + with self.assertRaises(AssertionError): + EnergyRecorder.energy_recorder_api = None + EnergyRecorder.load_config() + self.assertEquals(EnergyRecorder.energy_recorder_api, None) + + +if __name__ == "__main__": + unittest.main(verbosity=2) -- cgit 1.2.3-korg