Source code for tbot.machine.linux.ssh.machine

# tbot, Embedded Automation Tool
# Copyright (C) 2018  Harald Seiler
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import abc
import typing
from tbot import log_event
from tbot.machine import channel, linux
from tbot.machine.linux import auth


[docs]class SSHMachine(linux.LinuxMachine): """Generic machine that can be reached via SSH from the LabHost.""" @property def ignore_hostkey(self) -> bool: """ Ignore host key. Set this to true if the remote changes its host key often. """ return False @property @abc.abstractmethod def hostname(self) -> str: """ Return the hostname of this machine. :rtype: str """ pass @property def username(self) -> str: """ Return the username for logging in on this machine. Defaults to the username on the labhost. """ return self.labhost.username @property def authenticator(self) -> auth.Authenticator: """ Return an authenticator that allows logging in on this machine. .. danger:: It is strongly advised to use key authentication. If you use password auth, **THE PASSWORD WILL BE LEAKED** and **MIGHT EASILY BE STOLEN** by other users on your labhost. It will also be visible in the log file. If you decide to use this, you're doing this on your own risk. The only case where I support using passwords is when connecting to a test board with a default password. :rtype: tbot.machine.linux.auth.Authenticator """ return auth.NoneAuthenticator() @property def port(self) -> int: """ Return the port the SSH server is listening on. :rtype: int """ return 22 @property def ssh_config(self) -> typing.List[str]: """ Add additional ssh config options when connecting. **Example**:: class MySSHMach(linux.SSHMachine): ssh_config = ["ProxyJump=foo@example.com"] :rtype: list(str) .. versionadded:: 0.6.2 """ return [] def __repr__(self) -> str: return f"<{self.__class__.__name__} {self.username}@{self.hostname}:{self.port} (Lab: {self.labhost!r}>" def _connect(self) -> channel.Channel: chan = self.labhost.new_channel() hk_disable = ["-o", "StrictHostKeyChecking=no"] if self.ignore_hostkey else [] authenticator = self.authenticator if isinstance(authenticator, auth.PasswordAuthenticator): # Careful, password WILL BE LEAKED! cmd = ["sshpass", "-p", authenticator.password, "ssh"] elif isinstance(authenticator, auth.PrivateKeyAuthenticator): cmd = ["ssh", "-o", "BatchMode=yes", "-i", authenticator.key_file] elif isinstance(authenticator, auth.NoneAuthenticator): cmd = ["ssh", "-o", "BatchMode=yes"] else: raise RuntimeError(f"{authenticator!r} is not supported for SSH hosts!") cmd_str = self.build_command( *cmd, *hk_disable, *["-p", str(self.port)], *[arg for opt in self.ssh_config for arg in ["-o", opt]], f"{self.username}@{self.hostname}", ) try: with log_event.command(self.labhost.name, cmd_str) as ev: send_str = cmd_str + "; exit\n" chan.send(send_str) # Read back command chan.recv_n(len(send_str) + 1) head = b"" try: while True: head += chan.recv(timeout=0.1) except TimeoutError: pass finally: try: ev.write(head.decode("utf-8")) except UnicodeDecodeError: ev.write(head.decode("latin1")) # Reinitialize channel after ssh connection is established chan.initialize() except channel.ChannelClosedException: raise RuntimeError("SSH connection failed: Remote closed port") return chan def __init__(self, labhost: linux.LabHost) -> None: """ Connect to this SSH machine. :param tbot.machine.linux.LabHost labhost: LabHost from where to attempt connecting """ super().__init__() self.labhost = labhost self.channel = self._connect() def _obtain_channel(self) -> channel.Channel: return self.channel
[docs] def destroy(self) -> None: """Destory this SSHMachine instance.""" self.channel.close()
@property def lh(self) -> linux.LabHost: """Return the lab-host that was used to establish this machines connection.""" return self.labhost