tbot.machine.connector

Connectors are one of the three parts of a machine: The connector is responsible for establishing the initial channel for a machine. This can work in many different ways, either a simple subprocess, an ssh-connection, a serial-console device, or a telnet session.

All connectors should inherit from the Connector base-class.

Provided Connectors

For a lot of commonly used cases, tbot already has connectors at hand. These are:

Subprocess

class tbot.machine.connector.SubprocessConnector[source]

Bases: Connector

Connector using a subprocess shell.

Arguably the simplest connector; simply spawns a subprocess with a shell. This is the connector used by the default local lab-host.

Example:

from tbot.machine import connector, linux

class MyMachine(
    connector.SubprocessConnector,
    linux.Bash,
):
    pass

with MyMachine() as localhost:
    localhost.exec0("echo", "Hello!")
classmethod from_context(ctx: Context) Iterator[M][source]

Create this machine from a tbot context.

This method defines how tbot can automatically attempt creating this machine from a given context. It is usually defined by the connector but might be overridden by board config in certain more complex scenarios.

This method must return a context-manager that, upon entering, yields a fully initialized machine. In practical terms this means, the implementation must enter the “machine’s context” as well. As an example, the most basic implementation would look like this:

@contextlib.contextmanager
def from_context(cls, ctx):
    # Create instance and enter its context, in this example, no
    # args are passed to the constructor ...
    with cls() as m:
        yield m
clone() M[source]

Clone this machine.

ch: Channel

Channel to communicate with this machine.

Warning

Please refrain from interacting with the channel directly. Instead, write a Shell that wraps around the channel interaction. That way, the state of the channel is only managed in a single place and you won’t have to deal with nasty bugs when multiple parties make assumptions about the state of the channel.

Paramiko

class tbot.machine.connector.ParamikoConnector(other: ParamikoConnector | None = None)[source]

Bases: Connector

Connect to an ssh server using Paramiko.

When inheriting from this connector, you should overwrite the attributes documented below to make it connect to your remote.

Example:

from tbot.machine import connector, linux

class MyMachine(
    connector.ParamikoConnector,
    linux.Bash,
):
    hostname = "78.79.32.85"
    username = "tbot-user"

with MyMachine() as remotehost:
    remotehost.exec0("uname", "-a")
Parameters:

other (ParamikoConnector) – Build this connection by opening a new channel in an existing ssh-connection.

abstract property hostname: str

Hostname of this remote.

You must always specify this parameter in your Lab config!

property username: str

Username to log in as.

Defaults to the username from ~/.ssh/config or the local username.

property authenticator: PasswordAuthenticator | PrivateKeyAuthenticator | NoneAuthenticator | UndefinedAuthenticator

Return an authenticator that allows logging in on this machine.

See tbot.machine.linux.auth for available authenticators.

Return type:

tbot.machine.linux.auth.Authenticator

property port: int

Port the remote SSH server is listening on.

Defaults to 22 or the value of Port in ~/.ssh/config.

property ignore_hostkey: bool

Ignore remote host key.

Set this to true if the remote changes its host key often.

Defaults to False or the value of StrictHostKeyChecking in ~/.ssh/config.

classmethod from_context(ctx: Context) Iterator[Self][source]

Create this machine from a tbot context.

This method defines how tbot can automatically attempt creating this machine from a given context. It is usually defined by the connector but might be overridden by board config in certain more complex scenarios.

This method must return a context-manager that, upon entering, yields a fully initialized machine. In practical terms this means, the implementation must enter the “machine’s context” as well. As an example, the most basic implementation would look like this:

@contextlib.contextmanager
def from_context(cls, ctx):
    # Create instance and enter its context, in this example, no
    # args are passed to the constructor ...
    with cls() as m:
        yield m
ch: Channel

Channel to communicate with this machine.

Warning

Please refrain from interacting with the channel directly. Instead, write a Shell that wraps around the channel interaction. That way, the state of the channel is only managed in a single place and you won’t have to deal with nasty bugs when multiple parties make assumptions about the state of the channel.

clone() Self[source]

Clone this machine.

Note that an ssh-session cannot hold an unlimited number of channels so cloning too much might lead to issues. The exact limit is dependent on the server configuration.

Serial Console

class tbot.machine.connector.ConsoleConnector(mach: LinuxShell)[source]

Bases: Connector

Connector for serial-consoles.

As this can work in many different ways, this connector is intentionally as generic as possible. To configure a serial connection, you need to implement the ConsoleConnector.connect() method. That methods gets a lab-host channel which it should transform into a channel connected to the board’s serial console.

Example:

import tbot
from tbot.machine import board, connector

class MyBoard(
    connector.ConsoleConnector,
    board.Board,
):
    def connect(self, mach):
        return mach.open_channel("picocom", "-b", "115200", "/dev/ttyACM0")

with tbot.acquire_local() as lo:
    with MyBoard(lo) as b:
        ...
Parameters:

mach (LinuxShell) – A cloneable lab-host machine. The ConsoleConnector will try to clone this machine’s connection and use that to connect to the board. This means that you have to make sure you give the correct lab-host for your respective board to the constructor here.

abstract connect(mach: LinuxShell) AbstractContextManager[Channel][source]

Connect a machine to the serial console.

Overwrite this method with the necessary logic to connect the given machine mach to a channel connected to the board’s serial console.

