diff --git a/.travis.yml b/.travis.yml index beb708d..7f531db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: - '2.7' +- '3.6' before_install: - pip install -r test_requirements.txt -i https://testpypi.python.org/pypi @@ -18,4 +19,4 @@ script: - pip install cloudshell-core -f dist after_success: - - coveralls \ No newline at end of file + - coveralls diff --git a/README.md b/README.md index 0731bdf..b43da2e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Coverage Status](https://coveralls.io/repos/github/QualiSystems/cloudshell-core/badge.svg?branch=dev)](https://coveralls.io/github/QualiSystems/cloudshell-core?branch=dev) [![PyPI version](https://badge.fury.io/py/cloudshell-core.svg)](https://badge.fury.io/py/cloudshell-core) [![Dependency Status](https://dependencyci.com/github/QualiSystems/cloudshell-core/badge)](https://dependencyci.com/github/QualiSystems/cloudshell-core) -[![Stories in Ready](https://badge.waffle.io/QualiSystems/cloudshell-core.svg?label=ready&title=Ready)](http://waffle.io/QualiSystems/cloudshell-core)

diff --git a/cloudshell/core/logger/qs_config.ini b/cloudshell/core/logger/qs_config.ini index 26135aa..0366ec8 100644 --- a/cloudshell/core/logger/qs_config.ini +++ b/cloudshell/core/logger/qs_config.ini @@ -1,6 +1,7 @@ [Logging] -LOG_LEVEL='DEBUG' +LOG_LEVEL='INFO' LOG_FORMAT= '%(asctime)s [%(levelname)s]: %(name)s %(module)s - %(funcName)-20s %(message)s' TIME_FORMAT= '%d-%b-%Y--%H-%M-%S' -LOG_PATH='../../Logs' -#LOG_PATH = 'x:/Logs/' +WINDOWS_LOG_PATH='{ALLUSERSPROFILE}\QualiSystems\logs' +UNIX_LOG_PATH='/var/log/qualisystems' +DEFAULT_LOG_PATH='../../Logs' diff --git a/cloudshell/core/logger/qs_config_parser.py b/cloudshell/core/logger/qs_config_parser.py index d9b21a1..9c520d6 100644 --- a/cloudshell/core/logger/qs_config_parser.py +++ b/cloudshell/core/logger/qs_config_parser.py @@ -1,11 +1,16 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import ConfigParser import os +import sys DEFAULT_CONFIG_PATH = 'qs_config.ini' +if (sys.version_info >= (3,0)): + import configparser as ConfigParser +else: + import ConfigParser + class QSConfigParser: _configDict = None diff --git a/cloudshell/core/logger/qs_logger.py b/cloudshell/core/logger/qs_logger.py index c9bf424..e3b6ad6 100644 --- a/cloudshell/core/logger/qs_logger.py +++ b/cloudshell/core/logger/qs_logger.py @@ -24,9 +24,10 @@ # default settings DEFAULT_FORMAT = '%(asctime)s [%(levelname)s]: %(name)s %(module)s - %(funcName)-20s %(message)s' DEFAULT_TIME_FORMAT = '%Y%m%d%H%M%S' -DEFAULT_LEVEL = 'DEBUG' +DEFAULT_LEVEL = 'INFO' # DEFAULT_LOG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../', 'Logs') LOG_SECTION = 'Logging' +WINDOWS_OS_FAMILY = "nt" _LOGGER_CONTAINER = {} _LOGGER_LOCK = threading.Lock() @@ -47,9 +48,14 @@ def get_settings(): log_format = QSConfigParser.get_setting(LOG_SECTION, 'LOG_FORMAT') or DEFAULT_FORMAT config['FORMAT'] = log_format - # log_path - log_path = QSConfigParser.get_setting(LOG_SECTION, 'LOG_PATH') - config['LOG_PATH'] = log_path + # UNIX log path + config['UNIX_LOG_PATH'] = QSConfigParser.get_setting(LOG_SECTION, 'UNIX_LOG_PATH') + + # Windows log path + config['WINDOWS_LOG_PATH'] = QSConfigParser.get_setting(LOG_SECTION, 'WINDOWS_LOG_PATH') + + # Default log path for all systems + config['DEFAULT_LOG_PATH'] = QSConfigParser.get_setting(LOG_SECTION, 'DEFAULT_LOG_PATH') # Time format time_format = QSConfigParser.get_setting(LOG_SECTION, 'TIME_FORMAT') or DEFAULT_TIME_FORMAT @@ -58,47 +64,79 @@ def get_settings(): return config -# return accessable log path or None -def get_accessible_log_path(reservation_id='Autoload', handler='default'): - """Generate log path for the logger and verify that it's accessible using LOG_PATH/reservation_id/handler-%timestamp%.log +def _get_log_path_config(config): + """Get log path based on the environment variable or Windows/Unix config setting - :param reservation_id: part of log path - :param handler: handler name for logger - :return: generated log path + :param dict[str] config: + :rtype: str """ - - accessible_log_path = None - config = get_settings() - if 'LOG_PATH' in os.environ: - log_path = os.environ['LOG_PATH'] - elif 'LOG_PATH' in config and config['LOG_PATH']: - log_path = config['LOG_PATH'] + return os.environ['LOG_PATH'] + + if os.name == WINDOWS_OS_FAMILY: + tpl = config.get('WINDOWS_LOG_PATH') + if tpl: + try: + return tpl.format(**os.environ) + except KeyError: + print ("Environment variable is not defined in the template {}".format(tpl)) else: - return None + return config.get('UNIX_LOG_PATH') - if log_path.startswith('..'): - log_path = os.path.join(os.path.dirname(__file__), log_path) - time_format = config['TIME_FORMAT'] or DEFAULT_TIME_FORMAT +def _prepare_log_path(log_path, log_file_name): + """Create logs directory if needed and return full path to the log file - log_file_name = '{0}--{1}.log'.format(handler, datetime.now().strftime(time_format)) - log_path = os.path.join(log_path, reservation_id) + :param str log_path: + :param str log_file_name: + :rtype: str + """ + if log_path.startswith('..'): + log_path = os.path.join(os.path.dirname(__file__), log_path) log_file = os.path.join(log_path, log_file_name) # print(log_file) if os.path.isdir(log_path): if os.access(log_path, os.W_OK): - accessible_log_path = log_file + return log_file else: try: os.makedirs(log_path) - accessible_log_path = log_file + return log_file except: pass - return accessible_log_path + +# return accessable log path or None +def get_accessible_log_path(reservation_id='Autoload', handler='default'): + """Generate log path for the logger and verify that it's accessible using LOG_PATH/reservation_id/handler-%timestamp%.log + + :param reservation_id: part of log path + :param handler: handler name for logger + :return: generated log path + """ + config = get_settings() + time_format = config['TIME_FORMAT'] or DEFAULT_TIME_FORMAT + log_file_name = '{0}--{1}.log'.format(handler, datetime.now().strftime(time_format)) + + log_path = _get_log_path_config(config) + + if log_path: + env_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "..", "..") + shell_name = os.path.basename(os.path.abspath(env_folder)) + log_path = os.path.join(log_path, reservation_id, shell_name) + path = _prepare_log_path(log_path=log_path, + log_file_name=log_file_name) + if path: + return path + + default_log_path = config.get('DEFAULT_LOG_PATH') + + if default_log_path: + default_log_path = os.path.join(default_log_path, reservation_id) + return _prepare_log_path(log_path=default_log_path, + log_file_name=log_file_name) def log_execution_info(logger_hdlr, exec_info): @@ -113,7 +151,7 @@ def log_execution_info(logger_hdlr, exec_info): logger_hdlr.info('-----------------------------------------------------------\n') -def get_qs_logger(log_group='Ungrouped', log_category ='QS', log_file_prefix='QS'): +def get_qs_logger(log_group='Ungrouped', log_category='QS', log_file_prefix='QS'): """Create cloudshell specific singleton logger :param log_group: This folder will be grouped under this name. The default implementation of the group is a folder @@ -241,7 +279,7 @@ def normalize_buffer(input_buffer): result_buffer = '' - if not isinstance(input_buffer, basestring): + if not isinstance(input_buffer, str): input_buffer = str(input_buffer) match_iter = color_pattern.finditer(input_buffer) @@ -282,10 +320,10 @@ def format(self, record): s = logging.Formatter.format(self, record) header, footer = s.rsplit(record.message, self.MAX_SPLIT) s = s.replace('\n', '\n' + header) - except Exception, e: - print traceback.format_exc() - print 'logger.format: Unexpected error: ' + str(e) - print 'record = {}<<<'.format(traceback.format_exc()) + except Exception as e: + print (traceback.format_exc()) + print ('logger.format: Unexpected error: ' + str(e)) + print ('record = {}<<<'.format(traceback.format_exc())) return s diff --git a/setup.py b/setup.py index e161cd0..216b680 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +import sys + from setuptools import setup, find_packages import os @@ -27,6 +29,7 @@ classifiers=[ "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.0", "Topic :: Software Development :: Libraries", "License :: OSI Approved :: Apache Software License", ] diff --git a/test_requirements.txt b/test_requirements.txt index e69de29..db10e58 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -0,0 +1 @@ +mock; python_version <= '2.7' \ No newline at end of file diff --git a/tests/core/logger/config/test_qs_config.ini b/tests/core/logger/config/test_qs_config.ini index f2a0b76..0366ec8 100644 --- a/tests/core/logger/config/test_qs_config.ini +++ b/tests/core/logger/config/test_qs_config.ini @@ -2,4 +2,6 @@ LOG_LEVEL='INFO' LOG_FORMAT= '%(asctime)s [%(levelname)s]: %(name)s %(module)s - %(funcName)-20s %(message)s' TIME_FORMAT= '%d-%b-%Y--%H-%M-%S' -LOG_PATH='../../Logs' +WINDOWS_LOG_PATH='{ALLUSERSPROFILE}\QualiSystems\logs' +UNIX_LOG_PATH='/var/log/qualisystems' +DEFAULT_LOG_PATH='../../Logs' diff --git a/tests/core/logger/test_qs_config_parser.py b/tests/core/logger/test_qs_config_parser.py index b6d3d04..965c097 100644 --- a/tests/core/logger/test_qs_config_parser.py +++ b/tests/core/logger/test_qs_config_parser.py @@ -14,7 +14,9 @@ class TestQSConfigParser(TestCase): exp_response = {'Logging': {'time_format': '%d-%b-%Y--%H-%M-%S', - 'log_path': '../../Logs', + 'windows_log_path': r'{ALLUSERSPROFILE}\QualiSystems\logs', + 'unix_log_path': '/var/log/qualisystems', + 'default_log_path': '../../Logs', 'log_format': '%(asctime)s [%(levelname)s]: %(name)s %(module)s - %(funcName)-20s %(message)s', 'log_level': 'INFO'}} diff --git a/tests/core/logger/test_qs_logger.py b/tests/core/logger/test_qs_logger.py index 669fc61..349cf26 100644 --- a/tests/core/logger/test_qs_logger.py +++ b/tests/core/logger/test_qs_logger.py @@ -8,9 +8,16 @@ import os import re import shutil +import sys + +if (sys.version_info >= (3,0)): + from unittest.mock import MagicMock + from unittest import TestCase, mock +else: + from mock import MagicMock + import mock + from unittest import TestCase -from mock import MagicMock -from unittest import TestCase from cloudshell.core.logger import qs_logger from cloudshell.core.logger.interprocess_logger import MultiProcessingLog @@ -66,18 +73,53 @@ def tearDown(self): def test_get_settings(self): """ Test suite for get_settings method """ - exp_response = {'LOG_PATH': '../../Logs', + exp_response = {'WINDOWS_LOG_PATH': r'{ALLUSERSPROFILE}\QualiSystems\logs', + 'UNIX_LOG_PATH': '/var/log/qualisystems', + 'DEFAULT_LOG_PATH': '../../Logs', 'TIME_FORMAT': '%d-%b-%Y--%H-%M-%S', 'LOG_LEVEL': 'INFO', 'FORMAT': '%(asctime)s [%(levelname)s]: %(name)s %(module)s - %(funcName)-20s %(message)s'} self.assertEqual(qs_logger.get_settings(), exp_response) + @mock.patch("cloudshell.core.logger.qs_logger.os") + def test_get_log_path_config_from_environment_variable(self, os): + """Check that method will primarily return log path from the environment variable if such exists""" + config = {} + expected_path = MagicMock() + os.environ = {'LOG_PATH': expected_path} + # act + result = qs_logger._get_log_path_config(config=config) + # verify + self.assertEqual(result, expected_path) + + @mock.patch("cloudshell.core.logger.qs_logger.os") + def test_get_log_path_config_for_windows_os(self, os): + """Check that method will return windows log path setting with substituted environment variables""" + os.name = qs_logger.WINDOWS_OS_FAMILY + os.environ = {"SOME_EN_VARIABLE": "C:\\some_path"} + expected_path = "{SOME_EN_VARIABLE}\\qualisystems\\logs\\commands" + config = {"WINDOWS_LOG_PATH": expected_path} + # act + result = qs_logger._get_log_path_config(config=config) + # verify + self.assertEqual(result, "C:\\some_path\\qualisystems\\logs\\commands") + + @mock.patch("cloudshell.core.logger.qs_logger.os") + def test_get_log_path_config_for_unix_os(self, os): + """Check that method will return unix log path setting for posix OS""" + os.name = "posix" + expected_path = MagicMock() + config = {"UNIX_LOG_PATH": expected_path} + # act + result = qs_logger._get_log_path_config(config=config) + # verify + self.assertEqual(result, expected_path) + def test_get_accessible_log_path_default_params(self): """ Test suite for get_accessible_log_path method """ - path = qs_logger.get_accessible_log_path() - self.assertTrue(re.search(r"Logs[\\/]Autoload[\\/]default--\d{2}-\w+-\d{4}--\d{2}-\d{2}-\d{2}\.log", path)) + self.assertRegexpMatches(path, r"Logs[\\/]Autoload[\\/](.*[\\/])?default--\d{2}-\w+-\d{4}--\d{2}-\d{2}-\d{2}\.log") self.assertTrue(os.path.dirname(path)) def test_get_accessible_log_path_path_creation(self): @@ -88,9 +130,9 @@ def test_get_accessible_log_path_path_creation(self): def test_get_accessible_log_path(self): """ Test suite for get_accessible_log_path method """ - - path = qs_logger.get_accessible_log_path(qs_logger.get_accessible_log_path("reservation_id", "handler_name")) - self.assertTrue(re.search(r"Logs[\\/]reservation_id[\\/]handler_name--\d{2}-\w+-\d{4}--\d{2}-\d{2}-\d{2}\.log", path)) + path = qs_logger.get_accessible_log_path("reservation_id", "handler_name") + self.assertRegexpMatches(path, r"Logs[\\/]reservation_id[\\/](.*[\\/])?" + r"handler_name--\d{2}-\w+-\d{4}--\d{2}-\d{2}-\d{2}\.log") def test_get_accessible_log_path_log_path_setting_missing(self): """ Test suite for get_accessible_log_path method """ diff --git a/version.txt b/version.txt index 7f20734..66a6a14 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.0.1 \ No newline at end of file +3.0.001 \ No newline at end of file