daisy_workflows/linux_common/utils/guestfsprocess.py (59 lines of code) (raw):
#!/usr/bin/env python3
# Copyright 2020 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Run a process in a guestfs.GuestFS instance.
This library addresses a shortcoming of GuestFS.command and GuestFS.sh
that cause data loss. When the command is successful, stderr is discarded.
When the command fails, stdout is discarded.
This library is motivated by subprocess.run, which returns an object
containing stdout, stderr, and the return code of the process.
"""
import logging
import os
import shlex
import textwrap
import typing
def run(g: 'GuestFSInterface', command,
raiseOnError: bool = True) -> 'CompletedProcess':
"""Runs a process in a mounted GuestFS instance, ensuring that
standard output and standard error is always retained.
Args:
g: Mounted GuestFS instance.
command (str or List[str]): Script content that will be executed
by a bash interpeter on the guest.
raiseOnError (bool): When true and the process exits with a non-zero exit
code, a RuntimeError exception will be raised, using standard error as its
message. The process's stdout and stderr are written to logging.debug.
raiseOnError=True is good for commands where you don't inspect the output
and you want the failure to propagate automatically. For example:
`yum install package`; if it's successful you don't need the output.
Examples:
>>> run(g, 'date').stdout
Thu 05 Nov 2020 06:53:55 PM PST
>>> run(g, 'printf hi; exit 1', raiseOnError=False))
{'stdout': 'hi', 'stderr': '', 'code': 1, 'cmd': 'printf hi; exit 1'}
"""
tmp_dir = g.mkdtemp('/tmp/gprocXXXXXX')
program_path = os.path.join(tmp_dir, 'program.sh')
stdout_path = os.path.join(tmp_dir, 'stdout.txt')
stderr_path = os.path.join(tmp_dir, 'stderr.txt')
return_code_path = os.path.join(tmp_dir, 'return_code.txt')
if not isinstance(command, str):
command = ' '.join(shlex.quote(s) for s in command)
program = _make_wrapping_program(command, stdout_path, stderr_path,
return_code_path)
if raiseOnError:
logging.debug('Running %s', command)
g.write(program_path, program)
g.command(['/bin/bash', program_path])
p = CompletedProcess(cmd=command,
stdout=g.cat(stdout_path),
stderr=g.cat(stderr_path),
code=int(g.cat(return_code_path)))
if raiseOnError and p.code != 0:
logging.debug(p)
raise RuntimeError(p.stderr)
return p
def _make_wrapping_program(
cmd: str, stdout_path: str, stderr_path: str,
return_code_path: str) -> str:
"""Creates a shell script that captures the stdout, stderr, and return code
to files on the filesystem.
Args:
cmd: Command to execute.
stdout_path: Path to file for stdout.
stderr_path: Path to file for stderr.
return_code_path: Path to write return code of executing cmd.
Returns:
String containing the shell script.
"""
return textwrap.dedent("""
touch {stdout_path}
touch {stderr_path}
touch {return_code_path}
bash -c {cmd} 1> {stdout_path} 2> {stderr_path}
echo $? > {return_code_path}
exit 0
""".format(cmd=shlex.quote(cmd),
stdout_path=shlex.quote(stdout_path),
stderr_path=shlex.quote(stderr_path),
return_code_path=shlex.quote(return_code_path)))
class GuestFSInterface:
# The subset of guestfs.GuestFS that's used by this module.
def cat(self, path: str) -> str:
raise NotImplementedError()
def command(self, arguments: typing.List[str]) -> str:
raise NotImplementedError()
def mkdtemp(self, tmpl: str) -> str:
raise NotImplementedError()
def write(self, path: str, content: str):
raise NotImplementedError()
class CompletedProcess:
def __init__(self, stdout: str, stderr: str, code: int, cmd: str):
self.stdout = stdout
self.stderr = stderr
self.code = code
self.cmd = cmd
def __eq__(self, o: object) -> bool:
return (isinstance(o, CompletedProcess)
and o.stdout == self.stdout
and o.stderr == self.stderr
and o.code == self.code
and o.cmd == self.cmd)
def __repr__(self):
return str(self.__dict__)