diff --git a/README.md b/README.md index 8337fdc..f7ca9c7 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,22 @@ password you set there is the RCON password. admin password. Keep it on your LAN or a VPN; only Megalodon needs to reach it. If Megalodon and the server are on the same machine or Docker network, nothing needs to be opened at all. -## Running it +## Installing on Linux (systemd) + +```sh +curl -fsSL https://git.veillax.com/Veillax/megalodon/raw/branch/main/deploy/install.sh | sudo sh +``` + +This downloads the latest release (checking its SHA-256), installs `/usr/local/bin/megalodon`, +creates a `megalodon` user, `/etc/megalodon/` (settings) and `/var/lib/megalodon/` (data), opens +`/etc/megalodon/megalodon.env` in your editor, then starts the service and prints the first-run +admin password. Run it again to upgrade; your settings are kept. + +Options: `--version v0.1.0`, `--listen 0.0.0.0:8480`, `--source` (build from the repository with Go +instead), `--no-edit`, `--no-start`, and `--uninstall` (add `--purge` to delete settings and data). +Download the script and read it first if you prefer: it is [`deploy/install.sh`](deploy/install.sh). + +## Running it by hand ```sh # download a release, or: go build ./cmd/megalodon diff --git a/deploy/install.sh b/deploy/install.sh new file mode 100644 index 0000000..aa69508 --- /dev/null +++ b/deploy/install.sh @@ -0,0 +1,223 @@ +#!/bin/sh +# Installs (or upgrades) Megalodon as a systemd service. +# +# curl -fsSL https://git.veillax.com/Veillax/megalodon/raw/branch/main/deploy/install.sh | sudo sh +# sudo sh install.sh [--version v0.1.0] [--listen 127.0.0.1:8480] [--source] [--no-edit] [--no-start] +# sudo sh install.sh --uninstall [--purge] +# +# What it does: +# 1. downloads the release archive for this machine and checks its SHA-256 (or, with --source, +# clones the repository and builds it with Go), +# 2. installs the binary to /usr/local/bin/megalodon, +# 3. creates a "megalodon" user, /etc/megalodon (settings) and /var/lib/megalodon (data), +# 4. writes /etc/megalodon/megalodon.env and opens it in your editor, +# 5. installs and starts the systemd service, and prints the first-run admin password. +# +# Running it again upgrades the binary and keeps your settings. +set -eu + +REPO_URL=${MEGALODON_REPO_URL:-https://git.veillax.com/Veillax/megalodon} +API_URL=${MEGALODON_API_URL:-https://git.veillax.com/api/v1/repos/Veillax/megalodon} +BIN=/usr/local/bin/megalodon +CONF_DIR=/etc/megalodon +DATA_DIR=/var/lib/megalodon +ENV_FILE=$CONF_DIR/megalodon.env +CONF_FILE=$CONF_DIR/megalodon.json +UNIT=/etc/systemd/system/megalodon.service +USER_NAME=megalodon + +VERSION="" +LISTEN="127.0.0.1:8480" +SOURCE=0 +EDIT=1 +START=1 +UNINSTALL=0 +PURGE=0 + +say() { printf '%s\n' "$*"; } +die() { printf 'install.sh: %s\n' "$*" >&2; exit 1; } + +while [ $# -gt 0 ]; do + case $1 in + --version) VERSION=${2:?--version needs a value}; shift ;; + --listen) LISTEN=${2:?--listen needs a value}; shift ;; + --source) SOURCE=1 ;; + --no-edit) EDIT=0 ;; + --no-start) START=0 ;; + --uninstall) UNINSTALL=1 ;; + --purge) PURGE=1 ;; + -h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) die "unknown option $1" ;; + esac + shift +done + +[ "$(id -u)" -eq 0 ] || die "run as root (sudo sh install.sh)" +[ "$(uname -s)" = Linux ] || die "this installer is for Linux; on Windows, download the .zip from the releases page" +command -v systemctl >/dev/null 2>&1 || HAVE_SYSTEMD=0 +HAVE_SYSTEMD=${HAVE_SYSTEMD:-1} + +if [ "$UNINSTALL" -eq 1 ]; then + if [ "$HAVE_SYSTEMD" -eq 1 ]; then + systemctl disable --now megalodon 2>/dev/null || true + rm -f "$UNIT" + systemctl daemon-reload + fi + rm -f "$BIN" + if [ "$PURGE" -eq 1 ]; then + rm -rf "$CONF_DIR" "$DATA_DIR" + userdel "$USER_NAME" 2>/dev/null || true + say "Megalodon removed, including its settings and data." + else + say "Megalodon removed. Settings ($CONF_DIR) and data ($DATA_DIR) were kept; add --purge to delete them." + fi + exit 0 +fi + +fetch() { # fetch URL DEST + if command -v curl >/dev/null 2>&1; then curl -fsSL -o "$2" "$1" + elif command -v wget >/dev/null 2>&1; then wget -qO "$2" "$1" + else die "need curl or wget"; fi +} + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +# ---- 1. get the binary ---- +if [ "$SOURCE" -eq 1 ]; then + command -v git >/dev/null 2>&1 || die "--source needs git" + command -v go >/dev/null 2>&1 || die "--source needs Go (https://go.dev/dl/)" + say "Cloning $REPO_URL ..." + git clone --quiet ${VERSION:+--branch "$VERSION"} --depth 1 "$REPO_URL" "$TMP/src" + (cd "$TMP/src" && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=${VERSION:-source}" -o "$TMP/megalodon" ./cmd/megalodon) +else + case $(uname -m) in + x86_64|amd64) ARCH=amd64 ;; + aarch64|arm64) ARCH=arm64 ;; + *) die "no release build for $(uname -m); try --source" ;; + esac + if [ -z "$VERSION" ]; then + fetch "$API_URL/releases/latest" "$TMP/latest.json" + VERSION=$(sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$TMP/latest.json" | head -n 1) + [ -n "$VERSION" ] || die "could not find the latest release; use --version or --source" + fi + ARCHIVE="megalodon_${VERSION}_linux_${ARCH}.tar.gz" + say "Downloading Megalodon $VERSION ($ARCH) ..." + fetch "$REPO_URL/releases/download/$VERSION/$ARCHIVE" "$TMP/$ARCHIVE" + fetch "$REPO_URL/releases/download/$VERSION/SHA256SUMS" "$TMP/SHA256SUMS" + want=$(grep " $ARCHIVE\$" "$TMP/SHA256SUMS" | cut -d' ' -f1) + [ -n "$want" ] || die "$ARCHIVE is not listed in SHA256SUMS" + got=$(sha256sum "$TMP/$ARCHIVE" | cut -d' ' -f1) + [ "$want" = "$got" ] || die "checksum mismatch for $ARCHIVE; refusing to install" + tar -xzf "$TMP/$ARCHIVE" -C "$TMP" + cp "$TMP/megalodon_${VERSION}_linux_${ARCH}/megalodon" "$TMP/megalodon" +fi + +# ---- 2. install it ---- +UPGRADE=0 +[ -x "$BIN" ] && UPGRADE=1 +if [ "$UPGRADE" -eq 1 ] && [ "$HAVE_SYSTEMD" -eq 1 ]; then systemctl stop megalodon 2>/dev/null || true; fi +install -m 0755 "$TMP/megalodon" "$BIN" +say "Installed $("$BIN" version) to $BIN" + +# ---- 3. user and folders ---- +id "$USER_NAME" >/dev/null 2>&1 || useradd --system --home-dir "$DATA_DIR" --shell /usr/sbin/nologin "$USER_NAME" 2>/dev/null \ + || adduser --system --home "$DATA_DIR" --shell /usr/sbin/nologin "$USER_NAME" 2>/dev/null \ + || die "could not create the $USER_NAME user" +# The dashboard rewrites its settings file, so the service must own the folder that holds it. +mkdir -p "$CONF_DIR" "$DATA_DIR" +chown "$USER_NAME" "$CONF_DIR" "$DATA_DIR" +chmod 0750 "$CONF_DIR" "$DATA_DIR" + +# ---- 4. settings and environment file ---- +if [ ! -f "$CONF_FILE" ]; then + printf '{\n "listen": "%s",\n "data_dir": "%s"\n}\n' "$LISTEN" "$DATA_DIR" > "$CONF_FILE" + chown "$USER_NAME" "$CONF_FILE" + chmod 0600 "$CONF_FILE" +fi +if [ ! -f "$ENV_FILE" ]; then + cat > "$ENV_FILE" </dev/null || true +fi + +if [ "$EDIT" -eq 1 ] && [ -r /dev/tty ]; then + ED=${VISUAL:-${EDITOR:-}} + [ -n "$ED" ] || for c in nano vim vi; do command -v "$c" >/dev/null 2>&1 && { ED=$c; break; }; done + if [ -n "$ED" ]; then + say "Opening $ENV_FILE in $ED ..." + $ED "$ENV_FILE" /dev/tty || say "(the editor exited with an error; continuing)" + fi +fi + +# ---- 5. service ---- +if [ "$HAVE_SYSTEMD" -eq 0 ]; then + say "systemd was not found, so no service was installed. Run it yourself with:" + say " sudo -u $USER_NAME env \$(grep -v '^#' $ENV_FILE | xargs) $BIN serve -config $CONF_FILE" + exit 0 +fi + +cat > "$UNIT" </dev/null 2>&1 + +if [ "$START" -eq 1 ]; then + systemctl restart megalodon + sleep 3 + if systemctl is-active --quiet megalodon; then + say "Megalodon is running." + else + say "Megalodon did not start. Look at: journalctl -u megalodon -n 50" + exit 1 + fi + pw=$(journalctl -u megalodon --no-pager 2>/dev/null | sed -n 's/.*first run: created user "admin" with password \([^ ]*\).*/\1/p' | tail -n 1) + if [ "$UPGRADE" -eq 0 ] && [ -n "$pw" ]; then + say "" + say " Sign in as: admin" + say " Password: $pw (shown once; change it under Settings > Users)" + fi + say "" + say "Dashboard: http://$LISTEN" + say "Logs: journalctl -u megalodon -f" + say "Settings: $CONF_FILE and $ENV_FILE" + case $LISTEN in + 127.*|localhost*|\[::1\]*) say "It only listens on this machine. To reach it elsewhere, put an HTTPS reverse proxy (or Hammerhead) in front of it." ;; + esac +fi