systemrescue-zfs/airootfs/etc/systemd/scripts/sysrescue-autorun
Gerd v. Egidy 8cb9d2de6b harden sysrescue-autorun sysrescue-initialize.py against missing config values
Until now sysrescue-configuration.lua always ensured the default values were
in the effective JSON config because there was no way to remove them. Now the
lua script is improved to allow full config merging, including a delete function.
This could lead to the user accidently removing a value. The scripts did not
expect this and accessed non existing keys, leading to an exception.

This is fixed with this commit.
2022-04-24 01:08:27 +02:00

318 lines
13 KiB
Python
Executable file

#!/usr/bin/env python3
# Distributed under the terms of the GNU General Public License v2
# The original bash version of autorun was developed by Pierre Dorgueil in 2003
# The current python implementation has been developed by Francois Dupoux in 2008
#
# ----------------------- changeslog: -----------------------------------------
# 2003-10-01: Pierre Dorgueil --> original bash version of autorun for sysrescue
# 2008-01-26: Francois Dupoux --> rewrote autorun in python to support http
# 2008-01-27: Francois Dupoux --> added 'ar_ignorefail', 'ar_nodel', 'ar_disable'
# 2017-05-30: Gernot Fink --> ported the script from python2 to python3
# 2021-07-07: Alexander Mahr --> added 'ar_attempts'
# 2022-01-09: Francois Dupoux --> added support for yaml configuration
# 2022-01-09: Francois Dupoux --> option 'autoruns=' renamed 'ar_suffixes='
# 2022-01-23: Francois Dupoux --> use the generated effective configuration file
#
# ----------------------- autorun exec rules: ---------------------------------
# - pass 'ar_source=/dev/fd#' to request floppy device test
# - CD is tested if no floppy requested or no autorun found on floppy
# - if a file named 'autorun' is found on any media, it is always run, except if
# option 'ar_disable' is used
# - if a file named 'autorun[0-9A-F]' is found on any media, it is run if either
# - 'ar_suffixes=...' arg did specify its suffix (ex. ar_suffixes=1,3,5), or
# - no 'ar_suffixes=...' arg was passed
# - pass ar_suffixes=no to prevent running any 'autorun[0-9A-F]' file
# - defaults to allow all 'autorun[0-9A-F]' files
# - if many autorun files are to be run,
# - always in alphab order: autorun, then autorun0, then autorun1 etc...
# - first non-zero exit code stops all (except if ar_ignorefail is used)
# - if option 'ar_nodel' is used, the temp copy of the script will not be deleted
# - if option 'ar_ignorefail' is used, do not stop autorun if a script failed
# - if option 'ar_disable' is used, absolutely no autorun script will be run
import sys, os, re, subprocess, logging, time, glob, json
# ------------------------ initialize internal variables -----------------------
pidfile='/run/autorun.pid'
basedir='/var/autorun'
autorunlog=basedir+'/log'
autorunmnt=basedir+'/mnt'
autoruntmp=basedir+'/tmp'
defaultsrc=['/run/archiso/bootmnt/autorun','/run/archiso/bootmnt','/run/archiso/copytoram/autorun','/run/archiso/copytoram','/var/autorun/cdrom','/root','/usr/share/sys.autorun']
effectivecfg="/etc/sysrescue/sysrescue-effective-config.json"
autorunfiles=[]
config = {}
# ----------------------- functions definitions --------------------------------
def writemsg(message):
print(message)
logging.info(message)
# remove all '\r' in that file
def processdostextfiles(curfile):
txt=open(curfile,'rb').read()
origlen=len(txt)
txt=txt.replace(b'\r',b'')
if len(txt) != origlen:
writemsg(f'WARNING: \\r line endings removed from {curfile}.')
writemsg('Relying on automatic line ending sanitizing is deprecated and it will be removed from a future release.')
txtfile=open(curfile, 'wb')
txtfile.write(txt)
txtfile.close()
def is_elf_binary(filename):
with open(filename,'rb') as f:
content = f.read(4)
if len(content) == 4 and \
content[0] == '\x7f' and content[1] == 'E' and \
content[2] == 'L' and content[3] == 'F':
return True
else:
return False
def ensure_shebang(filename):
# does the file have a shebang?
with open(filename,'r+') as f:
content = f.read()
if len(content) > 2 and content[0] == '#' and content[1] == '!':
# we have a shebang, nothing to do
return
# no shebang, we have to add one
writemsg(f'WARNING: no shebang in {filename}.')
writemsg('This is deprecated and a shebang will be required in future releases.')
f.seek(0, 0)
f.write("#!/bin/sh\n" + content)
def format_title(title, padding):
totallen=80
startpos=int(totallen/2)-int(len(title)/2)
remain=totallen-startpos-len(title)
text=(padding*startpos)+title+(padding*remain)
return text
def copyfilefct_basic(src, dest):
if os.path.isfile(src):
dstfile=open(dest, 'wb')
dstfile.write(open(src,'rb').read())
dstfile.close()
os.chmod(dest, 755)
return 0
else:
return -1
def copyfilefct_http(src, dest):
logging.debug(f"Attempting to download {src} ...")
cmd=('wget','-q',src,'-O',dest)
p = subprocess.Popen(cmd)
p.wait()
if p.returncode == 0:
logging.info(f"Successfully downloaded {src}")
os.chmod(dest, 755)
return 0
else:
logging.warning(f"Failed to download {src}")
os.unlink(dest)
return -1
def search_autoruns(dirname, suffixes, copyfilefct):
found=0
for ext in suffixes:
curpath=os.path.join(dirname, f'autorun{ext}')
newpath=os.path.join(autoruntmp, f'autorun{ext}')
if copyfilefct(curpath, newpath)==0:
autorunfiles.append(newpath)
found+=1
return found
def read_cfg_value(name, defaultval=None):
if name in config:
val = config[name]
else:
val = defaultval
print(f"config['{name}']={val}")
return val
def main():
global config
errcnt=0 # in case no autorun executed
logging.basicConfig(filename='/var/log/sysrescue-autorun.log', format='%(asctime)s %(message)s', level=logging.DEBUG)
writemsg('Initializing autorun ...')
# ---- read the effective configuration file
if os.path.exists(effectivecfg) == False:
print (f"Failed to find effective configuration file in {effectivecfg}")
sys.exit(1)
with open(effectivecfg) as file:
fullcfg = json.load(file)
if 'autorun' in fullcfg:
config = fullcfg['autorun']
else:
config = { }
# ---- parse legacy options passed on the boot command line
for curopt in open("/proc/cmdline","r").read().split():
if re.match('^autoruns=', curopt): # "autoruns" is the legacy name for 'ar_suffixes'
config['ar_suffixes'] = curopt.replace('autoruns=','')
# ---- load the config into local variables, allow the keys not to exist
# ---- also print the effective configuration
logging.info(f"Showing the effective autorun configuration ...")
ar_suffixes = read_cfg_value('ar_suffixes', "")
ar_disable = read_cfg_value('ar_disable', False)
ar_attempts = read_cfg_value('ar_attempts', 1)
ar_source = read_cfg_value('ar_source', "")
ar_ignorefail = read_cfg_value('ar_ignorefail', False)
ar_nodel = read_cfg_value('ar_nodel', False)
ar_nowait = read_cfg_value('ar_nowait', False)
# ---- determine the effective script files suffixes
if ar_suffixes in (None, 'no', ''):
suffixes=['']
else:
suffixes=[''] + str(ar_suffixes).split(',')
logging.info(f"suffixes={suffixes}")
# ---- exit here is there is nothing to do
if ar_disable == True:
writemsg(f"Autorun has been disabled using ar_disable, exiting now")
sys.exit(0)
# ---- parse autorun sources ----
if re.match('^https?://', ar_source):
while ar_attempts > 0 and not autorunfiles:
time.sleep(1)
ar_attempts -= 1
search_autoruns(ar_source, suffixes, copyfilefct_http)
elif re.match('^/dev/', ar_source): # mount a partition/device
mnt1=('mount',ar_source,autorunmnt)
mnt2=('umount',autorunmnt)
p = subprocess.Popen(mnt1)
p.wait()
if p.returncode != 0:
writemsg('fatal error: cannot mount', mnt1)
sys.exit(1)
search_autoruns(autorunmnt, suffixes, copyfilefct_basic)
subprocess.Popen(mnt2)
elif re.match('^nfs://', ar_source): # mount an nfs share
source=ar_source.replace('nfs://','')
mnt1=('mount','-t','nfs','-o','nolock',source,autorunmnt)
mnt2=('umount',autorunmnt)
p = subprocess.Popen(mnt1)
p.wait()
if p.returncode != 0:
writemsg ('fatal error: cannot mount', mnt1)
sys.exit(1)
search_autoruns(autorunmnt, suffixes, copyfilefct_basic)
subprocess.Popen(mnt2)
elif re.match('^smb://', ar_source): # mount a samba share
source=ar_source.replace('smb://','')
mnt1=('mount','-t','cifs','//%s'%source,autorunmnt)
mnt2=('umount',autorunmnt)
p = subprocess.Popen(mnt1)
p.wait()
if p.returncode != 0:
writemsg ('fatal error: cannot mount',mnt1)
sys.exit(1)
search_autoruns(autorunmnt, suffixes, copyfilefct_basic)
subprocess.Popen(mnt2)
else: # search in all default directories
writemsg ('Cannot find a valid ar_source, searching scripts in the default directories')
found=0
for curdir in defaultsrc:
if found == 0:
found += search_autoruns(curdir, suffixes, copyfilefct_basic)
# ---- remove user setable ar_nowait flag if set ----
if os.path.isfile('/etc/ar_nowait'):
os.unlink('/etc/ar_nowait')
# ---- execute the autorun scripts found ----
for curfile in autorunfiles:
try:
if not is_elf_binary(curfile):
processdostextfiles(curfile)
# compatibility with old autorun: add #!/bin/sh if no shebang
ensure_shebang(curfile)
except:
pass
filebase=os.path.basename(curfile)
writemsg("\n")
writemsg(format_title(f'executing {filebase}', '='))
redir=os.path.join(autorunlog, filebase)
logoutput=open(redir,'wt')
try:
# directly (=without extra shell) execute the script
# stdin=None means the stdin of sysrescue-autorun will be passed through
# this allows the autorun script to take input from the terminal
proc = subprocess.Popen(curfile, stdin=None, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, shell=False,universal_newlines=True)
# pipe through stdout&stderr live, write it to the autorunlog too
# we do not expect too much data here, so reading byte-by-byte is ok
# but it allows us to show for example progress indicators live on the console
while not proc.stdout.closed and proc.stdout.readable() and proc.poll() is None:
output = proc.stdout.read(1)
sys.stdout.write(output)
sys.stdout.flush()
logoutput.write(output)
# the program has ended. read the rest of data that is in the buffer
if not proc.stdout.closed and proc.stdout.readable():
output = proc.stdout.read(-1)
sys.stdout.write(output)
sys.stdout.flush()
logoutput.write(output)
logoutput.close()
returncode = proc.returncode
except OSError as e:
# for example the program wasn't found or is not executable
writemsg (f'Execution of {filebase} failed: {e.strerror}')
returncode = e.errno
fileres=open(redir+'.return','wt')
fileres.write(str(returncode)+'\n')
fileres.close()
writemsg('='*80)
writemsg (f'Execution of {filebase} returned {returncode}')
if returncode != 0:
errcnt += 1
if ar_ignorefail == False:
writemsg (f'Now aborting autorun as {filebase} has failed')
break; # Stop on the first script failure
# ---- delete the copies of the scripts ----
if ar_nodel == False:
for curfile in autorunfiles:
writemsg (f'removing {curfile}')
os.unlink(curfile)
# ---- wait a keypress feature -----
if os.path.isfile('/etc/ar_nowait'):
ar_nowait = True
if (ar_nowait != True) and (len(autorunfiles) > 0):
writemsg(f'Autorun scripts completed with {errcnt} errors, press <Enter> to continue')
sys.stdin.read(1)
return errcnt
# ----------------------- autorun main ----------------------------------------
for curdir in (basedir, autorunlog, autorunmnt, autoruntmp):
if not os.path.isdir(curdir):
os.mkdir(curdir)
# Exit if already running
if os.path.isfile(pidfile):
sys.exit(0)
# create lockfile
lockfile = open(pidfile, 'wt')
lockfile.write(str(os.getpid()))
try:
res = main()
sys.exit(res)
finally:
os.unlink(pidfile)