warn when the services can't reach moved folders
- list, new, import and enable check TERRARIA_BASE and TERRARIA_DIST against the units' ProtectSystem/ProtectHome/ReadWritePaths (terraria@ always, terractld when it's on) and print the drop-in to add. prepare explains why it can't create an instance folder. - Guide: bind mounts and drop-ins under "Changing locations", and a new section for instances or zips under /home. - journal-access in tab completion and the command reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
96f7876729
commit
7adf5c2999
5 files changed
+195
-9
No files matched your search
@@ -8,6 +8,9 @@
|
|||||||
- Going to an older version needs `--downgrade`, because worlds saved by a newer version can't be opened by an older one.
|
- Going to an older version needs `--downgrade`, because worlds saved by a newer version can't be opened by an older one.
|
||||||
- Bash completion offers the instance's zips. Works with `--host`.
|
- Bash completion offers the instance's zips. Works with `--host`.
|
||||||
- `reinstall` now always unpacks the **same** version again. Before, an instance with a blank `ZIP=` picked the newest zip in `dist/`, so `reinstall` could silently upgrade it. It now pins the zip the instance runs first, and refuses if that zip is gone from `dist/`. It also keeps the record of the installed version (`server/.installed`) until the new unpack is done.
|
- `reinstall` now always unpacks the **same** version again. Before, an instance with a blank `ZIP=` picked the newest zip in `dist/`, so `reinstall` could silently upgrade it. It now pins the zip the instance runs first, and refuses if that zip is gone from `dist/`. It also keeps the record of the installed version (`server/.installed`) until the new unpack is done.
|
||||||
|
- `list`, `new`, `import` and `enable` warn when `TERRARIA_BASE` or `TERRARIA_DIST` is somewhere the services' sandbox doesn't allow (the instances folder not in `ReadWritePaths=`, or either folder under `/home`), and print the exact drop-in to add. The daemon (`terractld.service`) is checked too when it's on. A server that can't create its folder now says why in its log, instead of only "Read-only file system".
|
||||||
|
- The guide's "Changing locations" now covers bind mounts (the simplest way to use another disk) as well as drop-ins, and a new section explains keeping instances or zips under `/home`.
|
||||||
|
- `journal-access` is offered by tab completion and listed in the guide's command reference.
|
||||||
- `terractl` run by a relative path (e.g. `sudo ./bin/terractl prepare <name>` from a source tree) no longer fails to switch to the `terraria` user.
|
- `terractl` run by a relative path (e.g. `sudo ./bin/terractl prepare <name>` from a source tree) no longer fails to switch to the `terraria` user.
|
||||||
- New `fix-perms` command: `sudo terractl fix-perms <name>` gives every file of an instance back to the `terraria` user and reapplies the usual modes (group can read, others nothing; server binaries executable), e.g. after copying worlds in as root. Symlinks are not followed. Works with `--host` too.
|
- New `fix-perms` command: `sudo terractl fix-perms <name>` gives every file of an instance back to the `terraria` user and reapplies the usual modes (group can read, others nothing; server binaries executable), e.g. after copying worlds in as root. Symlinks are not followed. Works with `--host` too.
|
||||||
- `list` warns about zips in `dist/` that the `terraria` user can't read (e.g. copied in with `scp -p` from a private file), and `info` lists them as `unreadable_zip=`. A server that fails to install because of one now says how to fix it (`sudo chmod 644 <zip>`) instead of just "cannot read", and a missing zip is reported as not found.
|
- `list` warns about zips in `dist/` that the `terraria` user can't read (e.g. copied in with `scp -p` from a private file), and `info` lists them as `unreadable_zip=`. A server that fails to install because of one now says how to fix it (`sudo chmod 644 <zip>`) instead of just "cannot read", and a missing zip is reported as not found.
|
||||||
|
|||||||
+107
-2
@@ -311,8 +311,11 @@ instance_running() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_prepare() {
|
cmd_prepare() {
|
||||||
local name=$1 dir; dir=$(inst_dir "$name")
|
local name=$1 dir err; dir=$(inst_dir "$name")
|
||||||
mkdir -p "$dir/data/Worlds"
|
if ! err=$(mkdir -p "$dir/data/Worlds" 2>&1); then
|
||||||
|
location_warnings
|
||||||
|
die "can't create $dir/data/Worlds: ${err##*: }"
|
||||||
|
fi
|
||||||
take_lock "$name" # never re-unpack server/ under a running server
|
take_lock "$name" # never re-unpack server/ under a running server
|
||||||
[[ -f $dir/instance.conf ]] || write_instance_conf "$name" vanilla
|
[[ -f $dir/instance.conf ]] || write_instance_conf "$name" vanilla
|
||||||
load_conf "$name"
|
load_conf "$name"
|
||||||
@@ -833,6 +836,7 @@ cmd_import() {
|
|||||||
echo "imported '$name' into $dir"
|
echo "imported '$name' into $dir"
|
||||||
fi
|
fi
|
||||||
echo "Review $dir/serverconfig.txt, then: $(hint root enable "$name" --now)"
|
echo "Review $dir/serverconfig.txt, then: $(hint root enable "$name" --now)"
|
||||||
|
location_warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------- mods ----
|
# ---------------------------------------------------------------- mods ----
|
||||||
@@ -1017,6 +1021,7 @@ EOF
|
|||||||
$(hint root enable "$name" --now)
|
$(hint root enable "$name" --now)
|
||||||
$(hint group attach "$name")
|
$(hint group attach "$name")
|
||||||
EOF
|
EOF
|
||||||
|
location_warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
# Whether the server starts at boot. Needs root: systemd doesn't let polkit
|
# Whether the server starts at boot. Needs root: systemd doesn't let polkit
|
||||||
@@ -1035,6 +1040,7 @@ cmd_enable() {
|
|||||||
# Enabling a name without an instance would create a default one at boot
|
# Enabling a name without an instance would create a default one at boot
|
||||||
[[ -f $(inst_dir "$name")/instance.conf ]] ||
|
[[ -f $(inst_dir "$name")/instance.conf ]] ||
|
||||||
die "no instance '$name' (create it with: $(hint root new "$name"))"
|
die "no instance '$name' (create it with: $(hint root new "$name"))"
|
||||||
|
location_warnings
|
||||||
systemctl enable -q "$unit"
|
systemctl enable -q "$unit"
|
||||||
if [[ $now -eq 1 ]]; then
|
if [[ $now -eq 1 ]]; then
|
||||||
systemctl start "$unit"
|
systemctl start "$unit"
|
||||||
@@ -1088,6 +1094,104 @@ zip_warning() {
|
|||||||
log "fix it with$where: sudo chmod 644 ${bad[*]@Q}"
|
log "fix it with$where: sudo chmod 644 ${bad[*]@Q}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ locations ----
|
||||||
|
# The services' sandboxes (ProtectSystem=, ProtectHome=, ReadWritePaths=) are
|
||||||
|
# set in their unit files, which can't follow /etc/default/terractl. If
|
||||||
|
# TERRARIA_BASE or TERRARIA_DIST is moved somewhere a unit can't reach, it needs
|
||||||
|
# a drop-in; these checks say so, with the drop-in to add.
|
||||||
|
|
||||||
|
# Is <path> <dir>, or inside it?
|
||||||
|
path_in() { [[ $1 == "$2" || $1 == "$2"/* ]]; }
|
||||||
|
|
||||||
|
# Why <unit> can't use <path> (need: rw or ro): 'home' (ProtectHome hides it),
|
||||||
|
# 'home-ro' (ProtectHome=read-only, not writable) or 'system' (ProtectSystem
|
||||||
|
# makes it read-only). Prints nothing if the unit can use it, or if systemd or
|
||||||
|
# the unit isn't there to ask.
|
||||||
|
unit_path_problem() {
|
||||||
|
local unit=$1 path=$2 need=$3 key value load="" ps=no ph=no p covered=0
|
||||||
|
local -a rw=()
|
||||||
|
command -v systemctl >/dev/null || return 0
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
case $key in
|
||||||
|
LoadState) load=$value ;;
|
||||||
|
ProtectSystem) ps=$value ;;
|
||||||
|
ProtectHome) ph=$value ;;
|
||||||
|
ReadWritePaths) read -ra rw <<< "$value" ;;
|
||||||
|
esac
|
||||||
|
done < <(systemctl show -p LoadState,ProtectSystem,ProtectHome,ReadWritePaths "$unit" 2>/dev/null)
|
||||||
|
[[ $load == loaded ]] || return 0
|
||||||
|
path=$(readlink -m "$path") # where it really is, as systemd sees it
|
||||||
|
for p in "${rw[@]}"; do
|
||||||
|
p=${p#-} # '-': ignored if missing
|
||||||
|
if path_in "$path" "$(readlink -m "$p")"; then covered=1; fi
|
||||||
|
done
|
||||||
|
if path_in "$path" /home || path_in "$path" /root || path_in "$path" /run/user; then
|
||||||
|
case $ph in
|
||||||
|
no) ;;
|
||||||
|
read-only) if [[ $need == rw && $covered -eq 0 ]]; then echo home-ro; fi ;;
|
||||||
|
*) echo home ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
[[ $need == rw && $covered -eq 0 ]] || return 0
|
||||||
|
case $ps in
|
||||||
|
strict) echo system ;;
|
||||||
|
yes|full)
|
||||||
|
if path_in "$path" /usr || path_in "$path" /boot || path_in "$path" /efi ||
|
||||||
|
{ [[ $ps == full ]] && path_in "$path" /etc; }; then
|
||||||
|
echo system
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Warns about one unit: location_warning <unit> <name to edit> <who it breaks>
|
||||||
|
# <dist access needed>
|
||||||
|
location_warning() {
|
||||||
|
local unit=$1 edit=$2 effect=$3 dist_need=$4 what path need problem home=0 where=""
|
||||||
|
local -a lines=() rw=()
|
||||||
|
for what in TERRARIA_BASE TERRARIA_DIST; do
|
||||||
|
if [[ $what == TERRARIA_BASE ]]; then path=$BASE_DIR need=rw; else path=$DIST_DIR need=$dist_need; fi
|
||||||
|
problem=$(unit_path_problem "$unit" "$path" "$need")
|
||||||
|
case $problem in
|
||||||
|
home)
|
||||||
|
lines+=("$edit hides $path ($what) under ProtectHome=, so $effect")
|
||||||
|
home=1
|
||||||
|
if [[ $need == rw ]]; then rw+=("$path"); fi ;;
|
||||||
|
home-ro|system)
|
||||||
|
lines+=("$edit can't write to $path ($what), so $effect")
|
||||||
|
if [[ $problem == home-ro ]]; then home=1; fi
|
||||||
|
rw+=("$path") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[[ ${#lines[@]} -gt 0 ]] || return 0
|
||||||
|
if [[ -n ${TERRACTL_VIA:-} ]]; then where=" (on $(hostname 2>/dev/null || echo that host))"; fi
|
||||||
|
printf 'terractl: warning: %s\n' "${lines[@]}" >&2
|
||||||
|
{
|
||||||
|
echo " Fix it with a drop-in$where: sudo systemctl edit $edit"
|
||||||
|
echo " [Service]"
|
||||||
|
if [[ $home -eq 1 ]]; then echo " ProtectHome=read-only"; fi
|
||||||
|
for path in "${rw[@]}"; do echo " ReadWritePaths=$path"; done
|
||||||
|
if [[ $home -eq 1 ]]; then
|
||||||
|
echo " The '$SVC_USER' user also needs access to every folder above it; see"
|
||||||
|
echo " \"Instances or zips under /home\" in the guide."
|
||||||
|
else
|
||||||
|
echo " See \"Changing locations\" in the guide."
|
||||||
|
fi
|
||||||
|
} >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# The servers always; the daemon only if it's in use. The daemon writes zips
|
||||||
|
# into dist/ when instances are copied to this host, so it needs both writable.
|
||||||
|
location_warnings() {
|
||||||
|
location_warning terraria@terractl-check.service terraria@.service \
|
||||||
|
"servers can't start" ro
|
||||||
|
if systemctl is-enabled --quiet terractld.service 2>/dev/null ||
|
||||||
|
systemctl is-active --quiet terractld.service 2>/dev/null; then
|
||||||
|
location_warning terractld.service terractld.service \
|
||||||
|
"other hosts can't copy, move or import instances here" rw
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
cmd_list() {
|
cmd_list() {
|
||||||
local d n port boot state
|
local d n port boot state
|
||||||
printf '%-20s %-11s %-6s %-9s %s\n' INSTANCE TYPE PORT BOOT STATE
|
printf '%-20s %-11s %-6s %-9s %s\n' INSTANCE TYPE PORT BOOT STATE
|
||||||
@@ -1105,6 +1209,7 @@ cmd_list() {
|
|||||||
) 2>/dev/null || printf '%-20s %s\n' "$n" "(cannot read config - try sudo)"
|
) 2>/dev/null || printf '%-20s %s\n' "$n" "(cannot read config - try sudo)"
|
||||||
done
|
done
|
||||||
zip_warning
|
zip_warning
|
||||||
|
location_warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------ versions ----
|
# ------------------------------------------------------------ versions ----
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ _terractl() {
|
|||||||
cword=$((cword - 2))
|
cword=$((cword - 2))
|
||||||
commands+=" --help --version"
|
commands+=" --help --version"
|
||||||
else
|
else
|
||||||
commands+=" copy move remote daemon prepare run --host --help --version"
|
commands+=" copy move remote daemon prepare run journal-access --host --help --version"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( cword == 1 )); then
|
if (( cword == 1 )); then
|
||||||
@@ -151,7 +151,7 @@ _terractl() {
|
|||||||
|
|
||||||
local command=${words[1]}
|
local command=${words[1]}
|
||||||
case $command in
|
case $command in
|
||||||
list|help|-h|--help|version|-V|--version)
|
list|journal-access|help|-h|--help|version|-V|--version)
|
||||||
return ;;
|
return ;;
|
||||||
new)
|
new)
|
||||||
# new <new name> [vanilla|tmodloader] [port]: the name is new, so nothing to offer
|
# new <new name> [vanilla|tmodloader] [port]: the name is new, so nothing to offer
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
# Overrides for terractl. Uncomment to change.
|
# Overrides for terractl. Uncomment to change.
|
||||||
# Changes apply to new commands and to each instance's next start.
|
# Changes apply to new commands and to each instance's next start.
|
||||||
|
|
||||||
# Where instances live. If you change this, the service must also be allowed
|
# Where instances live. To use another disk, it's simplest to mount it at the
|
||||||
# to write there: sudo systemctl edit terraria@.service
|
# default location instead. If you change this, the service must also be
|
||||||
|
# allowed to write there: sudo systemctl edit terraria@.service
|
||||||
# [Service]
|
# [Service]
|
||||||
# ReadWritePaths=/your/new/path
|
# ReadWritePaths=/your/new/path
|
||||||
|
# Under /home it needs more than that. 'terractl list' warns about what's
|
||||||
|
# missing; see "Changing locations" in the guide.
|
||||||
#TERRARIA_BASE=/var/lib/terraria/instances
|
#TERRARIA_BASE=/var/lib/terraria/instances
|
||||||
|
|
||||||
# Where server zips are read from
|
# Where server zips are read from. Outside /home, no drop-in is needed.
|
||||||
#TERRARIA_DIST=/var/lib/terraria/dist
|
#TERRARIA_DIST=/var/lib/terraria/dist
|
||||||
|
|
||||||
# Seconds to wait for a server to save and exit on stop (keep below 90)
|
# Seconds to wait for a server to save and exit on stop (keep below 90)
|
||||||
|
|||||||
+77
-2
@@ -199,16 +199,86 @@ The version comes from the `VERSION` file. Set `Maintainer:` in `packaging/deb/c
|
|||||||
|
|
||||||
### Changing locations
|
### Changing locations
|
||||||
|
|
||||||
`/etc/default/terractl` can move the instances or zips directory. If you move the instances directory, the service also needs permission to write to the new location:
|
The servers run in a systemd sandbox: they can write only to `/var/lib/terraria/instances`, they can read but not change the rest of the system, and they can't see `/home` at all. These rules are in the unit files, which can't read `/etc/default/terractl`. So if the instances or zips go somewhere else, the services must be told as well.
|
||||||
|
|
||||||
|
**The simplest way: mount the storage at the usual place.** To keep instances on another disk, mount it (or a folder on it) at `/var/lib/terraria/instances`, and change no settings at all. For a folder on a disk that's already mounted, add a bind mount to `/etc/fstab`:
|
||||||
|
|
||||||
|
```
|
||||||
|
/srv/bigdisk/terraria /var/lib/terraria/instances none bind 0 0
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
sudo systemctl stop 'terraria@*' # if servers are running
|
||||||
|
sudo cp -a /var/lib/terraria/instances/. /srv/bigdisk/terraria/ # copy existing instances over
|
||||||
|
sudo mount /var/lib/terraria/instances
|
||||||
|
sudo chown terraria:terraria /var/lib/terraria/instances
|
||||||
|
sudo chmod 0750 /var/lib/terraria/instances
|
||||||
|
```
|
||||||
|
|
||||||
|
The old copies stay hidden underneath the mount. Once everything works, you can free that space: `sudo umount /var/lib/terraria/instances`, delete the old instance folders, and `sudo mount /var/lib/terraria/instances` again.
|
||||||
|
|
||||||
|
The same works for `dist/`. Use a mount, not a symbolic link: systemd follows a link to where it really points, and that place isn't covered by the sandbox rules.
|
||||||
|
|
||||||
|
**Or change the setting, and add a drop-in.** Set `TERRARIA_BASE` and/or `TERRARIA_DIST` in `/etc/default/terractl`, create the folders with the same owners and modes as the default ones, and let the servers write to the new instances folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo install -d -o terraria -g terraria -m 0750 /srv/terraria/instances
|
||||||
|
sudo install -d -o root -g terraria -m 2775 /srv/terraria/dist
|
||||||
sudo systemctl edit terraria@.service
|
sudo systemctl edit terraria@.service
|
||||||
```
|
```
|
||||||
```ini
|
```ini
|
||||||
[Service]
|
[Service]
|
||||||
ReadWritePaths=/your/new/path
|
ReadWritePaths=/srv/terraria/instances
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A zips folder outside `/home` needs no drop-in, because the servers only read it.
|
||||||
|
|
||||||
|
`terractl list`, `new`, `import` and `enable` check that the services can reach both folders, and print the exact drop-in to add if they can't. A server that can't create its folder says so in its log, with the same advice. If the daemon for other hosts (`terractld.service`) is on, it's checked too; it only needs a drop-in for folders under `/home` (see below).
|
||||||
|
|
||||||
|
### Instances or zips under `/home`
|
||||||
|
|
||||||
|
This works, but needs more changes, because the services are set up to never touch home folders (`ProtectHome=yes`). That hides `/home`, `/root` and `/run/user` from them completely. Prefer a folder outside `/home`, or a bind mount (above), if you can.
|
||||||
|
|
||||||
|
If they must be under `/home`:
|
||||||
|
|
||||||
|
1. **Let the services see `/home`, read-only, and write to the instances folder.** For the servers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl edit terraria@.service
|
||||||
|
```
|
||||||
|
```ini
|
||||||
|
[Service]
|
||||||
|
ProtectHome=read-only
|
||||||
|
ReadWritePaths=/home/you/terraria/instances
|
||||||
|
```
|
||||||
|
|
||||||
|
If the daemon for other hosts is on, it also needs to write to the zips folder, because copying an instance here can bring its server zip along:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl edit terractld.service
|
||||||
|
```
|
||||||
|
```ini
|
||||||
|
[Service]
|
||||||
|
ProtectHome=read-only
|
||||||
|
ReadWritePaths=/home/you/terraria/instances /home/you/terraria/dist
|
||||||
|
```
|
||||||
|
|
||||||
|
`ProtectHome=read-only` lets the servers see all home folders. Normal file permissions still apply, so as the `terraria` user they can only read what other users can.
|
||||||
|
|
||||||
|
2. **Let the `terraria` user into the folders above.** On Ubuntu (21.04 and later) and some other systems, home folders are private (mode `750`), so the `terraria` user can't even pass through `/home/you`. Give it just that, without letting it list or read anything there:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo setfacl -m u:terraria:x /home/you
|
||||||
|
```
|
||||||
|
|
||||||
|
Do the same for every folder between `/home/you` and the instances or zips folder that isn't open to others. `sudo -u terraria ls /home/you/terraria/instances` tells you whether it worked.
|
||||||
|
|
||||||
|
3. **Create the folders** with the right owners and modes, and set `TERRARIA_BASE` and/or `TERRARIA_DIST` in `/etc/default/terractl`, as above.
|
||||||
|
|
||||||
|
4. **Restart running servers** so they pick up the drop-in: `sudo terractl restart <name>`. If the daemon is on: `sudo systemctl restart terractld`.
|
||||||
|
|
||||||
|
`terractl list` stops warning once the drop-ins match. Members of the `terraria` group also need to get through `/home/you` to use `list`, `info` and tab completion without `sudo`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Migrating from the manual install
|
## Migrating from the manual install
|
||||||
@@ -948,6 +1018,9 @@ sudo cp -a /var/lib/terraria/instances/other/server/dotnet /var/lib/terraria/ins
|
|||||||
sudo systemctl start terraria@<name>
|
sudo systemctl start terraria@<name>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**The server won't start: "Read-only file system" or "No such file or directory" for its folder.**
|
||||||
|
The instances folder was moved (`TERRARIA_BASE`) to where the service isn't allowed to write, or under `/home`. The log names the drop-in to add, and so does `terractl list`. See [Changing locations](#changing-locations).
|
||||||
|
|
||||||
**The server can't save, or you see "Permission denied" errors.**
|
**The server can't save, or you see "Permission denied" errors.**
|
||||||
Some files are owned by root, or have the wrong modes. Run `sudo terractl fix-perms <name>`.
|
Some files are owned by root, or have the wrong modes. Run `sudo terractl fix-perms <name>`.
|
||||||
|
|
||||||
@@ -1056,6 +1129,8 @@ Used by the service:
|
|||||||
terractl prepare <name> Create dirs/configs, unpack zip if needed
|
terractl prepare <name> Create dirs/configs, unpack zip if needed
|
||||||
terractl run <name> Run the server
|
terractl run <name> Run the server
|
||||||
terractl service-stop <name> <pid> Save and shut down via the console (service only)
|
terractl service-stop <name> <pid> Save and shut down via the console (service only)
|
||||||
|
terractl journal-access Let the terraria group read the servers' journal
|
||||||
|
(runs before every start; safe to run by hand)
|
||||||
terractl daemon serve Run the daemon (terractld.service)
|
terractl daemon serve Run the daemon (terractld.service)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user