Source code for tbot.machine.board.linux

# 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
import tbot
from tbot.machine import board, channel, linux
from . import special

B = typing.TypeVar("B", bound=board.Board)
Self = typing.TypeVar("Self", bound="LinuxMachine")


class LinuxMachine(linux.LinuxMachine, board.BoardMachine[B]):
    """Abstract base class for board linux machines."""

    login_prompt = "login:"
    """Prompt that indicates tbot should send the username."""

    login_delay = 0
    """The delay between first occurence of login_prompt and actual login."""

    @property
    @abc.abstractmethod
    def shell(self) -> typing.Type[linux.shell.Shell]:
        """Shell that is in use on the board."""
        pass

    @property
    def name(self) -> str:
        """Name of this linux machine."""
        return self.board.name + "-linux"

    @property
    def workdir(self: Self) -> "linux.Path[Self]":  # noqa: D102
        return linux.Path(self, "/tmp")

    @property
    @abc.abstractmethod
    def password(self) -> typing.Optional[str]:
        """
        Password for logging in.

        Can be :class:`None` to indicate that no password is needed.
        """
        pass

    def __init__(self, b: typing.Union[B, board.UBootMachine[B]]) -> None:
        """Create a new instance of this LinuxMachine."""
        self.bootlog = ""

        # `login_wait` is deprecated
        if hasattr(self, "login_wait"):
            tbot.log.warning(
                f"""\
`{self.__class__.__name__}` defines `login_wait`, which is no longer in use!
    Please remove it from your board-config."""
            )

        super().__init__(b.board if isinstance(b, board.UBootMachine) else b)

    def _boot_to_shell(self, stream: typing.TextIO) -> str:
        """
        Wait for the login prompt.

        You are not supposed to call this function from testcases!
        """
        chan = self._obtain_channel()
        output = ""

        output += chan.read_until_prompt(
            self.login_prompt, stream=stream, must_end=False
        )

        # On purpose do not login immediately as we may get some
        # console flooding from upper SW layers (and tbot's console setup
        # may get broken)
        if self.password is None and self.login_delay != 0:
            try:
                output += chan.read_until_prompt(
                    "", stream=stream, timeout=self.login_delay
                )
            except TimeoutError:
                pass
            chan.send("\n")

        chan.send(self.username + "\n")
        if self.password is not None:
            chan.read_until_prompt("word: ", stream=stream, must_end=False)
            chan.send(self.password + "\n")
            stream.write("****\n")
            output += "Password: ****\n"
        else:
            stream.write(f"{self.username}\n")

        while True:
            chan.send("echo TBOT''LOGIN\n")
            try:
                chan.read_until_prompt("TBOTLOGIN", timeout=0.2, must_end=False)
            except TimeoutError:
                pass
            else:
                break

        chan.initialize(sh=self.shell)

        self.bootlog = output
        return output


