Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

How I installed Windows 98 VM in virt-manager

Here are some notes I've assembled during my journey to install Windows 98 SE on a virtual machine on modern hardware. This isn't a complete follow-me guide, but hopefully can get you further in the right direction.

Summary

  • Host:
  • CentOS 7
  • kvm, QEMU
  • libvirt
  • virt-manager on remote system (Fedora) with X11
  • existing bridged network interface
  • VM:
  • cpu pentium2
  • memory 128MB
  • graphics cirrus
  • sound (couldn't get to work even with drivers) Realtek AC'97
  • bridge nic, of type rtl8139

Narrative

I used virt-manager, and selected "Create VM." I gave my new system a 30GB hard drive because I remember that Windows XP kind of wanted 40GB, so 30GB should be in the range. Also, I gave my new VM 128MB of memory because I remember that being in the low end for Windows XP.

It took me quite a while to sus out the types of graphics and sound cards that libvirt can provide that should work with Windows 98 SE. I found that I could manually type in cirrus for the graphics card even thought it wasn't in the drop-down list. In the VM, use driver Cirrus Logic 5446 PCI, which will let you get up to 1024x768x16.

I already had a bridged network card for my virtualized environments, and I emulated the Realtek 8139 for which I found drivers on the Internet. But to get the NIC recognized, I needed to perform a few steps documented on reference 3:

In Windows 98 goto My Computer>Control Panel>System>Device Manager>

Is there a yellow ! next to Plug and Play BIOS(fail safe) ?If so double click on it.

Update Driver>Next>mark Display a list of all drivers....>mark Show all hardware,>click on PCI bus>Next>The driver that you have chosen was not.... CLICK Yes>Next>The driver you have chosen is older than....CLICK Yes>Finish> Yes Restart

I couldn't get sb16 for the sound card, so I tried to find Realtek AC'97 drivers but after I installed them I couldn't get audio to work.

I forgot how many reboots it took the get Windows 98 going! It would have been frustrating if I had to wait for real hardware to boot. The VM of course boots in under 2 seconds. I also discovered that the system would always hang at boot. But it works when I forcibly-reset and at the recovery menu chose "confirm step-by-step", and then half-slowly press ENTER until I had approved every single step. I suspect the speed of the VM has something to do with choking up Windows. Yeah, that's my line and I'm sticking to it.

Injecting files into the VM

Before I got my nic up (and even afterwards, without any usable TCP/IP-based file sharing), I would build an iso file with any files I wanted to pass to the VM. Then I can mount the iso in the virtualized IDE cd drive.

mkisofs -J -rock -V drvdisc1 -o w98drivers.iso path_with_all_files/

Alternatively, you can just pass each filename to be added.

References

Weblinks

  1. iso file
  2. How to install Windows 98 in QEMU - Computernewb Wiki
  3. How TO install Windows 98 with Qemu Hardy/ Gusty x86/ AMD64 was the most useful part.
  4. virtual machines - How to increase video memory in libvirt/KVM gui? - Server Fault
  5. https://www.claunia.com/qemu/drivers/index.html
  6. Windows 98 & Samba – framebuffer.io
  7. NfsAxe Windows NFS Client And NFS Server 3.6 Download page
  8. PC Audio Codecs > AC'97 Audio Codecs Software - REALTEK

Specific download files

  1. https://www.claunia.com/qemu/drivers/w2k_8139.zip (my mirror)
  2. https://www.claunia.com/qemu/drivers/w9x_sb16.zip (my mirror
  3. http://framebuffer.io/DSCLIENT.EXE (my mirror)
  4. https://web.archive.org/web/20160307000259/http://www.labtam-inc.com/download/win32/pnfslabp.exe (my mirror)
  5. https://www.realtek.com/en/agree-to-download?downloadid=f46b37f00890518f2f9bd795606dd56d which lead to CDN file https://realtekcdn.akamaized.net/rtdrivers/pc/audio/0001-WDM_A406.exe?token=exp=1644240073~acl=/rtdrivers/pc/audio/0001-WDM_A406.exe~hmac=9a498ee5fe5329f71a67d3aba0e9de67fa834c9fcdbf20c5d68135890732bb17 (my mirror)

Internal files

My original Windows 98 SE license key

Open letter to Senator Lindsey Graham

Dear Senator Graham: As your constituent, in 2020 I sent you a message asking that you reject the EARN IT Act. You have now introduced EARN IT Act of 2022 (S.3538), which I urge you as my duly elected senator, to reject.

Please stop sponsoring bills that seek to harm the citizens of our great nation. Removing the protections that encryption provides, regardless of the good intention of stopping CSAM, will harm the citizens of the United States.

The government is restricted from unlawful search and seizure of citizens and their property (US Bill of Rights, Amendment IV), and this Act that you have sponsored calls for the violation of that right of the citizens. Requiring that all contents be scanned, for any purpose, is broad-scope searching, which is a violation of your and my rights.

While in general I agree with the choices you make as my senator, the list of choices I disagree with keeps getting longer. Please stop adding to this list of our disagreements! Please remove your support of the EARN IT Act of 2022. Respectfully, Your constituent

References

  1. It's Back: Senators Want 'EARN IT' Bill To Scan All Online Messages - Slashdot
  2. Graham, Blumenthal Introduce EARN IT Act to Encourage Tech Industry to Take Online Child Sexual Exploitation Seriously - Press Releases - United States Senator Lindsey Graham
  3. It’s Back: Senators Want EARN IT Bill to Scan All Online Messages | Electronic Frontier Foundation
  4. EARN IT Act "Myths and Facts" document | Electronic Frontier Foundation
  5. Text - S.3538 - 117th Congress (2021-2022): EARN IT Act of 2022 | Congress.gov | Library of Congress
  6. U.S. Senate: States in the Senate | South Carolina

Shell trick: check if set -x

In a shell script, I wanted to manually set +x, which disables the echo-command before running each command. I didn't want any -x invocations in the parent scripts to display the crappy logic I was implementing in this one script. But, I also wanted the script to be dot-sourced, so it would set environment variables that will be used later.

So, I first had to learn how to restore the previous value of +/-x. And then I just grep and use an if statement at the end.

oldx="$( printf %s\\n "$-" | grep -q -e 'x' && echo YES )"
set +x # hide this cruft
# terrible and verbose logic that should never get displayed to a -x invocation
test "${oldx}" = "YES" && set -x

References

Weblinks

  1. shopt - How can I list Bash's options for the current shell? - Unix & Linux Stack Exchange

My scanpdf solution

I have not personally needed to forcibly scan a document, i.e., print one out, just to sign it, and then scan it in again. I have my trusty signatures.pdf that I scanned in a few years ago with 10 iterations of my signature. I always just screenshot a signature, and use LibreOffice Draw's ability to set a transparent color of an imported image.

But, after reading an interesting Hacker News discussion, I have whipped up a "scanpdf" solution. 99% of this is ripped straight from the author, mrb.

Here is my file ~/bin/scanpdf.sh, with typo/semantic fixes.

#!/bin/bash
# from https://news.ycombinator.com/item?id=30024165#30027344
# Make a pdf look like it was scanned.
if [ $# -ne 2 ]; then
   echo "Usage: $0 input output" >&2
   exit 1
fi
tmp="$1".scanner-look.tmp
mkdir "$tmp" &&
# without -flatten some PDF convert to a JPG with a black background
convert -density 150 "$1" -colorspace Gray -quality 60 -alpha flatten "$tmp"/p_in.jpg &&
: || exit 1
# each page is randomly shifted in the X and Y plane.
# units seem to depend on angle of rotation in ScaleRotateTranslate?
offset() { echo $(($RANDOM % 1000)); }
for f in "$tmp"/p_in*jpg; do
   # each page is randomly rotated by [-0.5 .. 0.5[ degrees
   angle=$(python3 -c 'import random; print(random.random()-0.5)')
   x=$(offset)
   y=$(offset)
   convert "$f" \
      -blur 0x0.5 \
        -distort ScaleRotateTranslate "$x,$y $angle" +repage \
      \( +clone +noise Random -fill white -colorize 95% \) \
      -compose darken \
      -composite \
      ${f/p_in/p_out}.pdf || exit 1
done
# concatenate all the pages to one PDF
# use "ls -v" to order files correctly (p_out-X.jpg where X is 0 1 2 ... 9 10 11 ...)
pdftk $(ls -v "$tmp"/p_out*.pdf) cat output "$2" &&
rm -rf "$tmp"

Dependencies include pkdftk(1) and ImageMagick. Also important is to make sure the ImageMagick policy had read and write acess to PDFs. Ensure something similar to this logic is in file /etc/ImageMagick-6/policy.xml.

<policy domain="coder" rights="read | write" pattern="PDF" />

References

Weblinks

  1. FalsiScan: Make it look like a PDF has been hand signed and scanned | Hacker News
  2. Edouard Klein / falsisign | Gitlab
  3. linux - ImageMagick security policy 'PDF' blocking conversion - Stack Overflow

Shell function: newest

One of my small shell functions in my user profile is newest which I use on a daily basis. I wrote it about 5 years ago as an easy way to get the filename of the most recently modified file in a given path.

Here's the source of the function:

newest() { 
   [[ ! "$1" = "" ]] && searchdir="$1" || searchdir=".";
   [[ ! "$2" = "" ]] && searchstring="$2" || searchstring="*";
   find $searchdir -name "*$searchstring*" 2> /dev/null | xargs ls -t 2> /dev/null | head -n 1
}

I can tell that my style has changed in the years since I wrote this. Here's how I would write it today. Optionally I would add -maxdepth 1 -mindepth 1 to the find invocation. Maybe that should be another parameter.

newest() {
   ___sdir="." ; test -n "${1}" && ___sdir="${1}" ;
   ___sstr="*" ; test -n "${2}" && ___sstr="${2}" ;
   { find "${___sdir}" -name "*${___sstr}*" | xargs ls -1t | head -n1 ; } 2>/dev/null
}

So what this function does is show the full pathname of the file that is most recently modified. It's great for when you want to edit the most recent file, such as today's log file:

vi $( newest /mnt/public/Support/Systems/server1/var/log/debmirror/ )

Alert on new forum topics to Discord

It came to my attention that my AoE2 clan mates and I don't frequent the Age of Empires forums. And it is there that the announcements for voting for the ranked map rotations occur. I decided to remedy our lack of awareness of this situation.

So, I searched on how to get notifications on a Discourse forum, which led me to a discussion on meta.discours.org. Somebody links to a python project that searches a Discourse forum and caches the results, so it can compare over time, and send notifications only on new topics, to a Mac OS notification center. This project works so great that it really only needed the Discord webhook, with no other modifications!

I secured a webhook for my clan's Discord "server" (so-called, because it's really just a namespace for us, not a dedicated VM or instance or anything) and added a few lines of Discord logic to that project, named sniffa.

I set up my sniffa input file, ~/.sniffa/watches-aoe2.ini with this contents:

[sniffa.domain]
url = https://forums.ageofempires.com
webhook = 'https://discord.com/api/webhooks/123498237429749274924/justanexamplewebhook123458987s87'
discord_user = AoE2 forum bot

[VOTE NOW ranked rotation #age-of-empires-ii:aoe2-de in:title]
ids = 666250,280747,280748,666257,651927

And I set up an unprivileged local user and installed pip package discord. And I placed in crontab:

*/30  *  *  *  *  bot   /home/bot/sniffa/sniffa.py aoe2 1>/dev/null 2>&1

How it works

The way sniffa works is it uses the ini section headings [VOTE NOW...] as the search expression, and it uses the modern web abilities to fetch results of mimetype application/json, from the native Discourse search function! It's really clever. And then, it gets better. It caches the results back to the ini file, so that it only alerts on new results.

I crafted my search query by visiting the forum and adjusting my search parameters and options until it was exactly what I want. Additional searches can be added by just adding a new [search query] heading!

Add Globalsign certs to Citrix Workspace

I was setting up my Citrix Receiver client, now calling itself Workspace, and I finally got ready to connect to the corporate VDI farm. I couldn't connect to it and got a TLS error. I'm amused this stuff isn't loaded up already in the package, but whatever. I know what to do.

I found in my Steam directory some GlobalSign certificates. I'm sure one could just visit the scumbags themselves and get the files, but why create extra network traffic when I didn't have to?

$ locate -i globalsign | tail -n8 > ~/globalsign.certfiles
$ cat ~/globalsign.certfiles
/home/bgstack15/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/var/tmp-LIHOF1/usr/etc/ssl/certs/GlobalSign_Root_CA_-_R3.pem
/home/bgstack15/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/var/tmp-LIHOF1/usr/etc/ssl/certs/GlobalSign_Root_CA_-_R6.pem
/home/bgstack15/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/var/tmp-LIHOF1/usr/share/ca-certificates/mozilla/GlobalSign_ECC_Root_CA_-_R4.crt
/home/bgstack15/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/var/tmp-LIHOF1/usr/share/ca-certificates/mozilla/GlobalSign_ECC_Root_CA_-_R5.crt
/home/bgstack15/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/var/tmp-LIHOF1/usr/share/ca-certificates/mozilla/GlobalSign_Root_CA.crt
/home/bgstack15/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/var/tmp-LIHOF1/usr/share/ca-certificates/mozilla/GlobalSign_Root_CA_-_R2.crt
/home/bgstack15/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/var/tmp-LIHOF1/usr/share/ca-certificates/mozilla/GlobalSign_Root_CA_-_R3.crt
/home/bgstack15/.local/share/Steam/steamapps/common/SteamLinuxRuntime_soldier/var/tmp-LIHOF1/usr/share/ca-certificates/mozilla/GlobalSign_Root_CA_-_R6.crt

Copy them to the correct path for the Fedora-based ICA Client.

for word in $( cat ~/globalsign.certfiles ) ; do sudo cp -pi "${word}" /opt/Citrix/ICAClient/keystore/cacerts/ ; done

Change to that directory and then run a poor man's c_rehash shell loop. Although now that I think about this, I don't think this was necessary.

for word in *.crt ; do a="$( openssl x509 -hash -noout -in "${word}" )" ; sudo ln -s "${word}" "${a}.0.pem" ; done

Netmounts trayicon

In another cute and dangerous way to loop status checks, here is my netmounts-trayicon script. My goal with this tool is to provide a system tray icon that lets me easily umount and mount my nfs exports. I probably should learn to use autofs, but that's a pain, and I'm convinced there's something wrong with the Debian-based autofs daemon.

I wrote this because when I switch from wired to wireless networking to take my laptop away from my desk, the nfs mounts don't survive changing network cards. And nfs timeouts are notoriously long (5 or 10 minutes or something), so I would rather just choose to unmount the shares, and then re-mount them.

In the script, I loop through grepping the output of the mount command and update my capital-M/lowercase-M tray icon. I also set it to have the tooltip contents of the list of current nfs mounts. Again, I used mktrayicon, with very simple icons, stylized lower- and upper-case letter "M." I got the icons under a linkware license from visualpharm.com.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/bin/sh
# Startdate: 2021-12-26 21:10
# Reference: keyboard-leds-trayicons
# Documentation:
#    for some stupid reason sudo /usr/local/bin/vpn-on doesn't work, so I just use the real commands here.

clean_vpn_trayicon() {
   { test -e "${vpn_trayicon}" && echo "q" > "${vpn_trayicon}" ; } 1>/dev/null 2>&1 &
   sleep 1 && rm -f "${vpn_trayicon}" "${vpn_KILLFILE}"
}

export vpn_trayicon="/var/run/user/$( id -u )/${$}.vpn.icon"
export vpn_KILLFILE=/tmp/kill-all-vpn-trayicons

test "ON" = "ON" && {
   mkfifo "${vpn_trayicon}"
   mktrayicon "${vpn_trayicon}" &
   echo "m Turn vpn on,sudo wg-quick up wg0|Turn vpn off,sudo wg-quick down wg0|quit,echo 'q' > ${vpn_trayicon} ; touch \"${vpn_KILLFILE}\"" > "${vpn_trayicon}"
   echo "i networkmanager" > "${vpn_trayicon}"
}

rm -f "${vpn_KILLFILE}"

trap 'trap "" 2 ; touch "${vpn_KILLFILE}" '  2 # CTRL-C

while ! test -e "${vpn_KILLFILE}" 2>/dev/null ;
do
   ip -o a s wg0 1>/dev/null 2>&1 ; status_now=$? ;
   if test "${status_now}" != "${status_old}" ;
   then
      test -p "${vpn_trayicon}" && case "${status_now}" in
         0) # vpn is on now
            test -n "${VPN_DEBUG}" && echo "vpn is on (icon file ${vpn_trayicon})" 1>&2
            echo "i /usr/local/share/vpn-on.svg" > "${vpn_trayicon}"
            echo "t vpn is on" > "${vpn_trayicon}"
            ;;
         1) # vpn is off now
            test -n "${VPN_DEBUG}" && echo "vpn is off (icon file ${vpn_trayicon})" 1>&2
            echo "i /usr/local/share/vpn-off.svg" > "${vpn_trayicon}"
            echo "t vpn is off" > "${vpn_trayicon}"
            ;;
      esac
   fi
   status_old="${status_now}"
   sleep 1
