2017-12-12 15:10:51 +03:00
#!@PYTHON3@
2016-02-18 02:53:35 +03:00
# 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
2016-08-12 23:23:05 +03:00
import os
2022-09-07 00:23:06 +03:00
import pty
2016-02-18 02:53:35 +03:00
import sys
2016-08-12 23:23:05 +03:00
import tempfile
import time
2023-02-27 17:57:24 +03:00
import threading
2016-08-12 23:23:05 +03:00
import select
try :
import simplejson as json
except ImportError :
import json
2016-02-18 02:53:35 +03:00
2022-09-21 18:05:36 +03:00
import lvmdbusd . cfg as cfg
2021-06-11 18:35:31 +03:00
from lvmdbusd . utils import log_debug , log_error , add_no_notify , make_non_block , \
2022-11-29 19:00:39 +03:00
read_decoded , extract_stack_trace , LvmBug , get_error_msg
2022-09-07 00:23:06 +03:00
SHELL_PROMPT = " lvm> "
2016-02-18 02:53:35 +03:00
def _quote_arg ( arg ) :
if len ( shlex . split ( arg ) ) > 1 :
return ' " %s " ' % arg
else :
return arg
class LVMShellProxy ( object ) :
2016-11-29 20:07:21 +03:00
2022-06-06 17:56:32 +03:00
# Read REPORT FD until we have a complete and valid JSON record or give
# up trying to get one.
#
# Returns stdout, report (JSON), stderr
2022-09-07 00:23:06 +03:00
def _read_response ( self , no_output = False ) :
2016-02-18 02:53:35 +03:00
stdout = " "
2016-08-12 23:23:05 +03:00
report = " "
stderr = " "
2016-11-17 00:57:04 +03:00
keep_reading = True
2016-11-29 20:07:21 +03:00
extra_passes = 3
report_json = { }
prev_report_len = 0
2016-02-18 02:53:35 +03:00
2016-08-12 23:23:05 +03:00
# Try reading from all FDs to prevent one from filling up and causing
2016-11-29 20:07:21 +03:00
# a hang. Keep reading until we get the prompt back and the report
# FD does not contain valid JSON
2022-06-06 17:56:32 +03:00
2022-09-21 18:05:36 +03:00
while keep_reading and cfg . run . value != 0 :
2016-02-18 02:53:35 +03:00
try :
2016-08-12 23:23:05 +03:00
rd_fd = [
2022-09-07 00:23:06 +03:00
self . parent_stdout_fd ,
2016-11-29 21:37:59 +03:00
self . report_stream . fileno ( ) ,
2022-09-07 00:23:06 +03:00
self . parent_stderr_fd ]
2024-03-27 19:49:05 +03:00
ready = select . select ( rd_fd , [ ] , [ ] , cfg . G_LOOP_TMO )
2016-08-12 23:23:05 +03:00
for r in ready [ 0 ] :
2022-09-07 00:23:06 +03:00
if r == self . parent_stdout_fd :
2022-09-08 23:42:26 +03:00
for line in self . parent_stdout . readlines ( ) :
stdout + = line
2016-11-29 21:37:59 +03:00
elif r == self . report_stream . fileno ( ) :
2021-06-11 18:35:31 +03:00
report + = read_decoded ( self . report_stream )
2022-09-07 00:23:06 +03:00
elif r == self . parent_stderr_fd :
2022-09-08 23:42:26 +03:00
for line in self . parent_stderr . readlines ( ) :
stderr + = line
2016-08-12 23:23:05 +03:00
2016-10-07 21:45:30 +03:00
# Check to see if the lvm process died on us
2022-05-25 23:51:14 +03:00
if self . lvm_shell . poll ( ) is not None :
2016-10-07 21:45:30 +03:00
raise Exception ( self . lvm_shell . returncode , " %s " % stderr )
2022-09-07 00:23:06 +03:00
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
if keep_reading :
extra_passes - = 1
if extra_passes < = 0 :
if len ( report ) :
raise LvmBug ( " Invalid json: %s " %
report )
else :
raise LvmBug (
" lvm returned no JSON output! " )
2023-02-27 17:57:24 +03:00
except Exception as e :
log_error ( " While reading from lvm shell we encountered an error %s " % str ( e ) )
log_error ( " stdout= %s \n stderr= %s \n " % ( stdout , stderr ) )
if self . lvm_shell . poll ( ) is not None :
log_error ( " Underlying lvm shell process unexpectedly exited: %d " % self . lvm_shell . returncode )
else :
log_error ( " Underlying lvm shell process is still present! " )
raise e
2016-02-18 02:53:35 +03:00
2022-09-21 18:05:36 +03:00
if keep_reading and cfg . run . value == 0 :
2022-08-26 19:10:24 +03:00
# We didn't complete as we are shutting down
# Try to clean up lvm shell process
log_debug ( " exiting lvm shell as we are shutting down " )
self . exit_shell ( )
raise SystemExit
2016-11-29 20:07:21 +03:00
return stdout , report_json , stderr
2016-02-18 02:53:35 +03:00
def _write_cmd ( self , cmd ) :
2022-09-07 00:23:06 +03:00
self . parent_stdin . write ( cmd )
self . parent_stdin . flush ( )
2016-02-18 02:53:35 +03:00
2016-08-12 23:23:05 +03:00
def __init__ ( self ) :
# Create a temp directory
tmp_dir = tempfile . mkdtemp ( prefix = " lvmdbus_ " )
tmp_file = " %s /lvmdbus_report " % ( tmp_dir )
2016-02-18 02:53:35 +03:00
2023-02-27 17:57:24 +03:00
# Create a lock so that we don't step on each other when we are waiting for a command
# to finish and some other request comes in concurrently, like to exit the shell.
self . shell_lock = threading . RLock ( )
2022-06-07 16:21:03 +03:00
# Create a fifo for the report output
os . mkfifo ( tmp_file , 0o600 )
2016-02-18 02:53:35 +03:00
2022-05-26 18:44:02 +03:00
# Open the fifo for use to read and for lvm child process to write to.
2016-11-29 21:37:59 +03:00
self . report_fd = os . open ( tmp_file , os . O_NONBLOCK )
self . report_stream = os . fdopen ( self . report_fd , ' rb ' , 0 )
2022-05-26 18:44:02 +03:00
lvm_fd = os . open ( tmp_file , os . O_WRONLY )
2016-08-12 23:23:05 +03:00
2022-05-26 18:44:02 +03:00
# Set up the environment for using our own socket for reporting and disable the abort
# logic if lvm logs too much, which easily happens when utilizing the lvm shell.
local_env = { " LC_ALL " : " C " , " LVM_REPORT_FD " : " %s " % lvm_fd , " LVM_COMMAND_PROFILE " : " lvmdbusd " ,
" LVM_LOG_FILE_MAX_LINES " : " 0 " }
2016-10-11 17:36:47 +03:00
2022-06-06 17:56:32 +03:00
# If any env variables contain LVM we will propagate them too
for k , v in os . environ . items ( ) :
2023-04-23 13:49:37 +03:00
if " PATH " in k :
local_env [ k ] = v
2022-06-06 17:56:32 +03:00
if " LVM " in k :
local_env [ k ] = v
2022-09-07 00:23:06 +03:00
self . parent_stdin_fd , child_stdin_fd = pty . openpty ( )
self . parent_stdout_fd , child_stdout_fd = pty . openpty ( )
self . parent_stderr_fd , child_stderr_fd = pty . openpty ( )
self . parent_stdin = os . fdopen ( self . parent_stdin_fd , " w " )
self . parent_stdout = os . fdopen ( self . parent_stdout_fd , " r " )
self . parent_stderr = os . fdopen ( self . parent_stderr_fd , " r " )
2016-02-18 02:53:35 +03:00
# run the lvm shell
self . lvm_shell = subprocess . Popen (
2022-09-21 18:05:36 +03:00
[ cfg . LVM_CMD ] ,
2022-09-07 00:23:06 +03:00
stdin = child_stdin_fd ,
stdout = child_stdout_fd , env = local_env ,
stderr = child_stderr_fd , close_fds = True ,
pass_fds = ( lvm_fd , ) , shell = False )
2016-10-07 21:46:59 +03:00
try :
2022-09-07 00:23:06 +03:00
make_non_block ( self . parent_stdout_fd )
make_non_block ( self . parent_stderr_fd )
2016-10-07 21:46:59 +03:00
2022-09-07 00:23:06 +03:00
# Close our copies of the child FDs there were created with the fork, we don't need them open.
2022-05-26 18:44:02 +03:00
os . close ( lvm_fd )
2022-09-07 00:23:06 +03:00
os . close ( child_stdin_fd )
os . close ( child_stdout_fd )
os . close ( child_stderr_fd )
# wait for the first prompt
log_debug ( " waiting for first prompt... " )
errors = self . _read_response ( no_output = True ) [ 2 ]
if errors and len ( errors ) :
raise LvmBug ( errors )
log_debug ( " lvm prompt read!!! " )
2016-10-07 21:46:59 +03:00
except :
raise
finally :
2022-06-06 17:57:20 +03:00
# These will get deleted when the FD count goes to zero, so we
2016-11-29 21:37:59 +03:00
# can be sure to clean up correctly no matter how we finish
2016-10-07 21:46:59 +03:00
os . unlink ( tmp_file )
os . rmdir ( tmp_dir )
2016-08-12 23:23:05 +03:00
2023-02-27 17:57:24 +03:00
def _get_last_log ( self ) :
# Precondition, lock is held
2016-08-12 23:23:05 +03:00
self . _write_cmd ( ' lastlog \n ' )
2022-08-25 16:33:50 +03:00
report_json = self . _read_response ( ) [ 1 ]
2022-11-29 19:00:39 +03:00
return get_error_msg ( report_json )
2016-02-18 02:53:35 +03:00
def call_lvm ( self , argv , debug = False ) :
2016-08-12 23:23:05 +03:00
rc = 1
error_msg = " "
2016-10-07 21:45:30 +03:00
if self . lvm_shell . poll ( ) :
raise Exception (
self . lvm_shell . returncode ,
" Underlying lvm shell process is not present! " )
2017-03-09 00:35:53 +03:00
argv = add_no_notify ( argv )
2016-02-18 02:53:35 +03:00
# 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
2023-02-27 17:57:24 +03:00
with self . shell_lock :
self . _write_cmd ( cmd )
# read everything from the STDOUT to the next prompt
stdout , report_json , stderr = self . _read_response ( )
# Parse the report to see what happened
if ' log ' in report_json :
ret_code = int ( report_json [ ' log ' ] [ - 1 : ] [ 0 ] [ ' log_ret_code ' ] )
# If we have an exported vg we get a log_ret_code == 5 when
# we do a 'fullreport'
# Note: 0 == error
if ( ret_code == 1 ) or ( ret_code == 5 and argv [ 0 ] == ' fullreport ' ) :
rc = 0
else :
# Depending on where lvm fails the command, it may not have anything
# to report for "lastlog", so we need to check for a message in the
# report json too.
error_msg = self . _get_last_log ( )
2022-08-17 20:05:06 +03:00
if error_msg is None :
2023-02-27 17:57:24 +03:00
error_msg = get_error_msg ( report_json )
if error_msg is None :
error_msg = ' No error reason provided! (missing " log " section) '
2016-02-18 02:53:35 +03:00
if debug or rc != 0 :
2022-08-26 19:10:24 +03:00
log_error ( ( " CMD= %s " % cmd ) )
log_error ( ( " EC= %d " % rc ) )
2016-08-12 23:23:05 +03:00
log_error ( ( " ERROR_MSG= \n %s \n " % error_msg ) )
2016-02-18 02:53:35 +03:00
2016-11-29 20:07:21 +03:00
return rc , report_json , error_msg
2016-02-18 02:53:35 +03:00
2016-08-25 02:29:35 +03:00
def exit_shell ( self ) :
2023-02-27 17:57:24 +03:00
with self . shell_lock :
try :
if self . lvm_shell is not None :
self . _write_cmd ( ' exit \n ' )
self . lvm_shell . wait ( 1 )
self . lvm_shell = None
except Exception as _e :
log_error ( " exit_shell: %s " % ( str ( _e ) ) )
2016-08-25 02:29:35 +03:00
2016-02-18 02:53:35 +03:00
def __del__ ( self ) :
2022-09-21 18:05:36 +03:00
# Note: When we are shutting down the daemon and the main process has already exited
# and this gets called we have a limited set of things we can do, like we cannot call
# log_error as _common_log is None!!!
if self . lvm_shell is not None :
try :
self . lvm_shell . wait ( 1 )
except subprocess . TimeoutExpired :
print ( " lvm shell child process did not exit as instructed, sending SIGTERM " )
cfg . ignore_sigterm = True
self . lvm_shell . terminate ( )
child_exit_code = self . lvm_shell . wait ( 1 )
print ( " lvm shell process exited with %d " % child_exit_code )
2016-02-18 02:53:35 +03:00
if __name__ == " __main__ " :
2022-09-21 18:05:36 +03:00
print ( " USING LVM BINARY: %s " % cfg . LVM_CMD )
2022-05-25 23:58:15 +03:00
2016-02-18 02:53:35 +03:00
try :
2022-05-25 23:58:15 +03:00
if len ( sys . argv ) > 1 and sys . argv [ 1 ] == " bisect " :
shell = LVMShellProxy ( )
shell . exit_shell ( )
else :
shell = LVMShellProxy ( )
in_line = " start "
try :
while in_line :
in_line = input ( " lvm> " )
if in_line :
2022-09-07 00:24:20 +03:00
if in_line == " exit " :
shell . exit_shell ( )
sys . exit ( 0 )
2022-05-25 23:58:15 +03:00
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
2022-08-31 19:18:55 +03:00
except Exception as e :
2022-09-08 23:40:03 +03:00
log_error ( " main process exiting on exception! \n %s " % extract_stack_trace ( e ) )
2022-05-25 23:58:15 +03:00
sys . exit ( 1 )
sys . exit ( 0 )