feat(install): custom, root-baked install locations (phase 3)
Make the three roots selectable at install and bake them into the CLI wrapper (the last /docker-hardcoded consumer). - init.sh: --system-dir= / --containers-dir= / --backups-dir= flags (=form keeps the single-token shift logic), plus --allow-home; LP_*_DIR env also honored. Re-derives paths after flag parsing. - libreportalValidatePaths (run only in the install flow): each root must be a non-root absolute path outside protected system trees; the three must not nest (except the legacy /docker compat layout); a containers/backups root inside a human home is refused unless --allow-home (rootless o+x traversal = privacy trade-off). The root helpers re-check at runtime (defence in depth). - CLI wrapper: a baked bootstrap (the same __ROOT__ placeholder mechanism as the helpers) exports LP_*_DIR and derives docker_dir/configs_dir/script_dir; every /docker literal in the heredoc now resolves from those at runtime. init.sh seds the placeholders into the root-owned wrapper after writing it. The scoped sudoers needs no change (it references only the fixed helper paths + system binaries, never a data root). Custom locations verified end-to-end: generate+bake the wrapper with /mnt/* roots → syntax OK, no placeholders left, paths resolve. Live box untouched (wrapper/helpers only change on reinstall). Phase 3b (external-drive guards) + phase 4 (verify) follow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
492e62b6d0
commit
38e531ed6e
110
init.sh
110
init.sh
@ -15,6 +15,15 @@
|
|||||||
# of tearing them down — so a following reinstall rebuilds
|
# of tearing them down — so a following reinstall rebuilds
|
||||||
# the WebUI image from cache (fast) instead of from
|
# the WebUI image from cache (fast) instead of from
|
||||||
# scratch. (No effect on install.)
|
# scratch. (No effect on install.)
|
||||||
|
# --system-dir=PATH Install root for the control plane (configs/logs/install/
|
||||||
|
# db). Default /libreportal-system. (Also: LP_SYSTEM_DIR.)
|
||||||
|
# --containers-dir=PATH Root for live app data. Default /libreportal-containers.
|
||||||
|
# (Also: LP_CONTAINERS_DIR.) Put on its own disk if wanted.
|
||||||
|
# --backups-dir=PATH Root for backup repos. Default /libreportal-backups.
|
||||||
|
# (Also: LP_BACKUPS_DIR.) Point at a separate disk/mount.
|
||||||
|
# --allow-home Permit a containers/backups root inside /home/<user>
|
||||||
|
# (needs rootless o+x traversal of that home — a privacy
|
||||||
|
# trade-off; refused without this flag).
|
||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
# ./init.sh --random-password --local init
|
# ./init.sh --random-password --local init
|
||||||
@ -97,6 +106,7 @@ init_unattended_mode=false
|
|||||||
init_skip_os_update=false
|
init_skip_os_update=false
|
||||||
init_skip_prereqs=false
|
init_skip_prereqs=false
|
||||||
init_skip_docker_images=false
|
init_skip_docker_images=false
|
||||||
|
init_allow_home=false
|
||||||
|
|
||||||
install_param="init"
|
install_param="init"
|
||||||
sudo_user_name=libreportal
|
sudo_user_name=libreportal
|
||||||
@ -152,6 +162,46 @@ libreportalDerivePaths() {
|
|||||||
}
|
}
|
||||||
libreportalDerivePaths
|
libreportalDerivePaths
|
||||||
|
|
||||||
|
# Validate the chosen roots before anything is created/baked. Called from the
|
||||||
|
# install flow only (NOT at source time — the CLI sources init.sh too). Aborts on
|
||||||
|
# an unsafe choice; the root helpers also re-check at runtime (defence in depth).
|
||||||
|
libreportalValidatePaths() {
|
||||||
|
local pair name d
|
||||||
|
for pair in "system:$LP_SYSTEM_DIR" "containers:$LP_CONTAINERS_DIR" "backups:$LP_BACKUPS_DIR"; do
|
||||||
|
name="${pair%%:*}"; d="${pair#*:}"
|
||||||
|
case "$d" in
|
||||||
|
""|/) isError "The $name root must be a non-root absolute path (got '$d')."; exit 1 ;;
|
||||||
|
/usr|/usr/*|/etc|/etc/*|/bin|/bin/*|/sbin|/sbin/*|/lib|/lib/*|/lib64|/lib64/*|/boot|/boot/*|/proc|/proc/*|/sys|/sys/*|/dev|/dev/*|/run|/run/*|/root|/root/*)
|
||||||
|
isError "Refusing $name root '$d' — inside a protected system path."; exit 1 ;;
|
||||||
|
/*) ;; # absolute, allowed
|
||||||
|
*) isError "The $name root must be an absolute path (got '$d')."; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
# The three must not nest inside one another — EXCEPT the legacy single-tree
|
||||||
|
# compat layout (/docker + /docker/{containers,backups}), which nests by design.
|
||||||
|
if [[ "$LP_SYSTEM_DIR" != "/docker" ]]; then
|
||||||
|
local a b
|
||||||
|
for a in "$LP_SYSTEM_DIR" "$LP_CONTAINERS_DIR" "$LP_BACKUPS_DIR"; do
|
||||||
|
for b in "$LP_SYSTEM_DIR" "$LP_CONTAINERS_DIR" "$LP_BACKUPS_DIR"; do
|
||||||
|
[[ "$a" == "$b" ]] && continue
|
||||||
|
if [[ "$a" == "$b"/* ]]; then
|
||||||
|
isError "Roots must not nest: '$a' is inside '$b'."; exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
# containers/backups inside a human home need the rootless user to traverse it
|
||||||
|
# (o+x up the chain) — a privacy trade-off. Require an explicit opt-in.
|
||||||
|
for pair in "containers:$LP_CONTAINERS_DIR" "backups:$LP_BACKUPS_DIR"; do
|
||||||
|
name="${pair%%:*}"; d="${pair#*:}"
|
||||||
|
if [[ "$d" == /home/* && "$init_allow_home" != "true" ]]; then
|
||||||
|
isError "The $name root '$d' is inside a user home; the rootless container user would need o+x traversal of that home (a privacy trade-off)."
|
||||||
|
isNotice "Re-run with --allow-home to accept, or choose a dedicated path/mount."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# Parse flags
|
# Parse flags
|
||||||
init_shift_count=0
|
init_shift_count=0
|
||||||
for ((i=1; i<=$#; i++)); do
|
for ((i=1; i<=$#; i++)); do
|
||||||
@ -183,9 +233,20 @@ for ((i=1; i<=$#; i++)); do
|
|||||||
init_skip_docker_images=true
|
init_skip_docker_images=true
|
||||||
((init_shift_count++))
|
((init_shift_count++))
|
||||||
;;
|
;;
|
||||||
|
# Relocatable roots (=form keeps the single-token shift logic). Validated
|
||||||
|
# by libreportalValidatePaths before any folder is created. Can also be set
|
||||||
|
# via the LP_*_DIR environment.
|
||||||
|
--system-dir=*) LP_SYSTEM_DIR="${!i#*=}"; ((init_shift_count++)) ;;
|
||||||
|
--containers-dir=*) LP_CONTAINERS_DIR="${!i#*=}"; ((init_shift_count++)) ;;
|
||||||
|
--backups-dir=*) LP_BACKUPS_DIR="${!i#*=}"; ((init_shift_count++)) ;;
|
||||||
|
--allow-home) init_allow_home=true; ((init_shift_count++)) ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Re-derive after flags (flags/env set LP_*_DIR; libreportalDerivePaths keeps any
|
||||||
|
# value already set and only fills the rest).
|
||||||
|
libreportalDerivePaths
|
||||||
|
|
||||||
# Shift parsed flags to get positional parameters
|
# Shift parsed flags to get positional parameters
|
||||||
if [ $init_shift_count -gt 0 ]; then
|
if [ $init_shift_count -gt 0 ]; then
|
||||||
for ((i=1; i<=init_shift_count; i++)); do
|
for ((i=1; i<=init_shift_count; i++)); do
|
||||||
@ -1057,6 +1118,18 @@ if [ "$CURRENT_USER" != "$CHECK_USER" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Relocatable roots (baked into this wrapper at install by init.sh) --------
|
||||||
|
# An unbaked copy keeps the "__" sentinel, which no real absolute path has.
|
||||||
|
# Exported so start.sh (and paths.sh) inherit them authoritatively.
|
||||||
|
LP_SYSTEM_DIR="__SYSTEM_DIR__"; [[ "$LP_SYSTEM_DIR" == *"__"* ]] && LP_SYSTEM_DIR="/libreportal-system"
|
||||||
|
LP_CONTAINERS_DIR="__CONTAINERS_DIR__"; [[ "$LP_CONTAINERS_DIR" == *"__"* ]] && LP_CONTAINERS_DIR="/libreportal-containers"
|
||||||
|
LP_BACKUPS_DIR="__BACKUPS_DIR__"; [[ "$LP_BACKUPS_DIR" == *"__"* ]] && LP_BACKUPS_DIR="/libreportal-backups"
|
||||||
|
export LP_SYSTEM_DIR LP_CONTAINERS_DIR LP_BACKUPS_DIR
|
||||||
|
docker_dir="$LP_SYSTEM_DIR"
|
||||||
|
configs_dir="$LP_SYSTEM_DIR/configs"
|
||||||
|
script_dir="$LP_SYSTEM_DIR/install"
|
||||||
|
install_scripts_dir="$script_dir/scripts"
|
||||||
|
|
||||||
command1="${1:-empty}"
|
command1="${1:-empty}"
|
||||||
command2="${2:-empty}"
|
command2="${2:-empty}"
|
||||||
command3="${3:-empty}"
|
command3="${3:-empty}"
|
||||||
@ -1082,7 +1155,7 @@ reset_git_config() {
|
|||||||
# Helper function to load config files in the libreportal command
|
# Helper function to load config files in the libreportal command
|
||||||
commandReloadConfigs() {
|
commandReloadConfigs() {
|
||||||
# Load new structure config files only
|
# Load new structure config files only
|
||||||
for category_dir in /docker/configs/*; do
|
for category_dir in "$configs_dir"/*; do
|
||||||
if [ -d "$category_dir" ] && [ -f "$category_dir/.category" ]; then
|
if [ -d "$category_dir" ] && [ -f "$category_dir/.category" ]; then
|
||||||
for config_file in "$category_dir"/*; do
|
for config_file in "$category_dir"/*; do
|
||||||
local should_load=true
|
local should_load=true
|
||||||
@ -1104,7 +1177,7 @@ commandReloadConfigs() {
|
|||||||
commandUpdateConfigOption() {
|
commandUpdateConfigOption() {
|
||||||
local config_option="$1"
|
local config_option="$1"
|
||||||
local config_value="$2"
|
local config_value="$2"
|
||||||
for category_dir in /docker/configs/*; do
|
for category_dir in "$configs_dir"/*; do
|
||||||
if [ -d "$category_dir" ] && [ -f "$category_dir/.category" ]; then
|
if [ -d "$category_dir" ] && [ -f "$category_dir/.category" ]; then
|
||||||
for config_file in "$category_dir"/*; do
|
for config_file in "$category_dir"/*; do
|
||||||
if [ -f "$config_file" ] && [[ ! "$config_file" =~ \.category$ ]]; then
|
if [ -f "$config_file" ] && [[ ! "$config_file" =~ \.category$ ]]; then
|
||||||
@ -1254,8 +1327,8 @@ setup_repo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sync_configs_from_install() {
|
sync_configs_from_install() {
|
||||||
local src="/docker/install/configs"
|
local src="$script_dir/configs"
|
||||||
local dst="/docker/configs"
|
local dst="$configs_dir"
|
||||||
if [ ! -d "$src" ]; then
|
if [ ! -d "$src" ]; then
|
||||||
echo "ERROR: $src missing — clone broken."
|
echo "ERROR: $src missing — clone broken."
|
||||||
return 1
|
return 1
|
||||||
@ -1266,7 +1339,7 @@ sync_configs_from_install() {
|
|||||||
echo "ERROR: Failed to sync configs from $src to $dst."
|
echo "ERROR: Failed to sync configs from $src to $dst."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
chown -R libreportal:libreportal "$dst"
|
chown -R "$CHECK_USER:$CHECK_USER" "$dst"
|
||||||
if [ ! -f "$dst/general/general_install" ]; then
|
if [ ! -f "$dst/general/general_install" ]; then
|
||||||
echo "ERROR: $dst/general/general_install missing after sync."
|
echo "ERROR: $dst/general/general_install missing after sync."
|
||||||
return 1
|
return 1
|
||||||
@ -1279,12 +1352,12 @@ sync_configs_from_install() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clone_repo() {
|
clone_repo() {
|
||||||
rm -rf /docker/install
|
rm -rf "$script_dir"
|
||||||
local clone_url
|
local clone_url
|
||||||
if [ "$CFG_GIT_USER" != "empty" ]; then
|
if [ "$CFG_GIT_USER" != "empty" ]; then
|
||||||
for clone_url in "$AUTH_HTTPS_REPO_URL" "$AUTH_HTTP_REPO_URL"; do
|
for clone_url in "$AUTH_HTTPS_REPO_URL" "$AUTH_HTTP_REPO_URL"; do
|
||||||
if git clone -q "$clone_url" "/docker/install" 2>/dev/null; then
|
if git clone -q "$clone_url" "$script_dir" 2>/dev/null; then
|
||||||
sudo cp -f /docker/install/init.sh /root/
|
sudo cp -f "$script_dir/init.sh" /root/
|
||||||
sync_configs_from_install || return 1
|
sync_configs_from_install || return 1
|
||||||
echo "SUCCESS: Clone complete. Run 'libreportal run' to continue."
|
echo "SUCCESS: Clone complete. Run 'libreportal run' to continue."
|
||||||
return 0
|
return 0
|
||||||
@ -1294,8 +1367,8 @@ clone_repo() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
for clone_url in "https://${CLEAN_GIT_URL}.git" "http://${CLEAN_GIT_URL}.git"; do
|
for clone_url in "https://${CLEAN_GIT_URL}.git" "http://${CLEAN_GIT_URL}.git"; do
|
||||||
if git clone -q "$clone_url" "/docker/install" 2>/dev/null; then
|
if git clone -q "$clone_url" "$script_dir" 2>/dev/null; then
|
||||||
sudo cp -f /docker/install/init.sh /root/
|
sudo cp -f "$script_dir/init.sh" /root/
|
||||||
sync_configs_from_install || return 1
|
sync_configs_from_install || return 1
|
||||||
echo "SUCCESS: Clone complete. Run 'libreportal run' to continue."
|
echo "SUCCESS: Clone complete. Run 'libreportal run' to continue."
|
||||||
return 0
|
return 0
|
||||||
@ -1316,18 +1389,24 @@ clone_and_install() {
|
|||||||
clone_repo;
|
clone_repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
cd /docker/
|
cd "$docker_dir/" 2>/dev/null || cd /
|
||||||
if [[ $command1 == "reset" ]]; then
|
if [[ $command1 == "reset" ]]; then
|
||||||
clone_and_install
|
clone_and_install
|
||||||
elif [ -f "/docker/install/start.sh" ]; then
|
elif [ -f "$script_dir/start.sh" ]; then
|
||||||
chmod 0755 /docker/install/*
|
chmod 0755 "$script_dir"/*
|
||||||
cd /docker/install
|
cd "$script_dir"
|
||||||
./start.sh "$command1" "$command2" "$command3" "$command4" "$command5" "$command6" "$command7" "$command8" "$command9"
|
./start.sh "$command1" "$command2" "$command3" "$command4" "$command5" "$command6" "$command7" "$command8" "$command9"
|
||||||
else
|
else
|
||||||
clone_and_install
|
clone_and_install
|
||||||
fi
|
fi
|
||||||
# LibrePortal Command End
|
# LibrePortal Command End
|
||||||
EOF
|
EOF
|
||||||
|
# Bake the three roots into the (root-owned) wrapper, same as the helpers.
|
||||||
|
sudo sed -i \
|
||||||
|
-e "s#__SYSTEM_DIR__#${LP_SYSTEM_DIR}#g" \
|
||||||
|
-e "s#__CONTAINERS_DIR__#${LP_CONTAINERS_DIR}#g" \
|
||||||
|
-e "s#__BACKUPS_DIR__#${LP_BACKUPS_DIR}#g" \
|
||||||
|
"$command_script"
|
||||||
sudo chmod +x $command_script
|
sudo chmod +x $command_script
|
||||||
sudo chown root:root $command_script
|
sudo chown root:root $command_script
|
||||||
# Put it on $PATH via a symlink (replaces any older real file at this path).
|
# Put it on $PATH via a symlink (replaces any older real file at this path).
|
||||||
@ -1546,6 +1625,9 @@ if [[ $EUID -ne 0 ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
if [[ "$param1" == "init" ]]; then
|
if [[ "$param1" == "init" ]]; then
|
||||||
|
# Validate the chosen install roots before creating/baking anything.
|
||||||
|
libreportalValidatePaths
|
||||||
|
|
||||||
# Always check existing config first
|
# Always check existing config first
|
||||||
initCheckConfigs
|
initCheckConfigs
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user