Skip to content

Commit 9111bff

Browse files
committed
Fix the log levels mapping
The libssh provides the most verbose logging with SSH_LOG_TRACE, which was not mapped to any of the standard values so the users are unable to get full debug logs. These are critical for libssh developers to be able to investigate issues. This also creates callback for libssh to feed the logs into the python logging system. Signed-off-by: Jakub Jelen <jjelen@redhat.com>
1 parent 0c2e27c commit 9111bff

5 files changed

Lines changed: 171 additions & 19 deletions

File tree

src/pylibsshext/includes/callbacks.pxd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,10 @@ cdef extern from "libssh/callbacks.h":
127127
ctypedef ssh_channel_callbacks_struct * ssh_channel_callbacks
128128

129129
int ssh_set_channel_callbacks(ssh_channel channel, ssh_channel_callbacks cb)
130+
131+
ctypedef void(*ssh_logging_callback)(
132+
int priority,
133+
const char *function,
134+
const char *buffer,
135+
void *userdata)
136+
int ssh_set_log_callback(ssh_logging_callback cb)

src/pylibsshext/logging.pxd

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# distutils: libraries = ssh
2+
#
3+
# This file is part of the ansible-pylibssh library
4+
#
5+
# This library is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU Lesser General Public
7+
# License as published by the Free Software Foundation; either
8+
# version 2.1 of the License, or (at your option) any later version.
9+
#
10+
# This library is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
# Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public
16+
# License along with this library; if not, see file LICENSE.rst in this
17+
# repository.
18+
19+
"""
20+
The logging module of ``ansible-pylibssh`` provides interface between libssh logging and
21+
python log facility.
22+
"""

src/pylibsshext/logging.pyx

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#
2+
# This file is part of the ansible-pylibssh library
3+
#
4+
# This library is free software; you can redistribute it and/or
5+
# modify it under the terms of the GNU Lesser General Public
6+
# License as published by the Free Software Foundation; either
7+
# version 2.1 of the License, or (at your option) any later version.
8+
#
9+
# This library is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
# Lesser General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU Lesser General Public
15+
# License along with this library; if not, see file LICENSE.rst in this
16+
# repository.
17+
18+
"""
19+
The logging module of ansible-pylibssh provides interface between libssh logging and
20+
Python :py:mod:`logging` facility.
21+
22+
It provides two new log levels, that can be used with to set the log verbosity
23+
using ``set_log_level()`` method on ``Session`` object.
24+
25+
.. data:: ANSIBLE_PYLIBSSH_NOLOG
26+
27+
Indicates that ``ansible-pylibssh`` will not emit any events into the
28+
:external+python:mod:`logging` subsystem.
29+
30+
.. data:: ANSIBLE_PYLIBSSH_TRACE
31+
32+
Indicates that ``ansible-pylibssh`` will make all possible logs available
33+
to the :mod:`logging` subsystem, generally useful for debugging low-level
34+
libssh operations.
35+
36+
The default log level is set to the ``ANSIBLE_PYLIBSSH_TRACE``, which means
37+
all the messages are fed into the python. Setting different levels from
38+
the above lost or any value from Python :py:mod:`logging` module will allow
39+
prevent libssh emitting these logs.
40+
"""
41+
42+
import logging
43+
44+
from pylibsshext.errors cimport LibsshSessionException
45+
from pylibsshext.includes cimport callbacks, libssh
46+
47+
48+
ANSIBLE_PYLIBSSH_NOLOG = logging.FATAL * 2
49+
ANSIBLE_PYLIBSSH_TRACE = int(logging.DEBUG / 2)
50+
51+
LOG_MAP = {
52+
ANSIBLE_PYLIBSSH_TRACE: libssh.SSH_LOG_TRACE,
53+
logging.DEBUG: libssh.SSH_LOG_DEBUG,
54+
logging.INFO: libssh.SSH_LOG_INFO,
55+
logging.WARNING: libssh.SSH_LOG_WARN,
56+
ANSIBLE_PYLIBSSH_NOLOG: libssh.SSH_LOG_NONE,
57+
}
58+
59+
LOG_MAP_REV = {
60+
**{
61+
libssh_name: py_name
62+
for py_name, libssh_name in LOG_MAP.items()
63+
},
64+
}
65+
66+
# mapping aliases
67+
LOG_MAP[logging.NOTSET] = libssh.SSH_LOG_TRACE
68+
LOG_MAP[logging.ERROR] = libssh.SSH_LOG_WARN
69+
LOG_MAP[logging.CRITICAL] = libssh.SSH_LOG_WARN
70+
71+
72+
def _add_trace_log_level():
73+
"""
74+
Add a trace log level to the Python :mod:`logging` system.
75+
"""
76+
level_num = ANSIBLE_PYLIBSSH_TRACE
77+
level_name = "TRACE"
78+
79+
logging.addLevelName(level_num, level_name)
80+
81+
82+
cdef void _pylibssh_log_wrapper(int priority,
83+
const char *function,
84+
const char *buffer,
85+
void *userdata) noexcept:
86+
log_level = LOG_MAP_REV[priority]
87+
logging.getLogger("ansible-pylibssh").log(log_level, buffer.decode('utf-8'))
88+
89+
90+
def _set_log_callback():
91+
"""
92+
Set libssh logging callback
93+
94+
Our function redirects the messages to python :py:mod:`logging` facility.
95+
"""
96+
# Note, that we could also set the set_log_userdata() to access the logger
97+
# object, but I did not find it much useful when it is global already.
98+
callbacks.ssh_set_log_callback(_pylibssh_log_wrapper)
99+
100+
101+
def _initialize_logging():
102+
"""
103+
Initialize libssh logging subsystem
104+
105+
This is implemenbed by adding our own log level and registering our callback
106+
with libssh.
107+
"""
108+
# This is done globally, as the libssh logging is not tied to specific
109+
# session (its thread-local state in libssh) so either very good care
110+
# needs to be taken to make sure the logger is in place when callback
111+
# can be called almost from anywhere in the code or just keep it global.
112+
_add_trace_log_level()
113+
_set_log_callback()
114+
115+
116+
def _set_level(level):
117+
"""
118+
Set logging level to the given value from ``LOG_MAP``.
119+
120+
:param level: The level to set.
121+
:type level: int
122+
123+
:raises LibsshSessionException: If the log level is not known to libssh or pylibssh.
124+
125+
:returns: Nothing.
126+
:rtype: None
127+
"""
128+
if level not in LOG_MAP:
129+
raise LibsshSessionException(f'Invalid log level [{level:d}]')
130+
131+
# can never fail for valid values from the map
132+
libssh.ssh_set_log_level(LOG_MAP[level])

