aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenoit HERARD <benoit.herard@orange.com>2017-04-27 11:23:00 +0200
committerBenoit HERARD <benoit.herard@orange.com>2017-05-09 14:43:02 +0200
commit57b3a9e6b836b33201cf4e2630fd228032e657e4 (patch)
tree84eac6e90e1dbf67529e317cbcd4cff43986c2b6
parent1d5e199517ff09d959a1240f3b4e715be799058b (diff)
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 <benoit.herard@orange.com>
-rw-r--r--functest/ci/config_functest.yaml5
-rw-r--r--functest/ci/logging.ini7
-rw-r--r--functest/energy/__init__.py0
-rw-r--r--functest/energy/energy.py203
-rw-r--r--functest/tests/unit/energy/__init__.py0
-rw-r--r--functest/tests/unit/energy/test_functest_energy.py277
6 files changed, 491 insertions, 1 deletions
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
--- /dev/null
+++ b/functest/energy/__init__.py
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
--- /dev/null
+++ b/functest/tests/unit/energy/__init__.py
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)