done

# safety shutoff
clean_vpn_trayicon

And my /usr/local/bin/netmounts-on file:

1
2
#!/bin/sh
sudo mount -av -t nfs

And netmounts-off script.

1
2
3
#!/bin/sh
mounts="$( mount | awk '$5~/nfs/{print $3}' )"
for word in ${mounts} ; do sudo umount -lv "${word}" ; done

Backup your system configs!

One of my yearly todo tasks is to backup my system configs. Years ago, I wrote host-bup which is a shell script that really just makes a tarball of the files flisted in a config file. The config file takes an exact syntax which includes some pre-scripts if you want them, which I use for listing installed packages.

Host-bup shell script

#!/bin/sh
# Filename: host-bup.sh
# Location:
# Author: bgstack15@gmail.com
# Startdate: 2017-05-24 19:51:55
# Title: Script that Bups Configs on a Host
# Purpose: To provide a single conf and command for backing up important conf files
# Package: bgscripts
# History:
#    2017-11-11a Added FreeBSD support
#    2018-12-10 change directory
# Usage: host-bup
# Reference: ftemplate.sh 2017-05-24a; framework.sh 2017-05-24a
# Improve:
fiversion="2017-05-24a"
hostbupversion="2017-11-11a"
usage() {
   less -F >&2 <<ENDUSAGE
usage: host-bup.sh [-duV] [-c conffile]
version ${hostbupversion}
-d debug   Show debugging info, including parsed variables.
-u usage   Show this usage block.
-V version Show script version number.
-c conf    Select conf file. Default is ${conffile}.
-n dryrun  Only perform debugging. Do not execute scripts or build tgz.
-f force   Ignore hostname mismatch.
host-bup.sh is designed to easily bup the config files on a host.
Debug level 5 and above will not execute the script_1_cmd values.
Return values:
0 Normal
1 Help or version info displayed
2 Hostname mismatch
3 Incorrect OS type
4 Unable to find dependency
5 Not run as root or sudo
ENDUSAGE
}
# DEFINE FUNCTIONS
# DEFINE TRAPS
clean_hostbup() {
   rm -f ${tmpfile1} > /dev/null 2>&1
   : #use at end of entire script if you need to clean up tmpfiles
}
CTRLC() {
   #trap "CTRLC" 2
   : #useful for controlling the ctrl+c keystroke
   clean_hostbup
   trap '' 0; exit
}
CTRLZ() {
   #trap "CTRLZ" 18
   : #useful for controlling the ctrl+z keystroke
}
parseFlag() {
   flag="$1"
   hasval=0
   case ${flag} in
      # INSERT FLAGS HERE
      "d" | "debug" | "DEBUG" | "dd" ) setdebug; ferror "debug level ${debug}";;
      "u" | "usage" | "help" | "h" ) usage; exit 1;;
      "V" | "fcheck" | "version" ) ferror "${scriptfile} version ${hostbupversion}"; exit 1;;
      #"i" | "infile" | "inputfile" ) getval;infile1=${tempval};;
      "c" | "conffile" | "conf" ) getval; conffile="${tempval}";;
      "n" | "dry" | "dryrun" ) HOSTBUP_DRYRUN=1;;
      "f" | "force" ) HOSTBUP_FORCE=1;;
   esac
   debuglev 10 && { test ${hasval} -eq 1 && ferror "flag: ${flag} = ${tempval}" || ferror "flag: ${flag}"; }
}
# DETERMINE LOCATION OF FRAMEWORK
while read flocation; do if test -e ${flocation} && test "$( sh ${flocation} --fcheck 2>/dev/null )" -ge 20170524; then frameworkscript="${flocation}"; break; fi; done <<EOFLOCATIONS
./framework.sh
${scriptdir}/framework.sh
~/bin/bgscripts/framework.sh
~/bin/framework.sh
~/bgscripts/framework.sh
~/framework.sh
/usr/local/bin/bgscripts/framework.sh
/usr/local/bin/framework.sh
/usr/bin/bgscripts/framework.sh
/usr/bin/framework.sh
/bin/bgscripts/framework.sh
/usr/local/share/bgscripts/framework.sh
/usr/share/bgscripts/framework.sh
/usr/libexec/bgscripts/framework.sh
EOFLOCATIONS
test -z "${frameworkscript}" && echo "$0: framework not found. Aborted." 1>&2 && exit 4
# INITIALIZE VARIABLES
# variables set in framework:
# today server thistty scriptdir scriptfile scripttrim
# is_cronjob stdin_piped stdout_piped stderr_piped sendsh sendopts
. ${frameworkscript} || echo "$0: framework did not run properly. Continuing..." 1>&2
infile1=
outfile1=
tmpfile1="$( mktemp )"
logfile=${scriptdir}/${scripttrim}.${today}.out
define_if_new interestedparties "bgstack15@gmail.com"
# SIMPLECONF
define_if_new default_conffile "/etc/installed/host-bup.conf"
conffile="${default_conffile}"
# REACT TO OPERATING SYSTEM TYPE
case $( uname -s ) in
   Linux) : ;;
   FreeBSD) : ;;
   *) echo "${scriptfile}: 3. Indeterminate OS: $( uname -s )" 1>&2 && exit 3;;