src/pylibsshext/session.pyx

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@
1515
# License along with this library; if not, see file LICENSE.rst in this
1616
# repository.
1717
import inspect
18-
import logging
1918

2019
from cpython.bytes cimport PyBytes_AS_STRING
2120

2221
from pylibsshext.channel import Channel
22+
2323
from pylibsshext.errors cimport LibsshSessionException
24+
25+
from pylibsshext.logging import (
26+
ANSIBLE_PYLIBSSH_TRACE, _initialize_logging, _set_level,
27+
)
2428
from pylibsshext.scp import SCP
2529
from pylibsshext.sftp import SFTP
2630

@@ -46,15 +50,6 @@ OPTS_DIR_MAP = {
4650
"add_identity": libssh.SSH_OPTIONS_ADD_IDENTITY,
4751
}
4852

49-
LOG_MAP = {
50-
logging.NOTSET: libssh.SSH_LOG_NONE,
51-
logging.DEBUG: libssh.SSH_LOG_DEBUG,
52-
logging.INFO: libssh.SSH_LOG_INFO,
53-
logging.WARNING: libssh.SSH_LOG_WARN,
54-
logging.ERROR: libssh.SSH_LOG_WARN,
55-
logging.CRITICAL: libssh.SSH_LOG_TRACE
56-
}
57-
5853
KNOW_HOST_MSG_MAP = {
5954
libssh.SSH_KNOWN_HOSTS_CHANGED: "Host key for server has changed: ",
6055
libssh.SSH_KNOWN_HOSTS_OTHER: "Host key type for server has changed: ",
@@ -114,6 +109,8 @@ cdef class Session(object):
114109
# the callbacks to be around even after we free the underlying channels so
115110
# we should free them only when we terminate the session.
116111
self._channel_callbacks = []
112+
_initialize_logging()
113+
_set_level(ANSIBLE_PYLIBSSH_TRACE)
117114

118115
def __cinit__(self, host=None, **kwargs):
119116
self._libssh_session = libssh.ssh_new()
@@ -541,12 +538,7 @@ cdef class Session(object):
541538
return SFTP(self)
542539

543540
def set_log_level(self, level):
544-
if level in LOG_MAP.keys():
545-
rc = libssh.ssh_set_log_level(LOG_MAP[level])
546-
if rc != libssh.SSH_OK:
547-
raise LibsshSessionException("Failed to set log level [%d] with error [%d]" % (level, rc))
548-
else:
549-
raise LibsshSessionException("Invalid log level [%d]" % level)
541+
_set_level(level)
550542

551543
def close(self):
552544
if self._libssh_session is not NULL:

tests/conftest.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
"""Pytest plugins and fixtures configuration."""
44

5-
import logging
65
import shutil
76
import socket
87
import subprocess
@@ -14,6 +13,7 @@
1413
wait_for_svc_ready_state,
1514
)
1615

16+
from pylibsshext.logging import ANSIBLE_PYLIBSSH_TRACE
1717
from pylibsshext.session import Session
1818

1919

@@ -117,8 +117,7 @@ def ssh_client_session(ssh_session_connect):
117117
# noqa: DAR101
118118
"""
119119
ssh_session = Session()
120-
# TODO Adjust when #597 will be merged
121-
ssh_session.set_log_level(logging.CRITICAL)
120+
ssh_session.set_log_level(ANSIBLE_PYLIBSSH_TRACE)
122121
ssh_session_connect(ssh_session)
123122
try: # noqa: WPS501
124123
yield ssh_session

0 commit comments

Comments
 (0)