From 6ee53595e2cdff038744634f84d4d67b8de2d6f8 Mon Sep 17 00:00:00 2001 From: Sven Sager Date: Thu, 31 Aug 2023 09:03:35 +0200 Subject: [PATCH] feat: SSH tunnel server extended to execute commands on remote host. Signed-off-by: Sven Sager --- src/revpicommander/ssh_tunneling/server.py | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/revpicommander/ssh_tunneling/server.py b/src/revpicommander/ssh_tunneling/server.py index 3e7c8aa..81738c0 100644 --- a/src/revpicommander/ssh_tunneling/server.py +++ b/src/revpicommander/ssh_tunneling/server.py @@ -10,14 +10,18 @@ __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "GPLv2" import select +from logging import getLogger from socketserver import BaseRequestHandler, ThreadingTCPServer from threading import Thread +from typing import Tuple, Union from paramiko.client import MissingHostKeyPolicy, SSHClient from paramiko.rsakey import RSAKey from paramiko.ssh_exception import PasswordRequiredException from paramiko.transport import Transport +log = getLogger("ssh_tunneling") + class ForwardServer(ThreadingTCPServer): daemon_threads = True @@ -33,10 +37,14 @@ class Handler(BaseRequestHandler): self.request.getpeername(), ) except Exception as e: + log.error(e) return if chan is None: + log.error("Could not create a ssh channel") return + log.info("Starting tunnel exchange loop") + while True: r, w, x = select.select([self.request, chan], [], [], 5.0) if self.request in r: @@ -50,6 +58,8 @@ class Handler(BaseRequestHandler): break self.request.send(data) + log.info("Stopped tunnel exchange loop") + chan.close() self.request.close() @@ -162,6 +172,30 @@ class SSHLocalTunnel: return True return False + def send_cmd(self, cmd: str, timeout: float = None) -> Union[Tuple[str, str], Tuple[None, None]]: + """ + Send simple command to ssh host. + + The output of stdout and stderr is returned as a tuple of two elements. + This elements could be None, in case of an internal error. + + :param cmd: Shell command to execute on remote host + :param timeout: Timeout for execution + :return: Tuple with stdout and stderr + """ + if not self._th_server.is_alive(): + raise RuntimeError("Not connected") + + try: + _, stdout, stderr = self._ssh_client.exec_command(cmd, 1024, timeout) + buffer_out = stdout.read() + buffer_err = stderr.read() + + return buffer_out.decode(), buffer_err.decode() + except Exception as e: + log.error(e) + return None, None + @property def connected(self): """Check connection state of ssh tunnel."""