Source code for tbot.machine.connector.ssh

# tbot, Embedded Automation Tool
# Copyright (C) 2019  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 contextlib
import typing

import tbot
from . import connector
from .. import linux, channel
from ..linux import auth

Self = typing.TypeVar("Self", bound="SSHConnector")


[docs]class SSHConnector(connector.Connector): """ Connect to remote using ``ssh`` by starting off from an existing machine. An :py:class:`SSHConnector` is different from a :py:class:`ParamikoConnector` as it requires an existing machine to start the connection from. This allows jumping via one host to a second. **Example**: .. code-block:: python import tbot from tbot.machine import connector, linux # Connect into a container running on the (possibly remote) lab-host class MyRemote( connector.SSHConnector, linux.Bash, ): hostname = "localhost" port = 20220 username = "root" with tbot.acquire_lab() as lh: # lh might be a ParamikoConnector machine. with MyRemote(lh) as ssh_session: ssh_session.exec0("uptime") """ @property def ignore_hostkey(self) -> bool: """ Ignore host key. Set this to true if the remote changes its host key often. """ return False @property def use_multiplexing(self) -> bool: """ Whether tbot should attempt to enable connection multiplexing. Connection multiplexing is a mechanism to share a connection between multiple sessions. This can drastically speed up your tests when many connections to the same machine are opened and closed. Refer to `ControlMaster in sshd_config(5)`_ for details. .. _ControlMaster in sshd_config(5): https://man.openbsd.org/ssh_config.5#ControlMaster .. versionadded:: 0.9.0 """ 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.host.username @property def authenticator(self) -> auth.Authenticator: """ Return an authenticator that allows logging in on this machine. See :mod:`tbot.machine.linux.auth` for available authenticators. .. 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(connector.SSHConnector, linux.Bash): ssh_config = ["ProxyJump=foo@example.com"] :rtype: list(str) .. versionadded:: 0.6.2 """ return [] def __init__(self, host: typing.Optional[linux.LinuxShell] = None) -> None: self.host: linux.LinuxShell self.host = host # type: ignore
[docs] @classmethod @contextlib.contextmanager def from_context( cls: typing.Type[Self], ctx: "tbot.Context" ) -> typing.Iterator[Self]: with contextlib.ExitStack() as cx: lh = None if not issubclass(cls, ctx.get_machine_class(tbot.role.LabHost)): lh = cx.enter_context(ctx.request(tbot.role.LabHost)) m = cx.enter_context(cls(lh)) # type: ignore yield typing.cast(Self, m)
@contextlib.contextmanager def _connect(self) -> typing.Iterator[channel.Channel]: with contextlib.ExitStack() as cx: if self.host is None: self.host = cx.enter_context(tbot.acquire_local()) h = cx.enter_context(self.host.clone()) authenticator = self.authenticator if isinstance(authenticator, auth.NoneAuthenticator): cmd = ["ssh", "-o", "BatchMode=yes"] elif isinstance(authenticator, auth.PrivateKeyAuthenticator): cmd = [ "ssh", "-o", "BatchMode=yes", "-i", authenticator.get_key_for_host(h), ] elif isinstance(authenticator, auth.PasswordAuthenticator): cmd = ["sshpass", "-p", authenticator.password, "ssh"] else: if typing.TYPE_CHECKING: authenticator._undefined_marker raise ValueError(f"Unknown authenticator {authenticator!r}") hk_disable = ( ["-o", "StrictHostKeyChecking=no"] if self.ignore_hostkey else [] ) multiplexing = [] if self.use_multiplexing: multiplexing_dir = self.host.workdir / ".ssh-multi" self.host.exec0("mkdir", "-p", multiplexing_dir) multiplexing += ["-o", "ControlMaster=auto"] multiplexing += ["-o", "ControlPersist=10m"] multiplexing += [ "-o", f"ControlPath={multiplexing_dir.at_host(self.host)}/%C", ] with h.open_channel( *cmd, *hk_disable, *multiplexing, *["-p", str(self.port)], *[arg for opt in self.ssh_config for arg in ["-o", opt]], f"{self.username}@{self.hostname}", ) as ch: yield ch
[docs] def clone(self) -> "SSHConnector": """Clone this machine.""" new = type(self)(self.host) new._orig = self._orig or self return new