tbot.machine.board

This module contains definitions for interacting with embedded hardware. tbot models hardware in a ‘layered’ approach:

  • The underlying physical hardware is called a board. The board class defines how tbot can connect to the board’s console, how to turn power on and off, etc.

  • Ontop of the board, the software ‘layer’ is defined. This might be a U-Boot, a Linux directly, or a Linux ontop of a U-Boot. It could also be something completely custom if you implement the relevant Initializer s and Shell s.

Each of these ‘layers’ is its own machine. See the docs for:

Board Hardware

The board-machine should inherit from tbot.machine.board.Board and define the pysical access for the hardware. This means the connector will most likely be a ConsoleConnector and it might make use of the tbot.machine.board.PowerControl initializer to turn on/off power for the board.

Example:

from tbot.machine import connector, board, linux

class MyBoard(
    connector.ConsoleConnector,
    board.PowerControl,
    board.Board,
):
    # TODO
class tbot.machine.board.Board[source]

Bases: RawShell

Base class for board-machines.

This class does nothing special except providing the .interactive() method for directly interacting with the serial-console.

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.

Board Initializers

class tbot.machine.board.PowerControl[source]

Bases: Initializer

Machine-initializer for controlling power for a hardware.

When initializing, poweron() is called and when deinitializing, poweroff() is called.

abstract poweron() None[source]

Power-on the hardware.

If the machine is using the ConsoleConnector, you can use self.host.exec0() to run commands on the lab-host.

Example:

def poweron(self):
    self.host.exec0("power-control.sh", "on")
abstract poweroff() None[source]

Power-off the hardware.

power_check() bool[source]

Check if the board is already on and someone else might be using it.

Implementations of this function should raise an exception in case they detect the board to be on or return False. If the board is off and ready to be used, an implementation should return True.

powercycle_delay: float = 0

Delay between a poweroff and a subsequent poweron.

This delay is only accounted for when both poweroff and poweron happen in the same tbot process. If you are calling tbot multiple times really fast, the delay is not performed!

Using this attribute over, for example, a time.sleep() call in the poweroff() implementation has the advantage that the delay is not performed at the end of a tbot run, only for powercycles “in the middle”.

New in version 0.9.3.

Board Software

The ‘software layer’ machines for a board should not directly use a connector to connect to the boards console, but instead rely on the generic tbot.machine.board.Connector class. This allows cleanly separating the hardware and software and thus reusing the software machines with different hardware.

In practice, these ‘software machines’ should be defined like this:

from tbot.machine import board

class MyBoard(
    ...
    board.Board,
):
    # Hardware machine
    ...

class MyBoardSoftware(
   board.Connector,
   ...
):
   # Software machine
   ...

And then used like

with MyBoard(lh) as b:
   with MyBoardSoftware(b) as bs:
      bs.exec(...)

# Or, more consise
with MyBoard(lh) as b, MyBoardSoftware(b) as bs:
   bs.exec(...)

Board U-Boot

If you are using U-Boot you’ll probably want access to it from your testcases. This is done using a U-Boot machine. Like any software ‘layer’, a U-Boot machine should use the tbot.machine.board.Connector class. The shell should be a tbot.machine.board.UBootShell.

In case your U-Boot is configured for autoboot, you’ll want to use the optional UBootAutobootIntercept initializer as well.

Example:

class BoardUBoot(
    board.Connector,
    board.UBootShell,
):
    prompt = "U-Boot> "
class tbot.machine.board.UBootShell[source]

Bases: Shell, UbootStartup

U-Boot shell.

The interface of this shell was designed to be close to the Linux shell design. This means that U-Boot shells also provide

  • ub.escape() - Escape args for the U-Boot shell.

  • ub.exec0() - Run command and ensure it succeeded.

  • ub.exec() - Run command and return output and return code.

  • ub.test() - Run command and return boolean whether it succeeded.

  • ub.env() - Get/Set environment variables.

  • ub.interactive() - Start an interactive session for this machine.

There is also the special ub.boot() which will boot a payload and return the machine’s channel, for use in a machine for the booted payload.

boot_timeout: float | None = None

Maximum time from power-on to U-Boot shell.

If tbot can’t reach the U-Boot shell during this time, an exception will be thrown.

Maximum time from power-on to U-Boot shell.

If tbot can’t reach the U-Boot shell during this time, an exception will be thrown.