[docs]class LinuxWithUBootMachine(LinuxMachine[B]): """ Linux machine that boots from U-Boot. **Example Implementation**:: from tbot.machine import board class MyBoard(board.Board): ... class MyBoardUBoot(board.UBootMachine[MyBoard]): ... class MyBoardLinux(board.LinuxWithUBootMachine[MyBoard]): uboot = MyBoardUBoot username = "root" password = None boot_commands = [ ["tftp", board.Env("loadaddr"), "zImage"], ["bootz", board.Env("loadaddr")], ] BOARD = MyBoard UBOOT = MyBoardUBoot LINUX = MyBoardLinux :ivar str bootlog: Messages that were printed out during startup. You can access this attribute inside your testcases to get info about what was going on during boot. .. versionadded:: 0.6.2 """ boot_commands: typing.Optional[ typing.List[typing.List[typing.Union[str, special.Special]]] ] = None """ List of commands to boot Linux from U-Boot. Commands are a list, of the arguments that are given to :meth:`~tbot.machine.board.UBootMachine.exec0` By default, ``do_boot`` is called, but if ``boot_commands`` is defined, it will be used instead (and ``do_boot`` is ignored). **Example**:: boot_commands = [ ["setenv", "console", "ttyS0"], ["setenv", "baudrate", str(115200)], [ "setenv", "bootargs", board.Raw("console=${console},${baudrate}"), ], ["tftp", board.Env("loadaddr"), "zImage"], ["bootz", board.Env("loadaddr")], ] """
[docs] def do_boot( self, ub: board.UBootMachine[B] ) -> typing.List[typing.Union[str, special.Special]]: """ Run commands to boot linux on ``ub``. The last command, the one that actually kicks off the boot process, needs to be returned as a list of arguments. ``do_boot`` will only be called, if ``boot_commands`` is None. **Example**:: def do_boot( self, ub: board.UBootMachine[B] ) -> typing.List[typing.Union[str, board.Special]]: ub.exec0("setenv", "console", "ttyS0") ub.exec0("setenv", "baudrate", str(115200)) ub.exec0( "setenv", "bootargs", board.Raw("console=${console},${baudrate}"), ) ub.exec0("tftp", board.Env("loadaddr"), "zImage") return ["bootz", board.Env("loadaddr")] """ return ["run", "bootcmd"]
@property @abc.abstractmethod def uboot(self) -> typing.Type[board.UBootMachine[B]]: """U-Boot machine for this board.""" pass def __init__(self, b: typing.Union[B, board.UBootMachine[B]]) -> None: # noqa: D107 super().__init__(b) if isinstance(b, board.UBootMachine): ub = b self.ub = None else: self.ub = self.uboot(b) ub = self.ub self.channel = ub.channel with tbot.log.EventIO( ["board", "linux", ub.board.name], tbot.log.c("LINUX").bold + f" ({self.name})", verbosity=tbot.log.Verbosity.QUIET, ) as boot_ev: if self.boot_commands is not None: for cmd in self.boot_commands[:-1]: ub.exec0(*cmd) bootcmd = self.boot_commands[-1] else: bootcmd = self.do_boot(ub) # Make it look like a normal U-Boot command last_command = ub.build_command(*bootcmd) with tbot.log_event.command(ub.name, last_command) as ev: ev.prefix = " <> " self.channel.send(last_command + "\n") log = self._boot_to_shell(channel.SkipStream(ev, len(last_command) + 1)) boot_ev.data["output"] = log[len(last_command) + 1 :]
[docs] def destroy(self) -> None: # noqa: D102 if self.ub is not None: self.ub.destroy()
def _obtain_channel(self) -> channel.Channel: return self.channel
[docs]class LinuxStandaloneMachine(LinuxMachine[B]): """ Linux machine that boots without U-Boot. **Example Implementation**:: from tbot.machine import board from tbot.machine import linux class MyBoard(board.Board): ... class MyBoardLinux(board.LinuxStandaloneMachine[MyBoard]): username = "root" password = None shell = linux.shell.Bash BOARD = MyBoard LINUX = MyBoardLinux :ivar str bootlog: Messages that were printed out during startup. You can access this attribute inside your testcases to get info about what was going on during boot. .. versionadded:: 0.6.2 """ def __init__(self, b: typing.Union[B, board.UBootMachine[B]]) -> None: # noqa: D107 super().__init__(b) if isinstance(b, board.UBootMachine): raise RuntimeError(f"{self!r} does not support booting from UBoot") if self.board.channel is not None: self.channel = self.board.channel else: raise RuntimeError("{board!r} does not support a serial connection!") with tbot.log.EventIO( ["board", "linux", self.board.name], tbot.log.c("LINUX").bold + f" ({self.name})", verbosity=tbot.log.Verbosity.QUIET, ) as ev: ev.prefix = " <> " ev.verbosity = tbot.log.Verbosity.STDOUT log = self._boot_to_shell(ev) ev.data["output"] = log
[docs] def destroy(self) -> None: # noqa: D102 self.channel.close()
def _obtain_channel(self) -> channel.Channel: return self.channel