In most cases you’ll accomplish this using mach.open_channel(...).

classmethod from_context(ctx: Context) Iterator[M][source]

Create this machine from a tbot context.

This method defines how tbot can automatically attempt creating this machine from a given context. It is usually defined by the connector but might be overridden by board config in certain more complex scenarios.

This method must return a context-manager that, upon entering, yields a fully initialized machine. In practical terms this means, the implementation must enter the “machine’s context” as well. As an example, the most basic implementation would look like this:

@contextlib.contextmanager
def from_context(cls, ctx):
    # Create instance and enter its context, in this example, no
    # args are passed to the constructor ...
    with cls() as m:
        yield m
clone() M[source]

This machine is not cloneable.

ch: Channel

Channel to communicate with this machine.

Warning

Please refrain from interacting with the channel directly. Instead, write a Shell that wraps around the channel interaction. That way, the state of the channel is only managed in a single place and you won’t have to deal with nasty bugs when multiple parties make assumptions about the state of the channel.

Plain SSH

class tbot.machine.connector.SSHConnector(host: LinuxShell | None = None)[source]

Bases: Connector

Connect to remote using ssh by starting off from an existing machine.

An SSHConnector is different from a ParamikoConnector as it requires an existing machine to start the connection from. This allows jumping via one host to a second.

Example:

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 ignore_hostkey: bool

Ignore host key.

Set this to true if the remote changes its host key often.

property use_multiplexing: 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.

New in version 0.9.0.

abstract property hostname: str

Return the hostname of this machine.

Return type:

str

property username: str

Return the username for logging in on this machine.

Defaults to the username on the labhost.

property authenticator: PasswordAuthenticator | PrivateKeyAuthenticator | NoneAuthenticator | UndefinedAuthenticator

Return an authenticator that allows logging in on this machine.

See 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.

Return type:

tbot.machine.linux.auth.Authenticator

property port: int

Return the port the SSH server is listening on.

Return type:

int

property ssh_config: List[str]

Add additional ssh config options when connecting.

Example:

class MySSHMach(connector.SSHConnector, linux.Bash):
    ssh_config = ["ProxyJump=foo@example.com"]
Return type:

list(str)

New in version 0.6.2.

classmethod from_context(ctx: Context) Iterator[Self][source]

Create this machine from a tbot context.

This method defines how tbot can automatically attempt creating this machine from a given context. It is usually defined by the connector but might be overridden by board config in certain more complex scenarios.

This method must return a context-manager that, upon entering, yields a fully initialized machine. In practical terms this means, the implementation must enter the “machine’s context” as well. As an example, the most basic implementation would look like this:

@contextlib.contextmanager
def from_context(cls, ctx):
    # Create instance and enter its context, in this example, no
    # args are passed to the constructor ...
    with cls() as m:
        yield m
clone() SSHConnector[source]

Clone this machine.

ch: Channel

Channel to communicate with this machine.

Warning

Please refrain from interacting with the channel directly. Instead, write a Shell that wraps around the channel interaction. That way, the state of the channel is only managed in a single place and you won’t have to deal with nasty bugs when multiple parties make assumptions about the state of the channel.

Base-Class

class tbot.machine.connector.Connector[source]

Bases: Machine

Base-class for machine connectors.

abstract _connect() AbstractContextManager[Channel][source]

Establish the channel.

This method will be called during machine-initialization and should yield a channel which will then be used for the machine.

This method’s return type is annotated as typing.ContextManager[channel.Channel], to allow more complex setup & teardown. As channels implement the context-manager protocol, simple connectors can just return the channel. A more complex connector can use the following pattern:

import contextlib

class MyConnector(Connector):
    @contextlib.contextmanager
    def _connect(self) -> typing.Iterator[channel.Channel]:
        try:
            # Do setup
            ...
            yield ch
        finally:
            # Do teardown
            ...
classmethod from_context(ctx: Context) AbstractContextManager[Self][source]

Create this machine from a tbot context.

This method defines how tbot can automatically attempt creating this machine from a given context. It is usually defined by the connector but might be overridden by board config in certain more complex scenarios.

This method must return a context-manager that, upon entering, yields a fully initialized machine. In practical terms this means, the implementation must enter the “machine’s context” as well. As an example, the most basic implementation would look like this:

@contextlib.contextmanager
def from_context(cls, ctx):
    # Create instance and enter its context, in this example, no
    # args are passed to the constructor ...
    with cls() as m:
        yield m
abstract clone() Self[source]

Create a duplicate of this machine.

For a lot of connections, it is trivial to open a second one in parallel. This can be exploited to easily connect further from one host to the next, thus building a tunnel.

On the other hand, a serial connection to a board is unique and can’t be cloned. Such connectors should raise an exception is .clone() is called.

Note

Important: You should always set the new machines _orig attribute to the original machine (either self._orig or, if that is None, self) so tbot knows these machines belong together! The common pattern is:

def clone(self):
    new = ...
    new._orig = self._orig or self
    return new

Not setting _orig means that tbot will treat the new and old instances as separate machines which (theoretically) can’t interact with each other.

ch: Channel

Channel to communicate with this machine.

Warning

Please refrain from interacting with the channel directly. Instead, write a Shell that wraps around the channel interaction. That way, the state of the channel is only managed in a single place and you won’t have to deal with nasty bugs when multiple parties make assumptions about the state of the channel.