1
0
mirror of git://sourceware.org/git/lvm2.git synced 2024-12-22 17:35:59 +03:00
lvm2/daemons/lvmdbusd/lvm_shell_proxy.py
Tony Asleson 25b5413f89 lvmdbusd: Make lvm shell read more robust
Make sure JSON is correct before we stop trying to read.
2016-11-29 16:50:30 -06:00

272 lines
6.9 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (C) 2015-2016 Red Hat, Inc. All rights reserved.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions
# of the GNU General Public License v.2.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright 2015-2016, Vratislav Podzimek <vpodzime@redhat.com>
import subprocess
import shlex
from fcntl import fcntl, F_GETFL, F_SETFL
import os
import traceback
import sys
import tempfile
import time
import select
import copy
try:
import simplejson as json
except ImportError:
import json
from lvmdbusd.cfg import LVM_CMD
from lvmdbusd.utils import log_debug, log_error
SHELL_PROMPT = "lvm> "
def _quote_arg(arg):
if len(shlex.split(arg)) > 1:
return '"%s"' % arg
else:
return arg
class LVMShellProxy(object):
# Read until we get prompt back and a result
# @param: no_output Caller expects no output to report FD
# Returns stdout, report, stderr (report is JSON!)
def _read_until_prompt(self, no_output=False):
stdout = ""
report = ""
stderr = ""
keep_reading = True
extra_passes = 3
report_json = {}
prev_report_len = 0
# Try reading from all FDs to prevent one from filling up and causing
# a hang. Keep reading until we get the prompt back and the report
# FD does not contain valid JSON
while keep_reading:
try:
rd_fd = [
self.lvm_shell.stdout.fileno(),
self.report_r,
self.lvm_shell.stderr.fileno()]
ready = select.select(rd_fd, [], [], 2)
for r in ready[0]:
if r == self.lvm_shell.stdout.fileno():
tmp = self.lvm_shell.stdout.read()
if tmp:
stdout += tmp.decode("utf-8")
elif r == self.report_r:
while True:
tmp = os.read(self.report_r, 16384)
if tmp:
report += tmp.decode("utf-8")
if len(tmp) != 16384:
break
else:
break
elif r == self.lvm_shell.stderr.fileno():
tmp = self.lvm_shell.stderr.read()
if tmp:
stderr += tmp.decode("utf-8")
# Check to see if the lvm process died on us
if self.lvm_shell.poll():
raise Exception(self.lvm_shell.returncode, "%s" % stderr)
if stdout.endswith(SHELL_PROMPT):
if no_output:
keep_reading = False
else:
cur_report_len = len(report)
if cur_report_len != 0:
# Only bother to parse if we have more data
if prev_report_len != cur_report_len:
prev_report_len = cur_report_len
# Parse the JSON if it's good we are done,
# if not we will try to read some more.
try:
report_json = json.loads(report)
keep_reading = False
except ValueError:
pass
else:
log_error("RACE!", 'bg_black', 'fg_light_red')
if keep_reading:
extra_passes -= 1
if extra_passes <= 0:
if len(report):
raise ValueError("Invalid json: %s" %
report)
else:
raise ValueError(
"lvm returned no JSON output!")
except IOError as ioe:
log_debug(str(ioe))
pass
return stdout, report_json, stderr
def _write_cmd(self, cmd):
cmd_bytes = bytes(cmd, "utf-8")
num_written = self.lvm_shell.stdin.write(cmd_bytes)
assert (num_written == len(cmd_bytes))
self.lvm_shell.stdin.flush()
def __init__(self):
# Create a temp directory
tmp_dir = tempfile.mkdtemp(prefix="lvmdbus_")
tmp_file = "%s/lvmdbus_report" % (tmp_dir)
try:
# Lets create fifo for the report output
os.mkfifo(tmp_file, 0o600)
except FileExistsError:
pass
self.report_r = os.open(tmp_file, os.O_NONBLOCK)
# Setup the environment for using our own socket for reporting
local_env = copy.deepcopy(os.environ)
local_env["LVM_REPORT_FD"] = "32"
local_env["LVM_COMMAND_PROFILE"] = "lvmdbusd"
# Disable the abort logic if lvm logs too much, which easily happens
# when utilizing the lvm shell.
local_env["LVM_LOG_FILE_MAX_LINES"] = "0"
flags = fcntl(self.report_r, F_GETFL)
fcntl(self.report_r, F_SETFL, flags | os.O_NONBLOCK)
# run the lvm shell
self.lvm_shell = subprocess.Popen(
[LVM_CMD + " 32>%s" % tmp_file],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=local_env,
stderr=subprocess.PIPE, close_fds=True, shell=True)
try:
flags = fcntl(self.lvm_shell.stdout, F_GETFL)
fcntl(self.lvm_shell.stdout, F_SETFL, flags | os.O_NONBLOCK)
flags = fcntl(self.lvm_shell.stderr, F_GETFL)
fcntl(self.lvm_shell.stderr, F_SETFL, flags | os.O_NONBLOCK)
# wait for the first prompt
errors = self._read_until_prompt(no_output=True)[2]
if errors and len(errors):
raise RuntimeError(errors)
except:
raise
finally:
# These will get deleted when the FD count goes to zero so we can be
# sure to clean up correctly no matter how we finish
os.unlink(tmp_file)
os.rmdir(tmp_dir)
def get_error_msg(self):
# We got an error, lets go fetch the error message
self._write_cmd('lastlog\n')
# read everything from the STDOUT to the next prompt
stdout, report_json, stderr = self._read_until_prompt()
if 'log' in report_json:
error_msg = ""
# Walk the entire log array and build an error string
for log_entry in report_json['log']:
if log_entry['log_type'] == "error":
if error_msg:
error_msg += ', ' + log_entry['log_message']
else:
error_msg = log_entry['log_message']
return error_msg
return 'No error reason provided! (missing "log" section)'
def call_lvm(self, argv, debug=False):
rc = 1
error_msg = ""
if self.lvm_shell.poll():
raise Exception(
self.lvm_shell.returncode,
"Underlying lvm shell process is not present!")
# create the command string
cmd = " ".join(_quote_arg(arg) for arg in argv)
cmd += "\n"
# run the command by writing it to the shell's STDIN
self._write_cmd(cmd)
# read everything from the STDOUT to the next prompt
stdout, report_json, stderr = self._read_until_prompt()
# Parse the report to see what happened
if 'log' in report_json:
if report_json['log'][-1:][0]['log_ret_code'] == '1':
rc = 0
else:
error_msg = self.get_error_msg()
if debug or rc != 0:
log_error(('CMD: %s' % cmd))
log_error(("EC = %d" % rc))
log_error(("ERROR_MSG=\n %s\n" % error_msg))
return rc, report_json, error_msg
def exit_shell(self):
try:
self._write_cmd('exit\n')
except Exception as e:
log_error(str(e))
def __del__(self):
try:
self.lvm_shell.terminate()
except:
pass
if __name__ == "__main__":
shell = LVMShellProxy()
in_line = "start"
try:
while in_line:
in_line = input("lvm> ")
if in_line:
start = time.time()
ret, out, err = shell.call_lvm(in_line.split())
end = time.time()
print(("RC: %d" % ret))
print(("OUT:\n%s" % out))
print(("ERR:\n%s" % err))
print("Command = %f seconds" % (end - start))
except KeyboardInterrupt:
pass
except EOFError:
pass
except Exception:
traceback.print_exc(file=sys.stdout)