prompt: str | bytes = 'U-Boot> '

Prompt which was configured for U-Boot.

Commonly "U-Boot> ", "=> ", or "U-Boot# ".

Warning

Don’t forget the trailing space, if your prompt has one!

bootlog: str

Transcript of console output during boot.

escape(*args: str | Special) str[source]

Escape a string so it can be used safely on the U-Boot command-line.

exec(*args: str | Special) Tuple[int, str][source]

Run a command in U-Boot.

Example:

retcode, output = ub.exec("version")
assert retcode == 0
Return type:

tuple(int, str)

Returns:

A tuple with the return code of the command and its console output. The output will also contain a trailing newline in most cases.

exec0(*args: str | Special) str[source]

Run a command and assert its return code to be 0.

Example:

output = ub.exec0("version")

# This will raise an exception!
ub.exec0("false")
Return type:

str

Returns:

The command’s console output. It will also contain a trailing newline in most cases.

test(*args: str | Special) bool[source]

Run a command and return a boolean value whether it succeeded.

Example:

if ub.test("true"):
    tbot.log.message("Is correct")
Return type:

bool

Returns:

Boolean representation of commands success. True if return code was 0, False otherwise.

env(var: str, value: str | Special | None = None) str[source]

Get or set an environment variable.

Example:

# Get the value of a var
value = ub.env("bootcmd")

# Set the value of a var
lnx.env("bootargs", "loglevel=7")
Parameters:
  • var (str) – Environment variable name.

  • value (str) – Optional value to set the variable to.

Return type:

str

Returns:

Current (new) value of the environment variable.

boot(*args: str | Special) Channel[source]

Boot a payload from U-Boot.

This method will run the given command and expects it to start booting a payload. ub.boot() will then return the channel so a new machine can be built on top of it for the booted payload.

Example:

ub.env("bootargs", "loglevel=7")
ch = ub.boot("bootm", "0x10000000")
Return type:

tbot.machine.channel.Channel

interactive() None[source]

Start an interactive session on this machine.

This method will connect tbot’s stdio to the machine’s channel so you can interactively run commands. This method is used by the interactive_uboot testcase.

property ram_base: int

Return the base address of RAM for this U-Boot instance.

This address can be used as a safe bet when your testcase needs to store something in RAM.

Example:

serverip =  # ...
filepath =  # ...
ub.exec0("tftp", hex(ub.ram_base), f"{serverip}:{filepath}")
ub.exec0("iminfo", hex(ub.ram_base))
class tbot.machine.board.UBootAutobootIntercept[source]

Bases: Initializer, UbootStartup

Machine-initializer to intercept U-Boot autobooting.

The default settings for this class should work for most cases, but if a custom autoboot prompt was configured, or a special key sequence is necessary, you will have to adjust this here.

Example:

import re

class MyUBoot(
    board.Connector,
    board.UBootAutobootIntercept,
    board.UBootShell,
):
    autoboot_prompt = tbot.Re("Press DEL 4 times.{0,100}", re.DOTALL)
    autoboot_keys = "\x7f\x7f\x7f\x7f"
autoboot_prompt: bytes | BoundedPattern | Pattern | str | None = re.compile(b'autoboot:\\s{0,5}\\d{0,3}\\s{0,3}.{0,80}')

Autoboot prompt to wait for.

autoboot_keys: str | bytes = '\r'

Keys to press as soon as autoboot prompt is detected.

Board Linux

The board’s Linux machine can be configured in different ways, depending on your setup and needs.

  • If you do not need any bootloader interaction, you can define it in a way that goes straight from power-on to waiting for Linux. This is done very similarly to the U-Boot machine above, by using the board.Connector.

  • If you do need bootloader interaction (because of manual commands to boot Linux for example), you should instead use the much more powerful board.LinuxUbootConnector. This class allows to boot Linux from U-Boot, either by passing it an existing U-Boot machine, or by automatically waiting for U-Boot first and then booting Linux.

In code, the two options look like this:

Example for a standalone Linux (no bootloader interaction):

from tbot.machine import board, linux

class StandaloneLinux(
    board.Connector,
    board.LinuxBootLogin,
    linux.Bash,
):
    # No config for the connector needed

    # LinuxBootLogin handles waiting for Linux to boot & logging in
    username = "root"
    password = "hunter2"

Example for a Linux booting from U-Boot:

from tbot.machine import board, linux

