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.
|
||||
- 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.
|
||||
- `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.
|
||||
- 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.
|
||||
|
||||
+107
-2
@@ -311,8 +311,11 @@ instance_running() {
|
||||
}
|
||||
|
||||
cmd_prepare() {
|
||||
local name=$1 dir; dir=$(inst_dir "$name")
|
||||
mkdir -p "$dir/data/Worlds"
|
||||
local name=$1 dir err; dir=$(inst_dir "$name")
|
||||
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
|
||||
[[ -f $dir/instance.conf ]] || write_instance_conf "$name" vanilla
|
||||
load_conf "$name"
|
||||
@@ -833,6 +836,7 @@ cmd_import() {
|
||||
echo "imported '$name' into $dir"
|
||||
fi
|
||||
echo "Review $dir/serverconfig.txt, then: $(hint root enable "$name" --now)"
|
||||
location_warnings
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- mods ----
|
||||
@@ -1017,6 +1021,7 @@ EOF
|
||||
$(hint root enable "$name" --now)
|
||||
$(hint group attach "$name")
|
||||
EOF
|
||||
location_warnings
|
||||
}
|
||||
|
||||
# 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
|
||||
[[ -f $(inst_dir "$name")/instance.conf ]] ||
|
||||
die "no instance '$name' (create it with: $(hint root new "$name"))"
|
||||
location_warnings
|
||||
systemctl enable -q "$unit"
|
||||
if [[ $now -eq 1 ]]; then
|
||||
systemctl start "$unit"
|
||||
@@ -1088,6 +1094,104 @@ zip_warning() {
|
||||
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() {
|
||||
local d n 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)"
|
||||
done
|
||||
zip_warning
|
||||
location_warnings
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------ versions ----
|
||||
|
||||
@@ -141,7 +141,7 @@ _terractl() {
|
||||
cword=$((cword - 2))
|
||||
commands+=" --help --version"
|
||||
else
|
||||
commands+=" copy move remote daemon prepare run --host --help --version"
|
||||
commands+=" copy move remote daemon prepare run journal-access --host --help --version"
|
||||
fi
|
||||
|
||||
if (( cword == 1 )); then
|
||||
@@ -151,7 +151,7 @@ _terractl() {
|
||||
|
||||
local command=${words[1]}
|
||||
case $command in
|
||||
list|help|-h|--help|version|-V|--version)
|
||||
list|journal-access|help|-h|--help|version|-V|--version)
|
||||
return ;;
|
||||
new)
|
||||
# new <new name> [vanilla|tmodloader] [port]: the name is new, so nothing to offer
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
# Overrides for terractl. Uncomment to change.
|
||||
# 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
|
||||
# to write there: sudo systemctl edit terraria@.service
|
||||
# Where instances live. To use another disk, it's simplest to mount it at the
|
||||
# default location instead. If you change this, the service must also be
|
||||
# allowed to write there: sudo systemctl edit terraria@.service
|
||||
# [Service]
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
`/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
|
||||
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
|
||||
```
|
||||
```ini
|
||||
[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
|
||||
@@ -948,6 +1018,9 @@ sudo cp -a /var/lib/terraria/instances/other/server/dotnet /var/lib/terraria/ins
|
||||
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.**
|
||||
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 run <name> Run the server
|
||||
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)
|
||||
```
|
||||
|
||||
|
||||
Reference in new issue
Block a user