Files
MiniServer/utils.sh
T
2026-10-28 16:41:02 -06:00

253 lines
8.1 KiB
Bash
Executable File

#!/bin/bash
# Function to check if a package is installed
is_installed() {
pacman -Qi "$1" &> /dev/null
}
# Function to check if a package is installed
is_group_installed() {
pacman -Qg "$1" &> /dev/null
}
# Function to install packages if not already installed
install_packages() {
local packages=("$@")
local to_install=()
for pkg in "${packages[@]}"; do
if ! is_installed "$pkg" && ! is_group_installed "$pkg"; then
to_install+=("$pkg")
fi
done
if [ ${#to_install[@]} -ne 0 ]; then
echo "Installing: ${to_install[*]}"
yay -S --noconfirm --answerdiff=None --answeredit=None --answerclean=None "${to_install[@]}"
fi
}
# Function to remove pacman/AUR packages if installed
remove_packages() {
local packages=("$@")
local to_remove=()
for pkg in "${packages[@]}"; do
if is_installed "$pkg"; then
to_remove+=("$pkg")
fi
done
if [ ${#to_remove[@]} -ne 0 ]; then
echo "Removing: ${to_remove[*]}"
sudo pacman -Rns --noconfirm "${to_remove[@]}"
fi
}
# Services you turned off with audit, as "<system|user> <unit>" lines
SERVICES_OFF="$HOME/.config/miniserver/services-off"
# True if you turned off the system service <unit> with audit
service_turned_off() {
grep -qsx "system $1" "$SERVICES_OFF"
}
# Function to enable systemd services if not already enabled. With --now, also
# start them. Services you turned off with audit are left off.
enable_services() {
local now=()
if [ "$1" = "--now" ]; then
now=(--now)
shift
sudo systemctl daemon-reload
fi
for service in "$@"; do
if service_turned_off "$service"; then
echo "$service is turned off (see audit)."
elif ! systemctl is-enabled "$service" &> /dev/null; then
echo "Enabling $service..."
sudo systemctl enable "${now[@]}" "$service"
else
echo "$service is already enabled."
fi
done
}
# Run each script in migrations/ that hasn't succeeded on this machine yet, in
# order, and record it once it does. Stops at the first failure so later
# migrations can rely on earlier ones; it runs again on the next update.
run_migrations() {
local dir="${MINISERVER:-$HOME/.local/share/MiniServer}/migrations"
local state="$HOME/.local/state/miniserver/migrations"
local file name
mkdir -p "$state"
for file in "$dir"/*.sh; do
[ -f "$file" ] || continue
name="$(basename "$file")"
[ -f "$state/$name" ] && continue
echo "Running migration $name..."
if ! bash "$file"; then
echo "Migration $name failed; it will run again on the next update."
return 1
fi
touch "$state/$name"
done
}
# Output of the last update's pacman -Su, written by updates.d/10-pacman.sh
PACMAN_UPDATE_LOG="$HOME/.local/state/miniserver/pacman-update.log"
# True if the last update rebuilt the initramfs and mkinitcpio reported a
# failure. Rebooting then can leave the machine unbootable.
initramfs_build_failed() {
[ -f "$PACMAN_UPDATE_LOG" ] || return 1
grep -q "Updating linux initcpios" "$PACMAN_UPDATE_LOG" || return 1
grep -q "==> ERROR" "$PACMAN_UPDATE_LOG" && return 0
# Every image built must report success. Older mkinitcpio says "==> Image
# generation successful", newer says "==> Initcpio image generation successful".
local built succeeded
built="$(grep -c "==> Building image from preset" "$PACMAN_UPDATE_LOG")"
succeeded="$(grep -cE "==> (Initcpio i|I)mage generation successful" "$PACMAN_UPDATE_LOG")"
(( succeeded == 0 || succeeded < built ))
}
# Install a script atomically (copy to a temp file, then rename over the target)
# so a copy of it that is running right now keeps reading the old file
install_script() {
local tmp
tmp="$(mktemp "$2.XXXXXX")" && cp "$1" "$tmp" && chmod 755 "$tmp" && mv -f "$tmp" "$2"
}
# Start Cockpit, unless you turned it off with audit. Safe to re-run: does
# nothing once set up. The cockpit package itself comes from packages.conf; its
# firewall port is in firewall.conf.
setup_cockpit() {
if service_turned_off cockpit.socket; then
echo "Cockpit is turned off (see audit)."
elif ! systemctl is-enabled --quiet cockpit.socket || ! systemctl is-active --quiet cockpit.socket; then
echo "Enabling Cockpit..."
sudo systemctl enable --now cockpit.socket
fi
}
# Print the rules a firewall.conf asks for, one per line, as service:<name> or
# port:<port>/<protocol>. Sourced in a subshell so the file can't change
# anything else.
firewall_entries() {
(
FIREWALL_SERVICES=()
FIREWALL_PORTS=()
source "$1" > /dev/null || exit 1
for e in "${FIREWALL_SERVICES[@]}"; do echo "service:$e"; done
for e in "${FIREWALL_PORTS[@]}"; do echo "port:$e"; done
)
}
# Rules setup_firewall opened, so it knows which ones it may close again
FIREWALL_STATE="$HOME/.local/state/miniserver/firewall-opened"
# Allow what MiniServer's and each sibling project's firewall.conf ask for
# through firewalld's default zone, and make sure firewalld is running.
# Closes a rule only if setup_firewall opened it and no firewall.conf asks for
# it any more (e.g. after `sibling remove`); rules that were already there or
# were added by hand are left alone. Safe to re-run.
setup_firewall() {
command -v firewall-offline-cmd &> /dev/null || return 0
# firewall-cmd only works while firewalld is running; before that, write the
# permanent config directly so SSH is allowed before the firewall comes up
local fw running=0 changed=0 complete=1 status=0
if systemctl is-active --quiet firewalld; then
running=1
fw=(sudo firewall-cmd --permanent)
else
fw=(sudo firewall-offline-cmd)
fi
local wanted=() opened=() kept=() conf out entry kind value port
for conf in "$HOME/.local/share/MiniServer/firewall.conf" "$HOME/.local/share/siblings/"*/firewall.conf; do
[ -f "$conf" ] || continue
if out="$(firewall_entries "$conf")"; then
[ -n "$out" ] && mapfile -t -O "${#wanted[@]}" wanted <<< "$out"
else
echo "Firewall: could not read $conf, so no rules will be closed this time."
complete=0
status=1
fi
done
# Keep SSH reachable if sshd listens on a port other than 22. If its config
# can't be read, don't close anything: that port could be the SSH port.
if command -v sshd &> /dev/null; then
if out="$(sudo sshd -T 2> /dev/null)"; then
for port in $(awk '$1 == "port" && $2 != 22 { print $2 }' <<< "$out"); do
wanted+=("port:$port/tcp")
done
else
complete=0
fi
fi
[ -f "$FIREWALL_STATE" ] && mapfile -t opened < "$FIREWALL_STATE"
for entry in "${wanted[@]}"; do
kind="${entry%%:*}"
value="${entry#*:}"
if ! { [[ "$kind" == service && "$value" =~ ^[A-Za-z0-9_.-]+$ ]] ||
[[ "$kind" == port && "$value" =~ ^[0-9]+(-[0-9]+)?/(tcp|udp|sctp|dccp)$ ]]; }; then
echo "Firewall: ignoring invalid $kind '$value'"
status=1
continue
fi
if ! "${fw[@]}" --query-"$kind"="$value" &> /dev/null; then
echo "Firewall: allowing $value"
if "${fw[@]}" --add-"$kind"="$value"; then
opened+=("$entry")
changed=1
else
status=1
fi
fi
done
for entry in "${opened[@]}"; do
[ -n "$entry" ] || continue
if (( ! complete )) || printf '%s\n' "${wanted[@]}" | grep -qxF -- "$entry"; then
kept+=("$entry")
continue
fi
kind="${entry%%:*}"
value="${entry#*:}"
if "${fw[@]}" --query-"$kind"="$value" &> /dev/null; then
echo "Firewall: closing $value (nothing asks for it any more)"
if "${fw[@]}" --remove-"$kind"="$value"; then
changed=1
else
kept+=("$entry")
status=1
fi
fi
done
mkdir -p "$(dirname "$FIREWALL_STATE")"
printf '%s\n' "${kept[@]}" | sed '/^$/d' | sort -u > "$FIREWALL_STATE"
if (( running )); then
if (( changed )); then
sudo firewall-cmd --reload || status=1
fi
else
echo "Starting firewalld..."
sudo systemctl enable --now firewalld || status=1
# Docker sets up its firewalld zone only when it starts
if systemctl is-active --quiet docker; then
echo "Docker started before firewalld. Restart it (sudo systemctl restart docker) or reboot so its containers' ports work through the firewall."
fi
fi
return $status
}