Create wrapper class to run commands via subprocess

This commit creates a class to wrap the main subprocess
commands used by USM, that are 'run', 'check_output' and
'check_call' so that their returned information is logged
into a json file. The purpose of this file is to enable
each deployment history to be recovered in an easier way
on the future.

The json files are stored inside directories named under
the corresponding release, and have a file named under the
deployment stage in which the object was instantiated, e.g.:

/opt/software/summary/starlingx_24.03.0/deploy_precheck.json

These files can grow incrementally in case multiple commands
are executed under the same deployment stage.

The wrapper functions have the same name as the ones from
the 'subprocess' library and behave exactly the same way.

The subprocess functions used on the code currenly will be
replaced by a follow-up commit.

Test Plan
PASS: manually replace subprocess functions on USM code, run
      the respective commands and verify:
      - Command is executed successfully
      - Output and behavior is maintained
      - The json file is created with the expected directory,
        filename and content

Story: 2010676
Task: 48955

Change-Id: Iccf1aef1b0cc064399163eeb58c23fa065a6dab5
Signed-off-by: Heitor Matsui <heitorvieira.matsui@windriver.com>
This commit is contained in:
Heitor Matsui 2023-10-11 09:10:49 -03:00
parent e6744eb9f5
commit 71dfcf8469
2 changed files with 145 additions and 0 deletions

View File

@ -111,3 +111,6 @@ LICENSE_FILE = "/etc/platform/.license"
VERIFY_LICENSE_BINARY = "/usr/bin/verify-license"
SOFTWARE_JSON_FILE = "/opt/software/software.json"
WORKER_SUMMARY_DIR = "%s/summary" % SOFTWARE_STORAGE_DIR
WORKER_DATETIME_FORMAT = "%Y%m%dT%H%M%S%f"

View File

@ -0,0 +1,142 @@
"""
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
"""
import json
import os
import subprocess
from datetime import datetime
import software.constants as constants
class SoftwareWorker(object):
"""This class wraps the subprocess commands used by USM
modules to run a command with parameters and write its
return code, stdout, stderr and other useful information into
a structured json file that can be later recovered to create
a deployment summary report.
"""
def __init__(self, release, stage):
"""SoftwareWorker constructor
:param release: target release name, used to define
the directory in which json files will be created
:param stage: deployment stage which the commands
are being executed, used to define the json filename
"""
self._release = release
self._stage = stage
self._directory = os.path.join(constants.WORKER_SUMMARY_DIR, self._release)
self._filename = os.path.join(self._directory, self._stage) + ".json"
os.makedirs(self._directory, exist_ok=True)
@staticmethod
def _convert_string(_str):
"""Convert a byte-string into a text string
:param _str: string to be converted
:returns: text string
"""
if not _str:
return ""
if isinstance(_str, bytes):
return _str.decode("utf-8")
return _str
def _read_file(self):
"""Reads the file and returns its content in a dictionary.
:returns: dictionary loaded with content from json file
"""
try:
with open(self._filename, "r") as f:
return json.loads(f.read())
except (FileNotFoundError, json.decoder.JSONDecodeError):
return {}
def _write_file(self, command, rc, stdout, stderr):
"""Writes the command in a structured format in the file.
:param command: command that was run via subprocess
:param rc: command return code
:param stdout: standard output returned by the command
:param stderr: standard error returned by the command
"""
commands = self._read_file()
if not isinstance(command, list):
command = [command]
with open(self._filename, "w") as f:
commands.update({
datetime.strftime(datetime.utcnow(), constants.WORKER_DATETIME_FORMAT): {
"command": " ".join(command),
"rc": rc,
"stdout": stdout,
"stderr": stderr,
}
})
f.write(json.dumps(commands))
def run(self, cmd, **kwargs):
"""Wraps 'run' function from 'subprocess' library and writes captured
rc, stdout and stderr from the executed command to a json file.
:param cmd: list or str with the command and parameters to execute
:param kwargs: extra parameters passed to native subprocess 'run' function
:returns: instance of CompletedProcess or CalledProcessError exception
"""
stdout = kwargs.get("stdout", None)
stderr = kwargs.get("stderr", None)
capture_output = kwargs.get("capture_output", False)
shell = kwargs.get("shell", False)
check = kwargs.get("check", False)
text = kwargs.get("text", None)
try:
if "capture_output" in kwargs:
ret = subprocess.run(cmd, shell=shell, check=check,
capture_output=capture_output, text=text)
else:
ret = subprocess.run(cmd, shell=shell, check=check,
stdout=stdout, stderr=stderr, text=text)
except subprocess.CalledProcessError as exc:
ret = exc
ret_stdout_text = self._convert_string(ret.stdout)
ret_stderr_text = self._convert_string(ret.stderr)
self._write_file(cmd, ret.returncode, ret_stdout_text, ret_stderr_text)
return ret
def check_output(self, cmd, **kwargs):
"""Wraps 'check_output' function from 'subprocess' library and writes
captured rc, stdout and stderr from the executed command to a json file.
Like the original 'subprocess' function, will return the command output
if the command is successful and raise an exception if it fails.
:param cmd: list or str with the command and parameters to execute
:param kwargs: extra parameters passed to native subprocess 'run' function
:returns: subprocess.CompletedProcess.stdout
"""
kwargs.update({"check": True, "stdout": subprocess.PIPE,
"stderr": kwargs.get("stderr", None)})
ret = self.run(cmd, **kwargs)
if ret.returncode != 0:
raise subprocess.CalledProcessError(ret.returncode, cmd)
return ret.stdout
def check_call(self, cmd, **kwargs):
"""Wraps 'check_call' function from 'subprocess' library and writes
captured rc, stdout='' and stderr='' from the executed command to a
json file. Like the original 'subprocess' function, will raise an
exception if the command fails.
:param cmd: list or str with the command and parameters to execute
:param kwargs: extra parameters passed to native subprocess 'run' function
"""
kwargs.update({"check": True, "stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL})
ret = self.run(cmd, **kwargs)
if ret.returncode != 0:
raise subprocess.CalledProcessError(ret.returncode, cmd)