esac
## REACT TO ROOT STATUS
#case ${is_root} in
#   1) # proper root
#      : ;;
#   sudo) # sudo to root
#      : ;;
#   "") # not root at all
#      #ferror "${scriptfile}: 5. Please run as root or sudo. Aborted."
#      #exit 5
#      :
#      ;;
#esac
# SET CUSTOM SCRIPT AND VALUES
#setval 1 sendsh sendopts<<EOFSENDSH      # if $1="1" then setvalout="critical-fail" on failure
#/usr/share/bgscripts/send.sh -hs     #                setvalout maybe be "fail" otherwise
#/usr/local/bin/send.sh -hs               # on success, setvalout="valid-sendsh"
#/usr/bin/mail -s
#EOFSENDSH
#test "${setvalout}" = "critical-fail" && ferror "${scriptfile}: 4. mailer not found. Aborted." && exit 4
# VALIDATE PARAMETERS
# objects before the dash are options, which get filled with the optvals
# to debug flags, use option DEBUG. Variables set in framework: fallopts
validateparams - "$@"
# CONFIRM TOTAL NUMBER OF FLAGLESSVALS IS CORRECT
#if test ${thiscount} -lt 2;
#then
#   ferror "${scriptfile}: 2. Fewer than 2 flaglessvals. Aborted."
#   exit 2
#fi
# CONFIGURE VARIABLES AFTER PARAMETERS
# GET CONFIG custom for host-bup
if ! test -f "${conffile}";
then
   ferror "${scripttrim}: 4. Cannot find conffile ${conffile}. See example at /usr/share/doc/bgscripts/host-bup.conf.example. Aborted."
   exit 4
