Compare commits

..

No commits in common. "23b8d1abd8ae28ab782b50f5a7901c1ee90fb51e" and "9b709b25b4056ba5ac9360802036dac0dbb40997" have entirely different histories.

24 changed files with 8 additions and 921 deletions

View file

@ -4,7 +4,6 @@
.PHONY: help all status up down logs restart clean minecraft teamspeak nextcloud .PHONY: help all status up down logs restart clean minecraft teamspeak nextcloud
.PHONY: health health-minecraft health-teamspeak health-nextcloud .PHONY: health health-minecraft health-teamspeak health-nextcloud
.PHONY: backup backup-minecraft backup-teamspeak backup-nextcloud backup-list backup-cleanup .PHONY: backup backup-minecraft backup-teamspeak backup-nextcloud backup-list backup-cleanup
.PHONY: deploy-email deploy-nginx deploy-ss-server deploy-ss-client deploy-frp-server deploy-frp-client
# Default target # Default target
help: help:
@ -22,14 +21,6 @@ help:
@echo " backup-list List available backups" @echo " backup-list List available backups"
@echo " backup-cleanup Remove old backups" @echo " backup-cleanup Remove old backups"
@echo "" @echo ""
@echo "Infrastructure Deploy (set INFRA_DIR first):"
@echo " deploy-email Deploy Postfix+Dovecot+OpenDKIM+SpamAssassin"
@echo " deploy-nginx Deploy Nginx vhosts"
@echo " deploy-ss-server Deploy Shadowsocks server"
@echo " deploy-ss-client Deploy Shadowsocks client + privoxy"
@echo " deploy-frp-server Deploy FRP server (frps)"
@echo " deploy-frp-client Deploy FRP client (frpc)"
@echo ""
@echo "Service-specific Commands:" @echo "Service-specific Commands:"
@echo " Minecraft:" @echo " Minecraft:"
@echo " minecraft-up Start Minecraft server" @echo " minecraft-up Start Minecraft server"
@ -64,36 +55,6 @@ help:
@echo " check Check prerequisites" @echo " check Check prerequisites"
@echo " clean Remove stopped containers and unused volumes" @echo " clean Remove stopped containers and unused volumes"
# ============================================================================
# Infrastructure Service Targets
# Requires INFRA_DIR pointing to the corresponding infra module directory.
# ============================================================================
# deploy-email: INFRA_DIR=/path/to/infra/services/email make deploy-email
deploy-email:
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/email"; exit 1; }
INFRA_DIR=$(INFRA_DIR) ./services/email/deploy.sh
deploy-nginx:
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/nginx"; exit 1; }
INFRA_DIR=$(INFRA_DIR) ./services/nginx/deploy.sh
deploy-ss-server:
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/shadowsocks/server"; exit 1; }
INFRA_DIR=$(INFRA_DIR) ./services/shadowsocks/server/deploy.sh
deploy-ss-client:
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/shadowsocks/client"; exit 1; }
INFRA_DIR=$(INFRA_DIR) ./services/shadowsocks/client/deploy.sh
deploy-frp-server:
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/frp/server"; exit 1; }
INFRA_DIR=$(INFRA_DIR) ./services/frp/server/deploy.sh
deploy-frp-client:
@[ -n "$(INFRA_DIR)" ] || { echo "Set INFRA_DIR=/path/to/infra/services/frp/client"; exit 1; }
INFRA_DIR=$(INFRA_DIR) ./services/frp/client/deploy.sh
# Check prerequisites # Check prerequisites
check: check:
@echo "Checking prerequisites..." @echo "Checking prerequisites..."

View file

