Source code for tbot.machine.board.linux

# 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 time
import typing

import tbot
import tbot.error

from .. import board, channel, connector, machine


class LinuxStartupEvent(tbot.log.EventIO):
    def __init__(self, lnx: machine.Machine) -> None:
        self.lnx = lnx
        super().__init__(
            ["board", "linux", lnx.name],
            tbot.log.c("LINUX").bold + f" ({lnx.name})",
            verbosity=tbot.log.Verbosity.QUIET,
        )

        self.prefix = "   <> "
        self.verbosity = tbot.log.Verbosity.STDOUT

    def close(self) -> None:
        setattr(self.lnx, "bootlog", self.getvalue())
        self.data["output"] = self.getvalue()
        super().close()


class LinuxBoot(machine.Machine):
    _linux_init_event: typing.Optional[tbot.log.EventIO] = None

    def _linux_boot_event(self) -> tbot.log.EventIO:
        if self._linux_init_event is None:
            self._linux_init_event = LinuxStartupEvent(self)

        return self._linux_init_event


[docs]class LinuxBootLogin(machine.Initializer, LinuxBoot): """ Machine :py:class:`~tbot.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**: .. code-block:: python 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: channel.channel.ConvenientSearchString = "assword: " """Prompt that indicates tbot should send the password.""" boot_timeout: typing.Optional[float] = 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 :py:class:`LinuxUbootConnector`). .. versionadded:: 0.10.0 """ # Used for timeout tracking _boot_start: typing.Optional[float] = None bootlog: str """Log of kernel-messages which were output during boot.""" @property @abc.abstractmethod def username(self) -> str: """Username to login as.""" pass @property @abc.abstractmethod def password(self) -> typing.Optional[str]: """Password to login with. Set to ``None`` if no password is needed.""" pass no_password_timeout: typing.Optional[float] = 5.0 """ Timeout after which login without a password should be attempted. Set to ``None`` to disable this mechanism. .. versionadded:: 0.10.1 """ def _timeout_remaining(self) -> typing.Optional[float]: if self.boot_timeout is None: return None if self._boot_start is None: self._boot_start = time.monotonic() remaining = self.boot_timeout - (time.monotonic() - self._boot_start) if remaining <= 0: raise TimeoutError else: return remaining @contextlib.contextmanager def _init_machine(self) -> typing.Iterator: with contextlib.ExitStack() as cx: ev = cx.enter_context(self._linux_boot_event()) cx.enter_context(self.ch.with_stream(ev)) if self._boot_start is None: self._boot_start = time.monotonic() self.ch.read_until_prompt( prompt=self.login_prompt, timeout=self.boot_timeout ) # 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.login_delay != 0: remaining = self._timeout_remaining() if remaining is not None and self.login_delay > remaining: # we know that we will hit the timeout by waiting for # login_delay so why not raise the TimeoutError now... raise TimeoutError( "login_delay would exceed boot_timeout, aborting." ) # Read everything while waiting for timeout to expire self.ch.read_until_timeout(self.login_delay) self.ch.sendline("") self.ch.read_until_prompt( prompt=self.login_prompt, timeout=self._timeout_remaining() ) self.ch.sendline(self.username) if self.password is not None: timeout = self._timeout_remaining() if self.no_password_timeout is not None: if timeout is None: timeout = self.no_password_timeout else: timeout = min(timeout, self.no_password_timeout) try: self.ch.read_until_prompt( prompt=self.password_prompt, timeout=timeout ) except TimeoutError: # Call _timeout_remaining() to abort if the boot-timeout was reached self._timeout_remaining() # If we get here, the no_password_timeout expired and we # should attempt continuing without a password. tbot.log.warning( "Didn't get asked for a password." + " Optimistically continuing without one..." ) else: # No timeout exception means we're at the password prompt. self.ch.sendline(self.password) yield None
[docs]class AskfirstInitializer(machine.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**: .. code-block:: python from tbot.machine import board, linux class StandaloneLinux( board.Connector, board.AskfirstInitializer, board.LinuxBootLogin, linux.Bash, ): # board.LinuxBootLogin config: username = "root" password = "hunter2" .. versionadded:: 0.10.6 """ askfirst_prompt = "Please press Enter to activate this console." """ Prompt that indicates the board is waiting for ``askfirst`` confirmation. """ # For proper integration with LinuxBootLogin boot_timeout: typing.Optional[float] = None _boot_start: typing.Optional[float] = None bootlog: str @contextlib.contextmanager def _init_machine(self) -> typing.Iterator: # This ExitStack holds the boot event until we successfully reached the # askfirst prompt. Then it releases the boot event such that further # initializers like LinuxBootLogin can continue using it. with contextlib.ExitStack() as cx: ev = cx.enter_context(self._linux_boot_event()) with self.ch.with_stream(ev): if self._boot_start is None: self._boot_start = time.monotonic() # Using expect() instead of read_until_prompt() so we are not # confused by garbage following the prompt. self.ch.expect(self.askfirst_prompt, timeout=self.boot_timeout) self.ch.sendline("") cx.pop_all() yield None
Self = typing.TypeVar("Self", bound="LinuxUbootConnector")
[docs]class LinuxUbootConnector(connector.Connector, LinuxBootLogin, board.BoardMachineBase): """ Connector for booting Linux from U-Boot. This connector can either boot from a :py:class:`~tbot.machine.board.Board` instance or from a :py:class:`~tbot.machine.board.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. """ @property @abc.abstractmethod def uboot(self) -> typing.Type[board.UBootShell]: """ U-Boot machine to use when booting directly from a :py:class:`~tbot.machine.board.Board` instance. """ raise tbot.error.AbstractMethodError()
[docs] def do_boot(self, ub: board.UBootShell) -> channel.Channel: """ 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 :py:meth:`tbot.machine.board.UBootShell.boot` method. **Example**: .. code-block:: python 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") ... """ return ub.boot("boot")
def __init__(self, b: typing.Union[board.Board, board.UBootShell]) -> None: self._b = b
[docs] @classmethod @contextlib.contextmanager def from_context( cls: typing.Type[Self], ctx: "tbot.Context" ) -> typing.Iterator[Self]: with contextlib.ExitStack() as cx: ub = cx.enter_context(ctx.request(tbot.role.BoardUBoot, exclusive=True)) m = cx.enter_context(cls(ub)) # type: ignore yield typing.cast(Self, m)
@contextlib.contextmanager def _connect(self) -> typing.Iterator[channel.Channel]: with contextlib.ExitStack() as cx: if isinstance(self._b, board.Board): ub = cx.enter_context(self.uboot(self._b)) # type: ignore elif isinstance(self._b, board.UBootShell): ub = cx.enter_context(self._b) else: raise TypeError(f"Got {self._b!r} instead of Board/U-Boot machine") self._linux_boot_event() yield self.do_boot(ub).take()
[docs] def clone(self: Self) -> Self: """This machine cannot be cloned.""" raise NotImplementedError("can't clone Linux_U-Boot Machine")
@property def board(self) -> board.Board: if isinstance(self._b, board.Board): return self._b elif isinstance(self._b, board.UBootShell): try: return getattr(self._b, "board") # type: ignore except AttributeError: raise Exception("U-Boot machine does not reference a board machine!")