fi
## START READ CONFIG FILE TEMPLATE
oIFS="${IFS}"; IFS="$( printf '\n' )"
infiledata=$( ${sed} ':loop;/^\/\*/{s/.//;:ccom;s,^.[^*]*,,;/^$/n;/^\*\//{s/..//;bloop;};bccom;}' "${conffile}") #the crazy sed removes c style multiline comments
IFS="${oIFS}"; infilelines=$( echo "${infiledata}" | wc -l )
{ echo "${infiledata}"; echo "ENDOFFILE"; } | {
   while read line; do
   # the crazy sed removes leading and trailing whitespace, blank lines, and comments
   if test ! "${line}" = "ENDOFFILE";
   then
      line=$( echo "${line}" | sed -e 's/^\s*//;s/\s*$//;/^[#$]/d;s/\s*[^\]#.*$//;' )
      if test -n "${line}";
      then
         debuglev 8 && ferror "line=\"${line}\""
         if echo "${line}" | grep -qiE "\[.*\]";
         then
            # new zone
            zone=$( echo "${line}" | tr -d '[]' | tr ':' '_' )
            debuglev 7 && ferror "zone=${zone}"
         else
            # directive
            varname=$( echo "${line}" | awk -F= '{print $1}' )
            varval=$( echo "${line}" | awk -F= '{$1=""; printf "%s", $0}' | sed 's/^ //;' )
            case "${zone}" in
               hostbup_main)
                  debuglev 7 && ferror "${zone}_${varname}=\"${varval}\""
                  # simple define variable
                  eval "${zone}_${varname}=\${varval}"
                  ;;
               hostbup_files)
                  debuglev 7 && ferror "${varname}"
                  echo "${varname}" >> "${tmpfile1}"
                  ;;
               *) # officially ignore it
                  :
                  ;;
            esac
         fi
         ## this part is untested
         #read -p "Please type something here:" response < ${thistty}
         #echo "${response}"
      fi
   else