class MyUBoot(board.Connector, board.UBootShell):
    ...

class LinuxFromUBoot(
    board.LinuxUbootConnector,
    board.LinuxBootLogin,
    linux.Bash,
):
    # Configuration for LinuxUbootConnector
    uboot = MyUBoot  # <- Our UBoot machine

    def do_boot(self, ub):  # <- Procedure to boot Linux
       ub.env("autoload", "false")
       ub.exec0("dhcp")
       return ub.boot("run", "nfsboot")

    # LinuxBootLogin handles waiting for Linux to boot & logging in
    username = "root"
    password = "hunter2"

With both options, you’ll use the LinuxBootLogin initializer to handle the boot and login.

class tbot.machine.board.LinuxUbootConnector(b: Board | UBootShell)[source]

Bases: Connector, LinuxBootLogin, BoardMachineBase

Connector for booting Linux from U-Boot.

This connector can either boot from a Board instance or from a UBootShell instance. If booting directly from the board, it will first initialize a U-Boot machine and then use it to kick off the boot to Linux. See above for an example.

abstract property uboot: Type[UBootShell]

U-Boot machine to use when booting directly from a Board instance.

do_boot(ub: UBootShell) Channel[source]

Boot procedure.

An implementation of this method should use the U-Boot machine given as ub to kick off the Linux boot. It should return the channel to the now booting Linux. This will in almost all cases be achieved by using the tbot.machine.board.UBootShell.boot() method.

Example:

from tbot.machine import board, linux

class LinuxFromUBoot(
    board.LinuxUbootConnector,
    board.LinuxBootLogin,
    linux.Bash,
):
    uboot = MyUBoot  # <- Our UBoot machine

    def do_boot(self, ub):  # <- Procedure to boot Linux
       # Any logic necessary to prepare for boot
       ub.env("autoload", "false")
       ub.exec0("dhcp")

       # Return the channel using ub.boot()
       return ub.boot("run", "nfsboot")

    ...
bootlog: str

Log of kernel-messages which were output during boot.

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() Self[source]

This machine cannot be cloned.

property board: Board

Returns the instance of the underlying board machine on which this machine is “running”.

class tbot.machine.board.LinuxBootLogin[source]

Bases: Initializer, LinuxBoot

Machine Initializer to wait for linux boot-up and automatically login.

Use this initializer whenever you have a serial-console for a Linux system.

Example:

from tbot.machine import board, linux

class StandaloneLinux(
    board.Connector,
    board.LinuxBootLogin,
    linux.Bash,
):
    # board.LinuxBootLogin config:
    username = "root"
    password = "hunter2"
login_prompt = 'login: '

Prompt that indicates tbot should send the username.

login_delay = 0

The delay between first occurrence of login_prompt and actual login.

This delay might be necessary if your system clutters the login prompt with log-messages during the first few seconds after boot.

password_prompt: bytes | BoundedPattern | Pattern | str = 'assword: '

Prompt that indicates tbot should send the password.

boot_timeout: float | None = None

Maximum time for Linux to reach the login prompt.

The timer starts after initiation of the boot. This may either be power-on if booting into Linux directly or the point where the boot process is initiated from the bootloader (when using LinuxUbootConnector).

New in version 0.10.0.

bootlog: str

Log of kernel-messages which were output during boot.

abstract property username: str

Username to login as.

abstract property password: str | None

Password to login with. Set to None if no password is needed.

no_password_timeout: float | None = 5.0

Timeout after which login without a password should be attempted. Set to None to disable this mechanism.

New in version 0.10.1.

class tbot.machine.board.AskfirstInitializer[source]

Bases: Initializer, LinuxBoot

Initializer to deal with askfirst TTYs.

On some boards, the console is configured with askfirst which means that the getty for logging in is only spawned after an initial ENTER is sent. This initializer takes care of that by first waiting for the askfirst prompt and then sending ENTER.

The askfirst prompt can be customized with the askfirst_prompt attribute.

Example:

from tbot.machine import board, linux

class StandaloneLinux(
    board.Connector,
    board.AskfirstInitializer,
    board.LinuxBootLogin,
    linux.Bash,
):
    # board.LinuxBootLogin config:
    username = "root"
    password = "hunter2"

New in version 0.10.6.

askfirst_prompt = 'Please press Enter to activate this console.'

Prompt that indicates the board is waiting for askfirst confirmation.