summaryrefslogtreecommitdiff
path: root/openstackclient
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient')
-rw-r--r--openstackclient/common/context.py149
-rw-r--r--openstackclient/shell.py55
-rw-r--r--openstackclient/tests/common/test_context.py102
-rw-r--r--openstackclient/tests/test_shell.py96
4 files changed, 387 insertions, 15 deletions
diff --git a/openstackclient/common/context.py b/openstackclient/common/context.py
new file mode 100644
index 00000000..b7b16e94
--- /dev/null
+++ b/openstackclient/common/context.py
@@ -0,0 +1,149 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+"""Context and Formatter"""
+
+import logging
+
+_LOG_MESSAGE_FORMAT = ('%(asctime)s.%(msecs)03d %(process)d '
+ '%(levelname)s %(name)s [%(clouds_name)s '
+ '%(username)s %(project_name)s] %(message)s')
+_LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
+
+
+def setup_handler_logging_level(handler_type, level):
+ """Setup of the handler for set the logging level
+
+ :param handler_type: type of logging handler
+ :param level: logging level
+ :return: None
+ """
+ # Set the handler logging level of FileHandler(--log-file)
+ # and StreamHandler
+ for h in logging.getLogger('').handlers:
+ if type(h) is handler_type:
+ h.setLevel(level)
+
+
+def setup_logging(shell, cloud_config):
+ """Get one cloud configuration from configuration file and setup logging
+
+ :param shell: instance of openstackclient shell
+ :param cloud_config:
+ instance of the cloud specified by --os-cloud
+ in the configuration file
+ :return: None
+ """
+
+ log_level = logging.WARNING
+ log_file = cloud_config.config.get('log_file', None)
+ if log_file:
+ # setup the logging level
+ get_log_level = cloud_config.config.get('log_level')
+ if get_log_level:
+ log_level = {
+ 'error': logging.ERROR,
+ 'info': logging.INFO,
+ 'debug': logging.DEBUG,
+ }.get(get_log_level, logging.WARNING)
+
+ # setup the logging context
+ log_cont = _LogContext(
+ clouds_name=cloud_config.config.get('cloud'),
+ project_name=cloud_config.auth.get('project_name'),
+ username=cloud_config.auth.get('username'),
+ )
+ # setup the logging handler
+ log_handler = _setup_handler_for_logging(
+ logging.FileHandler,
+ log_level,
+ file_name=log_file,
+ context=log_cont,
+ )
+ if log_level == logging.DEBUG:
+ # DEBUG only.
+ # setup the operation_log
+ shell.enable_operation_logging = True
+ shell.operation_log.setLevel(logging.DEBUG)
+ shell.operation_log.addHandler(log_handler)
+
+
+def _setup_handler_for_logging(handler_type, level, file_name, context):
+ """Setup of the handler
+
+ Setup of the handler for addition of the logging handler,
+ changes of the logging format, change of the logging level,
+
+ :param handler_type: type of logging handler
+ :param level: logging level
+ :param file_name: name of log-file
+ :param context: instance of _LogContext()
+ :return: logging handler
+ """
+
+ root_logger = logging.getLogger('')
+ handler = None
+ # Setup handler for FileHandler(--os-cloud)
+ handler = logging.FileHandler(
+ filename=file_name,
+ )
+ formatter = _LogContextFormatter(
+ context=context,
+ fmt=_LOG_MESSAGE_FORMAT,
+ datefmt=_LOG_DATE_FORMAT,
+ )
+ handler.setFormatter(formatter)
+ handler.setLevel(level)
+
+ # If both `--log-file` and `--os-cloud` are specified,
+ # the log is output to each file.
+ root_logger.addHandler(handler)
+
+ return handler
+
+
+class _LogContext(object):
+ """Helper class to represent useful information about a logging context"""
+
+ def __init__(self, clouds_name=None, project_name=None, username=None):
+ """Initialize _LogContext instance
+
+ :param clouds_name: one of the cloud name in configuration file
+ :param project_name: the project name in cloud(clouds_name)
+ :param username: the user name in cloud(clouds_name)
+ """
+
+ self.clouds_name = clouds_name
+ self.project_name = project_name
+ self.username = username
+
+ def to_dict(self):
+ return {
+ 'clouds_name': self.clouds_name,
+ 'project_name': self.project_name,
+ 'username': self.username
+ }
+
+
+class _LogContextFormatter(logging.Formatter):
+ """Customize the logging format for logging handler"""
+
+ def __init__(self, *args, **kwargs):
+ self.context = kwargs.pop('context', None)
+ logging.Formatter.__init__(self, *args, **kwargs)
+
+ def format(self, record):
+ d = self.context.to_dict()
+ for k, v in d.items():
+ setattr(record, k, v)
+ return logging.Formatter.format(self, record)
diff --git a/openstackclient/shell.py b/openstackclient/shell.py
index 319f10de..7cef51ba 100644
--- a/openstackclient/shell.py
+++ b/openstackclient/shell.py
@@ -30,6 +30,7 @@ from cliff import help
import openstackclient
from openstackclient.common import clientmanager
from openstackclient.common import commandmanager
+from openstackclient.common import context
from openstackclient.common import exceptions as exc
from openstackclient.common import timing
from openstackclient.common import utils
@@ -95,6 +96,10 @@ class OpenStackShell(app.App):
self.client_manager = None
+ # Operation log
+ self.enable_operation_logging = False
+ self.command_options = None
+
def configure_logging(self):
"""Configure logging for the app
@@ -107,24 +112,30 @@ class OpenStackShell(app.App):
self.options.verbose_level = 3
super(OpenStackShell, self).configure_logging()
- root_logger = logging.getLogger('')
# Set logging to the requested level
if self.options.verbose_level == 0:
# --quiet
- root_logger.setLevel(logging.ERROR)
+ log_level = logging.ERROR
warnings.simplefilter("ignore")
elif self.options.verbose_level == 1:
# This is the default case, no --debug, --verbose or --quiet
- root_logger.setLevel(logging.WARNING)
+ log_level = logging.WARNING
warnings.simplefilter("ignore")
elif self.options.verbose_level == 2:
# One --verbose
- root_logger.setLevel(logging.INFO)
+ log_level = logging.INFO
warnings.simplefilter("once")
elif self.options.verbose_level >= 3:
# Two or more --verbose
- root_logger.setLevel(logging.DEBUG)
+ log_level = logging.DEBUG
+
+ # Set the handler logging level of FileHandler(--log-file)
+ # and StreamHandler
+ if self.options.log_file:
+ context.setup_handler_logging_level(logging.FileHandler, log_level)
+
+ context.setup_handler_logging_level(logging.StreamHandler, log_level)
# Requests logs some stuff at INFO that we don't want
# unless we have DEBUG
@@ -147,9 +158,17 @@ class OpenStackShell(app.App):
stevedore_log.setLevel(logging.ERROR)
iso8601_log.setLevel(logging.ERROR)
+ # Operation logging
+ self.operation_log = logging.getLogger("operation_log")
+ self.operation_log.setLevel(logging.ERROR)
+ self.operation_log.propagate = False
+
def run(self, argv):
+ ret_val = 1
+ self.command_options = argv
try:
- return super(OpenStackShell, self).run(argv)
+ ret_val = super(OpenStackShell, self).run(argv)
+ return ret_val
except Exception as e:
if not logging.getLogger('').handlers:
logging.basicConfig()
@@ -157,7 +176,13 @@ class OpenStackShell(app.App):
self.log.error(traceback.format_exc(e))
else:
self.log.error('Exception raised: ' + str(e))
- return 1
+ if self.enable_operation_logging:
+ self.operation_log.error(traceback.format_exc(e))
+
+ return ret_val
+
+ finally:
+ self.log.info("END return value: %s", ret_val)
def build_option_parser(self, description, version):
parser = super(OpenStackShell, self).build_option_parser(
@@ -243,7 +268,6 @@ class OpenStackShell(app.App):
auth_type = 'token_endpoint'
else:
auth_type = 'osc_password'
- self.log.debug("options: %s", self.options)
project_id = getattr(self.options, 'project_id', None)
project_name = getattr(self.options, 'project_name', None)
@@ -266,14 +290,23 @@ class OpenStackShell(app.App):
# Ignore the default value of interface. Only if it is set later
# will it be used.
cc = cloud_config.OpenStackConfig(
- override_defaults={'interface': None,
- 'auth_type': auth_type, })
- self.log.debug("defaults: %s", cc.defaults)
+ override_defaults={
+ 'interface': None,
+ 'auth_type': auth_type,
+ },
+ )
self.cloud = cc.get_one_cloud(
cloud=self.options.cloud,
argparse=self.options,
)
+
+ # Set up every time record log in file and logging start
+ context.setup_logging(self, self.cloud)
+
+ self.log.info("START with options: %s", self.command_options)
+ self.log.debug("options: %s", self.options)
+ self.log.debug("defaults: %s", cc.defaults)
self.log.debug("cloud cfg: %s", self.cloud.config)
# Set up client TLS
diff --git a/openstackclient/tests/common/test_context.py b/openstackclient/tests/common/test_context.py
new file mode 100644
index 00000000..145546a3
--- /dev/null
+++ b/openstackclient/tests/common/test_context.py
@@ -0,0 +1,102 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+import logging
+import mock
+import os
+
+from openstackclient.common import context
+from openstackclient.tests import utils
+
+
+class TestContext(utils.TestCase):
+
+ TEST_LOG_FILE = "/tmp/test_log_file"
+
+ def setUp(self):
+ super(TestContext, self).setUp()
+
+ def tearDown(self):
+ super(TestContext, self).tearDown()
+ if os.path.exists(self.TEST_LOG_FILE):
+ os.remove(self.TEST_LOG_FILE)
+
+ def setup_handler_logging_level(self):
+ handler_type = logging.FileHandler
+ handler = logging.FileHandler(filename=self.TEST_LOG_FILE)
+ handler.setLevel(logging.ERROR)
+ logging.getLogger('').addHandler(handler)
+ context.setup_handler_logging_level(handler_type, logging.INFO)
+ self.log.info("test log")
+ ld = open(self.TEST_LOG_FILE)
+ line = ld.readlines()
+ ld.close()
+ if os.path.exists(self.TEST_LOG_FILE):
+ os.remove(self.TEST_LOG_FILE)
+ self.assertGreaterEqual(line.find("test log"), 0)
+
+ @mock.patch("openstackclient.common.context._setup_handler_for_logging")
+ def test_setup_logging(self, setuph):
+ setuph.return_value = mock.MagicMock()
+ shell = mock.MagicMock()
+ cloud_config = mock.MagicMock()
+ cloud_config.auth = {
+ 'project_name': 'heart-o-gold',
+ 'username': 'zaphod'
+ }
+ cloud_config.config = {
+ 'log_level': 'debug',
+ 'log_file': self.TEST_LOG_FILE,
+ 'cloud': 'megadodo'
+ }
+ context.setup_logging(shell, cloud_config)
+ self.assertEqual(True, shell.enable_operation_logging)
+
+
+class Test_LogContext(utils.TestCase):
+ def setUp(self):
+ super(Test_LogContext, self).setUp()
+
+ def test_context(self):
+ ctx = context._LogContext()
+ self.assertTrue(ctx)
+
+ def test_context_to_dict(self):
+ ctx = context._LogContext('cloudsName', 'projectName', 'userNmae')
+ ctx_dict = ctx.to_dict()
+ self.assertEqual('cloudsName', ctx_dict['clouds_name'])
+ self.assertEqual('projectName', ctx_dict['project_name'])
+ self.assertEqual('userNmae', ctx_dict['username'])
+
+
+class Test_LogContextFormatter(utils.TestCase):
+ def setUp(self):
+ super(Test_LogContextFormatter, self).setUp()
+ self.ctx = context._LogContext('cloudsName', 'projectName', 'userNmae')
+ self.addfmt = "%(clouds_name)s %(project_name)s %(username)s"
+
+ def test_contextrrormatter(self):
+ ctxfmt = context._LogContextFormatter()
+ self.assertTrue(ctxfmt)
+
+ def test_context_format(self):
+ record = mock.MagicMock()
+ logging.Formatter.format = mock.MagicMock()
+ logging.Formatter.format.return_value = record
+
+ ctxfmt = context._LogContextFormatter(context=self.ctx,
+ fmt=self.addfmt)
+ addctx = ctxfmt.format(record)
+ self.assertEqual('cloudsName', addctx.clouds_name)
+ self.assertEqual('projectName', addctx.project_name)
+ self.assertEqual('userNmae', addctx.username)
diff --git a/openstackclient/tests/test_shell.py b/openstackclient/tests/test_shell.py
index e2f0580b..5b844753 100644
--- a/openstackclient/tests/test_shell.py
+++ b/openstackclient/tests/test_shell.py
@@ -77,6 +77,8 @@ CLOUD_2 = {
'username': 'zaphod',
},
'region_name': 'occ-cloud',
+ 'log_file': '/tmp/test_log_file',
+ 'log_level': 'debug',
}
}
}
@@ -621,9 +623,12 @@ class TestShellCli(TestShell):
@mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
@mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
- def test_shell_args_cloud_public(self, config_mock, public_mock):
+ @mock.patch("openstackclient.common.context._setup_handler_for_logging")
+ def test_shell_args_cloud_public(self, setup_handler, config_mock,
+ public_mock):
config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_2))
public_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1))
+ setup_handler.return_value = mock.MagicMock()
_shell = make_shell()
fake_execute(
@@ -661,9 +666,12 @@ class TestShellCli(TestShell):
@mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
@mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
- def test_shell_args_precedence(self, config_mock, vendor_mock):
+ @mock.patch("openstackclient.common.context._setup_handler_for_logging")
+ def test_shell_args_precedence(self, setup_handler, config_mock,
+ vendor_mock):
config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_2))
vendor_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1))
+ setup_handler.return_value = mock.MagicMock()
_shell = make_shell()
# Test command option overriding config file value
@@ -715,9 +723,12 @@ class TestShellCliEnv(TestShell):
@mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
@mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
- def test_shell_args_precedence_1(self, config_mock, vendor_mock):
+ @mock.patch("openstackclient.common.context._setup_handler_for_logging")
+ def test_shell_args_precedence_1(self, setup_handler, config_mock,
+ vendor_mock):
config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_2))
vendor_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1))
+ setup_handler.return_value = mock.MagicMock()
_shell = make_shell()
# Test env var
@@ -756,9 +767,12 @@ class TestShellCliEnv(TestShell):
@mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
@mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
- def test_shell_args_precedence_2(self, config_mock, vendor_mock):
+ @mock.patch("openstackclient.common.context._setup_handler_for_logging")
+ def test_shell_args_precedence_2(self, setup_handler, config_mock,
+ vendor_mock):
config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_2))
vendor_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1))
+ setup_handler.return_value = mock.MagicMock()
_shell = make_shell()
# Test command option overriding config file value
@@ -796,3 +810,77 @@ class TestShellCliEnv(TestShell):
'krikkit',
_shell.cloud.config['region_name'],
)
+
+
+class TestShellCliLogging(TestShell):
+ def setUp(self):
+ super(TestShellCliLogging, self).setUp()
+
+ def tearDown(self):
+ super(TestShellCliLogging, self).tearDown()
+
+ @mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
+ @mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
+ @mock.patch("openstackclient.common.context._setup_handler_for_logging")
+ def test_shell_args_precedence_1(self, setup_handler, config_mock,
+ vendor_mock):
+ config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_2))
+ vendor_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1))
+ setup_handler.return_value = mock.MagicMock()
+ _shell = make_shell()
+
+ # These come from clouds.yaml
+ fake_execute(
+ _shell,
+ "--os-cloud megacloud list user",
+ )
+ self.assertEqual(
+ 'megacloud',
+ _shell.cloud.name,
+ )
+
+ self.assertEqual(
+ '/tmp/test_log_file',
+ _shell.cloud.config['log_file'],
+ )
+ self.assertEqual(
+ 'debug',
+ _shell.cloud.config['log_level'],
+ )
+
+ @mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
+ @mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
+ def test_shell_args_precedence_2(self, config_mock, vendor_mock):
+ config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_1))
+ vendor_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1))
+ _shell = make_shell()
+
+ # Test operation_log_file not set
+ fake_execute(
+ _shell,
+ "--os-cloud scc list user",
+ )
+ self.assertEqual(
+ False,
+ _shell.enable_operation_logging,
+ )
+
+ @mock.patch("os_client_config.config.OpenStackConfig._load_vendor_file")
+ @mock.patch("os_client_config.config.OpenStackConfig._load_config_file")
+ @mock.patch("openstackclient.common.context._setup_handler_for_logging")
+ def test_shell_args_precedence_3(self, setup_handler, config_mock,
+ vendor_mock):
+ config_mock.return_value = ('file.yaml', copy.deepcopy(CLOUD_2))
+ vendor_mock.return_value = ('file.yaml', copy.deepcopy(PUBLIC_1))
+ setup_handler.return_value = mock.MagicMock()
+ _shell = make_shell()
+
+ # Test enable_operation_logging set
+ fake_execute(
+ _shell,
+ "--os-cloud megacloud list user",
+ )
+ self.assertEqual(
+ True,
+ _shell.enable_operation_logging,
+ )