2022-09-10 00:05:11 +03:00
#!/usr/bin/python3
# Simulate errors by doing the following for both lvm and lvm shell:
# Randomly return
# - Bad exit code
# - Exit code 5 (exported VG)
# - Truncated JSON
# - Missing key in JSON
#
# This is done by sitting between lvm dbusd and lvm. If running via systemd, add the following to the service file
# Environment="LVM_BINARY=/path/to/this file/lvm2/test/dbus/lvm_error_inject.py"
# systemctl daemon-reload
# systemctl restart lvm2-lvmdbusd
import copy
import json
2023-03-09 20:27:19 +03:00
import multiprocessing
2022-09-10 00:05:11 +03:00
import os
import pty
import random
import select
2023-03-09 20:27:19 +03:00
import signal
2022-09-10 00:05:11 +03:00
import string
import subprocess
import sys
import tempfile
import traceback
from collections import deque
from fcntl import fcntl , F_GETFL , F_SETFL
from subprocess import Popen
CS = string . ascii_letters + " \n \t " + string . digits
2023-03-09 20:27:19 +03:00
run = multiprocessing . Value ( ' i ' , 1 )
SH = None
2022-09-10 00:05:11 +03:00
def rs ( length , character_set = CS ) :
return ' ' . join ( random . choice ( character_set ) for _ in range ( length ) )
RS = rs ( 512 )
2023-03-10 19:41:20 +03:00
d_out = open ( os . path . join ( tempfile . gettempdir ( ) , " mitm_lvm.txt " ) , " a " )
2022-09-10 00:05:11 +03:00
def debug ( msg ) :
m = str ( msg )
d_out . write ( m )
if m [ - 1 ] != " \n " :
d_out . write ( " \n " )
d_out . flush ( )
# Make stream non-blocking
def make_non_block ( stream ) :
flags = fcntl ( stream , F_GETFL )
fcntl ( stream , F_SETFL , flags | os . O_NONBLOCK )
def read_decoded ( stream ) :
tmp = stream . read ( )
if tmp :
return tmp . decode ( " utf-8 " )
return ' '
def write_some ( q , stream , remaining = False , binary = False ) :
if len ( q ) > 0 :
if remaining :
to_send = len ( q )
else :
to_send = random . randint ( 1 , len ( q ) )
for _ in range ( 0 , to_send ) :
c = q . popleft ( )
if binary :
stream . write ( bytes ( c , " utf-8 " ) )
else :
stream . write ( c )
stream . flush ( )
def del_random_key ( src_dict ) :
keys = list ( src_dict . keys ( ) )
pick = random . randint ( 0 , len ( keys ) - 1 )
2023-03-09 20:25:58 +03:00
debug ( " %s will be deleted " % keys [ pick ] )
2022-09-10 00:05:11 +03:00
del src_dict [ keys [ pick ] ]
def inject_key_error ( output_json ) :
debug ( " Deleting a key " )
for r in output_json [ ' report ' ] :
if ' lv ' in r :
for i in r [ ' lv ' ] :
2023-03-09 20:25:58 +03:00
debug ( " deleting a lv key " )
2022-09-10 00:05:11 +03:00
del_random_key ( i )
2023-03-09 20:25:58 +03:00
return
2022-09-10 00:05:11 +03:00
if ' vg ' in r :
for i in r [ " vg " ] :
2023-03-09 20:25:58 +03:00
debug ( " deleting a vg key " )
2022-09-10 00:05:11 +03:00
del_random_key ( i )
2023-03-09 20:25:58 +03:00
return
2022-09-10 00:05:11 +03:00
elif ' pv ' in r :
for i in r [ " pv " ] :
2023-03-09 20:25:58 +03:00
debug ( " deleting a pv key " )
2022-09-10 00:05:11 +03:00
del_random_key ( i )
2023-03-09 20:25:58 +03:00
return
2022-09-10 00:05:11 +03:00
def inject_exit_error ( output_json , val ) :
if ' log ' in output_json and len ( output_json [ ' log ' ] ) > 0 :
debug ( " Returning bad exit code " )
# Change the exit code to failure
output_json [ ' log ' ] [ - 1 : ] [ 0 ] [ ' log_ret_code ' ] = " %d " % val
else :
# We are in fork & exec mode, just exit.
if val == 0 :
sys . exit ( 1 )
sys . exit ( val )
def inject_error ( output_str , output_json = None ) :
try :
if random . randint ( 0 , 9 ) == 1 :
error_case = random . randint ( 0 , 3 )
if error_case == 0 :
debug ( " Truncating JSON " )
# Create bad JSON by truncating it
str_rep = output_str [ : - len ( output_str ) / / 2 ]
rc = str_rep
else :
if output_json is None :
output_json = json . loads ( output_str )
if error_case == 1 :
inject_key_error ( output_json )
elif error_case == 2 :
debug ( " Returning bad exit code " )
inject_exit_error ( output_json , 0 )
else :
debug ( " Returning exit code 5 " )
inject_exit_error ( output_json , 5 )
rc = json . dumps ( output_json ) + " \n "
else :
rc = output_str
except Exception as e :
debug ( " Exception %s occurred: JSON = \n %s \n " % ( str ( e ) , output_str ) )
sys . exit ( 100 )
return rc
def run_one ( cmd ) :
debug ( " run_one( %s ) " % str ( cmd ) )
result = subprocess . run ( cmd , capture_output = True , text = True )
if " fullreport " in cmd :
sys . stdout . write ( inject_error ( result . stdout ) )
else :
sys . stdout . write ( result . stdout )
sys . stdout . flush ( )
sys . stderr . write ( result . stderr )
sys . stderr . flush ( )
return result . returncode
class LvmShellHandler :
def __init__ ( self , cmd ) :
debug ( os . environ )
self . d_stdout = deque ( )
self . d_stderr = deque ( )
self . d_report = deque ( )
tmp_dir = tempfile . mkdtemp ( prefix = " pipeinmiddle_ " )
self . tmp_file = " %s /middle_man_report " % tmp_dir
# Let's create a fifo for the report output
os . mkfifo ( self . tmp_file , 0o600 )
self . child_report_fd = os . open ( self . tmp_file , os . O_NONBLOCK )
self . child_report_stream = os . fdopen ( self . child_report_fd , ' rb ' , 0 )
passed_report_fd = os . open ( self . tmp_file , os . O_WRONLY )
debug ( " passed_report_fd = %d " % passed_report_fd )
# The report FD from who executed us.
self . daemon_report_fd = int ( os . environ [ " LVM_REPORT_FD " ] )
self . daemon_report_stream = os . fdopen ( self . daemon_report_fd , " wb " , 0 )
env = copy . deepcopy ( os . environ )
env [ " LVM_REPORT_FD " ] = " %s " % str ( passed_report_fd )
2023-03-09 20:23:47 +03:00
env [ " LC_ALL " ] = " C "
env [ " LVM_COMMAND_PROFILE " ] = " lvmdbusd "
2022-09-10 00:05:11 +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 " )
debug ( " exec ' ing %s " % cmd )
self . process = Popen ( cmd ,
stdin = child_stdin_fd ,
stdout = child_stdout_fd ,
stderr = child_stderr_fd ,
close_fds = True ,
env = env ,
pass_fds = [ passed_report_fd , ] ,
shell = False )
os . close ( passed_report_fd )
os . close ( child_stdin_fd )
os . close ( child_stdout_fd )
os . close ( child_stderr_fd )
make_non_block ( self . parent_stdout_fd )
make_non_block ( self . parent_stderr_fd )
make_non_block ( sys . stdin )
self . report_text_in_progress = " "
self . last_request = " "
os . unlink ( self . tmp_file )
os . rmdir ( tmp_dir )
def _complete_response ( self ) :
try :
_complete_json = json . loads ( self . report_text_in_progress )
return _complete_json
except ValueError :
return None
def _write_all ( self ) :
write_some ( self . d_stderr , sys . stderr , remaining = True )
write_some ( self . d_report , self . daemon_report_stream , remaining = True , binary = True )
write_some ( self . d_stdout , sys . stdout , remaining = True )
def _handle_report ( self ) :
# Read from child report stream, write to parent report stream
report_text = read_decoded ( self . child_report_stream )
self . report_text_in_progress + = report_text
report_json = self . _complete_response ( )
# Always wait until we have a full response before we do anything with the output
if report_json is not None :
# Only add data to d_report after we have the entire JSON and have injected
# an error into it if we so wish, usually only for 'fullreport'
if " fullreport " in self . last_request :
self . d_report . extend ( inject_error ( self . report_text_in_progress , report_json ) )
else :
debug ( " Not the cmd we are looking for ... " )
self . d_report . extend ( self . report_text_in_progress )
self . report_text_in_progress = " "
def _handle_command ( self ) :
2023-03-09 20:27:19 +03:00
global run
2022-09-10 00:05:11 +03:00
stdin_text = sys . stdin . readline ( )
self . last_request = stdin_text
debug ( " stdin: %s ... " % stdin_text [ : min ( 10 , len ( stdin_text ) - 1 ) ] )
2023-03-09 20:27:19 +03:00
if " exit \n " in stdin_text :
debug ( " asking to exit ... " )
run . value = 0
2022-09-10 00:05:11 +03:00
self . parent_stdin . writelines ( stdin_text )
self . parent_stdin . flush ( )
@staticmethod
def _empty_stream_to_queue ( stream , queue ) :
read_text = stream . readlines ( )
for line in read_text :
queue . extend ( line )
def run ( self ) :
2023-03-09 20:27:19 +03:00
global run
2022-09-10 00:05:11 +03:00
select_tmo = 0.2
2023-03-09 20:27:19 +03:00
while run . value == 1 :
2022-09-10 00:05:11 +03:00
try :
rd_fd = [ sys . stdin . fileno ( ) , self . parent_stdout_fd , self . parent_stderr_fd , self . child_report_fd ]
ready = select . select ( rd_fd , [ ] , [ ] , select_tmo )
if len ( ready [ 0 ] ) == 0 :
write_some ( self . d_stderr , sys . stderr )
write_some ( self . d_report , self . daemon_report_stream , binary = True )
for r in ready [ 0 ] :
if r == self . parent_stdout_fd :
LvmShellHandler . _empty_stream_to_queue ( self . parent_stdout , self . d_stdout )
elif r == self . parent_stderr_fd :
LvmShellHandler . _empty_stream_to_queue ( self . parent_stderr , self . d_stderr )
elif r == self . child_report_fd :
self . _handle_report ( )
elif r == sys . stdin . fileno ( ) :
# Read from parent stdin write to child stdin, this is a command getting issued.
self . _handle_command ( )
else :
debug ( " FD %d not handled! " % r )
sys . exit ( 10 )
# We have handled all the FDs that were ready, write some output
if len ( self . d_stdout ) > 0 :
self . _write_all ( )
else :
write_some ( self . d_stderr , sys . stderr )
write_some ( self . d_report , self . daemon_report_stream , binary = True )
# Check to see if child process has terminated, None when running
if self . process . poll ( ) is not None :
self . _write_all ( )
debug ( " child process %s exited %d " % ( cmd , self . process . returncode ) )
break
except IOError as ioe :
debug ( " run_cmd: " + str ( ioe ) )
2023-03-09 20:27:19 +03:00
if self . process . poll ( ) is not None :
debug ( " exiting %d " % self . process . returncode )
else :
debug ( " lvm process still running, be we are exiting ... " )
2022-09-10 00:05:11 +03:00
return self . process . returncode
if __name__ == " __main__ " :
try :
args = sys . argv [ 1 : ]
exe = os . getenv ( " LVM_MAN_IN_MIDDLE " , " /usr/sbin/lvm " )
cmdline = [ exe , ]
if args :
cmdline . extend ( args )
ec = run_one ( cmdline )
else :
2023-03-10 19:41:50 +03:00
if " LVM_REPORT_FD " in os . environ :
SH = LvmShellHandler ( cmdline )
ec = SH . run ( )
else :
debug ( ' running as lvm shell requires: LVM_REPORT_FD to be set ' )
ec = 1
2022-09-10 00:05:11 +03:00
sys . exit ( ec )
except Exception :
traceback . print_exc ( file = d_out )
sys . exit ( 1 )