Browse Source

Update scripts and deploy with gnu stow

Bryan Roessler 2 days ago
parent
commit
537f23077e
37 changed files with 903 additions and 677 deletions
  1. 6 0
      docs/manual.desktop
  2. 0 19
      script-files-permissions-reset
  3. 0 36
      script-functions
  4. 0 50
      script-install-manual-scripts
  5. 0 272
      script-install-motd
  6. 0 31
      script-install-reverse-proxy
  7. 0 14
      script-system-update
  8. 0 7
      script-tree-to-markdown
  9. 0 87
      script-user-add
  10. 0 47
      script-user-remove
  11. 0 44
      script-user-reset-password
  12. 0 0
      scripts/README.md
  13. 47 0
      scripts/script-deploy-manual
  14. 17 0
      scripts/script-deploy-scripts
  15. 3 8
      scripts/script-drives-fix-btrfs-full
  16. 23 0
      scripts/script-files-permissions-reset
  17. 10 13
      scripts/script-files-permissions-set
  18. 24 0
      scripts/script-functions
  19. 3 8
      scripts/script-install-btrfsmaintenance
  20. 220 0
      scripts/script-install-motd
  21. 14 16
      scripts/script-system-scheduled-restart
  22. 11 0
      scripts/script-system-update
  23. 63 0
      scripts/script-user-add
  24. 47 0
      scripts/script-user-remove
  25. 3 6
      scripts/script-user-reset-desktop
  26. 44 0
      scripts/script-user-reset-password
  27. 3 4
      scripts/script-user-reset-x2go
  28. 3 6
      scripts/script-user-unban
  29. 75 0
      stow/etc/dconf/db/local.d/99-hartmanlab
  30. 2 0
      stow/etc/dconf/db/local.d/locks/99-hartmanlab
  31. 11 0
      stow/etc/fail2ban/jail.local
  32. 18 0
      stow/etc/fstab
  33. 22 0
      stow/etc/samba/smb.conf
  34. 137 0
      stow/etc/ssh/sshd_config
  35. 23 0
      stow/usr/share/glib-2.0/schemas/50-tweaks.gschema.override
  36. 72 0
      stow/usr/share/mate-panel/layouts/hartmanlab.layout
  37. 2 9
      stow/usr/share/smartmontools/smartd_warning.d/99-smartd-notify-all

+ 6 - 0
docs/manual.desktop

@@ -0,0 +1,6 @@
+[Desktop Entry]
+Encoding=UTF-8
+Name=Hartman Lab Server Manual
+Type=Link
+URL=https://docs.google.com/document/d/1K_KwAlv8Zljmy-enwmhT6gMTFutlAFglixvpLGBx0VY
+Icon=text-html

+ 0 - 19
script-files-permissions-reset

