mirror of https://github.com/F-Stack/f-stack.git
132 lines
5.5 KiB
Python
132 lines
5.5 KiB
Python
# SPDX-License-Identifier: BSD-3-Clause
|
|
# Copyright(c) 2023 University of New Hampshire
|
|
|
|
"""Common functionality for interactive shell handling.
|
|
|
|
This base class, InteractiveShell, is meant to be extended by other classes that
|
|
contain functionality specific to that shell type. These derived classes will often
|
|
modify things like the prompt to expect or the arguments to pass into the application,
|
|
but still utilize the same method for sending a command and collecting output. How
|
|
this output is handled however is often application specific. If an application needs
|
|
elevated privileges to start it is expected that the method for gaining those
|
|
privileges is provided when initializing the class.
|
|
"""
|
|
|
|
from abc import ABC
|
|
from pathlib import PurePath
|
|
from typing import Callable
|
|
|
|
from paramiko import Channel, SSHClient, channel # type: ignore[import]
|
|
|
|
from framework.logger import DTSLOG
|
|
from framework.settings import SETTINGS
|
|
|
|
|
|
class InteractiveShell(ABC):
|
|
"""The base class for managing interactive shells.
|
|
|
|
This class shouldn't be instantiated directly, but instead be extended. It contains
|
|
methods for starting interactive shells as well as sending commands to these shells
|
|
and collecting input until reaching a certain prompt. All interactive applications
|
|
will use the same SSH connection, but each will create their own channel on that
|
|
session.
|
|
|
|
Arguments:
|
|
interactive_session: The SSH session dedicated to interactive shells.
|
|
logger: Logger used for displaying information in the console.
|
|
get_privileged_command: Method for modifying a command to allow it to use
|
|
elevated privileges. If this is None, the application will not be started
|
|
with elevated privileges.
|
|
app_args: Command line arguments to be passed to the application on startup.
|
|
timeout: Timeout used for the SSH channel that is dedicated to this interactive
|
|
shell. This timeout is for collecting output, so if reading from the buffer
|
|
and no output is gathered within the timeout, an exception is thrown.
|
|
|
|
Attributes
|
|
_default_prompt: Prompt to expect at the end of output when sending a command.
|
|
This is often overridden by derived classes.
|
|
_command_extra_chars: Extra characters to add to the end of every command
|
|
before sending them. This is often overridden by derived classes and is
|
|
most commonly an additional newline character.
|
|
path: Path to the executable to start the interactive application.
|
|
dpdk_app: Whether this application is a DPDK app. If it is, the build
|
|
directory for DPDK on the node will be prepended to the path to the
|
|
executable.
|
|
"""
|
|
|
|
_interactive_session: SSHClient
|
|
_stdin: channel.ChannelStdinFile
|
|
_stdout: channel.ChannelFile
|
|
_ssh_channel: Channel
|
|
_logger: DTSLOG
|
|
_timeout: float
|
|
_app_args: str
|
|
_default_prompt: str = ""
|
|
_command_extra_chars: str = ""
|
|
path: PurePath
|
|
dpdk_app: bool = False
|
|
|
|
def __init__(
|
|
self,
|
|
interactive_session: SSHClient,
|
|
logger: DTSLOG,
|
|
get_privileged_command: Callable[[str], str] | None,
|
|
app_args: str = "",
|
|
timeout: float = SETTINGS.timeout,
|
|
) -> None:
|
|
self._interactive_session = interactive_session
|
|
self._ssh_channel = self._interactive_session.invoke_shell()
|
|
self._stdin = self._ssh_channel.makefile_stdin("w")
|
|
self._stdout = self._ssh_channel.makefile("r")
|
|
self._ssh_channel.settimeout(timeout)
|
|
self._ssh_channel.set_combine_stderr(True) # combines stdout and stderr streams
|
|
self._logger = logger
|
|
self._timeout = timeout
|
|
self._app_args = app_args
|
|
self._start_application(get_privileged_command)
|
|
|
|
def _start_application(self, get_privileged_command: Callable[[str], str] | None) -> None:
|
|
"""Starts a new interactive application based on the path to the app.
|
|
|
|
This method is often overridden by subclasses as their process for
|
|
starting may look different.
|
|
"""
|
|
start_command = f"{self.path} {self._app_args}"
|
|
if get_privileged_command is not None:
|
|
start_command = get_privileged_command(start_command)
|
|
self.send_command(start_command)
|
|
|
|
def send_command(self, command: str, prompt: str | None = None) -> str:
|
|
"""Send a command and get all output before the expected ending string.
|
|
|
|
Lines that expect input are not included in the stdout buffer, so they cannot
|
|
be used for expect. For example, if you were prompted to log into something
|
|
with a username and password, you cannot expect "username:" because it won't
|
|
yet be in the stdout buffer. A workaround for this could be consuming an
|
|
extra newline character to force the current prompt into the stdout buffer.
|
|
|
|
Returns:
|
|
All output in the buffer before expected string
|
|
"""
|
|
self._logger.info(f"Sending: '{command}'")
|
|
if prompt is None:
|
|
prompt = self._default_prompt
|
|
self._stdin.write(f"{command}{self._command_extra_chars}\n")
|
|
self._stdin.flush()
|
|
out: str = ""
|
|
for line in self._stdout:
|
|
out += line
|
|
if prompt in line and not line.rstrip().endswith(
|
|
command.rstrip()
|
|
): # ignore line that sent command
|
|
break
|
|
self._logger.debug(f"Got output: {out}")
|
|
return out
|
|
|
|
def close(self) -> None:
|
|
self._stdin.close()
|
|
self._ssh_channel.close()
|
|
|
|
def __del__(self) -> None:
|
|
self.close()
|