[#398] Removed ssh response objects.

This commit is contained in:
Fabio Manganiello 2024-05-10 01:36:44 +02:00
parent f978d708cb
commit b662e98447
Signed by: blacklight
GPG key ID: D90FBA7F76362774
4 changed files with 101 additions and 94 deletions

View file

@ -1,5 +0,0 @@
``ssh``
==================================
.. automodule:: platypush.message.response.ssh
:members:

View file

@ -8,6 +8,5 @@ Responses
platypush/responses/google.drive.rst platypush/responses/google.drive.rst
platypush/responses/printer.cups.rst platypush/responses/printer.cups.rst
platypush/responses/ssh.rst
platypush/responses/tensorflow.rst platypush/responses/tensorflow.rst
platypush/responses/translate.rst platypush/responses/translate.rst

View file

@ -1,18 +0,0 @@
from platypush.message.response import Response
class SSHResponse(Response):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class SSHKeygenResponse(SSHResponse):
def __init__(self, fingerprint: str, key_file: str, pub_key_file: str, *args, **kwargs):
super().__init__(*args, output={
'fingerprint': fingerprint,
'key_file': key_file,
'pub_key_file': pub_key_file,
}, **kwargs)
# vim:sw=4:ts=4:et:

View file

@ -19,15 +19,18 @@ from stat import (
from typing import Optional, Dict, Tuple, List, Union, Any from typing import Optional, Dict, Tuple, List, Union, Any
from paramiko import DSSKey, RSAKey, SSHClient, WarningPolicy, SFTPClient from paramiko import DSSKey, RSAKey, SSHClient, WarningPolicy, SFTPClient
from paramiko.py3compat import u
try: try:
from paramiko import GSS_AUTH_AVAILABLE from paramiko.util import u
except ImportError:
from paramiko.py3compat import u # type: ignore
try:
from paramiko import GSS_AUTH_AVAILABLE # type: ignore
except ImportError: except ImportError:
from paramiko.ssh_gss import GSS_AUTH_AVAILABLE from paramiko.ssh_gss import GSS_AUTH_AVAILABLE
from platypush import Response from platypush import Response
from platypush.message.response.ssh import SSHKeygenResponse
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
from platypush.plugins.ssh.tunnel.forward import forward_tunnel from platypush.plugins.ssh.tunnel.forward import forward_tunnel
from platypush.plugins.ssh.tunnel.reverse import reverse_tunnel, close_tunnel from platypush.plugins.ssh.tunnel.reverse import reverse_tunnel, close_tunnel
@ -44,8 +47,8 @@ class SshPlugin(Plugin):
self, key_file: Optional[str] = None, passphrase: Optional[str] = None, **kwargs self, key_file: Optional[str] = None, passphrase: Optional[str] = None, **kwargs
): ):
""" """
:param key_file: Default key file (default: any "id_rsa", "id_dsa", "id_ecdsa", or "id_ed25519" key discoverable :param key_file: Default key file (default: any "id_rsa", "id_dsa",
in ``~/.ssh/``. "id_ecdsa", or "id_ed25519" key discoverable in ``~/.ssh/``.
:param passphrase: Key file passphrase (default: None). :param passphrase: Key file passphrase (default: None).
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
@ -71,38 +74,46 @@ class SshPlugin(Plugin):
if host.find('@') >= 0: if host.find('@') >= 0:
user, host = host.split('@') user, host = host.split('@')
if host.find(':') >= 0: if host.find(':') >= 0:
host, port = host.split(':') host, p = host.split(':')
port = int(p)
if not user: if not user:
user = getpass.getuser() user = getpass.getuser()
return host, int(port), user return host, port, user
# noinspection PyShadowingBuiltins
@action @action
def keygen( def keygen(
self, self,
filename: str, filename: str,
type: str = 'rsa', type: str = 'rsa', # pylint: disable=redefined-builtin
bits: int = 4096, bits: int = 4096,
comment: Optional[str] = None, comment: Optional[str] = None,
passphrase: Optional[str] = None, passphrase: Optional[str] = None,
) -> SSHKeygenResponse: ) -> dict:
""" """
Generate an SSH keypair. Generate an SSH keypair.
:param filename: Output file name for the private key (the public key will be stored in <filename>.pub). :param filename: Output file name for the private key (the public key
will be stored in <filename>.pub).
:param type: Encryption algorithm, either "rsa" or "dsa" (default: "rsa"). :param type: Encryption algorithm, either "rsa" or "dsa" (default: "rsa").
:param bits: Key length in bits (default: 4096). :param bits: Key length in bits (default: 4096).
:param comment: Key comment (default: None). :param comment: Key comment (default: None).
:param passphrase: Key passphrase (default: None). :param passphrase: Key passphrase (default: None).
:return: :class:`platypush.message.response.ssh.SSHKeygenResponse`. :return:
.. code-block:: json
{
"fingerprint": "SHA256:...",
"key_file": "private_key_file",
"pub_key_file": "public_key_file"
}
""" """
assert type != 'dsa' or bits <= 1024, 'DSA keys support a maximum of 1024 bits' assert type != 'dsa' or bits <= 1024, 'DSA keys support a maximum of 1024 bits'
assert ( assert (
type in self.key_dispatch_table type in self.key_dispatch_table
), 'No such type: {}. Available types: {}'.format( ), f'No such type: {type}. Available types: {self.key_dispatch_table.keys()}'
type, self.key_dispatch_table.keys()
)
if filename: if filename:
filename = os.path.abspath(os.path.expanduser(filename)) filename = os.path.abspath(os.path.expanduser(filename))
@ -110,22 +121,23 @@ class SshPlugin(Plugin):
prv = self.key_dispatch_table[type].generate(bits=bits) prv = self.key_dispatch_table[type].generate(bits=bits)
prv.write_private_key_file(filename=filename, password=passphrase) prv.write_private_key_file(filename=filename, password=passphrase)
pub = self.key_dispatch_table[type](filename=filename, password=passphrase) pub = self.key_dispatch_table[type](filename=filename, password=passphrase)
pub_file = '{filename}.pub'.format(filename=filename) pub_file = f'{filename}.pub'
with open(pub_file, 'w') as f: with open(pub_file, 'w') as f:
f.write('{name} {key}'.format(name=pub.get_name(), key=pub.get_base64())) f.write(f'{pub.get_name()} {pub.get_base64()}')
if comment: if comment:
f.write(' ' + comment) f.write(' ' + comment)
hash = u(hexlify(pub.get_fingerprint())) return {
return SSHKeygenResponse( 'fingerprint': u(hexlify(pub.get_fingerprint())),
fingerprint=hash, key_file=filename, pub_key_file=pub_file 'key_file': filename,
) 'pub_key_file': pub_file,
}
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
try: try:
return super().run(*args, **kwargs) return super().run(*args, **kwargs)
except Exception as e: except Exception as e:
raise AssertionError(e) raise AssertionError(e) from e
def _connect( def _connect(
self, self,
@ -144,9 +156,10 @@ class SshPlugin(Plugin):
key = (host, port, user) key = (host, port, user)
if key in self._sessions: if key in self._sessions:
self.logger.info( self.logger.info(
'[Connect] The SSH session is already active: {user}@{host}:{port}'.format( '[Connect] The SSH session is already active: %s@%s:%d',
user=user, host=host, port=port user,
) host,
port,
) )
return self._sessions[key] return self._sessions[key]
@ -178,7 +191,7 @@ class SshPlugin(Plugin):
return client return client
except Exception as e: except Exception as e:
self.logger.exception(e) self.logger.exception(e)
raise AssertionError('Connection to {} failed: {}'.format(host, str(e))) raise AssertionError(f'Connection to {host} failed: {type(e)}: {e}')
@action @action
def connect( def connect(
@ -231,9 +244,7 @@ class SshPlugin(Plugin):
key = (host, port, user) key = (host, port, user)
if key not in self._sessions: if key not in self._sessions:
self.logger.info( self.logger.info(
'[Disconnect] The SSH session is not active: {user}@{host}:{port}'.format( '[Disconnect] The SSH session is not active: %s@%s:%d', user, host, port
user=user, host=host, port=port
)
) )
session = self._sessions[key] session = self._sessions[key]
@ -269,13 +280,13 @@ class SshPlugin(Plugin):
def decode(buf: bytes) -> str: def decode(buf: bytes) -> str:
try: try:
buf = buf.decode() s_buf = buf.decode()
except (ValueError, TypeError): except (ValueError, TypeError):
buf = base64.encodebytes(buf).decode() s_buf = base64.encodebytes(buf).decode()
if buf.endswith('\n'): if s_buf.endswith('\n'):
buf = buf[:-1] s_buf = s_buf[:-1]
return buf return s_buf
try: try:
_in, _out, _err = client.exec_command(cmd, timeout=timeout, environment=env) _in, _out, _err = client.exec_command(cmd, timeout=timeout, environment=env)
@ -300,6 +311,9 @@ class SshPlugin(Plugin):
@staticmethod @staticmethod
def is_directory(sftp: SFTPClient, path: str) -> bool: def is_directory(sftp: SFTPClient, path: str) -> bool:
f = sftp.lstat(path) f = sftp.lstat(path)
if f.st_mode is None:
return False
return S_ISDIR(f.st_mode) return S_ISDIR(f.st_mode)
@classmethod @classmethod
@ -308,7 +322,7 @@ class SshPlugin(Plugin):
folders = [] folders = []
for f in sftp.listdir_attr(path): for f in sftp.listdir_attr(path):
if S_ISDIR(f.st_mode): if f.st_mode is not None and S_ISDIR(f.st_mode):
folders.append(f.filename) folders.append(f.filename)
else: else:
files.append(f.filename) files.append(f.filename)
@ -317,8 +331,7 @@ class SshPlugin(Plugin):
for folder in folders: for folder in folders:
new_path = os.path.join(path, folder) new_path = os.path.join(path, folder)
for x in cls.sftp_walk(sftp, new_path): yield from cls.sftp_walk(sftp, new_path)
yield x
def sftp_get( def sftp_get(
self, self,
@ -341,10 +354,9 @@ class SshPlugin(Plugin):
for file in files: for file in files:
self.logger.info( self.logger.info(
'Downloading file {} from {} to {}'.format( 'Downloading file %s from %s to %s', file, path, new_local_path
file, path, new_local_path
)
) )
self.sftp_get( self.sftp_get(
sftp, sftp,
os.path.join(remote_path, path, file), os.path.join(remote_path, path, file),
@ -421,16 +433,17 @@ class SshPlugin(Plugin):
try: try:
sftp.mkdir(remote_path) sftp.mkdir(remote_path)
except Exception as e: except Exception as e:
self.logger.warning(f'mkdir {remote_path}: {e}') self.logger.warning(
'mkdir %s failed: %s: %s', remote_path, type(e), e
)
assert ( assert (
recursive recursive
), '{} is a directory but recursive has been set to False'.format( ), f'{local_path} is a directory but recursive has been set to False'
local_path
)
assert self.is_directory( assert self.is_directory(
sftp, remote_path sftp, remote_path
), '{} is not a directory on the remote host'.format(remote_path) ), f'{remote_path} is not a directory on the remote host'
sftp.chdir(remote_path) sftp.chdir(remote_path)
os.chdir(local_path) os.chdir(local_path)
@ -439,12 +452,14 @@ class SshPlugin(Plugin):
try: try:
sftp.mkdir(path) sftp.mkdir(path)
except Exception as e: except Exception as e:
self.logger.warning(f'mkdir {remote_path}: {e}') self.logger.warning(
'mkdir %s failed: %s: %s', remote_path, type(e), e
)
for file in files: for file in files:
src = os.path.join(path, file) src = os.path.join(path, file)
dst = os.path.join(path, file) dst = os.path.join(path, file)
self.logger.info('Copying {} to {}'.format(src, dst)) self.logger.info('Copying %s to %s', src, dst)
sftp.put(src, dst) sftp.put(src, dst)
else: else:
if self.is_directory(sftp, remote_path): if self.is_directory(sftp, remote_path):
@ -475,7 +490,9 @@ class SshPlugin(Plugin):
client = self._connect(**kwargs) client = self._connect(**kwargs)
sftp = client.open_sftp() sftp = client.open_sftp()
def get_file_type(st_mode: int) -> str: def get_file_type(st_mode: Optional[int]) -> str:
if st_mode is None:
return 'unknown'
if S_ISDIR(st_mode): if S_ISDIR(st_mode):
return 'directory' return 'directory'
elif S_ISBLK(st_mode): elif S_ISBLK(st_mode):
@ -503,16 +520,24 @@ class SshPlugin(Plugin):
'longname': f.longname, 'longname': f.longname,
'attributes': f.attr, 'attributes': f.attr,
'type': get_file_type(f.st_mode), 'type': get_file_type(f.st_mode),
'access_time': datetime.datetime.fromtimestamp(f.st_atime), 'access_time': (
'modify_time': datetime.datetime.fromtimestamp(f.st_mtime), datetime.datetime.fromtimestamp(f.st_atime)
if f.st_atime
else None
),
'modify_time': (
datetime.datetime.fromtimestamp(f.st_mtime)
if f.st_mtime
else None
),
'uid': f.st_uid, 'uid': f.st_uid,
'gid': f.st_gid, 'gid': f.st_gid,
'size': f.st_size, 'size': f.st_size,
} }
for f in sftp.listdir_attr(path) for f in sftp.listdir_attr(path)
} }
else:
return sftp.listdir(path) return sftp.listdir(path)
finally: finally:
if not keep_alive: if not keep_alive:
host, port, user = self._get_host_port_user(**kwargs) host, port, user = self._get_host_port_user(**kwargs)
@ -692,7 +717,7 @@ class SshPlugin(Plugin):
sftp = client.open_sftp() sftp = client.open_sftp()
try: try:
return sftp.getcwd() return sftp.getcwd() or '/'
finally: finally:
if not keep_alive: if not keep_alive:
host, port, user = self._get_host_port_user(**kwargs) host, port, user = self._get_host_port_user(**kwargs)
@ -704,24 +729,29 @@ class SshPlugin(Plugin):
local_port: int, local_port: int,
remote_host: str, remote_host: str,
remote_port: int, remote_port: int,
bind_addr: Optional[str] = '', bind_addr: str = '',
**kwargs, **kwargs,
): ):
""" """
Start an SSH forward tunnel, tunnelling <local_port> to <remote_host>:<remote_port>. Start an SSH forward tunnel, tunnelling ``<local_port>`` to
``<remote_host>:<remote_port>``.
:param local_port: Local port. :param local_port: Local port.
:param remote_host: Remote host. :param remote_host: Remote host.
:param remote_port: Remote port. :param remote_port: Remote port.
:param bind_addr: If set, the `local_port` will be bound to this address/subnet (default: '', or 0.0.0.0: any). :param bind_addr: If set, the `local_port` will be bound to this
:param kwargs: Arguments for :meth:`platypush.plugins.ssh.SshPlugin.connect`. address/subnet (default: '', or 0.0.0.0: any).
:param kwargs: Arguments for
:meth:`platypush.plugins.ssh.SshPlugin.connect`.
""" """
key = local_port, remote_host, remote_port key = local_port, remote_host, remote_port
if key in self._fwd_tunnels: if key in self._fwd_tunnels:
self.logger.info( self.logger.info(
'The tunnel {}:{}:{}:{} is already active'.format( 'The tunnel %s:%d:%s:%d is already active',
bind_addr, local_port, remote_host, remote_port bind_addr,
) local_port,
remote_host,
remote_port,
) )
return return
@ -753,9 +783,7 @@ class SshPlugin(Plugin):
key = (local_port, remote_host, remote_port) key = (local_port, remote_host, remote_port)
if key not in self._fwd_tunnels: if key not in self._fwd_tunnels:
self.logger.warning( self.logger.warning(
'No such forward tunnel: {}:{}:{}'.format( 'No such forward tunnel: %d:%s:%d', local_port, remote_host, remote_port
local_port, remote_host, remote_port
)
) )
return return
@ -772,7 +800,7 @@ class SshPlugin(Plugin):
server_port: int, server_port: int,
remote_host: str, remote_host: str,
remote_port: int, remote_port: int,
bind_addr: Optional[str] = '', bind_addr: str = '',
**kwargs, **kwargs,
): ):
""" """
@ -788,9 +816,11 @@ class SshPlugin(Plugin):
key = server_port, remote_host, remote_port key = server_port, remote_host, remote_port
if key in self._fwd_tunnels: if key in self._fwd_tunnels:
self.logger.info( self.logger.info(
'The tunnel {}:{}:{}:{} is already active'.format( 'The tunnel %s:%d:%s:%d is already active',
bind_addr, server_port, remote_host, remote_port bind_addr,
) server_port,
remote_host,
remote_port,
) )
return return
@ -823,9 +853,10 @@ class SshPlugin(Plugin):
key = (server_port, remote_host, remote_port) key = (server_port, remote_host, remote_port)
if key not in self._rev_tunnels: if key not in self._rev_tunnels:
self.logger.warning( self.logger.warning(
'No such reversed tunnel: {}:{}:{}'.format( 'No such reverse tunnel: %d:%s:%d',
server_port, remote_host, remote_port server_port,
) remote_host,
remote_port,
) )
return return