@@ -1,19 +0,0 @@
-#!/usr/bin/env bash
-# Smartly change permissions on selected directories
-# Copyright 2021 Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
-
-is_root
-
-[[ $# -eq 0 ]] && DIRS=("/mnt/data") || DIRS=("$@")
-
-ask_ok "Reset permissions on ${DIRS[*]}?"
-
-chgrp smbgrp -R "${DIRS[@]}" && \
-chmod 6775 -R "${DIRS[@]}"
-
-exit $?

+ 0 - 36
script-functions

@@ -1,36 +0,0 @@
-#!/usr/bin/env bash
-# Common functions for the lab scripts
-# Copyright Bryan C. Roessler
-
-# Don't run this script directly
-[[ "${BASH_SOURCE[0]}" == "${0}" ]] && exit 0
-
-
-### VARS ###
-export INSTALL_DIR=/usr/local/bin
-
-
-### FUNCTIONS ###
-prompt() { read -r -p "Enter $1: " "$1"; }
-
-ask_ok() {
-    declare response
-    (( YES_SWITCH )) && return 0
-    read -r -p "$* [y/N]: " response
-    [[ ${response,,} =~ ^(yes|y)$ ]]
-}
-
-is_root() {
-    [[ $EUID -gt 0 ]] && echo "Script must be run with sudo" && exit 1
-}
-
-copy_manual() {
-    cat <<-EOF > "$1/manual.desktop"
-    [Desktop Entry]
-    Encoding=UTF-8
-    Name=Hartman Lab Server Manual
-    Type=Link
-    URL=https://docs.google.com/document/d/1K_KwAlv8Zljmy-enwmhT6gMTFutlAFglixvpLGBx0VY
-    Icon=text-html
-EOF
-}

+ 0 - 50
script-install-manual-scripts

@@ -1,50 +0,0 @@
-#!/usr/bin/env bash
-# This script will add scripts-* to the PATH and the manual to each user's desktop
-# Copyright 2021-2024 Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-reload=0
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || reload=1
-
-sourcedir="/home/roessler/shared/hartmanlab"
-original_dir="$PWD"
-
-if [[ "$original_dir" != "$sourcedir" ]]; then
-    pushd "$sourcedir" || exit $?
-fi
-
-((reload)) && [[ -f $parent/script-functions ]] && . "$parent"/script-functions
-
-target=/usr/local/bin
-for script in script-*; do
-    echo "Linking $script to $target"
-    [[ $script == "script-functions" ]] && install -m 644 "$script" "$target"
-    sudo ln -s "$script" "$target/"
-done
-
-# Install manual link
-remove=("manual.pdf" "manual.odt" "Notes.pdf" "Notes.odt" 
-    "README.html" "Link to Manual.desktop" "manual-images" 
-    "manual.html" "manual-images" "Manual.desktop" "manual.desktop")
-for homedir in /home/*; do
-    desktop="$homedir/Desktop"
-    [[ -d $desktop ]] || continue
-    echo "Scanning $desktop for old manuals"
-    for f in "${remove[@]}"; do
-        [[ -e $desktop/$f || -L $desktop/$f ]] &&
-        echo "Removing $desktop/$f" &&
-        rm -f "${desktop:?}/$f"
-    done
-    echo "Installing manual to $desktop/manual.desktop"
-    cat <<-EOF > "$desktop/manual.desktop"
-		[Desktop Entry]
-		Encoding=UTF-8
-		Name=Hartman Lab Server Manual
-		Type=Link
-		URL=https://docs.google.com/document/d/1K_KwAlv8Zljmy-enwmhT6gMTFutlAFglixvpLGBx0VY
-		Icon=text-html
-	EOF
-done
-

+ 0 - 272
script-install-motd

@@ -1,272 +0,0 @@
-#!/usr/bin/env bash
-# Install and generate motd
-# Bryan C. Roessler
-
-main() {
-	if [[ " $1 " == " --motd " ]]; then
-		print_motd
-	else
-		parent="${BASH_SOURCE[0]}"
-		parent=${parent%/*}
-
-		[[ -f "$parent"/script-functions ]] && . "$parent"/script-functions || exit 1
-
-		is_root
-
-		script="/usr/local/bin/${0##*/}"
-		service="/usr/lib/systemd/system/motd.service"
-		timer="/usr/lib/systemd/system/motd.timer"
-
-		[[ -f $script ]] || cp "$0" /usr/local/bin/
-
-		install_services
-	fi
-}
-
-print_motd() {
-	# colors
-	default='\e[0m'
-	green='\e[32m'
-	red='\e[31m'
-	dim='\e[2m'
-	undim='\e[0m'
-
-# shellcheck disable=SC2016
-	echo -e \
-' _    _            _                           _           _     
-| |  | |          | |                         | |         | |    
-| |__| | __ _ _ __| |_ _ __ ___   __ _ _ __   | |     __ _| |__  
-|  __  |/ _` | `__| __| `_ ` _ \ / _` | `_ \  | |    / _` | `_ \ 
-| |  | | (_| | |  | |_| | | | | | (_| | | | | | |___| (_| | |_) |
-|_|  |_|\__,_|_|   \__|_| |_| |_|\__,_|_| |_| |______\__,_|_.__/ '
-
-	# System info
-	# get load averages
-	IFS=" " read -r LOAD1 LOAD5 LOAD15 <<<"$(awk '{ print $1,$2,$3 }' /proc/loadavg)"
-	# get free memory
-	IFS=" " read -r USED AVAIL TOTAL <<<"$(free -htm | grep "Mem" | awk '{print $3,$7,$2}')"
-	# get processes
-	PROCESS=$(ps -eo user=|sort|uniq -c | awk '{ print $2 " " $1 }')
-	PROCESS_ALL=$(echo "$PROCESS"| awk '{print $2}' | awk '{ SUM += $1} END { print SUM }')
-	PROCESS_ROOT=$(echo "$PROCESS"| grep root | awk '{print $2}')
-	PROCESS_USER=$(echo "$PROCESS"| grep -v root | awk '{print $2}' | awk '{ SUM += $1} END { print SUM }')
-	# get processors
-	PROCESSOR_NAME=$(grep "model name" /proc/cpuinfo | cut -d ' ' -f3- | awk '{print $0}' | head -1)
-	PROCESSOR_COUNT=$(grep -ioP 'processor\t:' /proc/cpuinfo | wc -l)
-
-	echo -e "
-  ${default}Distro......: $default$(cat /etc/*release | grep "PRETTY_NAME" | cut -d "=" -f 2- | sed 's/"//g')
-  ${default}Kernel......: $default$(uname -sr)
-  ${default}Uptime......: $default$(uptime -p)
-  ${default}Load........: $green$LOAD1$default (1m), $green$LOAD5$default (5m), $green$LOAD15$default (15m)
-  ${default}Processes...: $default$green$PROCESS_ROOT$default (root), $green$PROCESS_USER$default (user), $green$PROCESS_ALL$default (total)
-  ${default}CPU.........: $default$PROCESSOR_NAME ($green$PROCESSOR_COUNT$default vCPU)
-  ${default}Memory......: $green$USED$default used, $green$AVAIL$default avail, $green$TOTAL$default total$default"
-
-	# Disk usage
-	# config
-	max_usage=90
-	bar_width=50
-
-	# disk usage: ignore zfs, squashfs & tmpfs
-	while IFS= read -r line; do dfs+=("$line"); done < <(df -H -x zfs -x squashfs -x tmpfs -x devtmpfs -x overlay --output=target,pcent,size | tail -n+2)
-	printf "\nDisk usage\n"
-
-	for line in "${dfs[@]}"; do
-		# get disk usage
-		usage=$(echo "$line" | awk '{print $2}' | sed 's/%//')
-		used_width=$(((usage*bar_width)/100))
-		# color is green if usage < max_usage, else red
-		if [ "${usage}" -ge "${max_usage}" ]; then
-			color=$red
-		else
-			color=$green
-		fi
-		# print green/red bar until used_width
-		bar="[${color}"
-		for ((i=0; i<used_width; i++)); do
-			bar+="="
-		done
-		# print dimmmed bar until end
-		bar+="${default}${dim}"
-		for ((i=used_width; i<bar_width; i++)); do
-			bar+="="
-		done
-		bar+="${undim}]"
-		# print usage line & bar
-		echo "${line}" | awk '{ printf("%-31s%+3s used out of %+4s\n", $1, $2, $3); }' | sed -e 's/^/  /'
-		echo -e "${bar}" | sed -e 's/^/  /'
-	done
-
-	# # Disk health
-	# cat <<- 'EOF' >> "$script"
-	# # config
-	# MAX_TEMP=40
-	# # set column width
-	# COLUMNS=2
-	# # colors
-
-	# # disks to check
-	# disks=(sda sdb sdc sdd sde sdf sdg sdi)
-	# disknames=(sda sdb sdc sdd sde sdf sdg sdi)
-
-	# # hddtemp
-	# hddtemp_host=localhost
-	# hddtemp_port=7634
-
-	# # logfiles to check
-	# logfiles='/var/log/syslog /var/log/syslog.1'
-
-	# # get all lines with smartd entries from syslog
-	# lines=$(tac $logfiles | grep -hiP 'smartd\[[[:digit:]]+\]:' | grep -iP "previous self-test")
-	# # use nc to query temps from hddtemp daemon
-	# hddtemp=$(timeout 0.01 nc $hddtemp_host $hddtemp_port | sed 's/|//m' | sed 's/||/ \n/g')
-
-	# out=""
-	# for i in "${!disks[@]}"; do
-	#     disk=${disks[$i]}
-	#     # use disknames if given
-	#     diskname=${disknames[$i]}
-	#     if [ -z "${diskname}" ]; then
-	#         diskname=$disk
-	#     fi
-	#     uuid=$(blkid -s UUID -o value "/dev/${disk}")
-	#     status=$( (grep "${uuid}" <<< "${lines}") | grep -m 1 -oP "previous self-test.*" | awk '{ print $4 " " $5 }')
-	#     temp=$( (grep "${disk}" <<< "${hddtemp}") | awk -F'|' '{ print $3 }')
-
-	#     # color green if temp <= MAX_TEMP, else red
-	#     if [[ "${temp}" -gt "${MAX_TEMP}" ]]; then
-	#         color=$red
-	#     else
-	#         color=$green
-	#     fi
-	#     # add "C" if temp is numeric
-	#     if [[ "$temp" =~ ^[0-9]+$ ]]; then
-	#         temp="${temp}C"
-	#     fi
-	#     # color green if status is "without error", else red
-	#     if [[ "${status}" == "without error" ]]; then
-	#         status_color=$green
-	#     else
-	#         status_color=$red
-	#     fi
-
-	#     # print temp & smartd error
-	#     out+="${diskname}:,${color}${temp}${undim} | ${status_color}${status}${undim},"
-	#     # insert \n every $COLUMNS column
-	#     if [ $((($i+1) % $COLUMNS)) -eq 0 ]; then
-	#         out+="\n"
-	#     fi
-	# done
-	# out+="\n"
-
-	# printf "\ndisk status:\n"
-	# printf "$out" | column -ts $',' | sed -e 's/^/  /'
-	# EOF
-
-
-	# Services
-	COLUMNS=2
-
-	services=(
-		btrfs-balance.timer btrfs-scrub.timer backup.timer btrbk.timer fstrim.timer
-		fail2ban firewalld smb nmb motion smartd cockpit.socket
-		dnf-automatic.timer motd.timer
-	)
-
-	service_status=()
-	# get status of all services
-	for service in "${services[@]}"; do
-		service_status+=("$(systemctl is-active "$service")")
-	done
-
-	out=""
-	for i in "${!services[@]}"; do
-		# color green if service is active, else red
-		if [[ "${service_status[$i]}" == "active" ]]; then
-			out+="${services[$i]%.*}:,${green}${service_status[$i]}${undim},"
-		else
-			out+="${services[$i]%.*}:,${red}${service_status[$i]}${undim},"
-		fi
-		# insert \n every $COLUMNS column
-		if [[ $(((i+1) % COLUMNS)) -eq 0 ]]; then
-			out+="\n"
-		fi
-	done
-
-	printf "\nServices\n"
-	printf "%b\n" "$out" | column -ts $',' | sed -e 's/^/  /'
-
-
-	# Fail2Ban
-	# fail2ban-client status to get all jails, takes about ~70ms
-	read -r -a jails <<< "$(fail2ban-client status | grep "Jail list:" | sed "s/ //g" | awk '{split($2,a,",");for(i in a) print a[i]}')"
-
-	out="jail,failed,total,banned,total\n"
-
-	for jail in "${jails[@]}"; do
-		# slow because fail2ban-client has to be called for every jail (~70ms per jail)
-		status=$(fail2ban-client status "$jail")
-		failed=$(echo "$status" | grep -ioP '(?<=Currently failed:\t)[[:digit:]]+')
-		totalfailed=$(echo "$status" | grep -ioP '(?<=Total failed:\t)[[:digit:]]+')
-		banned=$(echo "$status" | grep -ioP '(?<=Currently banned:\t)[[:digit:]]+')
-		totalbanned=$(echo "$status" | grep -ioP '(?<=Total banned:\t)[[:digit:]]+')
-		out+="$jail,$failed,$totalfailed,$banned,$totalbanned\n"
-	done
-
-	printf "\nFail2ban\n"
-	printf "%b\n" "$out" | column -ts $',' | sed -e 's/^/  /'
-
-	# Help links
-	echo -e '
-Links (ctrl+click to follow)
-  Server Manual.........: https://tinyurl.com/jjz9h6fr
-  Cockpit (for admins)..: http://localhost:9090
-  Robot Camera..........: http://localhost:9999
-  JupyterLab............: http://localhost:8888
-  RStudio Server........: http://localhost:8787
-  Robot Computer........: vnc://192.168.16.101:5900
-  Windows 10 VM.........: vnc://localhost:5900 (pw: hartman)
-	'
-
-	# Scheduled reboot
-	if systemctl is-active scheduled-reboot.timer &>/dev/null; then
-		echo -n "Next scheduled reboot: "
-		time=$(systemctl cat scheduled-reboot.timer | grep OnCalendar=)
-		time=${time#*=}
-		echo "$time"
-	fi
-}
-
-install_services() {
-	cat <<-EOF > "$service"
-		[Unit]
-		Description=Generate MoTD
-
-		[Service]
-		Type=simple
-		ExecStart=/usr/bin/bash -c '$script --motd > /etc/motd'
-
-		[Install]
-		WantedBy=default.target
-	EOF
-	cat <<-'EOF' > "$timer"
-		[Unit]
-		Description=Generate MoTD every minute on a timer
-
-		[Timer]
-		OnCalendar=*:0/1
-		OnBootSec=10s
-
-		[Install]
-		WantedBy=timers.target
-	EOF
-
-	chmod +x "$script" &&
-	systemctl daemon-reload &&
-	systemctl enable --now "${timer##*/}"
-}
-
-main "$@"
-
-exit $?

+ 0 - 31
script-install-reverse-proxy

@@ -1,31 +0,0 @@
-#!/usr/bin/env bash
-# Adds a reverse proxy for local system services
-# Copyright 2021 Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
-
-is_root
-
-dnf install -y nginx || exit $?
-
-cat <<- 'EOF' > /etc/nginx/conf.d/hartmanlab.conf
-server {
-	listen 80 default_server;
-	server_name localhost;
-	location /cockpit {
-		proxy_pass http://localhost:9090;
-	}
-}
-EOF
-
-	# location /robot {
-    #     proxy_pass http://127.0.0.1:8888;
-    #     proxy_redirect http://127.0.0.1:8888/;
-    # }
-
-systemctl enable --now nginx
-
-exit $?

+ 0 - 14
script-system-update

@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-# Update the system
-# Copyright 2021 Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
-
-is_root
-
-ask_ok "Security updates are automatically installed, perform a full system update?" || exit $?
-
-dnf update --refresh

+ 0 - 7
script-tree-to-markdown

@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-# Make a nice markdown file from a dir tree
-# Copyright 2021 Bryan C. Roessler
-
-tree=$(tree -f --noreport --charset ascii "$1" |
-sed -e 's/| \+/  /g' -e 's/[|`]-\+/ */g' -e 's:\(* \)\(\(.*/\)\([^/]\+\)\):\1[\4](\2):g')
-printf "# Code/Directory Structure:\n\n%s" "$tree"

+ 0 - 87
script-user-add

@@ -1,87 +0,0 @@
-#!/usr/bin/env bash
-# Add a user to the Hartman Lab server
-# Copyright Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
-
-is_root
-
-echo "This script supports two optional arguments, a username and password"
-
-if [[ $# -eq 0 ]]; then
-    prompt user
-    prompt password
-elif [[ $# -eq 1 ]]; then
-    user="$1"
-    prompt password
-elif [[ $# -eq 2 ]]; then
-    user="$1"
-    password="$2"
-elif [[ $# -gt 2 ]]; then
-    echo "Too many arguments provided"
-    exit 1
-fi
-
-useradd_cmd=(useradd -m -U)
-
-if id -u "$user" &>/dev/null; then
-    ask_ok "User $user exists. Run script-user-remove first?" || exit $?
-    "$parent"/script-user-remove "$user" || exit $?
-fi
-
-ask_ok "Create user $user with password $password?" || exit $?
-
-restore=0
-if [[ -d /mnt/array/home-retired/$user ]]; then
-    ask_ok "Restore user $user's files from /mnt/array/home-retired/$user?" && restore=1
-fi
-
-samba=0
-ask_ok "Enable shared file access for user $user?" && group_str="smbgrp" && samba=1
-
-ask_ok "Make $user an admin?" && \
-    group_str+=",wheel"
-
-useradd_cmd+=("-G" "$group_str")
-useradd_cmd+=("$user")
-
-if (( restore )); then
-    if rsync -av --progress=info2 /mnt/array/home-retired/"$user" /home/"$user"; then
-        ask_ok "User $user's files successfully restored, remove backup at /mnt/array/home-retired/$user?" && \
-            rm -rf /mnt/array/home-retired/"$user"
-    fi
-fi
-
-# echo "Running: ${useradd_cmd[*]}"
-"${useradd_cmd[@]}"
-echo "$user":"$password" | chpasswd
-
-
-if (( samba )); then
-    (echo "$password"; echo "$password") | smbpasswd -a -s "$user"
-fi
-
-ask_ok "Prompt user to reset password on next login?" &&
-passwd --expire "$user" &&
-echo "NOTE: The file sharing (smbpasswd) will not be changed"
-
-# TODO check if centos 9 does by default
-# Add subuids & subgids for container namespace
-# id_offset=100000
-# id_num=65536
-# last_uid=$(tail -1 /etc/subuid | cut -d':' -f2)
-# last_gid=$(tail -1 /etc/subgid | cut -d':' -f2)
-# start_uid=$(( last_uid + id_offset ))
-# start_gid=$(( last_gid + id_offset ))
-# echo "$user:$start_uid:$id_num" >> /etc/subuid
-# echo "$user:$start_gid:$id_num" >> /etc/subgid
-
-# Copy manual to user desktop
-desktop="/home/$user/Desktop"
-[[ -d $desktop ]] || sudo -u "$user" mkdir -p "$desktop"
-copy_manual "$desktop"
-
-exit 0

+ 0 - 47
script-user-remove

@@ -1,47 +0,0 @@
-#!/usr/bin/env bash
-# Remove a user from the server
-# Copyright 2021-2023 Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
-
-is_root
-
-echo "This script supports one optional argument, a username"
-
-if [[ $# -eq 1 ]]; then
-    user="$1"
-else
-    prompt user
-fi
-
-if ! id -u "$user" &>/dev/null; then
-    echo "User $user does not exist"
-    exit 1
-fi
-
-ask_ok "Remove user $user?" || exit 1
-
-killall -u "$user"
-
-if ask_ok "Backup /home/$user?" && [[ -d /home/$user ]]; then
-    if [[ ! -d /mnt/array/home-retired/$user ]]; then 
-        btrfs subvolume create /mnt/array/home-retired/"$user" || exit 1
-    fi
-    rsync -av /home/"$user"/ /mnt/array/home-retired/"$user" || exit 1
-fi
-
-smbpasswd -x "$user"
-
-sed -i "/$user/d" /etc/subuid
-sed -i "/$user/d" /etc/subgid
-
-if ! userdel -fr "$user"; then
-  ask_ok "Userdel failed, kill processes again and retry?" || exit 1
-  killall -u "$user" -s SIGKILL
-  userdel -fr "$user"
-fi
-
-exit $?

+ 0 - 44
script-user-reset-password

@@ -1,44 +0,0 @@
-#!/usr/bin/env bash
-# This script will reset a user password on the server
-# Copyright 2021-24 Bryan C. Roessler
-
-unset user password
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
-
-is_root
-
-echo "This script supports two optional arguments, a username and password"
-
-if [[ $# -eq 0 ]]; then
-    prompt user
-    prompt password
-elif [[ $# -eq 1 ]]; then
-    user="$1"
-    prompt password
-elif [[ $# -eq 2 ]]; then
-    user="$1"
-    password="$2"
-elif [[ $# -gt 2 ]]; then
-    echo "Too many arguments provided"
-    exit 1
-fi
-
-if ! id -u "$user" &>/dev/null; then
-    echo "User $user does not exist"
-    exit 1
-fi
-
-if ask_ok "Change user $user's password to $password?"; then
-    echo "$user":"$password" | chpasswd
-    (echo "$password"; echo "$password") | smbpasswd -a -s "$user"
-fi
-
-ask_ok "Prompt user to reset password on next login?" &&
-passwd --expire "$user" &&
-echo "NOTE: The file sharing (smbpasswd) will not be changed"
-   
-exit 0

+ 0 - 0
README.md → scripts/README.md


+ 47 - 0
scripts/script-deploy-manual

@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+# This script installs the Hartman Lab Server Manual to each user's desktop
+# Usage: script-deploy-manual USERNAME|--all
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+
+script-deploy-manual() {
+    local user_arg="$1"
+    local manual_url="https://docs.google.com/document/d/1K_KwAlv8Zljmy-enwmhT6gMTFutlAFglixvpLGBx0VY"
+    local remove=("manual.pdf" "manual.odt" "Notes.pdf" "Notes.odt" \
+        "README.html" "Link to Manual.desktop" "manual-images" \
+        "manual.html" "Manual.desktop" "manual.desktop")
+    local users=()
+
+    if [[ "$user_arg" == "--all" ]]; then
+        for d in /home/*; do [[ -d $d ]] && users+=("${d##*/}"); done
+    else
+        users+=("$user_arg")
+    fi
+
+    for user in "${users[@]}"; do
+        desktop="/home/$user/Desktop"
+        [[ -d $desktop ]] || continue
+        echo "Scanning $desktop for old manuals"
+        for f in "${remove[@]}"; do
+            if [[ -e $desktop/$f || -L $desktop/$f ]]; then
+                echo "Removing $desktop/$f"
+                rm -f "${desktop:?}/$f"
+            fi
+        done
+        echo "Installing manual to $desktop/manual.desktop"
+    cat <<-EOF > "$desktop/manual.desktop"
+					[Desktop Entry]
+					Encoding=UTF-8
+					Name=Hartman Lab Server Manual
+					Type=Link
+					URL=$manual_url
+					Icon=text-html
+				EOF
+  done
+}
+
+# Allow script to be safely sourced
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+  script-deploy-manual "$@"
+  exit $?
+fi

+ 17 - 0
scripts/script-deploy-scripts

@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+# Adds the scripts directory to the global PATH 
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
+
+script-deploy-scripts() {
+    local profile_file="/etc/profile.d/99-hartmanlab_server_scripts.sh"
+    echo "Adding $p to global PATH in $profile_file"
+    sudo bash -c "echo 'export PATH=\"\$PATH:$p\"' > '$profile_file'"
+}
+
+# Allow script to be safely sourced
+if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
+	script-deploy-scripts
+	exit
+fi

+ 3 - 8
script-drives-fix-btrfs-full → scripts/script-drives-fix-btrfs-full

@@ -1,11 +1,8 @@
 #!/usr/bin/env bash
 # Fix the btrfs out of space error
-# Copyright 2021 Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
 
 is_root
 
@@ -16,5 +13,3 @@ for f in "${fs[@]}"; do
     btrfs balance start -dusage=0 "$f"
     btrfs balance start -musage=0 "$f"
 done
-
-exit $?

+ 23 - 0
scripts/script-files-permissions-reset

@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+# Smartly change permissions on selected directories
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
+
+is_root
+
+[[ $# -eq 0 ]] && DIRS=("/mnt/data") || DIRS=("$@")
+
+ask_ok "Reset permissions on ${DIRS[*]}?"
+
+if ! chgrp smbgrp -R "${DIRS[@]}"; then
+	echo "Failed to change group ownership" >&2
+	exit 1
+fi
+
+if ! chmod 6775 -R "${DIRS[@]}"; then
+	echo "Failed to change permissions" >&2
+	exit 1
+fi
+
+exit 0

+ 10 - 13
script-files-permissions-set → scripts/script-files-permissions-set

@@ -1,11 +1,8 @@
 #!/usr/bin/env bash
-# Smartly change permissions on selected directories
-# Copyright 2021 Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
+# Intelligently change permissions on selected directories
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
 
 is_root
 
@@ -36,7 +33,7 @@ for path in "${paths[@]}"; do
     fi
     og_user=$(stat -c "%U" "$path")
     og_group=$(stat -c "%G" "$path")
-    echo -e "PATH\tUSER\tGROUP"
+    printf "%-20s %-10s %-10s\n" "PATH" "USER" "GROUP"
     echo -e "$path\t$og_user\t$og_group"
     if [[ "$group" != "smbgrp" || "$og_group" != "smbgrp" ]]; then
         echo "$path is not world accessible by the smbgrp group"
@@ -44,19 +41,19 @@ for path in "${paths[@]}"; do
     fi
 done
 
-
 ask_ok "Apply user: $user and group: $group to ${paths[*]} and all subdirs?" && \
     chown -R "$user":"$group" "${paths[@]}"
 
+# Set mode based on group:
+# - 6775: Enables read/write/execute for owner and group, with setgid bit for group inheritance.
+# - 755: Enables read/write/execute for owner, and read/execute for group and others.
 [[ "$group" == "smbgrp" ]] && mode=6775 || mode=755
     
 ask_ok "Apply chmod $mode to ${paths[*]} and all subdirs?" && \
-    chmod -R $mode "${paths[@]}"
+    chmod -R "$mode" "${paths[@]}"
 
 # Let's do it in less steps (see above) for now unless it becomes a problem
+# TODO: Implement setuid/setgid functionality to ensure files and directories inherit user/group permissions.
 # echo "Apply setuid/setgid bits to ${paths[*]} and all subdirs?"
 # ask_ok "Files/dirs will inherit their " && \
 #     chmod -R g+s,u+s "${paths[@]}"    
-
-
-exit $?

+ 24 - 0
scripts/script-functions

@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+# Common functions for the lab scripts
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+
+export INSTALL_DIR=/usr/local/bin
+
+prompt() { 
+    local user_input
+    read -r -p "Enter $1: " user_input
+    echo "$user_input"
+}
+
+ask_ok() {
+    declare response
+    ((YES_SWITCH)) && return 0
+    read -r -p "$* [y/N]: " response
+    [[ ${response,,} =~ ^(yes|y)$ ]]
+}
+
+is_root() {
+    [[ $EUID -gt 0 ]] && echo "Script must be run with sudo" && exit 1
+}
+

+ 3 - 8
script-install-btrfsmaintenance → scripts/script-install-btrfsmaintenance

@@ -1,11 +1,8 @@
 #!/usr/bin/env bash
 # Generic btrfsmaintenance install script
-# Copyright 2021 Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
 
 is_root
 
@@ -57,5 +54,3 @@ systemctl daemon-reload
 
 # Optionally, start and enable the services
 # systemctl enable --now btrfs-scrub
-
-exit $?

+ 220 - 0
scripts/script-install-motd

@@ -0,0 +1,220 @@
+#!/usr/bin/env bash
+# Install and generate motd
+# Copyright 2019-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+set -euo pipefail
+
+PROG_NAME="$(basename "$0")"
+INSTALL_PATH="/usr/local/bin/$PROG_NAME"
+SERVICE_UNIT="/usr/lib/systemd/system/motd.service"
+TIMER_UNIT="/usr/lib/systemd/system/motd.timer"
+
+usage() {
+  cat <<-EOF
+		Usage: $PROG_NAME [--motd | --install] [--help]
+
+		Options:
+		  --motd       Output MOTD to stdout (used by systemd)
+		  --install    Instal motd systemd service and timer
+		  -h, --help   Show this help message
+EOF
+}
+
+print_motd() {
+  # ANSI color codes
+  local default='\033[0m' green='\033[0;32m' red='\033[0;31m'
+  local dim='\033[2m' undim='\033[22m'
+
+  cat <<-'EOF'
+		 _    _            _                           _           _     
+		| |  | |          | |                         | |         | |    
+		| |__| | __ _ _ __| |_ _ __ ___   __ _ _ __   | |     __ _| |__  
+		|  __  |/ _` | `__| __| `_ ` _ \ / _` | `_ \  | |    / _` | `_ \ 
+		| |  | | (_| | |  | |_| | | | | | (_| | | | | | |___| (_| | |_) |
+		|_|  |_|\__,_|_|   \__|_| |_| |_|\__,_|_| |_| |______\__,_|_.__/ 
+	EOF
+
+  # System info
+  read -r LOAD1 LOAD5 LOAD15 < <(awk '{print $1, $2, $3}' /proc/loadavg)
+  read -r USED AVAIL TOTAL < <(free -htm | awk '/^Mem:/ {print $3, $7, $2}')
+
+  local ROOT_PROCS NONROOT_PROCS TOTAL_PROCS
+  ROOT_PROCS=$(pgrep -u root | wc -l)
+  NONROOT_PROCS=$(( $(ps -eo user= | wc -l) - ROOT_PROCS ))
+  TOTAL_PROCS=$(( ROOT_PROCS + NONROOT_PROCS ))
+
+  local CPU_MODEL CPU_COUNT
+  CPU_MODEL=$(awk -F': ' '/model name/ {print $2; exit}' /proc/cpuinfo)
+  CPU_COUNT=$(grep -c '^processor' /proc/cpuinfo)
+
+  . /etc/os-release
+  local DISTRO="$PRETTY_NAME"
+  local KERNEL; KERNEL="$(uname -sr)"
+  local UPTIME; UPTIME="$(uptime -p)"
+
+  # Load
+  printf "\n"
+  printf "  Distro......: %s\n" "$DISTRO"
+  printf "  Kernel......: %s\n" "$KERNEL"
+  printf "  Uptime......: %s\n" "$UPTIME"
+  printf "  Load........: %b%s%b (1m), %b%s%b (5m), %b%s%b (15m)\n" \
+    "$green" "$LOAD1" "$default" \
+    "$green" "$LOAD5" "$default" \
+    "$green" "$LOAD15" "$default"
+  printf "  Processes...: %b%d%b (root), %b%d%b (user), %b%d%b (total)\n" \
+    "$green" "$ROOT_PROCS" "$default" \
+    "$green" "$NONROOT_PROCS" "$default" \
+    "$green" "$TOTAL_PROCS" "$default"
+  printf "  CPU.........: %s (%b%d%b vCPU)\n" \
+    "$CPU_MODEL" "$green" "$CPU_COUNT" "$default"
+  printf "  Memory......: %b%s%b used, %b%s%b avail, %b%s%b total\n" \
+    "$green" "$USED" "$default" \
+    "$green" "$AVAIL" "$default" \
+    "$green" "$TOTAL" "$default"
+
+  # Disks
+  echo -e "\n  Disk usage"
+  local max_usage=90 bar_width=50
+  while read -r target pcent size; do
+    local use=${pcent%\%}
+    local filled=$(( use * bar_width / 100 ))
+    local bar="["
+
+    local col=$green
+    (( use >= max_usage )) && col=$red
+
+    bar+="$col"
+    for ((i=0;i<filled;i++)); do bar+="="; done
+    bar+="$default$dim"
+    for ((i=filled;i<bar_width;i++)); do bar+="="; done
+    bar+="$undim]"
+
+    printf "    %-25s %3s of %-6s\n" "$target" "$pcent" "$size"
+    printf "    %b\n" "$bar"
+  done < <(
+    df -H -x zfs -x squashfs -x tmpfs -x devtmpfs -x overlay \
+      --output=target,pcent,size | tail -n +2
+  )
+
+  # Services
+  echo -e "\n  Services"
+  local services=(
+    btrfs-balance.timer btrfs-scrub.timer backup.timer btrbk.timer fstrim.timer
+    fail2ban firewalld smb nmb motion smartd cockpit.socket
+    dnf-automatic.timer motd.timer
+  )
+  for ((i=0; i<${#services[@]}; i+=2)); do
+    local s1=${services[i]} s2=${services[i+1]:-}
+    local st1 st2 c1 c2
+    st1=$(systemctl is-active "$s1" 2>/dev/null || echo inactive)
+    st2=$(systemctl is-active "$s2" 2>/dev/null || echo inactive)
+    [[ $st1 == active ]] && c1="${green}$st1${default}" || c1="${red}$st1${default}"
+    [[ $st2 == active ]] && c2="${green}$st2${default}" || c2="${red}$st2${default}"
+    printf "    %-15s : %b    %-15s : %b\n" \
+      "${s1%.*}" "$c1" "${s2%.*}" "$c2"
+  done
+
+	# Fail2Ban
+	read -r -a jails <<< "$(fail2ban-client status | grep "Jail list:" | sed "s/ //g" | awk '{split($2,a,",");for(i in a) print a[i]}')"
+
+	out="jail,failed,total,banned,total\n"
+
+	for jail in "${jails[@]}"; do
+		# Slow because fail2ban-client has to be called for every jail (~70ms per jail)
+		status=$(fail2ban-client status "$jail")
+		failed=$(echo "$status" | grep -ioP '(?<=Currently failed:\t)[[:digit:]]+')
+		totalfailed=$(echo "$status" | grep -ioP '(?<=Total failed:\t)[[:digit:]]+')
+		banned=$(echo "$status" | grep -ioP '(?<=Currently banned:\t)[[:digit:]]+')
+		totalbanned=$(echo "$status" | grep -ioP '(?<=Total banned:\t)[[:digit:]]+')
+		out+="$jail,$failed,$totalfailed,$banned,$totalbanned\n"
+	done
+
+	printf "\nFail2ban\n"
+	printf "%b\n" "$out" | column -ts $',' | sed -e 's/^/  /'
+
+  # Help links
+  cat <<-EOF
+
+		Links (ctrl+click to follow)
+		  Server Manual.........: https://tinyurl.com/jjz9h6fr
+		  Cockpit (for admins)..: http://localhost:9090
+		  Robot Camera..........: http://localhost:9999
+		  JupyterLab............: http://localhost:8888
+		  RStudio Server........: http://localhost:8787
+		  Robot Computer........: vnc://192.168.16.101:5900
+		  Windows 10 VM.........: vnc://localhost:5900 (pw: hartman)
+	EOF
+
+	# Scheduled reboot
+	if systemctl is-active scheduled-reboot.timer &>/dev/null; then
+		echo -n "Next scheduled reboot: "
+		time=$(systemctl cat scheduled-reboot.timer | grep OnCalendar=)
+		time=${time#*=}
+		echo "$time"
+	fi
+
+  if systemctl is-active scheduled-reboot.timer &>/dev/null; then
+    echo -n "  Next reboot  : "
+    systemctl show -p OnCalendar scheduled-reboot.timer | cut -d= -f2
+  fi
+
+  printf "\n"
+}
+
+install_services() {
+  # Write the service unit
+  cat >"$SERVICE_UNIT" <<-EOF
+		[Unit]
+		Description=Generate MoTD
+
+		[Service]
+		Type=oneshot
+		ExecStart=$INSTALL_PATH --motd > /etc/motd
+
+		[Install]
+		WantedBy=multi-user.target
+	EOF
+
+  # Write the timer unit
+  cat >"$TIMER_UNIT" <<-EOF
+		[Unit]
+		Description=Generate MoTD every minute
+
+		[Timer]
+		OnCalendar=*:0/1
+		OnBootSec=10s
+
+		[Install]
+		WantedBy=timers.target
+	EOF
+
+  chmod +x "$INSTALL_PATH"
+  systemctl daemon-reload
+  systemctl enable --now motd.timer
+  echo "Installed and started motd.timer → $SERVICE_UNIT, $TIMER_UNIT"
+}
+
+main() {
+  if [[ ${1:-} == --motd ]]; then
+    print_motd
+    exit 0
+  fi
+
+  if [[ ${1:-} == --install ]]; then
+    if [[ $EUID -ne 0 ]]; then
+      echo "Error: Must run as root to install." >&2
+      exit 1
+    fi
+    cp -f "$0" "$INSTALL_PATH"
+    install_services
+  fi
+
+  if [[ ${1:-} =~ ^(-h|--help)$ ]]; then
+    usage
+    exit 0
+  fi
+
+
+}
+
+main "$@"

+ 14 - 16
script-system-scheduled-restart → scripts/script-system-scheduled-restart

@@ -1,11 +1,8 @@
 #!/usr/bin/env bash
 # Update and restart the system
-# Copyright 2021 Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
 
 is_root
 
@@ -17,15 +14,15 @@ script-system-update
 ask_ok "Set a scheduled reboot for $time?" || exit 1
 
 cat <<- EOF > "/usr/lib/systemd/system/scheduled-reboot.timer"
-[Unit]
-Description=Scheduled reboot
+	[Unit]
+	Description=Scheduled reboot
 
-[Timer]
-OnCalendar=$time
-Unit=reboot.target
+	[Timer]
+	OnCalendar=$time
+	Unit=reboot.target
 
-[Install]
-WantedBy=timers.target
+	[Install]
+	WantedBy=timers.target
 EOF
 
 systemctl daemon-reload
@@ -34,7 +31,10 @@ systemctl start scheduled-reboot.timer
 # Current date
 dt=$(date '+%d/%m/%Y %H:%M:%S');
 
-message="System restart scheduled for $time. The current time is $dt. Make sure all changes are saved."
+msg_part1="System restart scheduled for $time."
+msg_part2="The current time is $dt."
+msg_part3="Make sure all changes are saved."
+message="$msg_part1 $msg_part2 $msg_part3"
 
 # Graphical notification
 IFS=$'\n'
@@ -47,5 +47,3 @@ done
 
 # Wall notification
 wall -n "$message"
-
-exit $?

+ 11 - 0
scripts/script-system-update

@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+# Update the system
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
+
+is_root
+
+ask_ok "Security updates are automatically installed, perform a full system update?" || exit $?
+
+dnf update --refresh

+ 63 - 0
scripts/script-user-add

@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+# Add a user to the Hartman Lab server
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
+
+is_root
+
+echo "Usage: $0 [username] [password]"
+
+case $# in
+  0) prompt user; prompt password ;;
+  1) user="$1"; prompt password ;;
+  2) user="$1"; password="$2" ;;
+  *) echo "Too many arguments provided"; exit 1 ;;
+esac
+
+useradd_cmd=(useradd -m -U)
+group_str=""
+
+if id -u "$user" &>/dev/null; then
+  ask_ok "User $user exists. Run script-user-remove first?" || exit $?
+  "$p/script-user-remove" "$user" || exit $?
+fi
+
+ask_ok "Create user $user with password $password?" || exit $?
+
+restore=0
+if [[ -d /mnt/array/home-retired/$user ]]; then
+  ask_ok "Restore user $user's files from /mnt/array/home-retired/$user?" && restore=1
+fi
+
+samba=0
+ask_ok "Enable shared file access for user $user?" && group_str="smbgrp" && samba=1
+ask_ok "Make $user an admin?" && group_str+=",wheel"
+
+[[ -n $group_str ]] && useradd_cmd+=("-G" "$group_str")
+useradd_cmd+=("$user")
+
+"${useradd_cmd[@]}"
+echo "$user:$password" | chpasswd
+
+if (( restore )); then
+  if rsync -av --progress=info2 "/mnt/array/home-retired/$user/" "/home/$user/"; then
+    ask_ok "User $user's files successfully restored, remove backup at /mnt/array/home-retired/$user?" && \
+      rm -rf "/mnt/array/home-retired/$user"
+  fi
+fi
+
+if (( samba )); then
+  (echo "$password"; echo "$password") | smbpasswd -a -s "$user"
+fi
+
+ask_ok "Prompt user to reset password on next login?" &&
+  passwd --expire "$user" &&
+  echo "NOTE: The file sharing (smbpasswd) will not be changed"
+
+# Copy manual to user desktop
+desktop="/home/$user/Desktop"
+mkdir -p "$desktop"
+"$p/script-deploy-manual" "$user"
+
+exit 0

+ 47 - 0
scripts/script-user-remove

@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+# Remove a user from the server
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
+
+is_root
+
+echo "This script supports one optional argument, a username"
+
+if [[ $# -eq 1 ]]; then
+  user="$1"
+else
+  prompt user
+fi
+
+if ! id -u "$user" &>/dev/null; then
+  echo "User $user does not exist"
+  exit 1
+fi
+
+ask_ok "Remove user $user?" || exit 1
+
+# Kill user processes
+killall -u "$user"
+
+# Optional backup
+if ask_ok "Backup /home/$user?" && [[ -d /home/$user ]]; then
+  backup_dir="/mnt/array/home-retired/$user"
+  if [[ ! -d $backup_dir ]]; then 
+    btrfs subvolume create "$backup_dir" || exit 1
+  fi
+  rsync -av "/home/$user/" "$backup_dir/" || exit 1
+fi
+
+# Remove from Samba and subuid/subgid
+smbpasswd -x "$user"
+sed -i "/^$user:/d" /etc/subuid
+sed -i "/^$user:/d" /etc/subgid
+
+# Remove user and home directory
+if ! userdel -fr "$user"; then
+  ask_ok "Userdel failed, kill processes again and retry?" || exit 1
+  killall -u "$user" -s SIGKILL
+  userdel -fr "$user"
+fi
+

+ 3 - 6
script-user-reset-desktop → scripts/script-user-reset-desktop

@@ -1,11 +1,8 @@
 #!/usr/bin/env bash
 # Reset default desktop preferences
-# Copyright Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
 
 echo "This script will only work for the current user"
 

+ 44 - 0
scripts/script-user-reset-password

@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+# Reset a user password on the server
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
+
+is_root
+
+echo "Usage: $0 [username] [password]"
+
+case $# in
+  0)
+    prompt user
+    prompt password
+    ;;
+  1)
+    user="$1"
+    prompt password
+    ;;
+  2)
+    user="$1"
+    password="$2"
+    ;;
+  *)
+    echo "Too many arguments provided"
+    exit 1
+    ;;
+esac
+
+if ! id -u "$user" &>/dev/null; then
+  echo "User $user does not exist"
+  exit 1
+fi
+
+if ask_ok "Change user $user's password to $password?"; then
+  echo "$user:$password" | chpasswd
+  (echo "$password"; echo "$password") | smbpasswd -a -s "$user"
+fi
+
+ask_ok "Prompt user to reset password on next login?" &&
+  passwd --expire "$user" &&
+  echo "NOTE: The file sharing (smbpasswd) will not be changed"
+
+exit 0

+ 3 - 4
script-user-reset-x2go → scripts/script-user-reset-x2go

@@ -1,10 +1,9 @@
 #!/usr/bin/env bash
 # This script will reset x2go sessions to a working state
 # Use --all to reset all user X2Go sessions
-# Copyright Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]%/*}"
-[[ -f $parent/script-functions ]] && . "$parent/script-functions" || exit 1
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
 
 echo "Usage: $0 [username|--all]"
 

+ 3 - 6
script-user-unban → scripts/script-user-unban

@@ -1,11 +1,8 @@
 #!/usr/bin/env bash
 # Unbans a fail2ban IP
-# Copyright 2021 Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
+p="${BASH_SOURCE[0]%/*}"; [[ -r $p/script-functions ]] && . "$p"/script-functions || exit 1
 
 is_root
 

+ 75 - 0
stow/etc/dconf/db/local.d/99-hartmanlab

@@ -0,0 +1,75 @@
+#/etc/dconf/db/local.d/99-hartmanlab
+
+[org/mate/desktop/background]
+primary-color='rgb(59,110,165)'
+color-shading-type='solid'
+picture-filename=''
+
+[org/mate/desktop/font-rendering]
+antialiasing='rgba'
+hinting='slight'
+
+[org/mate/desktop/media-handling]
+automount=false
+automount-open=false
+autorun-never=true
+
+[org/mate/desktop/interface]
+gtk-decoration-layout='menu:minimize,maximize,close'
+font-name='Liberation Sans 10'
+monospace-font-name='Liberation Mono 10'
+document-font-name='Liberation Sans 10'
+enable-animations=false
+gtk-enable-animations=false
+gtk-theme='BlueMenta'
+
+[org/mate/screensaver]
+lock-enabled=false
+
+[org/mate/panel/general]
+default-layout='hartmanlab'
+toplevel-id-list=['bottom']
+
+[org/mate/panel/objects/clock/prefs]
+format='12-hour'
+
+[org/mate/panel/toplevels/bottom]
+orientation='bottom'
+y-bottom=0
+
+[org/mate/mate-menu]
+applet-text=''
+plugins-list=['places','system_management', 'newpane', 'applications', 'newpane', 'recent']
+
+[org/mate/mate-menu/plugins/applications]
+last-active-tab=1
+
+[org/mate/mate-menu/plugins/places]
+show-computer=false
+
+[org/mate/mate-menu/plugins/system_management]
+show-control-center=false
+show-lock-screen=true
+show-package-manager=false
+show-quit=false
+show-terminal=true
+
+[org/mate/marco/general]
+compositing-manager=false
+action-double-click-titlebar='toggle_maximize'
+button-layout='menu:minimize,maximize,close'
+num-workspaces=4
+theme='BlueMenta'
+titlebar-font='Liberation Sans Bold 11'
+
+[org/mate/caja/desktop]
+font='Liberation Sans 10'
+
+[org/mate/caja/preferences]
+always-use-location-entry=true
+default-folder-viewer='list-view'
+show-backup-files=true
+
+[org/mate/notification-daemon]
+popup-location='bottom_right'
+theme='slider'

+ 2 - 0
stow/etc/dconf/db/local.d/locks/99-hartmanlab

@@ -0,0 +1,2 @@
+#/etc/dconf/db/local.db/locks/99-hartmanlab
+/org/mate/mate-menu/plugins-list

+ 11 - 0
stow/etc/fail2ban/jail.local

@@ -0,0 +1,11 @@
+[DEFAULT]
+bantime = 30m
+bantime.multipliers = 1 2 4 8 16 32 64
+findtime  = 60m
+maxretry = 3
+ignoreip = 127.0.0.0/8 10.0.0.0/8 138.26.0.0/16 
+banaction = iptables-multiport
+
+[sshd]
+enabled = true
+port = ssh

+ 18 - 0
stow/etc/fstab

@@ -0,0 +1,18 @@
+# /etc/fstab
+#
+# Accessible filesystems, by reference, are maintained under '/dev/disk'
+# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
+#
+UUID=c6c096c8-d635-4890-a080-5de8c88b5441       /                               ext4    defaults 1 1
+UUID=1C8B-AF1E                                  /boot/efi                       vfat    umask=0077,shortname=winnt 0 0
+UUID=32e4f38a-8097-433b-878f-2096f9cad6d5       swap                            swap    defaults 0 0
+UUID=d9f4c4c5-41d5-463d-abf9-b2070e5d3acc       /mnt/array                      btrfs   defaults,compress=zstd:2,x-gvfs-hide,nofail,x-systemd.device-timeout=180s,discard=async,X-fstrim.notrim 0 0
+UUID=d9f4c4c5-41d5-463d-abf9-b2070e5d3acc       /mnt/data                       btrfs   defaults,subvol=data,compress=zstd:2,x-gvfs-show,nofail,x-systemd.device-timeout=180s,discard=async,X-fstrim.notrim 0 0
+UUID=8d4bf94c-f307-40b1-8315-5b15f041c120       /mnt/backup                     btrfs   defaults,compress=zstd:2,nofail,discard=async,X-fstrim.notrim 0 0
+#UUID=6E323E4F323E1C91                          /media/ext1                     ntfs-3g defaults,user,nofail,x-systemd.device-timeout=1,uid=root,gid=smbgrp,dmask=002,fmask=002 0 0
+#UUID=8433-7BB5                                 /media/ext2                     vfat    defaults,user,nofail,x-systemd.device-timeout=1,uid=root,gid=smbgrp,dmask=002,fmask=002 0 0
+#UUID=F474B7AA74B76DCC                          /media/ext3                     ntfs-3g defaults,user,nofail,x-systemd.device-timeout=1,uid=root,gid=smbgrp,dmask=002,fmask=002 0 0
+#UUID=12C23AD8C23AC031                          /media/ext4                     ntfs-3g defaults,user,nofail,x-systemd.device-timeout=1,uid=root,gid=smbgrp,dmask=002,fmask=002 0 0
+#UUID=829AF4939AF484C7                          /media/ext5                     ntfs-3g defaults,user,nofail,x-systemd.device-timeout=1,uid=root,gid=smbgrp,dmask=002,fmask=002 0 0
+#UUID=0628B809375069C3                          /media/ext6                     ntfs-3g defaults,user,nofail,x-systemd.device-timeout=1,uid=root,gid=smbgrp,dmask=002,fmask=002 0 0
+#UUID=686A557F6A554B48                          /media/ext7                     ntfs-3g defaults,user,nofail,x-systemd.device-timeout=1,uid=root,gid=smbgrp,dmask=002,fmask=002 0 0

+ 22 - 0
stow/etc/samba/smb.conf

@@ -0,0 +1,22 @@
+[global]
+workgroup = WORKGROUP
+server string = Samba Server %v
+netbios name = hartmanlab
+security = user
+map to guest = bad user
+dns proxy = no
+#============================ Share Definitions ============================== 
+[data]
+path = /mnt/data
+valid users = @smbgrp
+browseable = yes
+writeable = yes
+guest ok = no
+
+
+# Special homes share (do not edit!)
+[homes]
+comment = Home Directories
+browseable = yes
+valid users = %S
+writeable = yes

+ 137 - 0
stow/etc/ssh/sshd_config

@@ -0,0 +1,137 @@
+# This is the sshd server system-wide configuration file.  See
+# sshd_config(5) for more information.
+
+# This sshd was compiled with PATH=/usr/local/bin:/usr/bin
+
+# The strategy used for options in the default sshd_config shipped with
+# OpenSSH is to specify options with their default value where
+# possible, but leave them commented.  Uncommented options override the
+# default value.
+
+# If you want to change the port on a SELinux system, you have to tell
+# SELinux about this change.
+# semanage port -a -t ssh_port_t -p tcp #PORTNUMBER
+#
+#Port 22
+#AddressFamily any
+#ListenAddress 0.0.0.0
+#ListenAddress ::
+
+HostKey /etc/ssh/ssh_host_rsa_key
+#HostKey /etc/ssh/ssh_host_dsa_key
+HostKey /etc/ssh/ssh_host_ecdsa_key
+HostKey /etc/ssh/ssh_host_ed25519_key
+
+# Ciphers and keying
+#RekeyLimit default none
+
+# Logging
+#SyslogFacility AUTH
+SyslogFacility AUTHPRIV
+#LogLevel INFO
+
+# Authentication:
+
+#LoginGraceTime 2m
+PermitRootLogin no
+#StrictModes yes
+MaxAuthTries 4
+#MaxSessions 10
+
+#PubkeyAuthentication yes
+
+# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
+# but this is overridden so installations will only check .ssh/authorized_keys
+AuthorizedKeysFile      .ssh/authorized_keys
+
+#AuthorizedPrincipalsFile none
+
+#AuthorizedKeysCommand none
+#AuthorizedKeysCommandUser nobody
+
+# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
+#HostbasedAuthentication no
+# Change to yes if you don't trust ~/.ssh/known_hosts for
+# HostbasedAuthentication
+#IgnoreUserKnownHosts no
+# Don't read the user's ~/.rhosts and ~/.shosts files
+#IgnoreRhosts yes
+
+# To disable tunneled clear text passwords, change to no here!
+#PasswordAuthentication yes
+#PermitEmptyPasswords no
+PasswordAuthentication yes
+
+# Change to no to disable s/key passwords
+#ChallengeResponseAuthentication yes
+ChallengeResponseAuthentication no
+
+# Kerberos options
+#KerberosAuthentication no
+#KerberosOrLocalPasswd yes
+#KerberosTicketCleanup yes
+#KerberosGetAFSToken no
+#KerberosUseKuserok yes
+
+# GSSAPI options
+GSSAPIAuthentication yes
+GSSAPICleanupCredentials no
+#GSSAPIStrictAcceptorCheck yes
+#GSSAPIKeyExchange no
+#GSSAPIEnablek5users no
+
+# Set this to 'yes' to enable PAM authentication, account processing,
+# and session processing. If this is enabled, PAM authentication will
+# be allowed through the ChallengeResponseAuthentication and
+# PasswordAuthentication.  Depending on your PAM configuration,
+# PAM authentication via ChallengeResponseAuthentication may bypass
+# the setting of "PermitRootLogin without-password".
+# If you just want the PAM account and session checks to run without
+# PAM authentication, then enable this but set PasswordAuthentication
+# and ChallengeResponseAuthentication to 'no'.
+# WARNING: 'UsePAM no' is not supported in Red Hat Enterprise Linux and may cause several
+# problems.
+UsePAM yes
+
+#AllowAgentForwarding yes
+#AllowTcpForwarding yes
+GatewayPorts yes
+X11Forwarding yes
+#X11DisplayOffset 10
+#X11UseLocalhost yes
+#PermitTTY yes
+PrintMotd yes
+#PrintLastLog yes
+#TCPKeepAlive yes
+#UseLogin no
+#UsePrivilegeSeparation sandbox
+#PermitUserEnvironment no
+#Compression delayed
+ClientAliveInterval 7200
+#ClientAliveCountMax 3
+#ShowPatchLevel no
+#UseDNS yes
+#PidFile /var/run/sshd.pid
+#MaxStartups 10:30:100
+PermitTunnel yes
+#ChrootDirectory none
+#VersionAddendum none
+
+# no default banner path
+#Banner none
+
+# Accept locale-related environment variables
+AcceptEnv LANG LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY LC_MESSAGES
+AcceptEnv LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT
+AcceptEnv LC_IDENTIFICATION LC_ALL LANGUAGE
+AcceptEnv XMODIFIERS
+
+# override default of no subsystems
+Subsystem       sftp    /usr/libexec/openssh/sftp-server
+
+# Example of overriding settings on a per-user basis
+#Match User anoncvs
+#       X11Forwarding no
+#       AllowTcpForwarding no
+#       PermitTTY no
+#       ForceCommand cvs server

+ 23 - 0
stow/usr/share/glib-2.0/schemas/50-tweaks.gschema.override

@@ -0,0 +1,23 @@
+[org.mate.panel.general]
+default-layout='redmond'
+
+[org.mate.desktop.font-rendering]
+antialiasing='rgba'
+hinting='slight'
+
+#[org.mate.desktop.interface]
+#font-name='Liberation Sans 10'
+#document-font-name='Liberation Sans 10'
+#monospace-font-name='Liberation Mono 10'
+
+[org.mate.Marco.general]
+side-by-side-tiling=true
+num-workspaces=2
+compositing-manager=false
+
+[org.mate.media-handling]
+automount=false
+automount-open=false
+
+[org.mate.screensaver]
+lock-enabled=false

+ 72 - 0
stow/usr/share/mate-panel/layouts/hartmanlab.layout

@@ -0,0 +1,72 @@
+#/usr/share/mate-panel/layouts/hartmanlab.layout
+
+[Toplevel bottom]
+expand=true
+orientation=bottom
+size=24
+
+[Object mate-menu]
+object-type=applet
+toplevel-id=bottom
+locked=true
+position=0
+applet-iid=MateMenuAppletFactory::MateMenuApplet
+
+[Object separator]
+object-type=separator
+toplevel-id=bottom
+locked=true
+position=30
+
+[Object firefox]
+object-type=launcher
+toplevel-id=bottom
+locked=true
+position=40
+launcher-location=/usr/share/applications/firefox.desktop
+
+[Object mate-terminal]
+object-type=launcher
+toplevel-id=bottom
+locked=true
+position=64
+launcher-location=/usr/share/applications/mate-terminal.desktop
+
+[Object caja]
+object-type=launcher
+toplevel-id=bottom
+locked=true
+position=88
+launcher-location=/usr/share/applications/caja-browser.desktop
+
+[Object window-list]
+object-type=applet
+toplevel-id=bottom
+locked=true
+position=112
+applet-iid=WnckletFactory::WindowListApplet
+
+[Object workspace-switcher]
+object-type=applet
+toplevel-id=bottom
+locked=true
+panel-right-stick=true
+position=20
+applet-iid=WnckletFactory::WorkspaceSwitcherApplet
+
+[Object notification-area]
+object-type=applet
+toplevel-id=bottom
+locked=true
+panel-right-stick=true
+position=10
+applet-iid=NotificationAreaAppletFactory::NotificationArea
+
+[Object clock]
+object-type=applet
+toplevel-id=bottom
+locked=true
+panel-right-stick=true
+position=0
+applet-iid=ClockAppletFactory::ClockApplet
+

+ 2 - 9
smartd-notify-all → stow/usr/share/smartmontools/smartd_warning.d/99-smartd-notify-all

@@ -1,15 +1,8 @@
 #!/usr/bin/env bash
 # Notify all users on S.M.A.R.T errors
 # Place in /usr/share/smartmontools/smartd_warning.d/ or use "DEVICESCAN -m @smartd-notify-all" in /etc/smartd.conf
-# Copyright 2021 Bryan C. Roessler
-
-parent="${BASH_SOURCE[0]}"
-parent=${parent%/*}
-
-[[ -f $parent/script-functions ]] && . "$parent"/script-functions || exit 1
-
-is_root
-
+# Copyright 2021-2025 Bryan C. Roessler
+# Licensed under the Apache License, Version 2.0
 IFS=$'\n'
 for LINE in $(w -hs); do
     USER=$(echo "$LINE" | awk '{print $1}')