Add Energy recording support
authorBenoit HERARD <benoit.herard@orange.com>
Thu, 27 Apr 2017 09:23:00 +0000 (11:23 +0200)
committerBenoit HERARD <benoit.herard@orange.com>
Tue, 9 May 2017 12:43:02 +0000 (14:43 +0200)
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 <benoit.herard@orange.com>
functest/ci/config_functest.yaml
functest/ci/logging.ini
functest/energy/__init__.py [new file with mode: 0644]
functest/energy/energy.py [new file with mode: 0644]
functest/tests/unit/energy/__init__.py [new file with mode: 0644]
functest/tests/unit/energy/test_functest_energy.py [new file with mode: 0644]

index fd663ab..677c485 100644 (file)
@@ -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: ""
index 8036ed2..210c8f5 100644 (file)
@@ -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 (file)
index 0000000..e69de29
diff --git a/functest/energy/energy.py b/functest/energy/energy.py
new file mode 100644 (file)
index 0000000..a20c799
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/functest/tests/unit/energy/test_functest_energy.py b/functest/tests/unit/energy/test_functest_energy.py
new file mode 100644 (file)
index 0000000..ffe044b
--- /dev/null
@@ -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)