## REACT TO BEING A CRONJOB
#if test ${is_cronjob} -eq 1;
#then
#   :
#else
#   :
#fi
# SET TRAPS
trap "CTRLC" 2
#trap "CTRLZ" 18
trap "clean_hostbup" 0
eval hostbup_main_tar_out_file="${hostbup_main_tar_out_file}"
# DEBUG SIMPLECONF
debuglev 5 && {
   ferror "Using values:"
   # used values: EX_(OPT1|OPT2|VERBOSE)
   set | grep -iE "^hostbup" 1>&2
   ferror "Back up files:"
   cat "${tmpfile1}" 1>&2
}
define_if_new HOSTBUP_DRYRUN "${hostbup_main_dryrun}"
# MAIN LOOP
#{
   # Check against hostname
   if ! test "$( hostname -f )" = "${hostbup_main_hostname}";
   then
      if ! test "${HOSTBUP_FORCE}";
      then
         ferror "${scripttrim}: 2. Current hostname $( hostname -f ) does not match conffile hostname ${hostbup_main_hostname}."
         ferror "Override this with the --force option. Aborted."
         exit 2
      fi
   fi
   # Show if dry run
   fistruthy "${HOSTBUP_DRYRUN}" && ferror "Dry run"
   # Execute pre scripts
   x=0; _shown=0
   while test $x -lt "${hostbup_main_script_count}";
   do
      x=$(( x + 1 ))
      eval thiscommand="\${hostbup_main_script_${x}_cmd}"
      debuglev 5 && {
         test "${_shown}" = "0" && { ferror "Commands to run:"; _shown=1; }
         ferror "${thiscommand}"
      }
      ! fistruthy "${HOSTBUP_DRYRUN}" && eval ${thiscommand}
   done
   # Tarball everything
   debuglev 1 && echo tar -zcf "${hostbup_main_tar_out_file}" -C / $( cat "${tmpfile1}" )
   ! fistruthy "${HOSTBUP_DRYRUN}" && { {
      tar -zcf "${hostbup_main_tar_out_file}" --ignore-failed-read -C / $( cat "${tmpfile1}" ) 2>&1 1>&3
   } | grep -viE "tar: Removing leading ." 1>&2; } 3>&1
   [ ]
#} | tee -a ${logfile}
# EMAIL LOGFILE
#${sendsh} ${sendopts} "${server} ${scriptfile} out" ${logfile} ${interestedparties}
# STOP THE READ CONFIG FILE
exit 0
fi; done; }

An example config file:

[hostbup:main]
hostname=server1.ipa.example.com
tar_out_file=/mnt/public/Support/Systems/server1/config.server1.$( date "+%Y-%m-%d" ).tgz
script_count=1
script_1_cmd=/usr/bin/dli . > /tmp/dnf.installed.log
dryrun=0

[hostbup:files]
# this file
/etc/installed/host-bup.conf

# host changelog and installed packages
/etc/installed/server1.log
/tmp/dnf.installed.log
# Samba
/etc/samba/smb.conf
# NFS
/etc/exports

To execute the script, just run

host-bup -c /path/to/host-bup.conf

I always place my config file as /etc/installed/host-bup.conf which is the default path for it, so I don't need the -c parameter.

In case any of my systems blow up, I have the important config files tarballed and stored on my main nfs export which has its own backup processes already in place.