@ -1,27 +1,6 @@
# Automa # Automa
Deployment scripts for self-hosted infrastructure. Pairs with [infra](https://github.com/m1ngsama/infra) (private) for configuration. A collection of self-hosted service automation tools following the Unix philosophy: do one thing well, be composable, and stay simple.
```
infra/services/<name>/.env → automa/services/<name>/deploy.sh
```
## Relationship with infra
**infra** (private) holds config templates and `.env.example` files — the "what" and "how to configure".
**automa** (public) holds deployment scripts — the "how to deploy". Zero hardcoded values, zero domain names.
Workflow:
1. Clone infra (private), fill in `.env` files for each service you want
2. Clone automa (public), run the matching deploy script
3. Each script reads `INFRA_DIR` to locate the corresponding `.env`
```bash
# Example
cd infra/services/email && cp .env.example .env && $EDITOR .env
cd automa/services/email
INFRA_DIR=../../infra/services/email ./deploy.sh
```
## Philosophy ## Philosophy
@ -31,49 +10,7 @@ This project embraces Unix principles:
- **Composability**: Tools work together through standard interfaces - **Composability**: Tools work together through standard interfaces
- **Transparency**: Plain text configuration, readable scripts - **Transparency**: Plain text configuration, readable scripts
## Infrastructure Services ## Services
System services deployed from infra module configs.
### Email
Postfix + Dovecot + OpenDKIM + SpamAssassin.
```bash
INFRA_DIR=/path/to/infra/services/email ./services/email/deploy.sh
```
### Nginx
Web server and reverse proxy vhosts.
```bash
INFRA_DIR=/path/to/infra/services/nginx ./services/nginx/deploy.sh
```
### Shadowsocks
GFW-resistant proxy.
```bash
# Server (VPS)
INFRA_DIR=/path/to/infra/services/shadowsocks/server ./services/shadowsocks/server/deploy.sh
# Client (home machine)
INFRA_DIR=/path/to/infra/services/shadowsocks/client ./services/shadowsocks/client/deploy.sh
```
### FRP
Reverse tunnel — expose home services through VPS.
```bash
# Server (VPS)
INFRA_DIR=/path/to/infra/services/frp/server ./services/frp/server/deploy.sh
# Client (home machine)
INFRA_DIR=/path/to/infra/services/frp/client ./services/frp/client/deploy.sh
```
## Home Services
Docker-based services with their own config.
### Minecraft Server ### Minecraft Server
Automated Minecraft Fabric server deployment with mod management. Automated Minecraft Fabric server deployment with mod management.
@ -135,21 +72,12 @@ Batch clone all repositories from a GitHub organization.
``` ```
automa/ automa/
├── bin/ # Utility scripts ├── bin/ # Utility scripts
│ └── lib/common.sh # Shared logging + env helpers │ └── org-clone.sh # GitHub org repo cloner
├── services/ # Infrastructure deploy scripts (reads infra .env) ├── minecraft/ # Minecraft server setup
│ ├── email/deploy.sh ├── teamspeak/ # TeamSpeak server setup
│ ├── nginx/deploy.sh ├── nextcloud/ # Nextcloud setup
│ ├── shadowsocks/ └── README.md # This file
│ │ ├── server/deploy.sh
│ │ └── client/deploy.sh
│ └── frp/
│ ├── server/deploy.sh
│ └── client/deploy.sh
├── minecraft/ # Minecraft server (Docker)
├── teamspeak/ # TeamSpeak server (Docker)
├── nextcloud/ # Nextcloud (Docker)
└── README.md
``` ```
## Common Operations ## Common Operations

View file

@ -1,7 +0,0 @@
# role: vps
# description: Postfix + Dovecot + OpenDKIM + SpamAssassin full email stack
DOMAIN=your-domain.com
MAIL_HOST=mail.your-domain.com
SERVER_IP=x.x.x.x
MAIL_USER=contact

View file

@ -1,37 +0,0 @@
#!/usr/bin/env bash
# Deploys Postfix + Dovecot + OpenDKIM + SpamAssassin email stack.
# Usage: INFRA_DIR=/path/to/infra/services/email ./deploy.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../bin/lib/common.sh"
ENV_FILE="${INFRA_DIR:-.}/.env"
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
set -a; source "$ENV_FILE"; set +a
require_env DOMAIN MAIL_HOST MAIL_USER
log_info "Installing packages..."
apt-get install -y postfix dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd \
dovecot-sieve opendkim opendkim-tools spamassassin spamc
log_info "Deploying Postfix config..."
envsubst < "${INFRA_DIR}/postfix/main.cf" > /etc/postfix/main.cf
cp "${INFRA_DIR}/postfix/aliases" /etc/aliases
newaliases
log_info "Deploying Dovecot config..."
cp "${INFRA_DIR}/dovecot/dovecot.conf" /etc/dovecot/dovecot.conf
cp "${INFRA_DIR}/dovecot/99-stats-fix.conf" /etc/dovecot/conf.d/99-stats-fix.conf
log_info "Adding postfix to dovecot group..."
usermod -aG dovecot postfix
log_info "Enabling services..."
systemctl enable --now postfix dovecot opendkim spamassassin
log_info "Email stack deployed. Remaining manual steps:"
echo " 1. Run certbot for mail.${DOMAIN}"
echo " 2. Generate DKIM key: opendkim-genkey -b 2048 -d ${DOMAIN} -s mail -D /etc/opendkim/keys/${DOMAIN}/"
echo " 3. Add DNS records (see services/email/README.md)"

View file

@ -1,6 +0,0 @@
# role: homeserver
# description: FRP client (frpc) — tunnels local services through VPS
FRP_SERVER_ADDR=your-vps-ip
FRP_SERVER_PORT=7000
FRP_TOKEN=

View file

@ -1,52 +0,0 @@
#!/usr/bin/env bash
# Deploys frpc (FRP client) on home machine.
# Usage: INFRA_DIR=/path/to/infra/services/frp/client ./deploy.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../bin/lib/common.sh"
ENV_FILE="${INFRA_DIR:-.}/.env"
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
set -a; source "$ENV_FILE"; set +a
require_env FRP_SERVER_ADDR FRP_SERVER_PORT FRP_TOKEN
find_template() {
local f="$1"
if [[ -n "${INFRA_DIR:-}" && -f "${INFRA_DIR}/$f" ]]; then
echo "${INFRA_DIR}/$f"
elif [[ -f "$SCRIPT_DIR/$f" ]]; then
echo "$SCRIPT_DIR/$f"
else
log_error "Template not found: $f"
return 1
fi
}
FRPC_BIN="/opt/frp/frpc"
if [[ -x "$FRPC_BIN" ]]; then
log_info "frpc already at $FRPC_BIN, skipping download"
else
log_info "Downloading FRP..."
VERSION=$(curl -s https://api.github.com/repos/fatedier/frp/releases/latest \
| python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'][1:])")
ARCHIVE="frp_${VERSION}_linux_amd64.tar.gz"
wget -q "https://github.com/fatedier/frp/releases/download/v${VERSION}/${ARCHIVE}"
tar -xf "$ARCHIVE"
mkdir -p /opt/frp
cp "frp_${VERSION}_linux_amd64/frpc" /opt/frp/
chmod +x /opt/frp/frpc
rm -rf "$ARCHIVE" "frp_${VERSION}_linux_amd64"
fi
log_info "Deploying config..."
envsubst < "$(find_template frpc.toml.example)" > /opt/frp/frpc.toml
log_info "Installing service..."
cp "$(find_template frpc.service)" /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now frpc
log_info "FRP client deployed, connecting to ${FRP_SERVER_ADDR}:${FRP_SERVER_PORT}"

View file

@ -1,15 +0,0 @@
[Unit]
Description=FRP Client
After=network.target
[Service]
Type=simple
ExecStart=/opt/frp/frpc -c /opt/frp/frpc.toml
WorkingDirectory=/opt/frp
Restart=on-failure
RestartSec=5
User=root
Group=root
[Install]
WantedBy=multi-user.target

View file

@ -1,21 +0,0 @@
serverAddr = "${FRP_SERVER_ADDR}"
serverPort = ${FRP_SERVER_PORT}
auth.method = "token"
auth.token = "${FRP_TOKEN}"
# Example: expose home SSH
[[proxies]]
name = "home-ssh"
type = "tcp"
localIP = "127.0.0.1"
localPort = 22
remotePort = 1234
# Example: expose Minecraft
[[proxies]]
name = "minecraft"
type = "tcp"
localIP = "127.0.0.1"
localPort = 25565
remotePort = 25565

View file

@ -1,6 +0,0 @@
# role: vps
# description: FRP server (frps) — public entry point for reverse tunnels
FRP_TOKEN=
FRP_WEB_PASSWORD=
FRP_BIND_PORT=7000

View file

@ -1,52 +0,0 @@
#!/usr/bin/env bash
# Deploys frps (FRP server) on VPS.
# Usage: INFRA_DIR=/path/to/infra/services/frp/server ./deploy.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../bin/lib/common.sh"
ENV_FILE="${INFRA_DIR:-.}/.env"
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
set -a; source "$ENV_FILE"; set +a
require_env FRP_TOKEN FRP_WEB_PASSWORD FRP_BIND_PORT
find_template() {
local f="$1"
if [[ -n "${INFRA_DIR:-}" && -f "${INFRA_DIR}/$f" ]]; then
echo "${INFRA_DIR}/$f"
elif [[ -f "$SCRIPT_DIR/$f" ]]; then
echo "$SCRIPT_DIR/$f"
else
log_error "Template not found: $f"
return 1
fi
}
FRPS_BIN="/opt/frp/frps"
if [[ -x "$FRPS_BIN" ]]; then
log_info "frps already at $FRPS_BIN, skipping download"
else
log_info "Downloading FRP..."
VERSION=$(curl -s https://api.github.com/repos/fatedier/frp/releases/latest \
| python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'][1:])")
ARCHIVE="frp_${VERSION}_linux_amd64.tar.gz"
wget -q "https://github.com/fatedier/frp/releases/download/v${VERSION}/${ARCHIVE}"
tar -xf "$ARCHIVE"
mkdir -p /opt/frp
cp "frp_${VERSION}_linux_amd64/frps" /opt/frp/
chmod +x /opt/frp/frps
rm -rf "$ARCHIVE" "frp_${VERSION}_linux_amd64"
fi
log_info "Deploying config..."
envsubst < "$(find_template frps.toml.example)" > /opt/frp/frps.toml
log_info "Installing service..."
cp "$(find_template frps.service)" /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now frps
log_info "FRP server deployed on port ${FRP_BIND_PORT}"

View file

@ -1,15 +0,0 @@
[Unit]
Description=FRP Server
After=network.target
[Service]
Type=simple
ExecStart=/opt/frp/frps -c /opt/frp/frps.toml
WorkingDirectory=/opt/frp
Restart=on-failure
RestartSec=5
User=root
Group=root
[Install]
WantedBy=multi-user.target

View file

@ -1,17 +0,0 @@
bindAddr = "0.0.0.0"
bindPort = ${FRP_BIND_PORT}
vhostHTTPPort = 8080
vhostHTTPSPort = 8443
webServer.addr = "127.0.0.1"
webServer.port = 7500
webServer.user = "root"
webServer.password = "${FRP_WEB_PASSWORD}"
log.to = "./frps.log"
log.level = "info"
log.maxDays = 3
auth.method = "token"
auth.token = "${FRP_TOKEN}"

View file

@ -1,8 +0,0 @@
# role: vps
# description: Nginx web server and reverse proxy vhosts
DOMAIN=your-domain.com
BLOG_DOMAIN=blog.your-domain.com
CHAN_DOMAIN=chan.your-domain.com
MAIL_DOMAIN=mail.your-domain.com
GIT_DOMAIN=git.your-domain.com

View file

@ -1,36 +0,0 @@
#!/usr/bin/env bash
# Deploys Nginx web server and vhost configs.
# Usage: INFRA_DIR=/path/to/infra/services/nginx ./deploy.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../bin/lib/common.sh"
ENV_FILE="${INFRA_DIR:-.}/.env"
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
set -a; source "$ENV_FILE"; set +a
require_env DOMAIN
log_info "Installing nginx..."
apt-get install -y nginx certbot python3-certbot-nginx
log_info "Deploying nginx.conf..."
cp "${INFRA_DIR}/nginx.conf" /etc/nginx/nginx.conf
log_info "Deploying vhost configs..."
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
for conf in "${INFRA_DIR}/sites/"*.conf; do
name="$(basename "$conf" .conf)"
envsubst < "$conf" > "/etc/nginx/sites-available/${name}"
ln -sf "/etc/nginx/sites-available/${name}" "/etc/nginx/sites-enabled/${name}"
log_info " Deployed ${name}"
done
log_info "Testing nginx config..."
nginx -t
log_info "Nginx deployed. Remaining manual steps:"
echo " 1. Get TLS certs: certbot --nginx -d ${DOMAIN} -d ${CHAN_DOMAIN:-chan.${DOMAIN}} -d ${BLOG_DOMAIN:-blog.${DOMAIN}}"
echo " 2. systemctl reload nginx"

View file

@ -1,7 +0,0 @@
# role: homeserver
# description: sslocal + privoxy proxy chain (SOCKS5 :1080, HTTP :8118)
SS_SERVER=your-vps-ip
SS_PORT=41268
SS_PASSWORD=
SS_METHOD=2022-blake3-aes-256-gcm

View file

@ -1,11 +0,0 @@
{
"server": "${SS_SERVER}",
"server_port": ${SS_PORT},
"password": "${SS_PASSWORD}",
"method": "${SS_METHOD}",
"local_address": "127.0.0.1",
"local_port": 1080,
"timeout": 300,
"fast_open": true,
"mode": "tcp_and_udp"
}

View file

@ -1,70 +0,0 @@
#!/usr/bin/env bash
# Installs shadowsocks-rust client (sslocal) + privoxy proxy chain.
# Usage: INFRA_DIR=/path/to/infra/services/shadowsocks/client ./deploy.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../bin/lib/common.sh"
ENV_FILE="${INFRA_DIR:-.}/.env"
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
set -a; source "$ENV_FILE"; set +a
require_env SS_SERVER SS_PORT SS_PASSWORD SS_METHOD
# Find a template file: prefer INFRA_DIR override, fall back to bundled default.
find_template() {
local f="$1"
if [[ -n "${INFRA_DIR:-}" && -f "${INFRA_DIR}/$f" ]]; then
echo "${INFRA_DIR}/$f"
elif [[ -f "$SCRIPT_DIR/$f" ]]; then
echo "$SCRIPT_DIR/$f"
else
log_error "Template not found: $f"
return 1
fi
}
SSLOCAL_BIN="/usr/local/bin/sslocal"
# Install sslocal — skip if already present, symlink if installed elsewhere.
if [[ -x "$SSLOCAL_BIN" ]]; then
log_info "sslocal already at $SSLOCAL_BIN ($($SSLOCAL_BIN --version 2>&1 | head -1)), skipping download"
elif command -v sslocal &>/dev/null; then
existing="$(command -v sslocal)"
log_info "sslocal found at $existing, symlinking to $SSLOCAL_BIN"
ln -sf "$existing" "$SSLOCAL_BIN"
else
log_info "Downloading shadowsocks-rust client..."
VERSION=$(curl -s https://api.github.com/repos/shadowsocks/shadowsocks-rust/releases/latest \
| python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])")
ARCHIVE="shadowsocks-${VERSION}.x86_64-unknown-linux-gnu.tar.xz"
wget -q "https://github.com/shadowsocks/shadowsocks-rust/releases/download/${VERSION}/${ARCHIVE}"
tar -xf "$ARCHIVE"
cp sslocal "$SSLOCAL_BIN"
chmod +x "$SSLOCAL_BIN"
rm -f "$ARCHIVE" ssserver sslocal ssurl ssmanager redir tunnel
fi
log_info "Installing privoxy..."
if command -v pacman &>/dev/null; then
pacman -S --noconfirm --needed privoxy
else
apt-get install -y privoxy
fi
log_info "Deploying configs..."
mkdir -p /etc/shadowsocks-rust
envsubst < "$(find_template config.json.example)" > /etc/shadowsocks-rust/client.json
cp "$(find_template privoxy.conf.example)" /etc/privoxy/config
log_info "Stopping any existing shadowsocks service on port 1080..."
systemctl stop shadowsocks 2>/dev/null || true
log_info "Installing service..."
cp "$(find_template shadowsocks-client.service)" /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now shadowsocks-client
systemctl enable --now privoxy
log_info "Proxy chain ready: SOCKS5 at 127.0.0.1:1080, HTTP at 127.0.0.1:8118"

View file

@ -1,5 +0,0 @@
# Privoxy config — bridges SOCKS5 (sslocal) to HTTP proxy
# Listens on :8118, forwards to sslocal SOCKS5 on :1080
listen-address 127.0.0.1:8118
forward-socks5t / 127.0.0.1:1080 .

View file

@ -1,14 +0,0 @@
[Unit]
Description=Shadowsocks-Rust Client
Documentation=https://github.com/shadowsocks/shadowsocks-rust
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/sslocal -c /etc/shadowsocks-rust/client.json
Restart=on-failure
RestartSec=5
LimitNOFILE=51200
[Install]
WantedBy=multi-user.target

View file

@ -1,6 +0,0 @@
# role: vps
# description: Shadowsocks-Rust server (GFW-resistant proxy)
SS_PORT=41268
SS_PASSWORD=
SS_METHOD=2022-blake3-aes-256-gcm

View file

@ -1,10 +0,0 @@
{
"server": "0.0.0.0",
"server_port": ${SS_PORT},
"password": "${SS_PASSWORD}",
"method": "${SS_METHOD}",
"timeout": 300,
"fast_open": true,
"no_delay": true,
"mode": "tcp_and_udp"
}

View file

@ -1,56 +0,0 @@
#!/usr/bin/env bash
# Installs shadowsocks-rust server and configures systemd service.
# Usage: INFRA_DIR=/path/to/infra/services/shadowsocks/server ./deploy.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../bin/lib/common.sh"
ENV_FILE="${INFRA_DIR:-.}/.env"
[ -f "$ENV_FILE" ] || { log_error "No .env found at $ENV_FILE"; exit 1; }
set -a; source "$ENV_FILE"; set +a
require_env SS_PORT SS_PASSWORD SS_METHOD
find_template() {
local f="$1"
if [[ -n "${INFRA_DIR:-}" && -f "${INFRA_DIR}/$f" ]]; then
echo "${INFRA_DIR}/$f"
elif [[ -f "$SCRIPT_DIR/$f" ]]; then
echo "$SCRIPT_DIR/$f"
else
log_error "Template not found: $f"
return 1
fi
}
SSSERVER_BIN="/usr/local/bin/ssserver-rust"
if [[ -x "$SSSERVER_BIN" ]]; then
log_info "ssserver-rust already at $SSSERVER_BIN ($($SSSERVER_BIN --version 2>&1 | head -1)), skipping download"
elif command -v ssserver &>/dev/null; then
existing="$(command -v ssserver)"
log_info "ssserver found at $existing, symlinking to $SSSERVER_BIN"
ln -sf "$existing" "$SSSERVER_BIN"
else
log_info "Downloading shadowsocks-rust..."
VERSION=$(curl -s https://api.github.com/repos/shadowsocks/shadowsocks-rust/releases/latest \
| python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])")
ARCHIVE="shadowsocks-${VERSION}.x86_64-unknown-linux-gnu.tar.xz"
wget -q "https://github.com/shadowsocks/shadowsocks-rust/releases/download/${VERSION}/${ARCHIVE}"
tar -xf "$ARCHIVE"
cp ssserver "$SSSERVER_BIN"
chmod +x "$SSSERVER_BIN"
rm -f "$ARCHIVE" ssserver sslocal ssurl ssmanager redir tunnel
fi
log_info "Deploying config..."
mkdir -p /etc/shadowsocks-rust
envsubst < "$(find_template config.json.example)" > /etc/shadowsocks-rust/config.json
log_info "Installing service..."
cp "$(find_template shadowsocks-rust.service)" /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now shadowsocks-rust
log_info "Shadowsocks server deployed on port ${SS_PORT}"

View file

@ -1,14 +0,0 @@
[Unit]
Description=Shadowsocks-Rust Server
Documentation=https://github.com/shadowsocks/shadowsocks-rust
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/ssserver-rust -c /etc/shadowsocks-rust/config.json
Restart=on-failure
RestartSec=5
LimitNOFILE=51200
[Install]
WantedBy=multi-user.target

337
setup.sh
View file

@ -1,337 +0,0 @@
#!/usr/bin/env bash
# Interactive installer for infra services.
#
# Usage:
# ./setup.sh # fully interactive
# ./setup.sh --infra-dir PATH # use pre-filled .env files from local infra checkout
# ./setup.sh --dry-run # show what would be deployed, no changes
# INFRA_DIR=/path/to/infra ./setup.sh # same as --infra-dir via env var
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/bin/lib/common.sh"
# ============================================================================
# Args
# ============================================================================
INFRA_DIR="${INFRA_DIR:-}"
DRY_RUN=0
while [[ $# -gt 0 ]]; do
case "$1" in
--infra-dir) INFRA_DIR="$2"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
*) log_error "Unknown argument: $1"; exit 1 ;;
esac
done
# ============================================================================
# Banner
# ============================================================================
echo ""
echo " ┌─────────────────────────────────────┐"
echo " │ automa setup │"
echo " │ interactive infrastructure deploy │"
echo " └─────────────────────────────────────┘"
echo ""
# ============================================================================
# Prerequisites (skipped in dry-run)
# ============================================================================
check_prerequisites() {
log_info "Checking prerequisites..."
local missing=0
for cmd in curl wget systemctl envsubst; do
if ! command -v "$cmd" &>/dev/null; then
log_warn " missing: $cmd"
missing=1
fi
done
if [[ "$missing" -eq 1 ]]; then
log_error "Install missing tools before continuing."
exit 1
fi
log_info "Prerequisites OK."
}
[[ "$DRY_RUN" -eq 0 ]] && check_prerequisites
# ============================================================================
# Infra dir resolution
# ============================================================================
if [[ -n "$INFRA_DIR" ]]; then
if [[ ! -d "$INFRA_DIR" ]]; then
log_error "INFRA_DIR does not exist: $INFRA_DIR"
exit 1
fi
log_info "Using infra dir: $INFRA_DIR"
else
echo ""
echo " No --infra-dir specified."
echo " Point to a local infra checkout (with pre-filled .env files) for auto-config,"
echo " or leave blank to enter all values interactively."
echo ""
read -rp " Path to local infra repo [blank = interactive mode]: " infra_input
if [[ -n "$infra_input" ]]; then
infra_input="${infra_input/#\~/$HOME}"
if [[ -d "$infra_input" ]]; then
INFRA_DIR="$infra_input"
log_info "Using infra dir: $INFRA_DIR"
else
log_warn "Directory not found, continuing in interactive mode."
fi
fi
fi
# ============================================================================
# Module discovery
# Parallel arrays indexed by position: MODULES[i], ROLES[i], DESCS[i]
# MENU_MODS[menu_number] = module_path (1-based; index 0 = "")
# ============================================================================
MODULES=()
ROLES=()
DESCS=()
MENU_MODS=("") # index 0 unused — menu numbers start at 1
discover_modules() {
while IFS= read -r deploy; do
local mod env_ex role desc r d
mod="$(dirname "$deploy" | sed "s|^$SCRIPT_DIR/services/||")"
env_ex="$SCRIPT_DIR/services/$mod/.env.example"
role="any"
desc="$mod"
if [[ -f "$env_ex" ]]; then
# grep returns 1 if no match — use || true to avoid pipefail exit
r="$(grep '^# role:' "$env_ex" | head -1 | sed 's/^# role: *//' || true)"
d="$(grep '^# description:' "$env_ex" | head -1 | sed 's/^# description: *//' || true)"
[[ -n "$r" ]] && role="$r"
[[ -n "$d" ]] && desc="$d"
fi
MODULES+=("$mod")
ROLES+=("$role")
DESCS+=("$desc")
done < <(find "$SCRIPT_DIR/services" -name "deploy.sh" | sort)
}
discover_modules
# ============================================================================
# Interactive menu (grouped by role)
# Populates MENU_MODS as a side effect.
# ============================================================================
print_menu() {
local printed_any=0
for role_group in "vps" "homeserver" "any"; do
local label header_printed=0
case "$role_group" in
vps) label="VPS services" ;;
homeserver) label="Home server services" ;;
any) label="Any machine" ;;
esac
for i in "${!MODULES[@]}"; do
[[ "${ROLES[$i]}" != "$role_group" ]] && continue
if [[ "$header_printed" -eq 0 ]]; then
echo " $label:"
header_printed=1
printed_any=1
fi
local menu_idx=${#MENU_MODS[@]}
MENU_MODS+=("${MODULES[$i]}")
printf " [%2d] %-30s %s\n" "$menu_idx" "${MODULES[$i]}" "${DESCS[$i]}"
done
[[ "$header_printed" -eq 1 ]] && echo ""
done
if [[ "$printed_any" -eq 0 ]]; then
log_error "No deployable modules found."
exit 1
fi
}
print_menu
echo " Enter module numbers to deploy (space-separated, e.g. \"1 3\")."
echo " Press Enter to select all, or type \"none\" to exit."
echo ""
read -rp " > " selection
if [[ "$selection" == "none" ]]; then
echo " Exiting."
exit 0
fi
SELECTED_MODULES=()
if [[ -z "$selection" ]]; then
SELECTED_MODULES=("${MODULES[@]}")
else
for num in $selection; do
if [[ "$num" =~ ^[0-9]+$ ]] \
&& [[ "$num" -gt 0 ]] \
&& [[ "$num" -lt "${#MENU_MODS[@]}" ]]; then
SELECTED_MODULES+=("${MENU_MODS[$num]}")
else
log_warn "Unknown module number: $num (skipped)"
fi
done
fi
if [[ ${#SELECTED_MODULES[@]} -eq 0 ]]; then
log_error "No valid modules selected."
exit 1
fi
echo ""
log_info "Selected: ${SELECTED_MODULES[*]}"
# ============================================================================
# Env resolution
# Priority:
# 1. $INFRA_DIR/services/$mod/.env (pre-filled, use as-is)
# 2. $INFRA_DIR/services/$mod/.env.example (prompt, fill from infra template)
# 3. $SCRIPT_DIR/services/$mod/.env.example (prompt, fill from automa template)
# Prints the resolved .env path to stdout.
# ============================================================================
fill_env_interactive() {
# All user-visible output → stderr so caller can capture stdout for the path.
local env_example="$1"
local output="$2"
printf "" > "$output"
echo " Fill in values (Enter = accept default shown in [brackets]):" >&2
echo "" >&2
while IFS= read -r line; do
if [[ "$line" =~ ^#\ (role|description): ]]; then
continue
fi
if [[ -z "$line" ]]; then
continue
fi
if [[ "$line" =~ ^# ]]; then
printf " %s\n" "$line" >&2
continue
fi
local key default hint val
key="${line%%=*}"
default="${line#*=}"
hint=""
if [[ -n "$default" && "$default" != "your-"* && "$default" != "x.x.x.x" ]]; then
hint=" [$default]"
fi
read -rp " $key$hint: " val <&0
printf "%s=%s\n" "$key" "${val:-$default}" >> "$output"
done < "$env_example"
}
resolve_env_for_module() {
# Prints resolved .env path to stdout; all messages → stderr.
local mod="$1"
local dry_run="${2:-0}"
local infra_mod_dir=""
if [[ -n "$INFRA_DIR" ]]; then
infra_mod_dir="$INFRA_DIR/services/$mod"
fi
# Case 1: pre-filled .env in infra — use directly
if [[ -n "$infra_mod_dir" && -f "$infra_mod_dir/.env" ]]; then
log_info " Using pre-filled .env from infra" >&2
printf "%s" "$infra_mod_dir/.env"
return 0
fi
# Determine .env.example template source
local env_example=""
if [[ -n "$infra_mod_dir" && -f "$infra_mod_dir/.env.example" ]]; then
env_example="$infra_mod_dir/.env.example"
elif [[ -f "$SCRIPT_DIR/services/$mod/.env.example" ]]; then
env_example="$SCRIPT_DIR/services/$mod/.env.example"
else
log_error " No .env.example found for: $mod" >&2
return 1
fi
# Dry-run: skip interactive fill, just return path to the example
if [[ "$dry_run" -eq 1 ]]; then
printf "%s" "$env_example"
return 0
fi
local tmp_env
tmp_env="$(mktemp /tmp/automa-env-XXXXXX)"
fill_env_interactive "$env_example" "$tmp_env"
printf "%s" "$tmp_env"
}
# ============================================================================
# Deployment
# ============================================================================
DEPLOYED=()
FAILED=()
for mod in "${SELECTED_MODULES[@]}"; do
echo ""
echo " ── $mod ──"
local_env=""
if ! local_env="$(resolve_env_for_module "$mod" "$DRY_RUN")"; then
FAILED+=("$mod")
continue
fi
deploy_script="$SCRIPT_DIR/services/$mod/deploy.sh"
if [[ ! -x "$deploy_script" ]]; then
log_error " deploy.sh not found or not executable: $deploy_script"
FAILED+=("$mod")
continue
fi
if [[ "$DRY_RUN" -eq 1 ]]; then
log_info " [dry-run] $mod$deploy_script (env: $local_env)"
DEPLOYED+=("$mod")
continue
fi
if INFRA_DIR="$(dirname "$local_env")" bash "$deploy_script"; then
DEPLOYED+=("$mod")
else
log_error " Deployment failed: $mod"
FAILED+=("$mod")
fi
done
# ============================================================================
# Summary
# ============================================================================
echo ""
echo " ┌─────────────────────────────────────┐"
echo " │ Summary │"
echo " └─────────────────────────────────────┘"
if [[ ${#DEPLOYED[@]} -gt 0 ]]; then
echo ""
log_info "Deployed (${#DEPLOYED[@]}):"
for m in "${DEPLOYED[@]}"; do echo "$m"; done
fi
if [[ ${#FAILED[@]} -gt 0 ]]; then
echo ""
log_error "Failed (${#FAILED[@]}):"
for m in "${FAILED[@]}"; do echo "$m"; done
exit 1
fi
echo ""
log_info "Done."