Files
2026-06-23 11:27:54 +08:00

179 lines
8.0 KiB
Python

# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import subprocess
from lib.environment import Environment
from lib.exceptions import CommandExecutionError, CommandExecutionTimeout
from lib.execution_replay import ReplayContext
class CommandRunner(object):
"""
A utility class to execute external command and obtain the output
This class also provide a mechanism for rerouting the execution on
remote machine via SSH session, capturing the execution result and
replaying it back for development and testing purpose.
"""
def __init__(self, *command_args, **options):
"""
CommandRunner constructor
:param command_args: the command and its arguments
:param options: Supports the following optional parameters:
- expected_return_code: if specified, CommandRunner will check the command
return value and will raise CommandExecutionError it doesn't match
- command_input: Text to be supplied to stdin when executing the command
- binary_output: indicating that the command is expected to return binary output
"""
self.command_args = command_args
self.options = options
self.timeout = None
self.replay_context = None
if options.get('expected_return_code') is not None:
self.expected_return_code = options['expected_return_code']
else:
self.expected_return_code = None
if options.get('command_input') is not None:
self.command_input = options['command_input']
else:
self.command_input = None
if options.get('binary_output') is not None:
self.binary_output = options['binary_output'] is True
else:
self.binary_output = False
if options.get('timeout') is not None:
self.timeout = options['timeout']
self.remote_hostname = None
self.remote_username = None
self.is_remote = False
self.setup_remote_exec()
def setup_remote_exec(self):
"""
Setup the remote execution, capture/replay mechanism if the required
keys are set in the environment
"""
env = Environment.get_environment()
if env.get_value('VCERT_REMOTE_EXEC'):
self.set_remote(env.get_value('VCERT_REMOTE_HOSTNAME'),
env.get_value('VCERT_REMOTE_USERNAME'))
if env.get_value('VCERT_REMOTE_EXEC_REPLAY') is True \
or env.get_value('VCERT_REMOTE_EXEC_CAPTURE') is True:
self.replay_context = ReplayContext.get_replay_context()
def set_remote(self, hostname, username='root'):
self.is_remote = True
self.remote_hostname = hostname
self.remote_username = username if not username else 'root'
def set_input(self, command_input):
self.command_input = command_input
def run(self):
"""
Run the command, redirect to run_remote if remote execution is set
"""
if self.is_remote:
return self.run_remote(self.command_args, self.command_input, self.timeout,
self.expected_return_code, self.binary_output)
else:
return self.run_local(self.command_args, self.command_input, self.timeout,
self.expected_return_code, self.binary_output)
def run_and_get_output(self):
"""
Run the command and get the standard output only
"""
_, stdout, _ = self.run()
return stdout
@staticmethod
def run_local(command_args, command_input, timeout, expected_return_code, binary_output) -> (int, str, str):
"""
Run the command locally
:param command_args: external command and the arguments
:param command_input: Text to be supplied as stdin
:param timeout: Timeout value for waiting the external command to
return. It will raise CommandExecutionTimeout when this happen
:param expected_return_code: The expected return code. If it's
specified and it doesn't match to the actual return code,
CommandExecutionError will be raised
:param binary_output: need to handle binary output instead of text
:return: a tuple of (return code, stdout output, stderr output)
"""
try:
if binary_output:
ret = subprocess.run(command_args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, timeout=timeout)
elif command_input == subprocess.DEVNULL:
ret = subprocess.run(command_args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, universal_newlines=True, timeout=timeout)
else:
ret = subprocess.run(command_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
input=command_input, universal_newlines=True, timeout=timeout)
if expected_return_code is not None:
if ret.returncode != expected_return_code:
raise CommandExecutionError("External command '{}' returned {}, error message: {}".format(
" ".join(command_args), ret.returncode, ret.stderr))
return ret.returncode, ret.stdout, ret.stderr
except subprocess.TimeoutExpired:
raise CommandExecutionTimeout("External command '{}' timed out".format(" ".join(command_args)))
def run_remote(self, command_args, command_input, timeout, expected_return_code, binary_output):
"""
Run the command on remote machine, or perform capture/replay when
set in the environment variables
It will append the required ssh command arguments 'ssh', '-l', '<user>', '<hostname>'
The capture and replay mechanism will use the remote setting for storing
and retrieve the command result.
Refer to run_local for the parameter description
"""
env = Environment.get_environment()
remote_hostname = env.get_value('VCERT_REMOTE_HOSTNAME')
remote_username = env.get_value('VCERT_REMOTE_USERNAME')
local_hostname = env.get_value('LOCAL_HOSTNAME')
if self.replay_context and self.replay_context.is_replaying:
result = self.replay_context.get_execution_result('command', command_args, command_input)
if result is not None:
if expected_return_code is not None:
return_code, _, _ = result
if return_code != expected_return_code:
raise CommandExecutionError("External command '{}' returned {}".format(
" ".join(command_args), return_code))
return result
if local_hostname and remote_hostname.lower() == local_hostname.lower():
# run locally
final_args = command_args
else:
final_args = ['ssh', '-l', remote_username, remote_hostname]
final_args.extend(command_args)
CommandRunner.add_quotation_escape(final_args)
return_code, stdout, stderr = CommandRunner.run_local(final_args, command_input, timeout,
None, binary_output)
if self.replay_context and self.replay_context.is_capturing:
self.replay_context.store_result('command', command_args, command_input, return_code, stdout, stderr)
if expected_return_code is not None and expected_return_code != return_code:
raise CommandExecutionError("External command '{}' returned {}".format(
" ".join(command_args), return_code))
return return_code, stdout, stderr
@staticmethod
def add_quotation_escape(args):
for index, arg in enumerate(args):
if ' ' in arg or '"' in arg or '\'' in arg or '\\' in arg:
args[index] = "\"{}\"".format(arg.replace('"', '\\"'))