Files
vmware-tools/vCert-6.1.1-20260401/lib/execution_replay.py
T
2026-06-23 11:27:54 +08:00

241 lines
9.3 KiB
Python

# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import base64
import json
import glob
import os
import subprocess
from collections.abc import Mapping
from datetime import datetime
from lib.environment import Environment
from lib.exceptions import ReplayEntryNotFound, CommandExecutionError
replay_context = None
class ReplayEntry(object):
"""
Class for representing execution result entry
"""
def __init__(self, category, command_args, input_string, tag):
"""
ReplayEntry constructor
:param category: category of result (e.g. 'command', 'ldap')
:param command_args: command and the parameters
:param input_string: Input text to be supplied as stdin
"""
self.category = category
self.command_args = command_args
self.input_string = input_string if input_string is not None else ''
self.used_count = 0
self.key = self.generate_key(category, command_args, input_string, tag)
self.results = []
def add_result(self, timestamp, return_code, stdout, stderr):
"""
Add command result entry
:param timestamp: the timestamp of command execution
:param return_code: command's return code
:param stdout: The command output from stdout
:param stderr: The command output from stderr
"""
# always add result in sorted
added = False
value = (timestamp, return_code, stdout, stderr)
for index, (ts, _, _, _) in enumerate(self.results):
if ts > timestamp:
self.results.insert(index, value)
added = True
break
if not added:
self.results.append(value)
@staticmethod
def generate_key(category, command_args, input_string, tag):
"""
Generate key based on category, command_args, and input_string.
The key is not guaranteed to be unique but should be good enough for
debugging and testing purpose. This key will be used to retrieve
the previous command result for replay
:param category: the category, 'command', 'ldap'
:param command_args: command arguments
:param input_string: input string
:param tag: additional tag to differentiate the same commands in a different condition
:return: generated key
"""
for idx, arg in enumerate(command_args):
if not isinstance(arg, str):
command_args[idx] = str(arg)
if input_string == subprocess.DEVNULL:
input_string = '__/dev/null__'
return "{}||{}||{}||{}".format(category, ">>".join(command_args), input_string, tag)
class ReplayContext(object):
"""
The class holding context object for capturing or replaying command result
"""
categories = ['command', 'ldap', 'other']
def __init__(self, replay_dir, is_replaying=True, is_capturing=False):
"""
ReplayContext constructor
:param replay_dir: base directory for storing or retrieving command result
:param is_capturing: True when capturing, otherwise it's in replay mode
"""
self.replay_dir = replay_dir
self.replay_entries = None
self.is_replaying = is_replaying
self.is_capturing = is_capturing
self.tag = None
def get_replay_entry(self, key) -> ReplayEntry:
"""
Get replay entry based on the key
:param key: key to be used to retrieve the command result
"""
if self.replay_entries is None:
self.load_all_replays()
return self.replay_entries.get(key)
def store_result(self, category, command_args, command_input, return_code, stdout, stderr):
"""
Store the command result
The file will be stored in <replay-dir>/<hostname>/replay_<category>_<1st command_args>_<timestamp>.json
"""
command = command_args[0].split('/')[-1]
timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S_%f')
file_name = "{}/replay_{}_{}_{}.json".format(self.replay_dir, category,
command, timestamp)
with open(file_name, 'w') as file:
content = {
"command_args": ReplayContext.encode_bytes(command_args),
"input": command_input if command_input != subprocess.DEVNULL else '__/dev/null__',
"tag": self.tag,
"timestamp": timestamp,
"return_code": return_code,
"stdout": self.encode_bytes(stdout),
"stderr": self.encode_bytes(stderr)
}
try:
file.write(json.dumps(content, indent=4))
except TypeError as te:
raise CommandExecutionError("Unserializable object: {}".format(content))
if self.is_replaying:
self.add_replay_entry(category, command_args, command_input, self.tag, timestamp,
return_code, stdout, stderr)
def set_tag(self, tag):
self.tag = tag
def load_all_replays(self):
"""
Load all replay entries
"""
self.replay_entries = {}
for category in ReplayContext.categories:
for file in sorted(glob.glob("{}/replay_{}_*.json".format(self.replay_dir, category))):
with open(file, 'r') as f:
replay = json.load(f)
command_args = ReplayContext.decode_bytes(replay['command_args'])
command_input = replay['input']
stdout = ReplayContext.decode_bytes(replay['stdout'])
stderr = ReplayContext.decode_bytes(replay['stderr'])
self.add_replay_entry(category, command_args, command_input, replay['tag'], replay['timestamp'],
replay['return_code'], stdout, stderr)
def add_replay_entry(self, category, command_args, command_input, tag, timestamp,
return_code, stdout, stderr):
"""
Add replay entry
"""
key = ReplayEntry.generate_key(category, command_args, command_input, tag)
entry = self.get_replay_entry(key)
if entry is None:
entry = ReplayEntry(category, command_args, command_input, tag)
self.replay_entries[key] = entry
entry.add_result(timestamp, return_code, stdout, stderr)
def reset_context(self):
for entry in self.replay_entries:
entry.used_count = 0
def get_execution_result(self, category, command_args, command_input) -> (int, str, str):
"""
Get the previous execution result based on {category}, {command_args}, and {command_input}
"""
key = ReplayEntry.generate_key(category, command_args, command_input, self.tag)
entry = self.get_replay_entry(key)
if entry is None:
if self.is_capturing:
return None
else:
raise ReplayEntryNotFound("Replay entry not found for key {}".format(key))
result = entry.results[entry.used_count]
entry.used_count = (entry.used_count + 1) % len(entry.results)
return result[1:]
@staticmethod
def get_replay_context(reload=False):
"""
Static method to get ReplayContext instance from other modules
:param reload: if True, it will dispose the previous object and create a new instance
"""
global replay_context
env = Environment.get_environment()
is_remote = env.get_value('VCERT_REMOTE_EXEC') is True
is_replay = env.get_value('VCERT_REMOTE_EXEC_REPLAY') is True
is_capture = env.get_value('VCERT_REMOTE_EXEC_CAPTURE') is True
if reload:
replay_context = None
if is_remote and (is_replay or is_capture) and replay_context is None:
remote_hostname = env.get_value('VCERT_REMOTE_HOSTNAME')
replay_dir = "{}/{}".format(env.get_value('VCERT_REMOTE_EXEC_REPLAY_DIR'),
remote_hostname)
if not os.path.exists(replay_dir):
os.makedirs(replay_dir)
replay_context = ReplayContext(replay_dir, is_replay, is_capture)
return replay_context
@staticmethod
def encode_bytes(obj):
if isinstance(obj, bytes):
return "__base64__({})".format(base64.b64encode(obj).decode('utf-8'))
if isinstance(obj, list):
for idx, value in enumerate(obj):
obj[idx] = ReplayContext.encode_bytes(value)
elif isinstance(obj, dict):
for key in obj.keys():
obj[key] = ReplayContext.encode_bytes(obj[key])
elif isinstance(obj, Mapping):
new_obj = dict()
for key in obj.keys():
new_obj[key] = ReplayContext.encode_bytes(obj[key])
obj = new_obj
return obj
@staticmethod
def decode_bytes(obj):
if isinstance(obj, str) and obj.startswith('__base64__(') and obj.endswith(')'):
return base64.b64decode(obj[11:-1])
if isinstance(obj, list):
for idx, value in enumerate(obj):
obj[idx] = ReplayContext.decode_bytes(value)
elif isinstance(obj, dict):
for key in obj.keys():
obj[key] = ReplayContext.decode_bytes(obj[key])
return obj