Initial completion
This commit is contained in:
parent
74ae914886
commit
adf4ab5d92
@ -1,13 +1,81 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Online SQLite backup using the .backup API (safe while the bot is running).
|
# Online SQLite backup using the .backup API.
|
||||||
# TODO: implement; outline below.
|
# Safe to run while the bot is live — SQLite's backup API acquires only a short
|
||||||
|
# shared lock per page, never blocking the application for the full duration.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# backup.sh --config /etc/mnemosyne/mnemosyne.conf [--keep-days N]
|
||||||
|
#
|
||||||
|
# Environment overrides:
|
||||||
|
# MNEMOSYNE_BACKUP_DIR — destination directory (default: /var/backups/mnemosyne)
|
||||||
|
# MNEMOSYNE_DB_PATH — database path (overrides config)
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# TODO: read DB_PATH from config or env
|
# ── Defaults ─────────────────────────────────────────────────────────────────
|
||||||
# TODO: BACKUP_DIR default /var/backups/mnemosyne, override via $MNEMOSYNE_BACKUP_DIR
|
CONFIG_FILE=""
|
||||||
# TODO: filename: mnemosyne-$(date +%Y%m%d-%H%M%S).db
|
KEEP_DAYS=30
|
||||||
# TODO: sqlite3 "$DB_PATH" ".backup '$BACKUP_DIR/$FILENAME'"
|
BACKUP_DIR="${MNEMOSYNE_BACKUP_DIR:-/var/backups/mnemosyne}"
|
||||||
# (uses the SQLite backup API — safe for concurrent access, no locks held)
|
|
||||||
# TODO: optionally prune backups older than N days
|
|
||||||
|
|
||||||
echo "TODO: backup.sh not yet implemented"
|
# ── Argument parsing ─────────────────────────────────────────────────────────
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--config) CONFIG_FILE="$2"; shift 2 ;;
|
||||||
|
--keep-days) KEEP_DAYS="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown argument: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Resolve DB path ──────────────────────────────────────────────────────────
|
||||||
|
# Priority: env var > config file > error
|
||||||
|
if [[ -n "${MNEMOSYNE_DB_PATH:-}" ]]; then
|
||||||
|
DB_PATH="$MNEMOSYNE_DB_PATH"
|
||||||
|
elif [[ -n "$CONFIG_FILE" ]]; then
|
||||||
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||||
|
echo "Config file not found: $CONFIG_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Parse db.path from the INI config (section [db], key path)
|
||||||
|
DB_PATH="$(awk -F'=' '/^\[db\]/{in_db=1; next} /^\[/{in_db=0} in_db && /^[[:space:]]*path[[:space:]]*=/{gsub(/[[:space:]#].*/, "", $2); print $2; exit}' "$CONFIG_FILE")"
|
||||||
|
DB_PATH="${DB_PATH// /}" # strip spaces
|
||||||
|
if [[ -z "$DB_PATH" ]]; then
|
||||||
|
echo "Could not read db.path from $CONFIG_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Provide --config or set MNEMOSYNE_DB_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$DB_PATH" ]]; then
|
||||||
|
echo "Database not found: $DB_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Ensure backup directory exists ───────────────────────────────────────────
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
# ── Run backup ───────────────────────────────────────────────────────────────
|
||||||
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
BACKUP_FILE="$BACKUP_DIR/mnemosyne-${TIMESTAMP}.db"
|
||||||
|
|
||||||
|
# sqlite3 .backup uses the SQLite Online Backup API — consistent snapshot,
|
||||||
|
# safe under concurrent writes, no application downtime required.
|
||||||
|
sqlite3 "$DB_PATH" ".backup '$BACKUP_FILE'"
|
||||||
|
|
||||||
|
# Verify the backup is a valid SQLite database
|
||||||
|
if ! sqlite3 "$BACKUP_FILE" "PRAGMA integrity_check;" | grep -q "^ok$"; then
|
||||||
|
echo "Backup integrity check failed: $BACKUP_FILE" >&2
|
||||||
|
rm -f "$BACKUP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BYTES="$(wc -c < "$BACKUP_FILE")"
|
||||||
|
echo "Backup written: $BACKUP_FILE (${BYTES} bytes)"
|
||||||
|
|
||||||
|
# ── Prune old backups ─────────────────────────────────────────────────────────
|
||||||
|
if [[ "$KEEP_DAYS" -gt 0 ]]; then
|
||||||
|
PRUNED="$(find "$BACKUP_DIR" -maxdepth 1 -name 'mnemosyne-*.db' \
|
||||||
|
-mtime "+${KEEP_DAYS}" -print -delete | wc -l)"
|
||||||
|
[[ "$PRUNED" -gt 0 ]] && echo "Pruned $PRUNED backup(s) older than ${KEEP_DAYS} days"
|
||||||
|
fi
|
||||||
|
|||||||
@ -1,44 +1,192 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Mnemosyne install script — idempotent; safe to re-run.
|
# Mnemosyne install script — idempotent; safe to re-run.
|
||||||
# Does NOT clobber an existing database or config file.
|
# Does NOT clobber an existing database or config file.
|
||||||
# TODO: implement; outline below.
|
#
|
||||||
|
# Usage (as root or with sudo):
|
||||||
|
# bash scripts/install.sh [--app-dir /opt/mnemosyne] [--config-dir /etc/mnemosyne]
|
||||||
|
# [--data-dir /var/lib/mnemosyne]
|
||||||
|
#
|
||||||
|
# After running:
|
||||||
|
# 1. Edit /etc/mnemosyne/mnemosyne.conf (fill in token, secret, URL, chat ID)
|
||||||
|
# 2. Copy and adjust nginx/mnemosyne.conf.example → /etc/nginx/conf.d/mnemosyne.conf
|
||||||
|
# 3. Obtain a Let's Encrypt cert: certbot --nginx -d your.subdomain.com
|
||||||
|
# 4. Register the Telegram webhook: /opt/mnemosyne/bin/mnemosyne-webhook --config /etc/mnemosyne/mnemosyne.conf
|
||||||
|
# 5. Start the bot: systemctl start mnemosyne-bot
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# --- Detect distro family ---
|
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||||
# TODO: detect RHEL/AlmaLinux (dnf/yum) vs Debian/Ubuntu (apt) vs other
|
APP_DIR="/opt/mnemosyne"
|
||||||
# TODO: on RHEL family: dnf install -y perl perl-App-cpanminus perl-DBI sqlite
|
CONFIG_DIR="/etc/mnemosyne"
|
||||||
# TODO: on Debian family: apt-get install -y perl cpanminus libdbi-perl sqlite3
|
DATA_DIR="/var/lib/mnemosyne"
|
||||||
# TODO: document: on either family, run `cpanm --installdeps .` after this step
|
SERVICE_USER="mnemosyne"
|
||||||
|
SERVICE_GROUP="mnemosyne"
|
||||||
|
SYSTEMD_DIR="/etc/systemd/system"
|
||||||
|
CRON_FILE="/etc/cron.d/mnemosyne"
|
||||||
|
|
||||||
# --- System user ---
|
# ── Argument parsing ─────────────────────────────────────────────────────────
|
||||||
# TODO: create 'mnemosyne' system user/group if not present (useradd -r -s /sbin/nologin)
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--app-dir) APP_DIR="$2"; shift 2 ;;
|
||||||
|
--config-dir) CONFIG_DIR="$2"; shift 2 ;;
|
||||||
|
--data-dir) DATA_DIR="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown argument: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# --- Directories ---
|
# ── Must run as root ─────────────────────────────────────────────────────────
|
||||||
# TODO: /opt/mnemosyne — app files (copy repo here or symlink)
|
if [[ $EUID -ne 0 ]]; then
|
||||||
# TODO: /var/lib/mnemosyne — database directory, owned by mnemosyne user
|
echo "Error: this script must be run as root." >&2
|
||||||
# TODO: /etc/mnemosyne — config directory (skip if already exists to avoid clobbering)
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Config ---
|
# ── Detect source directory (where this script lives) ───────────────────────
|
||||||
# TODO: if /etc/mnemosyne/mnemosyne.conf does not exist:
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
# copy config/mnemosyne.conf.example → /etc/mnemosyne/mnemosyne.conf
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
# chmod 640, chown root:mnemosyne
|
|
||||||
# print reminder to edit the file before starting the service
|
|
||||||
|
|
||||||
# --- CPAN deps ---
|
echo "==> Mnemosyne installer"
|
||||||
# TODO: cd /opt/mnemosyne && cpanm --installdeps .
|
echo " Source: $REPO_DIR"
|
||||||
|
echo " App: $APP_DIR"
|
||||||
|
echo " Config: $CONFIG_DIR"
|
||||||
|
echo " Data: $DATA_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
# --- Systemd ---
|
# ── Detect distro family ─────────────────────────────────────────────────────
|
||||||
# TODO: copy systemd/mnemosyne-bot.service → /etc/systemd/system/
|
detect_distro() {
|
||||||
# TODO: systemctl daemon-reload
|
if command -v dnf &>/dev/null || command -v yum &>/dev/null; then
|
||||||
# TODO: systemctl enable mnemosyne-bot (but do NOT start — user must configure first)
|
echo "rhel"
|
||||||
|
elif command -v apt-get &>/dev/null; then
|
||||||
|
echo "debian"
|
||||||
|
else
|
||||||
|
echo "unknown"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
DISTRO="$(detect_distro)"
|
||||||
|
|
||||||
# --- Cron for digest ---
|
# ── 1. System packages ───────────────────────────────────────────────────────
|
||||||
# TODO: install a cron entry for mnemosyne user:
|
echo "==> Installing system packages (distro: $DISTRO)"
|
||||||
# */5 * * * * /opt/mnemosyne/bin/mnemosyne-digest --config /etc/mnemosyne/mnemosyne.conf
|
case "$DISTRO" in
|
||||||
# This fires every 5 minutes; the script itself checks against digest_time and exits early.
|
rhel)
|
||||||
|
PKG_MGR="dnf"
|
||||||
|
command -v yum &>/dev/null && ! command -v dnf &>/dev/null && PKG_MGR="yum"
|
||||||
|
$PKG_MGR install -y perl perl-App-cpanminus perl-DBI sqlite perl-local-lib 2>&1 | grep -E '(Installed|already|Error)' || true
|
||||||
|
# DBD::SQLite needs C compiler for XS build
|
||||||
|
$PKG_MGR install -y gcc make perl-devel 2>&1 | grep -E '(Installed|already|Error)' || true
|
||||||
|
;;
|
||||||
|
debian)
|
||||||
|
apt-get install -y perl cpanminus libdbi-perl sqlite3 gcc make 2>&1 | grep -E '(Setting up|already|Error)' || true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo " Warning: unknown distro. Install Perl, cpanminus, DBI, and SQLite manually." >&2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
# --- Nginx ---
|
# ── 2. CPAN dependencies ─────────────────────────────────────────────────────
|
||||||
# TODO: remind user to copy nginx/mnemosyne.conf.example and adjust, then reload nginx
|
echo "==> Installing CPAN dependencies"
|
||||||
|
# Install into system Perl (running as root) or local::lib if preferred
|
||||||
|
cpanm --notest --quiet --installdeps "$REPO_DIR" || {
|
||||||
|
echo " Warning: cpanm reported errors. Check output above." >&2
|
||||||
|
}
|
||||||
|
|
||||||
echo "TODO: install.sh not yet implemented"
|
# ── 3. System user/group ─────────────────────────────────────────────────────
|
||||||
|
echo "==> Creating system user: $SERVICE_USER"
|
||||||
|
if ! getent group "$SERVICE_GROUP" &>/dev/null; then
|
||||||
|
groupadd --system "$SERVICE_GROUP"
|
||||||
|
echo " Created group $SERVICE_GROUP"
|
||||||
|
fi
|
||||||
|
if ! id "$SERVICE_USER" &>/dev/null; then
|
||||||
|
useradd --system --gid "$SERVICE_GROUP" --no-create-home \
|
||||||
|
--home-dir "$DATA_DIR" --shell /sbin/nologin "$SERVICE_USER"
|
||||||
|
echo " Created user $SERVICE_USER"
|
||||||
|
else
|
||||||
|
echo " User $SERVICE_USER already exists — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. Directories ───────────────────────────────────────────────────────────
|
||||||
|
echo "==> Creating directories"
|
||||||
|
install -d -m 755 "$APP_DIR"
|
||||||
|
install -d -m 750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$DATA_DIR"
|
||||||
|
install -d -m 750 "$CONFIG_DIR"
|
||||||
|
|
||||||
|
# ── 5. Copy application files ────────────────────────────────────────────────
|
||||||
|
echo "==> Installing application to $APP_DIR"
|
||||||
|
# Use rsync if available (preserves structure, skips .git), otherwise cp
|
||||||
|
if command -v rsync &>/dev/null; then
|
||||||
|
rsync -a --exclude='.git' --exclude='config/mnemosyne.conf' \
|
||||||
|
"$REPO_DIR/" "$APP_DIR/"
|
||||||
|
else
|
||||||
|
cp -rp "$REPO_DIR/." "$APP_DIR/"
|
||||||
|
rm -rf "$APP_DIR/.git"
|
||||||
|
rm -f "$APP_DIR/config/mnemosyne.conf"
|
||||||
|
fi
|
||||||
|
chmod +x "$APP_DIR/bin/"*
|
||||||
|
chown -R root:"$SERVICE_GROUP" "$APP_DIR"
|
||||||
|
chmod -R o-rwx "$APP_DIR"
|
||||||
|
|
||||||
|
# ── 6. Config file ───────────────────────────────────────────────────────────
|
||||||
|
echo "==> Config"
|
||||||
|
if [[ -f "$CONFIG_DIR/mnemosyne.conf" ]]; then
|
||||||
|
echo " $CONFIG_DIR/mnemosyne.conf already exists — not overwriting"
|
||||||
|
else
|
||||||
|
install -m 640 -o root -g "$SERVICE_GROUP" \
|
||||||
|
"$REPO_DIR/config/mnemosyne.conf.example" \
|
||||||
|
"$CONFIG_DIR/mnemosyne.conf"
|
||||||
|
echo ""
|
||||||
|
echo " *** ACTION REQUIRED ***"
|
||||||
|
echo " Edit $CONFIG_DIR/mnemosyne.conf and fill in:"
|
||||||
|
echo " bot.token — from @BotFather"
|
||||||
|
echo " bot.webhook_secret — run: openssl rand -hex 32"
|
||||||
|
echo " bot.webhook_url — your public HTTPS webhook URL"
|
||||||
|
echo " bot.allowed_chat_ids — your Telegram chat ID (@userinfobot)"
|
||||||
|
echo " db.path — default: $DATA_DIR/mnemosyne.db"
|
||||||
|
echo " app.timezone — IANA timezone (e.g. America/New_York)"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 7. Systemd service ───────────────────────────────────────────────────────
|
||||||
|
echo "==> Installing systemd service"
|
||||||
|
# Rewrite paths in the service template
|
||||||
|
sed "s|/opt/mnemosyne|$APP_DIR|g; s|/etc/mnemosyne|$CONFIG_DIR|g" \
|
||||||
|
"$REPO_DIR/systemd/mnemosyne-bot.service" \
|
||||||
|
> "$SYSTEMD_DIR/mnemosyne-bot.service"
|
||||||
|
chmod 644 "$SYSTEMD_DIR/mnemosyne-bot.service"
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable mnemosyne-bot
|
||||||
|
echo " Enabled mnemosyne-bot.service (not started — configure first)"
|
||||||
|
|
||||||
|
# ── 8. Cron entry for digest ─────────────────────────────────────────────────
|
||||||
|
echo "==> Installing cron job for digest"
|
||||||
|
cat > "$CRON_FILE" <<CRON
|
||||||
|
# Mnemosyne morning digest — fires every 5 min; the script checks digest_time itself.
|
||||||
|
*/5 * * * * $SERVICE_USER $APP_DIR/bin/mnemosyne-digest --config $CONFIG_DIR/mnemosyne.conf 2>&1 | logger -t mnemosyne-digest
|
||||||
|
CRON
|
||||||
|
chmod 644 "$CRON_FILE"
|
||||||
|
echo " Installed $CRON_FILE"
|
||||||
|
|
||||||
|
# ── 9. Backup cron (optional) ────────────────────────────────────────────────
|
||||||
|
echo "==> Installing daily backup cron"
|
||||||
|
BACKUP_DIR="/var/backups/mnemosyne"
|
||||||
|
install -d -m 750 -o "$SERVICE_USER" -g "$SERVICE_GROUP" "$BACKUP_DIR"
|
||||||
|
cat >> "$CRON_FILE" <<CRON
|
||||||
|
# Daily backup at 02:15
|
||||||
|
15 2 * * * $SERVICE_USER MNEMOSYNE_BACKUP_DIR=$BACKUP_DIR $APP_DIR/scripts/backup.sh --config $CONFIG_DIR/mnemosyne.conf 2>&1 | logger -t mnemosyne-backup
|
||||||
|
CRON
|
||||||
|
echo " Backup cron added (target: $BACKUP_DIR)"
|
||||||
|
|
||||||
|
# ── Done ─────────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "==> Installation complete."
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Edit $CONFIG_DIR/mnemosyne.conf"
|
||||||
|
echo " 2. Copy nginx/mnemosyne.conf.example → /etc/nginx/conf.d/mnemosyne.conf"
|
||||||
|
echo " and adjust server_name + port"
|
||||||
|
echo " 3. Obtain TLS cert: certbot --nginx -d your.subdomain.example.com"
|
||||||
|
echo " 4. Reload nginx: systemctl reload nginx"
|
||||||
|
echo " 5. Register webhook: $APP_DIR/bin/mnemosyne-webhook --config $CONFIG_DIR/mnemosyne.conf"
|
||||||
|
echo " 6. Start bot: systemctl start mnemosyne-bot"
|
||||||
|
echo " 7. Check logs: journalctl -u mnemosyne-bot -f"
|
||||||
|
echo ""
|
||||||
|
echo " Note (RHEL/AlmaLinux): if SELinux is enforcing, you may need:"
|
||||||
|
echo " setsebool -P httpd_can_network_connect 1"
|
||||||
|
echo " (allows nginx to proxy to the local bot port)"
|
||||||
|
|||||||
@ -1,19 +1,63 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Mnemosyne Telegram bot (webhook receiver)
|
Description=Mnemosyne Telegram bot (webhook receiver)
|
||||||
After=network.target
|
Documentation=https://git.castlehollow.com/rodger/mnemosyne
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=mnemosyne
|
User=mnemosyne
|
||||||
Group=mnemosyne
|
Group=mnemosyne
|
||||||
WorkingDirectory=/opt/mnemosyne
|
WorkingDirectory=/opt/mnemosyne
|
||||||
ExecStart=/usr/bin/perl /opt/mnemosyne/bin/mnemosyne-bot --config /etc/mnemosyne/mnemosyne.conf
|
|
||||||
|
ExecStart=/usr/bin/perl /opt/mnemosyne/bin/mnemosyne-bot \
|
||||||
|
--config /etc/mnemosyne/mnemosyne.conf
|
||||||
|
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
# Keep logs in the journal
|
# Cap restart rate: no more than 5 restarts in 60 seconds
|
||||||
|
StartLimitBurst=5
|
||||||
|
StartLimitIntervalSec=60
|
||||||
|
|
||||||
|
# Logging
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
SyslogIdentifier=mnemosyne-bot
|
SyslogIdentifier=mnemosyne-bot
|
||||||
|
|
||||||
|
# ── Hardening ────────────────────────────────────────────────────────────────
|
||||||
|
# Prevent privilege escalation
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
# Private /tmp — isolates temp files from other services
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
# Read-only /usr, /boot, /etc (except the config dir we explicitly allow)
|
||||||
|
ProtectSystem=strict
|
||||||
|
# The bot needs to write the SQLite database; allow the data dir
|
||||||
|
ReadWritePaths=/var/lib/mnemosyne
|
||||||
|
|
||||||
|
# No access to home directories
|
||||||
|
ProtectHome=true
|
||||||
|
|
||||||
|
# Prevent writing to kernel tunables
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
|
||||||
|
# Restrict address families: only IPv4/IPv6 (outbound Telegram API) + Unix sockets
|
||||||
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||||
|
|
||||||
|
# Syscall filtering — Perl/DBI only needs the basics
|
||||||
|
SystemCallFilter=@system-service
|
||||||
|
SystemCallErrorNumber=EPERM
|
||||||
|
|
||||||
|
# No realtime scheduling, no raw I/O, no device access
|
||||||
|
RestrictRealtime=true
|
||||||
|
PrivateDevices=true
|
||||||
|
|
||||||
|
# Tighten /proc view
|
||||||
|
ProtectProc=invisible
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user