Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-ncclient for openSUSE:Factory
checked in at 2025-12-29 15:16:39
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-ncclient (Old)
and /work/SRC/openSUSE:Factory/.python-ncclient.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-ncclient"
Mon Dec 29 15:16:39 2025 rev:28 rq:1324587 version:0.7.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-ncclient/python-ncclient.changes
2025-07-22 12:21:36.983414341 +0200
+++
/work/SRC/openSUSE:Factory/.python-ncclient.new.1928/python-ncclient.changes
2025-12-29 15:17:22.148403183 +0100
@@ -1,0 +2,21 @@
+Mon Nov 17 22:07:58 UTC 2025 - Dirk Müller <[email protected]>
+
+- update to 0.7.0:
+ * Introduced `ssh-python` as option instead of `Paramiko`.
+ * Bumped to `0.7.0` as the `manager.connnect` call is NOT 100%
+ backwards compatible today with all possible parameters that
+ may be passed to `Paramiko`.
+ * update Paramiko dependency
+ * set default workflow permissions to read-only and pin action
+ hashes f…
+ * Fix error in .github/workflows/check.yml
+ * Load certificate data when a corresponding public key is
+ found in an ssh agent
+ * Lazy-load some submodules inside ncclient.transport only when
+ needed
+ * Update tox.ini for test-requirements.txt → requirements-
+ test.txt
+ * Add alternative SSH transport using libssh
+ * new: remove paramiko.DSSKey
+
+-------------------------------------------------------------------
Old:
----
ncclient-0.6.19.tar.gz
New:
----
ncclient-0.7.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-ncclient.spec ++++++
--- /var/tmp/diff_new_pack.OrIQO5/_old 2025-12-29 15:17:22.944435879 +0100
+++ /var/tmp/diff_new_pack.OrIQO5/_new 2025-12-29 15:17:22.948436044 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-ncclient
#
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2025 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
%{?sle15allpythons}
Name: python-ncclient
-Version: 0.6.19
+Version: 0.7.0
Release: 0
Summary: Python library for NETCONF clients
License: Apache-2.0
++++++ ncclient-0.6.19.tar.gz -> ncclient-0.7.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/.github/workflows/check.yaml
new/ncclient-0.7.0/.github/workflows/check.yaml
--- old/ncclient-0.6.19/.github/workflows/check.yaml 2025-03-02
19:30:10.000000000 +0100
+++ new/ncclient-0.7.0/.github/workflows/check.yaml 2025-08-25
17:24:39.000000000 +0200
@@ -2,8 +2,10 @@
on: [push, pull_request]
-jobs:
+# Declare default permissions as read only.
+permissions: read-all
+jobs:
check_linux:
runs-on: ubuntu-22.04
@@ -11,12 +13,12 @@
matrix:
python-version: ['3.7.17', '3.8.18', '3.9.20', '3.10.15', '3.11.10',
'3.12.9', '3.13.2']
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with:
submodules: 'recursive'
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c #
v4
with:
python-version: ${{ matrix.python-version }}
@@ -37,12 +39,12 @@
matrix:
python-version: ['3.12.9']
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with:
submodules: 'recursive'
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c #
v4
with:
python-version: ${{ matrix.python-version }}
@@ -72,12 +74,12 @@
matrix:
python-version: ['3.11.9', '3.12.9', '3.13.2']
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
with:
submodules: 'recursive'
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c #
v4
with:
python-version: ${{ matrix.python-version }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/README.md
new/ncclient-0.7.0/README.md
--- old/ncclient-0.6.19/README.md 2025-03-02 19:30:10.000000000 +0100
+++ new/ncclient-0.7.0/README.md 2025-08-25 17:24:39.000000000 +0200
@@ -19,6 +19,7 @@
| Date | Release | Description |
| :----: | :-----: | :---------- |
+| 08/25/25 | `0.7.0` | See [release
page](https://github.com/ncclient/ncclient/releases/tag/v0.7.0)|
| 10/18/23 | `0.6.15` | See [release
page](https://github.com/ncclient/ncclient/releases/tag/v0.6.15)|
| 04/10/22 | `0.6.13` | See [release
page](https://github.com/ncclient/ncclient/releases/tag/v0.6.13)|
| 05/29/21 | `0.6.12` | See [release
page](https://github.com/ncclient/ncclient/releases/tag/v0.6.12)|
@@ -56,13 +57,19 @@
pip install ncclient
+ # to install dependencies to use ssh-python instead of Paramiko
+ pip install ncclient[libssh]
+
Also locally via pip from within local clone:
pip install -U .
## Examples
- [ncclient] $ python examples/juniper/*.py
+ [ncclient] $ python examples/base/*.py
+
+As of `0.7.0` it is possible to use `ssh-python` instead of `Paramiko`. For a
+simple example of how to use `ssh-python` see [nc11.py](examples/base/nc11.py)
## Usage
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/examples/base/nc11.py
new/ncclient-0.7.0/examples/base/nc11.py
--- old/ncclient-0.6.19/examples/base/nc11.py 1970-01-01 01:00:00.000000000
+0100
+++ new/ncclient-0.7.0/examples/base/nc11.py 2025-08-25 17:24:39.000000000
+0200
@@ -0,0 +1,30 @@
+#! /usr/bin/env python
+#
+# Connect to the NETCONF server passed on the command line using libssh
+# and display the client capabilities. For brevity and clarity of the
+# examples, we omit proper exception handling.
+#
+# $ ./nc11.py host username password
+
+import logging
+import os
+import sys
+import warnings
+
+warnings.simplefilter("ignore", DeprecationWarning)
+from ncclient import manager
+
+def demo(host, user, password):
+ with manager.connect(
+ host=host, port=830,
+ username=user, password=password,
+ use_libssh=True,
+ hostkey_verify=False) as m:
+
+ for c in m.client_capabilities:
+ print(c)
+
+if __name__ == '__main__':
+ LOG_FORMAT = '%(asctime)s %(levelname)s %(filename)s:%(lineno)d
%(message)s'
+ logging.basicConfig(stream=sys.stdout, level=logging.INFO,
format=LOG_FORMAT)
+ demo(sys.argv[1], sys.argv[2], sys.argv[3])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/ncclient/_version.py
new/ncclient-0.7.0/ncclient/_version.py
--- old/ncclient-0.6.19/ncclient/_version.py 2025-03-02 19:30:10.000000000
+0100
+++ new/ncclient-0.7.0/ncclient/_version.py 2025-08-25 17:24:39.000000000
+0200
@@ -26,9 +26,9 @@
# setup.py/versioneer.py will grep for the variable names, so they must
# each be defined on a line of their own. _version.py will just call
# get_keywords().
- git_refnames = " (HEAD -> master, tag: v0.6.19)"
- git_full = "cdf54a49159d8192ae6d6410fad207d3489a9015"
- git_date = "2025-03-02 18:30:10 +0000"
+ git_refnames = " (tag: v0.7.0)"
+ git_full = "97d9c3fd01ffc7e44dafdc0dd3dca380ef16e596"
+ git_date = "2025-08-25 16:24:39 +0100"
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
return keywords
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/ncclient/manager.py
new/ncclient-0.7.0/ncclient/manager.py
--- old/ncclient-0.6.19/ncclient/manager.py 2025-03-02 19:30:10.000000000
+0100
+++ new/ncclient-0.7.0/ncclient/manager.py 2025-08-25 17:24:39.000000000
+0200
@@ -159,6 +159,24 @@
raise
return Manager(session, device_handler, **manager_params)
+def connect_libssh(*args, **kwargs):
+ """Initialize a :class:`Manager` over the LibSSH transport."""
+ if not hasattr(transport, 'libssh'):
+ raise ValueError("LibSSH transport is not available, install
'ssh-python' package.")
+
+ device_params = _extract_device_params(kwargs)
+ manager_params = _extract_manager_params(kwargs)
+ nc_params = _extract_nc_params(kwargs)
+ ignore_errors, raise_mode = _extract_errors_params(kwargs)
+ manager_params["raise_mode"] = raise_mode
+
+ device_handler = make_device_handler(device_params, ignore_errors)
+ device_handler.add_additional_ssh_connect_params(kwargs)
+ device_handler.add_additional_netconf_params(nc_params)
+ session = transport.libssh.LibSSHSession(device_handler)
+
+ session.connect(*args, **kwargs)
+ return Manager(session, device_handler, **manager_params)
def connect_tls(*args, **kwargs):
"""Initialize a :class:`Manager` over the TLS transport."""
@@ -218,6 +236,8 @@
if host == 'localhost' and device_params.get('name') == 'junos' \
and device_params.get('local'):
return connect_ioproc(*args, **kwds)
+ elif kwds.pop("use_libssh", False):
+ return connect_libssh(*args, **kwds)
else:
return connect_ssh(*args, **kwds)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/ncclient/transport/__init__.py
new/ncclient-0.7.0/ncclient/transport/__init__.py
--- old/ncclient-0.6.19/ncclient/transport/__init__.py 2025-03-02
19:30:10.000000000 +0100
+++ new/ncclient-0.7.0/ncclient/transport/__init__.py 2025-08-25
17:24:39.000000000 +0200
@@ -17,21 +17,41 @@
import sys
from ncclient.transport.session import Session, SessionListener, NetconfBase
-from ncclient.transport.ssh import SSHSession
-from ncclient.transport.tls import TLSSession
from ncclient.transport.errors import *
+
+def __getattr__(name):
+ if name == 'SSHSession':
+ from ncclient.transport.ssh import SSHSession
+ return SSHSession
+ elif name == 'TLSSession':
+ from ncclient.transport.tls import TLSSession
+ return TLSSession
+ elif name == 'UnixSocketSession':
+ #
+ # Windows does not support Unix domain sockets; assume all other
platforms do
+ #
+ if sys.platform != 'win32':
+ try:
+ from ncclient.transport.unixSocket import UnixSocketSession
+ return UnixSocketSession
+ except Exception:
+ pass
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+
+
__all__ = [
'Session',
'SessionListener',
- 'SSHSession',
- 'TLSSession',
'TransportError',
'AuthenticationError',
'SessionCloseError',
'NetconfBase',
'SSHError',
'SSHUnknownHostError',
+ 'SSHSession',
+ 'TLSSession',
+ 'UnixSocketSession',
]
#
@@ -43,3 +63,14 @@
__all__.append('UnixSocketSession')
except Exception:
pass
+
+#
+# check if ssh-python is installed
+#
+try:
+ import ssh
+except ImportError:
+ pass
+else:
+ from ncclient.transport.libssh import LibSSHSession
+ __all__.append('LibSSHSession')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/ncclient/transport/libssh.py
new/ncclient-0.7.0/ncclient/transport/libssh.py
--- old/ncclient-0.6.19/ncclient/transport/libssh.py 1970-01-01
01:00:00.000000000 +0100
+++ new/ncclient-0.7.0/ncclient/transport/libssh.py 2025-08-25
17:24:39.000000000 +0200
@@ -0,0 +1,387 @@
+import base64
+import hashlib
+import logging
+import os
+import socket
+import threading
+from io import BytesIO as StringIO
+from typing import Any, Callable
+
+from ncclient.capabilities import Capabilities
+from ncclient.logging_ import SessionLoggerAdapter
+from ncclient.transport.errors import AuthenticationError, SSHError,
SSHUnknownHostError
+from ncclient.transport.parser import DefaultXMLParser
+from ncclient.transport.session import Session
+from ssh.channel import Channel
+from ssh.exceptions import AuthenticationDenied, ChannelOpenFailure,
KeyImportError
+from ssh.exceptions import SSHError as LibSSHError
+from ssh.key import import_privkey_file, import_privkey_base64
+from ssh.options import HOST, KNOWNHOSTS, USER
+from ssh.session import Session as LSession
+
+logger = logging.getLogger("ncclient.transport.libssh")
+
+PORT_NETCONF_DEFAULT = 830
+BUF_SIZE = 4096
+
+
+def default_unknown_host_cb(host: str, fingerprint: str) -> bool:
+ """
+ An unknown host callback returns True if it finds the key acceptable, and
False if not. This default
+ callback always returns False, which would lead to connect() raising a
SSHUnknownHost exception. Supply
+ another valid callback if you need to verify the host key programmatically.
+
+ host - the hostname or IP address of the host
+ fingerprint - hex string representing the host key fingerprint,
+ colon-delimited e.g. "4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21"
+ """
+ return False
+
+
+class LibSSHSession(Session):
+
+ _buffer: StringIO
+ _channel: Channel | None
+ _closing: threading.Event
+ _connected: bool
+ _device_handler: Any
+ _host: str | None
+ _logger: SessionLoggerAdapter
+ _receiver_thread: threading.Thread | None
+ _session: LSession | None
+ _socket: socket.socket | None
+ _socket_r: socket.socket
+ _socket_w: socket.socket
+ parser: DefaultXMLParser
+
+ def __init__(self, device_handler):
+ capabilities = Capabilities(device_handler.get_capabilities())
+ Session.__init__(self, capabilities)
+ self._host = None
+ self._connected = False
+ self._socket = None
+ self._channel = None
+ self._session = None
+ self._buffer = StringIO()
+ self._device_handler = device_handler
+ self._message_list = []
+ self._closing = threading.Event()
+ self.parser = DefaultXMLParser(self)
+ self.logger = SessionLoggerAdapter(logger, {"session": self})
+ self._socket_r, self._socket_w = socket.socketpair()
+ self._receiver_thread = None
+
+ def _receiver_loop(self, socket_w: socket.socket, channel):
+ """
+ The receiver loop reads data from the SSH channel and writes it to the
transport socket.
+
+ ### Parameters:
+ - socket_w: The writable end of the socket pair used for communication.
+ - channel: The SSH channel to read data from.
+ """
+ while True:
+ try:
+ data = channel.read()
+ except LibSSHError:
+ logger.error("Channel read error")
+ break
+ if data[0] == 0:
+ # EOF received
+ self._closing.set()
+ break
+ socket_w.send(data[1])
+ socket_w.close()
+
+ def connect(
+ self,
+ host: str,
+ port: int = PORT_NETCONF_DEFAULT,
+ username: str | None = None,
+ password: str | None = None,
+ key_filename: str | None = None,
+ key_base64: bytes | None = None,
+ key_passphrase: bytes | None = None,
+ allow_agent: bool = False,
+ hostkey_verify: bool = True,
+ hostkey_b64: str | None = None,
+ timeout: int | None = None,
+ unknown_host_cb: Callable = default_unknown_host_cb,
+ bind_addr: str | None = None,
+ ):
+ """
+ Connect to the SSH server and establish a NETCONF session.
+
+ ### Parameters:
+ - host: The hostname or IP address of the SSH server.
+ - port: The port number to connect to (default is 830).
+ - username: The username for authentication.
+ - password: The password for authentication.
+ - key_filename: Path to the private key file for public key
authentication.
+ - key_base64: Base64-encoded private key for public key authentication.
+ - key_passphrase: Passphrase for the private key, if required.
+ - allow_agent: Whether to use the SSH agent for authentication.
+ - hostkey_verify: Whether to verify the server's host key.
+ - hostkey_b64: Base64-encoded public key of the server for host key
verification.
+ - timeout: Timeout for the connection attempt.
+ - unknown_host_cb: Callback function for handling unknown hosts.
+ - bind_addr: Local address to bind the socket to.
+ """
+ for i in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM):
+ af, socktype, proto, _, sa = i
+ try:
+ sock = socket.socket(af, socktype, proto)
+ except socket.error:
+ continue
+ try:
+ if bind_addr:
+ sock.bind((bind_addr, 0))
+ sock.settimeout(timeout)
+ sock.connect(sa)
+ except socket.error:
+ sock.close()
+ continue
+ break
+
+ else:
+ raise SSHError(f"Could not open socket to {host}:{port}")
+
+ if not username:
+ username = os.getenv("USER") or os.getenv("LOGNAME") or "root"
+ if not username:
+ raise AuthenticationError(
+ "Username must be provided or set in environment variables"
+ )
+
+ self._session = LSession()
+ assert self._session is not None
+
+ self._session.options_set(HOST, host)
+ self._session.options_set(USER, username)
+ self._session.options_set(KNOWNHOSTS,
os.path.expanduser("~/.ssh/known_hosts"))
+ self._session.options_set_port(port)
+ self._session.set_socket(sock)
+ self._session.connect()
+
+ if hostkey_verify:
+ is_known_host = False
+ server_pubkey = self._session.get_server_publickey()
+ server_pubkey_base64 =
server_pubkey.export_pubkey_base64().decode()
+ if hostkey_b64 is not None:
+ # Check if the provided hostkey matches the server's public key
+ if server_pubkey_base64 == hostkey_b64:
+ is_known_host = True
+ else:
+ # check known hosts file
+ if self._session.is_server_known():
+ is_known_host = True
+
+ def openssh_fingerprint_md5(base64_key: str) -> str:
+ """
+ Calculate md5 fingerprint of the base64-encoded public key.
+
+ ### Parameters:
+ - base64_key: Base64-encoded public key string.
+
+ ### Returns:
+ - Fingerprint as a colon-separated hex string.
+ """
+ key_bytes = base64.b64decode(base64_key)
+ digest = hashlib.md5(key_bytes).hexdigest()
+ fingerprint = ":".join(
+ digest[i : i + 2] for i in range(0, len(digest), 2)
+ )
+ return fingerprint
+
+ if not is_known_host:
+ fingerprint = openssh_fingerprint_md5(server_pubkey_base64)
+ if not unknown_host_cb(host, fingerprint):
+ # If the host key is not known, raise an error
+ raise SSHUnknownHostError(host, fingerprint)
+
+ def auth(
+ sess: LSession,
+ user: str,
+ passwd: str | None,
+ keyfile: str | None,
+ keybase64: bytes | None,
+ agent: bool,
+ passphrase: bytes | None,
+ ):
+ """
+ Authenticate the session using the provided credentials.
+
+ ### Parameters:
+ - sess: The SSH session object.
+ - user: Username.
+ - passwd: Password.
+ - keyfile: Path to the private key file.
+ - agent: Whether to use the SSH agent for authentication.
+ """
+ if passphrase is None:
+ passphrase = b""
+
+ if user and passwd:
+ try:
+ sess.userauth_password(user, passwd)
+ self.logger.debug(
+ "Password authentication successful for user %s", user
+ )
+ return
+ except AuthenticationDenied:
+ self.logger.debug(
+ "Password authentication failed for user %s", user
+ )
+
+ if keyfile:
+ try:
+ privkey = import_privkey_file(keyfile)
+ except KeyImportError:
+ self.logger.debug(
+ "Could not import private key from file %s", keyfile
+ )
+ else:
+ try:
+ sess.userauth_publickey(privkey)
+ self.logger.debug(
+ "Public key authentication successful for user
%s", user
+ )
+ return
+ except AuthenticationDenied:
+ self.logger.debug(
+ "Public key authentication failed for user %s",
user
+ )
+
+ if keybase64:
+ try:
+ privkey = import_privkey_base64(keybase64, passphrase)
+ except KeyImportError:
+ self.logger.debug("Could not import public key from base64
string")
+ else:
+ try:
+ sess.userauth_publickey(privkey)
+ self.logger.debug(
+ "Public key authentication successful for user
%s", user
+ )
+ return
+ except AuthenticationDenied:
+ self.logger.debug(
+ "Public key authentication failed for user %s",
user
+ )
+
+ if agent:
+ try:
+ sess.userauth_agent(user)
+ self.logger.debug(
+ "Agent authentication successful for user %s", user
+ )
+ return
+ except AuthenticationDenied:
+ self.logger.debug("Agent authentication failed for user
%s", user)
+
+ raise AuthenticationError(
+ f"Authentication failed for user {user} on host {host}:{port}"
+ )
+
+ auth(
+ self._session,
+ username,
+ password,
+ key_filename,
+ key_base64,
+ allow_agent,
+ key_passphrase,
+ )
+
+ try:
+ self._channel = self._session.channel_new()
+ except ChannelOpenFailure as e:
+ raise SSHError(f"Could not open channel to {host}:{port}") from e
+
+ assert self._channel is not None
+ try:
+ self._channel.open_session()
+ self._channel.request_subsystem("netconf")
+ except LibSSHError as e:
+ raise SSHError(
+ f"Could not open NETCONF session on channel to {host}:{port}"
+ ) from e
+
+ self._receiver_thread = threading.Thread(
+ target=self._receiver_loop,
+ args=(self._socket_w, self._channel),
+ daemon=True,
+ )
+ self._receiver_thread.start()
+
+ self._host = host
+ self._socket = sock
+ self._connected = True
+ self._post_connect()
+
+ def _send_ready(self):
+ """
+ Check if the session is ready to send data.
+
+ ### Returns:
+ - True if the session is connected and thus the channel is open, False
otherwise.
+ """
+ assert self._channel is not None
+ return self._channel.is_open()
+
+ def _transport_read(self):
+ """
+ Read data from the transport layer.
+
+ ### Returns:
+ - Data read from the transport layer.
+ """
+ return self._socket_r.recv(BUF_SIZE)
+
+ def _transport_write(self, data: bytes) -> int:
+ """
+ Write data to the transport layer.
+
+ ### Parameters:
+ - data: The data to be sent over the transport layer.
+
+ ### Returns:
+ - The number of bytes written to the transport layer.
+ """
+ assert self._channel is not None
+ res = self._channel.write(data)
+ return res[0]
+
+ def _transport_register(self, selector, event):
+ """
+ Register the socket with the selector for the specified event.
+
+ ### Parameters:
+ - selector: The selector to register the socket with.
+ - event: The event type to register for (e.g., read, write).
+ """
+ selector.register(self._socket_r, event)
+
+ def close(self):
+ """
+ Close the SSH session and clean up resources.
+ """
+ assert self._channel is not None
+ assert self._socket is not None
+ self._closing.set()
+ if not self._channel.is_closed():
+ self._channel.close()
+ # wait for the transport thread to close.
+ if self.is_alive() and (self is not threading.current_thread()):
+ self.join()
+ self._channel = None
+ self._socket.close()
+ self._connected = False
+
+ @property
+ def host(self):
+ """
+ Host this session is connected to, or None if not connected.
+ """
+ if self._connected:
+ return self._host
+ return None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/ncclient/transport/ssh.py
new/ncclient-0.7.0/ncclient/transport/ssh.py
--- old/ncclient-0.6.19/ncclient/transport/ssh.py 2025-03-02
19:30:10.000000000 +0100
+++ new/ncclient-0.7.0/ncclient/transport/ssh.py 2025-08-25
17:24:39.000000000 +0200
@@ -292,7 +292,7 @@
if hostkey_b64:
# If we need to connect with a specific hostkey, negotiate for
only its type
hostkey_obj = None
- for key_cls in [paramiko.DSSKey, paramiko.Ed25519Key,
paramiko.RSAKey, paramiko.ECDSAKey]:
+ for key_cls in [paramiko.Ed25519Key, paramiko.RSAKey,
paramiko.ECDSAKey]:
try:
hostkey_obj = key_cls(data=base64.b64decode(hostkey_b64))
except paramiko.SSHException:
@@ -414,7 +414,12 @@
append_agent_keys=list(paramiko.Agent().get_keys())
for key_filename in key_filenames:
- pubkey_filename=key_filename.strip(".pub")+".pub"
+ if key_filename.endswith("-cert.pub"):
+ pubkey_filename=key_filename[:-9]+".pub"
+ elif key_filename.endswith(".pub"):
+ pubkey_filename=key_filename[:-4]+".pub"
+ else:
+ pubkey_filename=key_filename+".pub"
try:
file_key=paramiko.PublicBlob.from_file(pubkey_filename).key_blob
except (FileNotFoundError, ValueError):
@@ -423,6 +428,11 @@
for idx, agent_key in enumerate(append_agent_keys):
if agent_key.asbytes() == file_key:
self.logger.debug("Prioritising SSH agent key found in
%s",key_filename )
+ try:
+
append_agent_keys[idx].load_certificate(key_filename)
+ except (FileNotFoundError, ValueError):
+ continue
+
prepend_agent_keys.append(append_agent_keys.pop(idx))
break
@@ -441,26 +451,20 @@
keyfiles = []
if look_for_keys:
rsa_key = os.path.expanduser("~/.ssh/id_rsa")
- dsa_key = os.path.expanduser("~/.ssh/id_dsa")
ecdsa_key = os.path.expanduser("~/.ssh/id_ecdsa")
ed25519_key = os.path.expanduser("~/.ssh/id_ed25519")
if os.path.isfile(rsa_key):
keyfiles.append(rsa_key)
- if os.path.isfile(dsa_key):
- keyfiles.append(dsa_key)
if os.path.isfile(ecdsa_key):
keyfiles.append(ecdsa_key)
if os.path.isfile(ed25519_key):
keyfiles.append(ed25519_key)
# look in ~/ssh/ for windows users:
rsa_key = os.path.expanduser("~/ssh/id_rsa")
- dsa_key = os.path.expanduser("~/ssh/id_dsa")
ecdsa_key = os.path.expanduser("~/ssh/id_ecdsa")
ed25519_key = os.path.expanduser("~/ssh/id_ed25519")
if os.path.isfile(rsa_key):
keyfiles.append(rsa_key)
- if os.path.isfile(dsa_key):
- keyfiles.append(dsa_key)
if os.path.isfile(ecdsa_key):
keyfiles.append(ecdsa_key)
if os.path.isfile(ed25519_key):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/requirements.txt
new/ncclient-0.7.0/requirements.txt
--- old/ncclient-0.6.19/requirements.txt 2025-03-02 19:30:10.000000000
+0100
+++ new/ncclient-0.7.0/requirements.txt 2025-08-25 17:24:39.000000000 +0200
@@ -1,2 +1,2 @@
-paramiko>=1.15.0
+paramiko>=3.2.0
lxml>=3.3.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/setup.py new/ncclient-0.7.0/setup.py
--- old/ncclient-0.6.19/setup.py 2025-03-02 19:30:10.000000000 +0100
+++ new/ncclient-0.7.0/setup.py 2025-08-25 17:24:39.000000000 +0200
@@ -33,6 +33,8 @@
req_lines = [line.strip() for line in open("requirements.txt").readlines()]
install_reqs = list(filter(None, req_lines))
+extras_reqs = {"libssh": ["ssh-python >= 1.1.1"]}
+
test_req_lines = [line.strip() for line in
open("requirements-test.txt").readlines()]
test_reqs = list(filter(None, test_req_lines))
@@ -50,6 +52,7 @@
url="https://github.com/ncclient/ncclient",
packages=find_packages(exclude=['test', 'test.*']),
install_requires=install_reqs,
+ extras_require=extras_reqs,
tests_require=test_reqs,
license=__licence__,
platforms=["Posix; OS X; Windows"],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/ncclient-0.6.19/test/unit/transport/certs/id_ed25519_test-cert.pub
new/ncclient-0.7.0/test/unit/transport/certs/id_ed25519_test-cert.pub
--- old/ncclient-0.6.19/test/unit/transport/certs/id_ed25519_test-cert.pub
1970-01-01 01:00:00.000000000 +0100
+++ new/ncclient-0.7.0/test/unit/transport/certs/id_ed25519_test-cert.pub
2025-08-25 17:24:39.000000000 +0200
@@ -0,0 +1 @@
[email protected]
AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIDFDuIwn7v8UkWg6cu7vHu97hj1ckpURvkale1VTlsX6AAAAIIreHgBY9enVjEKDZSy9jgaynTA3u4YKmR8hmB/2k3tMAAAAAAAAAAAAAAABAAAAB3Rlc3RrZXkAAAALAAAAB3Rlc3RrZXkAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgit4eAFj16dWMQoNlLL2OBrKdMDe7hgqZHyGYH/aTe0wAAABTAAAAC3NzaC1lZDI1NTE5AAAAQPKXSfM+27KtVMRnNUP8OWm9DMoESW6P7rgncdg7f0ZMNpcNcmprEpu4XGcBpDWOCoR5baaOYc0Tq5f+8RCocg0=
testkey
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/ncclient-0.6.19/test/unit/transport/certs/id_ed25519_test.pub
new/ncclient-0.7.0/test/unit/transport/certs/id_ed25519_test.pub
--- old/ncclient-0.6.19/test/unit/transport/certs/id_ed25519_test.pub
1970-01-01 01:00:00.000000000 +0100
+++ new/ncclient-0.7.0/test/unit/transport/certs/id_ed25519_test.pub
2025-08-25 17:24:39.000000000 +0200
@@ -0,0 +1 @@
+ssh-ed25519
AAAAC3NzaC1lZDI1NTE5AAAAIIreHgBY9enVjEKDZSy9jgaynTA3u4YKmR8hmB/2k3tM testkey
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/test/unit/transport/test_ssh.py
new/ncclient-0.7.0/test/unit/transport/test_ssh.py
--- old/ncclient-0.6.19/test/unit/transport/test_ssh.py 2025-03-02
19:30:10.000000000 +0100
+++ new/ncclient-0.7.0/test/unit/transport/test_ssh.py 2025-08-25
17:24:39.000000000 +0200
@@ -13,6 +13,10 @@
import selectors2 as selectors
+SSH_KEYTYPE = 'ssh-ed25519'
+SSH_PUBKEY = 'test/unit/transport/certs/id_ed25519_test.pub'
+SSH_CERT = 'test/unit/transport/certs/id_ed25519_test-cert.pub'
+
reply_data = """<rpc-reply
xmlns:junos="http://xml.juniper.net/junos/12.1X46/junos" attrib1 = "test">
<software-information>
<host-name>R1</host-name>
@@ -134,6 +138,50 @@
@patch('paramiko.transport.Transport.auth_publickey')
@patch('paramiko.agent.AgentSSH.get_keys')
+ def test_auth_agent_with_key(self, mock_get_key, mock_auth_public_key):
+ expected_blob = paramiko.PublicBlob.from_file(SSH_PUBKEY).key_blob
+ expected = paramiko.PKey.from_type_string(SSH_KEYTYPE, expected_blob)
+ expected.load_certificate(SSH_PUBKEY)
+
+ agentkey_blob = paramiko.PublicBlob.from_file(SSH_PUBKEY).key_blob
+ agentkey = paramiko.PKey.from_type_string(SSH_KEYTYPE, agentkey_blob)
+ mock_get_key.return_value = [agentkey]
+
+ device_handler = JunosDeviceHandler({'name': 'junos'})
+ obj = SSHSession(device_handler)
+ obj._transport = paramiko.Transport(MagicMock())
+ obj._auth('user', 'password', [SSH_PUBKEY], True, True)
+ self.assertEqual(
+ (mock_auth_public_key.call_args_list[0][0][1]).__repr__(),
+ expected.__repr__())
+ self.assertEqual(
+ (mock_auth_public_key.call_args_list[0][0][1]).public_blob,
+ expected.public_blob)
+
+ @patch('paramiko.transport.Transport.auth_publickey')
+ @patch('paramiko.agent.AgentSSH.get_keys')
+ def test_auth_agent_with_cert(self, mock_get_key, mock_auth_public_key):
+ expected_blob = paramiko.PublicBlob.from_file(SSH_PUBKEY).key_blob
+ expected = paramiko.PKey.from_type_string(SSH_KEYTYPE, expected_blob)
+ expected.load_certificate(SSH_CERT)
+
+ agentkey_blob = paramiko.PublicBlob.from_file(SSH_PUBKEY).key_blob
+ agentkey = paramiko.PKey.from_type_string(SSH_KEYTYPE, agentkey_blob)
+ mock_get_key.return_value = [agentkey]
+
+ device_handler = JunosDeviceHandler({'name': 'junos'})
+ obj = SSHSession(device_handler)
+ obj._transport = paramiko.Transport(MagicMock())
+ obj._auth('user', 'password', [SSH_CERT], True, True)
+ self.assertEqual(
+ (mock_auth_public_key.call_args_list[0][0][1]).__repr__(),
+ expected.__repr__())
+ self.assertEqual(
+ (mock_auth_public_key.call_args_list[0][0][1]).public_blob,
+ expected.public_blob)
+
+ @patch('paramiko.transport.Transport.auth_publickey')
+ @patch('paramiko.agent.AgentSSH.get_keys')
def test_auth_agent_exception(self, mock_get_key, mock_auth_public_key):
key = paramiko.PKey()
mock_get_key.return_value = [key]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/ncclient-0.6.19/tox.ini new/ncclient-0.7.0/tox.ini
--- old/ncclient-0.6.19/tox.ini 2025-03-02 19:30:10.000000000 +0100
+++ new/ncclient-0.7.0/tox.ini 2025-08-25 17:24:39.000000000 +0200
@@ -9,7 +9,7 @@
install_command = pip install {opts} {packages}
deps = -r{toxinidir}/requirements.txt
- -r{toxinidir}/test-requirements.txt
+ -r{toxinidir}/requirements-test.txt
commands =
pytest {posargs}
@@ -18,7 +18,7 @@
pytest --cov=ncclient
[testenv:docs]
-deps = -r{toxinidir}/test-requirements.txt
+deps = -r{toxinidir}/requirements-test.txt
commands = sphinx-build -b html docs/source docs/html
[testenv:pep8]