Skip to content

Commit 5a03278

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. Signed-off-by: Jakub Jelen <[email protected]>
1 parent b5e2404 commit 5a03278

File tree

5 files changed

+166
-19
lines changed

5 files changed

+166
-19
lines changed

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 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 pylibssh providers interface between libssh logging and
21+
python log facility.
22+
"""

src/pylibsshext/logging.pyx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#
2+
# This file is part of the 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 pylibssh providers interface between libssh logging and
20+
python log 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 the pylibssh will not log any events.
28+
29+
.. data:: ANSIBLE_PYLIBSSH_TRACE
30+
31+
Indicates the pylibssh will produce all possible logs, generally useful for debugging low-level libssh operations.
32+
33+
"""
34+
35+
import logging
36+
37+
from pylibsshext.errors cimport LibsshSessionException
38+
from pylibsshext.includes cimport callbacks, libssh
39+
40+
41+
ANSIBLE_PYLIBSSH_NOLOG = logging.FATAL * 2
42+
ANSIBLE_PYLIBSSH_TRACE = int(logging.DEBUG / 2)
43+
44+
_NOT_SET_SENTINEL = object()
45+
46+
LOG_MAP = {
47+
logging.NOTSET: _NOT_SET_SENTINEL,
48+
ANSIBLE_PYLIBSSH_TRACE: libssh.SSH_LOG_TRACE,
49+
logging.DEBUG: libssh.SSH_LOG_DEBUG,
50+
logging.INFO: libssh.SSH_LOG_INFO,
51+
logging.WARNING: libssh.SSH_LOG_WARN,
52+
ANSIBLE_PYLIBSSH_NOLOG: libssh.SSH_LOG_NONE,
53+
}
54+
55+
LOG_MAP_REV = {
56+
**{
57+
libssh_name: py_name
58+
for py_name, libssh_name in LOG_MAP.items()
59+
},
60+
}
61+
62+
# mapping aliases
63+
LOG_MAP[logging.ERROR] = libssh.SSH_LOG_WARN
64+
LOG_MAP[logging.CRITICAL] = libssh.SSH_LOG_WARN
65+
66+
67+
_logger = logging.getLogger("ansible-pylibssh")
68+
69+
70+
def _add_trace_log_level():
71+
"""
72+
Adds a trace log level to the python logging system.
73+
"""
74+
level_num = ANSIBLE_PYLIBSSH_TRACE
75+
level_name = "TRACE"
76+
77+
logging.addLevelName(level_num, level_name)
78+
79+
80+
cdef void _pylibssh_log_wrapper(int priority,
81+
const char *function,
82+
const char *buffer,
83+
void *userdata) noexcept:
84+
log_level = LOG_MAP_REV[priority]
85+
_logger.log(log_level, buffer.decode('utf-8'))
86+
87+
88+
def _set_log_callback():
89+
"""
90+
Note, that we could also set the set_log_userdata() to access the logger object,
91+
but I did not find it much useful when it is global already.
92+
"""
93+
callbacks.ssh_set_log_callback(_pylibssh_log_wrapper)
94+
95+
96+
def _initialize_logging():
97+
"""
98+
This is done globally, as the libssh logging is not tied to specific session
99+
(its thread-local state in libssh) so either very good care needs to be taken
100+
to make sure the logger is in place when callback can be called almost from
101+
anywhere in the code or just keep it global.
102+
"""
103+
_add_trace_log_level()
104+
_set_log_callback()
105+
106+
107+
def _set_level(level):
108+
"""
109+
Set logging level to the given value from LOG_MAP
110+
111+
:param level: The level to set.
112+
113+
:raises LibsshSessionException: If the log level is not known by pylibssh.
114+
115+
:return: Nothing.
116+
:rtype: NoneType
117+
"""
118+
_initialize_logging()
119+
if level not in LOG_MAP:
120+
raise LibsshSessionException(f'Invalid log level [{level:d}]')
121+
122+
# Special case not-set does not go deeper
123+
if level == _NOT_SET_SENTINEL:
124+
return
125+
126+
rc = libssh.ssh_set_log_level(LOG_MAP[level])
127+
if rc != libssh.SSH_OK:
128+
raise LibsshSessionException(
129+
f'Failed to set log level [{level:d}] with error [{rc:d}]',
130+
)
131+
_logger.setLevel(level)

src/pylibsshext/session.pyx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
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 _set_level
2426
from pylibsshext.scp import SCP
2527
from pylibsshext.sftp import SFTP
2628

@@ -45,15 +47,6 @@ OPTS_DIR_MAP = {
4547
"add_identity": libssh.SSH_OPTIONS_ADD_IDENTITY,
4648
}
4749

48-
LOG_MAP = {
49-
logging.NOTSET: libssh.SSH_LOG_NONE,
50-
logging.DEBUG: libssh.SSH_LOG_DEBUG,
51-
logging.INFO: libssh.SSH_LOG_INFO,
52-
logging.WARNING: libssh.SSH_LOG_WARN,
53-
logging.ERROR: libssh.SSH_LOG_WARN,
54-
logging.CRITICAL: libssh.SSH_LOG_TRACE
55-
}
56-
5750
KNOW_HOST_MSG_MAP = {
5851
libssh.SSH_KNOWN_HOSTS_CHANGED: "Host key for server has changed: ",
5952
libssh.SSH_KNOWN_HOSTS_OTHER: "Host key type for server has changed: ",
@@ -528,12 +521,7 @@ cdef class Session(object):
528521
return SFTP(self)
529522

530523
def set_log_level(self, level):
531-
if level in LOG_MAP.keys():
532-
rc = libssh.ssh_set_log_level(LOG_MAP[level])
533-
if rc != libssh.SSH_OK:
534-
raise LibsshSessionException("Failed to set log level [%d] with error [%d]" % (level, rc))
535-
else:
536-
raise LibsshSessionException("Invalid log level [%d]" % level)
524+
_set_level(level)
537525

538526
def close(self):
539527
if self._libssh_session is not NULL:

tests/conftest.py

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

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

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

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

1919

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

0 commit comments

Comments
 (0)