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]

Reply via email to