Server yang dinyalakan lalu dilupakan adalah server yang kegagalannya juga diam-diam: layanan bisa mati berhari-hari tanpa ada yang sadar, disk mengisi pelan-pelan sampai backup tidak lagi muat, dan timer yang gagal sejak minggu lalu terus tercatat “terjadwal”. Bagian ini memasang pengawasnya. Kita membangun skrip status yang membaca snapshot beku — pola yang sudah kita pakai di proyek Otomasi Pemeliharaan Sistem — untuk menjawab tiga pertanyaan berharga: apakah layanan jalan, berapa penuh disk data, dan kapan backup terakhir yang berhasil. Temuannya dilaporkan ke layar, dicatat ke log, dan diberi kode keluar yang bisa dibaca mesin — lalu seluruh janji itu dikunci oleh test suite.

Pola Snapshot untuk Status #

Skrip status tidak menjalankan ss atau df langsung, melainkan membaca snapshot yang dibekukan di folder data/. Alasannya sama dengan proyek pemeliharaan: hasil bisa diulang, bisa diaudit, dan bisa diuji — laporan hari ini tidak berubah walau dibaca sepuluh kali. Di server sungguhan, dua snapshot itu diambil dengan dua perintah sebelum skrip dijalankan: ss -tln > data/ss.txt dan df -h > data/df.txt. Blok berikut mengerjakan persis itu — menyalakan layanan mini dari Bagian 2, membekukan ss.txt saat ia mendengarkan, lalu menghentikannya lagi; df.txt kita tulis sebagai data contoh karena angka disk aslinya berbeda di tiap mesin:

bash
mkdir -p latihan/data latihan/srv/berbagi
printf 'jadwal piknik keluarga\n' > latihan/srv/berbagi/jadwal-piknik.txt

python3 -m http.server 8765 --directory latihan/srv/berbagi >/dev/null 2>&1 &
server_pid=$!
for i in 1 2 3 4 5 6 7 8 9 10; do
    curl -s -o /dev/null http://127.0.0.1:8765/ && break
    sleep 0.3
done
ss -tln > latihan/data/ss.txt
kill "$server_pid"
wait "$server_pid" 2>/dev/null
echo "== isi snapshot ss.txt (hanya baris port 8765) =="
awk 'NR == 1 || /:8765 /' latihan/data/ss.txt
cat > latihan/data/df.txt <<'EOF'
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1         50G   20G   28G  41% /
/dev/sdb1       200G  143G   47G  72% /srv/data
EOF
mkdir -p latihan/srv/backup/run/backup-20260214
printf 'arsip foto keluarga\n' > latihan/srv/backup/run/backup-20260214/foto-keluarga.tar.gz
tree latihan

Output yang Dihasilkan:

text
== isi snapshot ss.txt (hanya baris port 8765) ==
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port
LISTEN 0      5            0.0.0.0:8765       0.0.0.0:*
latihan
├── data
│   ├── df.txt
│   └── ss.txt
└── srv
    ├── backup
    │   └── run
    │       └── backup-20260214
    │           └── foto-keluarga.tar.gz
    └── berbagi
        └── jadwal-piknik.txt

7 directories, 4 files

Dua hal dari blok itu: baris LISTEN pada 0.0.0.0:8765 adalah bukti yang dibaca skrip status nanti (snapshot diambil saat layanan masih hidup, sebelum kill), dan folder backup-20260214 sengaja dinamai bertanggal — nama itulah yang membuat umur backup bisa dihitung tanpa menyentuh waktu modifikasi.

Skrip status-home-server.sh #

Skrip kita membaca konfigurasi dari berkas terpisah (home-server.conf — pola yang sama dengan seluruh proyek sebelumnya), memeriksa tiga hal dari snapshot, lalu mencetak laporan empat baris. Karena tiap blok latihan mandiri, blok berikut menuliskan ulang snapshot yang dibekukan tadi persis seperti hasilnya, lalu menjalankan skrip pada dua skenario: sehat, dan ketiga-ketiganya bermasalah sekaligus. Versi lengkapnya tersedia sebagai deploy/status-home-server.sh pada bagian Berkas Sumber di bawah halaman ini:

bash
mkdir -p latihan/data latihan/srv/backup/run
cat > latihan/data/ss.txt <<'EOF'
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port
LISTEN 0      128          0.0.0.0:22             0.0.0.0:*
LISTEN 0      5            0.0.0.0:8765       0.0.0.0:*
EOF
cat > latihan/data/df.txt <<'EOF'
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1         50G   20G   28G  41% /
/dev/sdb1       200G  143G   47G  72% /srv/data
EOF
mkdir -p latihan/srv/backup/run/backup-20260214
cat > latihan/home-server.conf <<'EOF'
PORT_BERBAGI=8765
DATA_MOUNT=/srv/data
BACKUP_DIR=latihan/srv/backup/run
BACKUP_MAX_UMUR_HARI=2
DISK_AMBANG_PCT=85
LOG_FILE=latihan/status-home-server.log
EOF
cat > latihan/status-home-server.sh <<'BASH'
#!/usr/bin/env bash
# status-home-server.sh — dashboard status home server.
#
# Membaca snapshot beku dari folder data/ (ss.txt: hasil `ss -tln`,
# df.txt: hasil `df -h`) dan daftar run backup (backup-YYYYMMDD) di
# folder BACKUP_DIR, lalu menulis laporan status ke stdout sekaligus
# baris temuan ke berkas log. Pola snapshot membuat hasilnya bisa
# diulang dan diuji — pola yang sama dipakai Proyek Otomasi
# Pemeliharaan Sistem.
#
# Variabel lingkungan (berguna untuk pengujian):
#   CONF      berkas konfigurasi yang dibaca (default: home-server.conf)
#   DATA_DIR  folder snapshot (default: data)
#   HARI_INI  tanggal acuan umur backup (default: hari ini, YYYY-MM-DD)
#
# Kode keluar (temuan bisa gabungan — jumlahkan nilainya):
#   0 sehat                        1 disk data melewati ambang
#   2 layanan berbagi tidak jalan  4 backup terlalu tua / belum ada

set -u

CONF="${CONF:-home-server.conf}"
DATA_DIR="${DATA_DIR:-data}"
HARI_INI="${HARI_INI:-$(date +%F)}"

if [ -f "$CONF" ]; then
    # shellcheck source=/dev/null
    . "$CONF"
fi

PORT_BERBAGI="${PORT_BERBAGI:-8765}"
DATA_MOUNT="${DATA_MOUNT:-/srv/data}"
BACKUP_DIR="${BACKUP_DIR:-/srv/backup/run}"
BACKUP_MAX_UMUR_HARI="${BACKUP_MAX_UMUR_HARI:-2}"
DISK_AMBANG_PCT="${DISK_AMBANG_PCT:-85}"
LOG_FILE="${LOG_FILE:-status-home-server.log}"

GARIS='----------------------------------------------------------------'

# --- 1. Layanan berbagi: cari baris LISTEN pada port dari snapshot ----
# Kolom ke-4 output ss adalah "alamat:port", jadi port dicocokkan
# sebagai akhiran kolom — bentuk [::]:8765 pun lolos pola yang sama.
layanan="MATI"
if awk -v port="$PORT_BERBAGI" '
        $1 == "LISTEN" && $4 ~ ":" port "$" { ketemu = 1 }
        END { exit ketemu ? 0 : 1 }' "$DATA_DIR/ss.txt" 2> /dev/null; then
    layanan="JALAN"
fi

# --- 2. Disk data: persentase pemakaian dari snapshot df --------------
# Kolom ke-5 df adalah Use% ("+0" membuang tanda %), kolom ke-6
# mount point. Mount yang tidak ada berarti disk data gagal termuat.
disk_pct=$(awk -v mnt="$DATA_MOUNT" '
    $6 == mnt { print $5 + 0; exit }' "$DATA_DIR/df.txt" 2> /dev/null)

# --- 3. Backup terakhir: umur dihitung dari nama folder run -----------
backup_terakhir=$(ls -1 "$BACKUP_DIR" 2> /dev/null |
    grep -E '^backup-[0-9]{8}$' | sort | tail -n 1)

umur=""
if [ -n "$backup_terakhir" ]; then
    tgl="${backup_terakhir#backup-}"
    tgl_iso="${tgl:0:4}-${tgl:4:2}-${tgl:6:2}"
    umur=$(( ( $(date -d "$HARI_INI" +%s) -
               $(date -d "$tgl_iso" +%s) ) / 86400 ))
fi

# --- 4. Kumpulkan temuan ------------------------------------------------
temuan=0
n_temuan=0
daftar_temuan=""

catat() {
    n_temuan=$((n_temuan + 1))
    daftar_temuan="$daftar_temuan
- $1"
}

if [ "$layanan" = "MATI" ]; then
    temuan=$((temuan + 2))
    catat "layanan berbagi tidak jalan (port $PORT_BERBAGI tidak mendengarkan)"
fi

if [ -z "$disk_pct" ]; then
    temuan=$((temuan + 1))
    catat "disk data $DATA_MOUNT tidak termuat — periksa fstab"
elif [ "$disk_pct" -ge "$DISK_AMBANG_PCT" ]; then
    temuan=$((temuan + 1))
    catat "disk data melewati ambang: ${disk_pct}% dari ${DISK_AMBANG_PCT}%"
fi

if [ -z "$backup_terakhir" ]; then
    temuan=$((temuan + 4))
    catat "belum ada run backup di $BACKUP_DIR"
elif [ "$umur" -gt "$BACKUP_MAX_UMUR_HARI" ]; then
    temuan=$((temuan + 4))
    catat "backup terlalu tua: $umur hari (maksimal $BACKUP_MAX_UMUR_HARI)"
fi

# --- 5. Tulis laporan ke stdout dan temuan ke log -----------------------
printf '================================================================\n'
printf 'STATUS HOME SERVER\n'
printf 'Tanggal acuan : %s\n' "$HARI_INI"
printf '================================================================\n'
if [ "$layanan" = "JALAN" ]; then
    printf 'layanan berbagi : JALAN (port %s)\n' "$PORT_BERBAGI"
else
    printf 'layanan berbagi : MATI (port %s tidak mendengarkan)\n' "$PORT_BERBAGI"
fi
if [ -n "$disk_pct" ]; then
    if [ "$disk_pct" -ge "$DISK_AMBANG_PCT" ]; then
        printf 'disk data       : %s %s%% MELEWATI AMBANG %s%%\n' \
            "$DATA_MOUNT" "$disk_pct" "$DISK_AMBANG_PCT"
    else
        printf 'disk data       : %s %s%% (ambang %s%%)\n' \
            "$DATA_MOUNT" "$disk_pct" "$DISK_AMBANG_PCT"
    fi
else
    printf 'disk data       : %s tidak ada di snapshot df\n' "$DATA_MOUNT"
fi
if [ -n "$backup_terakhir" ]; then
    printf 'backup terakhir : %s (umur %s hari, maksimal %s)\n' \
        "$backup_terakhir" "$umur" "$BACKUP_MAX_UMUR_HARI"
else
    printf 'backup terakhir : belum ada run backup\n'
fi
printf '%s\n' "$GARIS"

if [ "$temuan" -eq 0 ]; then
    printf 'temuan          : tidak ada — server sehat\n'
else
    printf 'temuan          : %d\n' "$n_temuan"
    # ${var#$'\n'} membuang baris kosong pertama dari daftar.
    printf '%s\n' "${daftar_temuan#$'\n'}"
    # Alert sederhana: satu baris per temuan ke berkas log.
    mkdir -p "$(dirname "$LOG_FILE")"
    printf '%s\n' "${daftar_temuan#$'\n'}" |
        while IFS= read -r baris; do
            printf '[%s] TEMUAN: %s\n' "$HARI_INI" "${baris#- }" >> "$LOG_FILE"
        done
    printf 'temuan dicatat ke: %s\n' "$LOG_FILE"
fi

exit "$temuan"
BASH
echo "== skenario sehat =="
CONF=latihan/home-server.conf DATA_DIR=latihan/data HARI_INI=2026-02-15 \
    bash latihan/status-home-server.sh
echo "exit=$?"
echo "== skenario tiga temuan =="
sed -i '/8765/d' latihan/data/ss.txt
cat > latihan/data/df.txt <<'EOF'
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1         50G   20G   28G  41% /
/dev/sdb1       200G  178G   12G  91% /srv/data
EOF
rm -rf latihan/srv/backup/run/backup-20260214
mkdir -p latihan/srv/backup/run/backup-20260120
CONF=latihan/home-server.conf DATA_DIR=latihan/data HARI_INI=2026-02-15 \
    bash latihan/status-home-server.sh
echo "exit=$?"
echo "== log temuan =="
cat latihan/status-home-server.log

Output yang Dihasilkan:

text
== skenario sehat ==
================================================================
STATUS HOME SERVER
Tanggal acuan : 2026-02-15
================================================================
layanan berbagi : JALAN (port 8765)
disk data       : /srv/data 72% (ambang 85%)
backup terakhir : backup-20260214 (umur 1 hari, maksimal 2)
----------------------------------------------------------------
temuan          : tidak ada — server sehat
exit=0
== skenario tiga temuan ==
================================================================
STATUS HOME SERVER
Tanggal acuan : 2026-02-15
================================================================
layanan berbagi : MATI (port 8765 tidak mendengarkan)
disk data       : /srv/data 91% MELEWATI AMBANG 85%
backup terakhir : backup-20260120 (umur 26 hari, maksimal 2)
----------------------------------------------------------------
temuan          : 3
- layanan berbagi tidak jalan (port 8765 tidak mendengarkan)
- disk data melewati ambang: 91% dari 85%
- backup terlalu tua: 26 hari (maksimal 2)
temuan dicatat ke: latihan/status-home-server.log
exit=7
== log temuan ==
[2026-02-15] TEMUAN: layanan berbagi tidak jalan (port 8765 tidak mendengarkan)
[2026-02-15] TEMUAN: disk data melewati ambang: 91% dari 85%
[2026-02-15] TEMUAN: backup terlalu tua: 26 hari (maksimal 2)

Bagian-bagian kuncinya:

  1. awk ... $4 ~ ":" port "$": kolom ke-4 output ss berbentuk alamat:port, jadi port dicocokkan sebagai akhiran kolom — 0.0.0.0:8765 dan [::]:8765 sama-sama lolos. Snapshot tanpa baris itu berarti tidak ada yang mendengarkan: layanan mati.
  2. $6 == mnt { print $5 + 0 }: untuk disk, kolom ke-6 df adalah mount point dan kolom ke-5 persentasenya; + 0 membuang tanda %. Bila mount /srv/data tidak ada sama sekali di snapshot, itu temuan tersendiri — kemungkinan besar disk data gagal termuat dan fstab perlu diperiksa.
  3. Umur backup dari nama folder: backup-20260214 dipilih paling baru lewat sort | tail -n 1 (nama bertanggal urut wajar), lalu tanggalnya dipotong dari nama dan selisihnya dihitung terhadap HARI_INI dengan aritmetika epoch — $(( ( ... ) / 86400 )) menghasilkan selisih hari. Mengunci HARI_INI saat menguji membuat umur deterministik; format tanggal yang diterima date -d terdokumentasi di date(1) .
  4. catat() dan bit temuan: setiap temuan menambahkan satu baris ke daftar sekaligus menyalakan satu bit pada variabel temuan. Pada skenario kedua, tiga temuan menumpuk menjadi 2+1+4 = 7.
  5. Laporan dan log: status per pemeriksaan dicetak apa adanya (JALAN/MATI, persentase, umur), temuan dirangkum di bawah garis, dan setiap temuan juga menempel satu baris [tanggal] TEMUAN: ... ke berkas log — jejak yang bisa dibaca mundur bila kita ingin tahu sejak kapan masalah itu hidup.

Temuan sebagai Kode Keluar #

Bagian paling penting dari laporan justru yang tidak tercetak: exit code. Skrip ini memakai bitmask — setiap jenis temuan menyala satu bit, dan nilainya dijumlahkan:

KodeArtiPenyebab tipikal
0sehat—
1disk data bermasalahmelewati ambang, atau tidak termuat
2layanan berbagi matiunit berhenti, port tidak didengarkan
3kombinasi 1+2disk dan layanan sekaligus
4backup bermasalahterlalu tua, atau belum pernah jalan
5, 6, 7kombinasi lainnyajumlahkan bitnya

Kode gabungan inilah alasan memilih bitmask: satu angka sudah menceritakan kombinasi temuan apa saja yang hidup — 7 berarti ketiganya, 5 berarti disk dan backup tanpa masalah layanan. Pemakaian praktisnya sederhana:

bash
bash deploy/status-home-server.sh || echo "ada temuan, kode $?"

if ! bash deploy/status-home-server.sh; then
    echo "server bermasalah — periksa laporan di atas"
fi

Dijadwalkan lewat cron, output skrip otomatis dikirim email ke pemilik crontab bila exit code-nya bukan nol — alert paling sederhana yang bisa dibangun tanpa layanan eksternal. Pola penjadwalan dan opsi mail cron dibahas di Scheduling Scripts .

Backup dan Pemeliharaan Berkala #

Mesin backup di balik backup-keluarga.service adalah skrip dari proyek Skrip Backup Otomatis : mirror rsync untuk kondisi terkini, arsip tar.gz per run bertanggal, checksum SHA-256, dan rotasi retensi. Timer dari Bagian 2 hanya mengganti “ingat menjalankan backup” menjadi “sistem yang menjalankannya” — skripnya sendiri sudah kita buktikan di proyek itu, termasuk simulasi bencana dan test restore-nya. Konfigurasi yang perlu disesuaikan di server: BACKUP_DIRS memuat /srv/foto-keluarga dan /srv/berbagi, sementara BACKUP_DEST mengarah ke disk data (/srv/backup).

Selain backup, server juga butuh dirawat: cache dan log lama dibersihkan, paket update tertunda ditinjau, dan service gagal ditindaklanjuti. Ketiganya adalah pekerjaan yang sudah dibangun tuntas di proyek Otomasi Pemeliharaan Sistem — cukup pasang skripnya di server yang sama, dan home server kita jadi merawat dirinya sendiri secara berkala.

Test Suite #

Terakhir, seluruh klaim proyek dikunci oleh test suite. Suite menyiapkan folder kerja terisolasi, menyalin unit dan skrip dari paket proyek, lalu menguji empat lapis: validasi unit lewat systemd-analyze verify, directive penting lewat grep, layanan mini yang benar-benar mendengarkan dan menjawab, serta kedua skrip pada fixture. Versi lengkapnya tersedia sebagai tests/run-tests.sh pada bagian Berkas Sumber di bawah halaman ini:

bash
mkdir -p proyek/config proyek/deploy proyek/tests
cat > proyek/config/berbagi-file.service <<'EOF'
# berbagi-file.service — layanan berbagi file keluarga.
# Dipasang sebagai /etc/systemd/system/berbagi-file.service
# pada server; aktifkan dengan: systemctl enable --now berbagi-file

[Unit]
Description=Berbagi file keluarga via HTTP sederhana
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=berbagi
Group=berbagi
WorkingDirectory=/srv/berbagi
ExecStart=/usr/bin/python3 -m http.server 8765 --bind 0.0.0.0
Restart=on-failure
RestartSec=5

# --- hardening: lihat systemd.exec(5) ---
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF
cat > proyek/config/backup-keluarga.service <<'EOF'
# backup-keluarga.service — unit oneshot untuk backup harian.
# Skrip yang dijalankan adalah hasil Proyek Skrip Backup Otomatis,
# dipasang sebagai /usr/local/bin/backup-keluarga.sh pada server.
# Unit ini tidak punya [Install]: hanya timer yang menariknya.

[Unit]
Description=Backup harian data keluarga (foto dan dokumen)
After=local-fs.target

[Service]
Type=oneshot
User=berbagi
Group=berbagi
ExecStart=/bin/bash /usr/local/bin/backup-keluarga.sh
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
NoNewPrivileges=true
PrivateTmp=true
EOF
cat > proyek/config/backup-keluarga.timer <<'EOF'
# backup-keluarga.timer — jadwal harian untuk backup-keluarga.service.
# Aktifkan dengan: systemctl enable --now backup-keluarga.timer

[Unit]
Description=Jalankan backup keluarga setiap hari jam 02:30

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
Unit=backup-keluarga.service

[Install]
WantedBy=timers.target
EOF
cat > proyek/config/home-server.conf <<'EOF'
# home-server.conf — konfigurasi status-home-server.sh.
# Sunting nilai di bawah ini sesuai server Anda; skrip tidak perlu
# disentuh. Jalur relatif dihitung dari direktori kerja saat skrip
# dijalankan; jalur absolut (mis. /srv/backup/run) juga boleh dipakai.

# Port yang didengarkan layanan berbagi file.
PORT_BERBAGI=8765

# Mount point disk data — harus cocok dengan kolom terakhir output df.
DATA_MOUNT=/srv/data

# Folder yang berisi run backup-YYYYMMDD (lihat Proyek Skrip Backup
# Otomatis). Di server sungguhan biasanya /srv/backup/run.
BACKUP_DIR=srv/backup/run

# Backup dianggap terlalu tua bila umurnya melewati nilai ini (hari).
BACKUP_MAX_UMUR_HARI=2

# Ambang pemakaian disk data (%) yang menyalakan temuan (1-99).
DISK_AMBANG_PCT=85

# Berkas log temuan (opsional; default: status-home-server.log).
LOG_FILE=srv/backup/status-home-server.log
EOF
cat > proyek/deploy/cek-kesiapan.sh <<'BASH'
#!/usr/bin/env bash
# cek-kesiapan.sh — periksa kesiapan struktur home server.
#
# Memeriksa folder layanan di bawah SRV_ROOT, permission dan
# kepemilikan folder berbagi, serta entri fstab untuk disk data.
# Semua target bisa digeser lewat variabel lingkungan sehingga
# pemeriksaan bisa dilatih pada fixture:
#   SRV_ROOT     akar struktur layanan (default: /srv)
#   FSTAB_FILE   berkas fstab yang diperiksa (default: /etc/fstab)
#   LAYANAN_USER user pemilik folder berbagi (default: user saat ini)
#   DATA_MOUNT   mount point disk data (default: /srv/data)
#
# Kode keluar: 0 = siap dijalankan; selain itu = jumlah temuan.

set -u

SRV_ROOT="${SRV_ROOT:-/srv}"
FSTAB_FILE="${FSTAB_FILE:-/etc/fstab}"
LAYANAN_USER="${LAYANAN_USER:-$(id -un)}"
DATA_MOUNT="${DATA_MOUNT:-/srv/data}"

temuan=0

lolos() {
    printf 'OK    %s\n' "$1"
}

gagal() {
    printf 'GAGAL %s\n' "$1"
    temuan=$((temuan + 1))
}

# --- folder berbagi: ada, mode 750, dimiliki user layanan --------------
if [ -d "$SRV_ROOT/berbagi" ]; then
    mode=$(stat -c %a "$SRV_ROOT/berbagi")
    if [ "$mode" = "750" ]; then
        lolos "folder $SRV_ROOT/berbagi (mode $mode)"
    else
        gagal "folder $SRV_ROOT/berbagi mode $mode, harapan 750"
    fi
    pemilik=$(stat -c %U "$SRV_ROOT/berbagi")
    if [ "$pemilik" = "$LAYANAN_USER" ]; then
        lolos "pemilik $SRV_ROOT/berbagi sesuai user layanan"
    else
        gagal "pemilik $SRV_ROOT/berbagi bukan user layanan"
    fi
else
    gagal "folder $SRV_ROOT/berbagi tidak ditemukan"
    # permission dan kepemilikan tidak bisa diperiksa bila foldernya
    # pun tidak ada; dua temuan cukup mewakili satu folder hilang.
fi

# --- folder data keluarga -------------------------------------------------
for d in foto-keluarga backup; do
    if [ -d "$SRV_ROOT/$d" ]; then
        lolos "folder $SRV_ROOT/$d"
    else
        gagal "folder $SRV_ROOT/$d tidak ditemukan"
    fi
done

# --- entri fstab disk data: nofail wajib ----------------------------------
# $2 mount point, $4 opsi; nofail dicocokkan sebagai satu opsi utuh
# sehingga opsi seperti "mynofailx" tidak lolos mengaku nofail.
if awk -v mnt="$DATA_MOUNT" '
        $0 !~ /^[[:space:]]*#/ && $2 == mnt &&
        $4 ~ /(^|,)nofail(,|$)/ { found = 1 }
        END { exit found ? 0 : 1 }' "$FSTAB_FILE" 2> /dev/null; then
    lolos "entri fstab untuk $DATA_MOUNT memuat nofail"
else
    gagal "fstab belum memuat entri nofail untuk $DATA_MOUNT"
fi

# --- ringkasan ------------------------------------------------------------
if [ "$temuan" -eq 0 ]; then
    printf 'server siap dijalankan: 0 temuan\n'
else
    printf 'server belum siap: %d temuan\n' "$temuan"
fi
exit "$temuan"
BASH
cat > proyek/deploy/status-home-server.sh <<'BASH'
#!/usr/bin/env bash
# status-home-server.sh — dashboard status home server.
#
# Membaca snapshot beku dari folder data/ (ss.txt: hasil `ss -tln`,
# df.txt: hasil `df -h`) dan daftar run backup (backup-YYYYMMDD) di
# folder BACKUP_DIR, lalu menulis laporan status ke stdout sekaligus
# baris temuan ke berkas log. Pola snapshot membuat hasilnya bisa
# diulang dan diuji — pola yang sama dipakai Proyek Otomasi
# Pemeliharaan Sistem.
#
# Variabel lingkungan (berguna untuk pengujian):
#   CONF      berkas konfigurasi yang dibaca (default: home-server.conf)
#   DATA_DIR  folder snapshot (default: data)
#   HARI_INI  tanggal acuan umur backup (default: hari ini, YYYY-MM-DD)
#
# Kode keluar (temuan bisa gabungan — jumlahkan nilainya):
#   0 sehat                        1 disk data melewati ambang
#   2 layanan berbagi tidak jalan  4 backup terlalu tua / belum ada

set -u

CONF="${CONF:-home-server.conf}"
DATA_DIR="${DATA_DIR:-data}"
HARI_INI="${HARI_INI:-$(date +%F)}"

if [ -f "$CONF" ]; then
    # shellcheck source=/dev/null
    . "$CONF"
fi

PORT_BERBAGI="${PORT_BERBAGI:-8765}"
DATA_MOUNT="${DATA_MOUNT:-/srv/data}"
BACKUP_DIR="${BACKUP_DIR:-/srv/backup/run}"
BACKUP_MAX_UMUR_HARI="${BACKUP_MAX_UMUR_HARI:-2}"
DISK_AMBANG_PCT="${DISK_AMBANG_PCT:-85}"
LOG_FILE="${LOG_FILE:-status-home-server.log}"

GARIS='----------------------------------------------------------------'

# --- 1. Layanan berbagi: cari baris LISTEN pada port dari snapshot ----
# Kolom ke-4 output ss adalah "alamat:port", jadi port dicocokkan
# sebagai akhiran kolom — bentuk [::]:8765 pun lolos pola yang sama.
layanan="MATI"
if awk -v port="$PORT_BERBAGI" '
        $1 == "LISTEN" && $4 ~ ":" port "$" { ketemu = 1 }
        END { exit ketemu ? 0 : 1 }' "$DATA_DIR/ss.txt" 2> /dev/null; then
    layanan="JALAN"
fi

# --- 2. Disk data: persentase pemakaian dari snapshot df --------------
# Kolom ke-5 df adalah Use% ("+0" membuang tanda %), kolom ke-6
# mount point. Mount yang tidak ada berarti disk data gagal termuat.
disk_pct=$(awk -v mnt="$DATA_MOUNT" '
    $6 == mnt { print $5 + 0; exit }' "$DATA_DIR/df.txt" 2> /dev/null)

# --- 3. Backup terakhir: umur dihitung dari nama folder run -----------
backup_terakhir=$(ls -1 "$BACKUP_DIR" 2> /dev/null |
    grep -E '^backup-[0-9]{8}$' | sort | tail -n 1)

umur=""
if [ -n "$backup_terakhir" ]; then
    tgl="${backup_terakhir#backup-}"
    tgl_iso="${tgl:0:4}-${tgl:4:2}-${tgl:6:2}"
    umur=$(( ( $(date -d "$HARI_INI" +%s) -
               $(date -d "$tgl_iso" +%s) ) / 86400 ))
fi

# --- 4. Kumpulkan temuan ------------------------------------------------
temuan=0
n_temuan=0
daftar_temuan=""

catat() {
    n_temuan=$((n_temuan + 1))
    daftar_temuan="$daftar_temuan
- $1"
}

if [ "$layanan" = "MATI" ]; then
    temuan=$((temuan + 2))
    catat "layanan berbagi tidak jalan (port $PORT_BERBAGI tidak mendengarkan)"
fi

if [ -z "$disk_pct" ]; then
    temuan=$((temuan + 1))
    catat "disk data $DATA_MOUNT tidak termuat — periksa fstab"
elif [ "$disk_pct" -ge "$DISK_AMBANG_PCT" ]; then
    temuan=$((temuan + 1))
    catat "disk data melewati ambang: ${disk_pct}% dari ${DISK_AMBANG_PCT}%"
fi

if [ -z "$backup_terakhir" ]; then
    temuan=$((temuan + 4))
    catat "belum ada run backup di $BACKUP_DIR"
elif [ "$umur" -gt "$BACKUP_MAX_UMUR_HARI" ]; then
    temuan=$((temuan + 4))
    catat "backup terlalu tua: $umur hari (maksimal $BACKUP_MAX_UMUR_HARI)"
fi

# --- 5. Tulis laporan ke stdout dan temuan ke log -----------------------
printf '================================================================\n'
printf 'STATUS HOME SERVER\n'
printf 'Tanggal acuan : %s\n' "$HARI_INI"
printf '================================================================\n'
if [ "$layanan" = "JALAN" ]; then
    printf 'layanan berbagi : JALAN (port %s)\n' "$PORT_BERBAGI"
else
    printf 'layanan berbagi : MATI (port %s tidak mendengarkan)\n' "$PORT_BERBAGI"
fi
if [ -n "$disk_pct" ]; then
    if [ "$disk_pct" -ge "$DISK_AMBANG_PCT" ]; then
        printf 'disk data       : %s %s%% MELEWATI AMBANG %s%%\n' \
            "$DATA_MOUNT" "$disk_pct" "$DISK_AMBANG_PCT"
    else
        printf 'disk data       : %s %s%% (ambang %s%%)\n' \
            "$DATA_MOUNT" "$disk_pct" "$DISK_AMBANG_PCT"
    fi
else
    printf 'disk data       : %s tidak ada di snapshot df\n' "$DATA_MOUNT"
fi
if [ -n "$backup_terakhir" ]; then
    printf 'backup terakhir : %s (umur %s hari, maksimal %s)\n' \
        "$backup_terakhir" "$umur" "$BACKUP_MAX_UMUR_HARI"
else
    printf 'backup terakhir : belum ada run backup\n'
fi
printf '%s\n' "$GARIS"

if [ "$temuan" -eq 0 ]; then
    printf 'temuan          : tidak ada — server sehat\n'
else
    printf 'temuan          : %d\n' "$n_temuan"
    # ${var#$'\n'} membuang baris kosong pertama dari daftar.
    printf '%s\n' "${daftar_temuan#$'\n'}"
    # Alert sederhana: satu baris per temuan ke berkas log.
    mkdir -p "$(dirname "$LOG_FILE")"
    printf '%s\n' "${daftar_temuan#$'\n'}" |
        while IFS= read -r baris; do
            printf '[%s] TEMUAN: %s\n' "$HARI_INI" "${baris#- }" >> "$LOG_FILE"
        done
    printf 'temuan dicatat ke: %s\n' "$LOG_FILE"
fi

exit "$temuan"
BASH
cat > proyek/tests/run-tests.sh <<'TESTS'
#!/usr/bin/env bash
# run-tests.sh — test suite untuk paket layanan home server.
#
# Suite menyiapkan folder kerja terisolasi (mktemp), menyalin unit
# systemd dari config/ dan kedua skrip dari deploy/, lalu menjalankan:
#   1. validasi unit lewat systemd-analyze verify,
#   2. pemeriksaan directive penting lewat grep,
#   3. layanan mini yang benar-benar mendengarkan dan menjawab HTTP,
#   4. skrip kesiapan pada fixture siap dan rusak,
#   5. skrip status pada fixture sehat dan kombinasi temuan.
# Jalankan dari mana saja: bash tests/run-tests.sh

set -u

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")

PASS=0
FAIL=0

# assert_eq <harapan> <nyata> <pesan>: bandingkan dua nilai.
assert_eq() {
    if [ "$1" = "$2" ]; then
        PASS=$((PASS + 1))
        printf 'OK    %s\n' "$3"
    else
        FAIL=$((FAIL + 1))
        printf 'GAGAL %s: harapan %s, nyata %s\n' "$3" "$1" "$2"
    fi
}

# --- siapkan folder kerja terisolasi ------------------------------------
WORK=$(mktemp -d)
cp "$PROJ_DIR/config/berbagi-file.service" \
   "$PROJ_DIR/config/backup-keluarga.service" \
   "$PROJ_DIR/config/backup-keluarga.timer" \
   "$PROJ_DIR/config/home-server.conf" \
   "$PROJ_DIR/deploy/status-home-server.sh" \
   "$PROJ_DIR/deploy/cek-kesiapan.sh" "$WORK/"
cd "$WORK" || exit 1

server_pid=""
cleanup() {
    if [ -n "$server_pid" ]; then
        kill "$server_pid" 2> /dev/null
        wait "$server_pid" 2> /dev/null
    fi
}
trap cleanup EXIT

# --- uji 1: unit valid menurut systemd-analyze verify -------------------
systemd-analyze verify berbagi-file.service
assert_eq 0 "$?" "berbagi-file.service lolos systemd-analyze verify"
systemd-analyze verify backup-keluarga.service backup-keluarga.timer
assert_eq 0 "$?" "unit backup lolos systemd-analyze verify"

# --- uji 2: directive penting ada di tempatnya ----------------------------
grep -q '^User=berbagi$' berbagi-file.service
assert_eq 0 "$?" "unit berbagi berjalan sebagai user layanan non-root"
grep -q '^WorkingDirectory=/srv/berbagi$' berbagi-file.service
assert_eq 0 "$?" "WorkingDirectory mengarah ke folder berbagi"
grep -q '^ExecStart=/usr/bin/python3 -m http.server 8765' \
    berbagi-file.service
assert_eq 0 "$?" "ExecStart menjalankan layanan pada port 8765"
grep -q '^ProtectSystem=' berbagi-file.service
grep -q '^PrivateTmp=true$' berbagi-file.service
assert_eq 0 "$?" "hardening ProtectSystem dan PrivateTmp aktif"
grep -q '^OnCalendar=' backup-keluarga.timer
assert_eq 0 "$?" "timer memuat OnCalendar"
grep -q '^Persistent=true$' backup-keluarga.timer
assert_eq 0 "$?" "timer Persistent mengejar jadwal yang terlewat"

# --- uji 3: layanan mini benar-benar menjawab ------------------------------
mkdir -p srv/berbagi
printf 'jadwal piknik keluarga\n' > srv/berbagi/jadwal-piknik.txt
python3 -m http.server 8765 --directory srv/berbagi >/dev/null 2>&1 &
server_pid=$!
siap=1
for i in 1 2 3 4 5 6 7 8 9 10; do
    curl -s -o /dev/null http://127.0.0.1:8765/ && { siap=0; break; }
    sleep 0.3
done
assert_eq 0 "$siap" "layanan mini siap menerima permintaan"
kode=$(curl -s -o /dev/null -w '%{http_code}' \
    http://127.0.0.1:8765/jadwal-piknik.txt)
assert_eq 200 "$kode" "berkas dijawab HTTP 200"
isi=$(curl -s http://127.0.0.1:8765/jadwal-piknik.txt)
assert_eq "jadwal piknik keluarga" "$isi" "isi berkas utuh sampai klien"
ss -tln | awk -v port="8765" '$1 == "LISTEN" && $4 ~ ":" port "$"' \
    | grep -q .
assert_eq 0 "$?" "ss melihat port 8765 dalam keadaan LISTEN"

# --- uji 4: skrip kesiapan pada fixture -------------------------------------
mkdir -p srv/foto-keluarga srv/backup srv/backup/run
chmod 750 srv/berbagi
printf 'UUID=5c1b2a3d-1111-4222-8333-444455556666 /srv/data ext4 defaults,nofail 0 2\n' > fstab.data
SRV_ROOT=srv FSTAB_FILE=fstab.data bash cek-kesiapan.sh > cek-siap.txt 2>&1
assert_eq 0 "$?" "layout siap lolos pemeriksaan"
grep -q 'server siap dijalankan: 0 temuan' cek-siap.txt
assert_eq 0 "$?" "laporan kesiapan menyatakan siap"
chmod 755 srv/berbagi
sed 's/,nofail//' fstab.data > fstab.rusak
SRV_ROOT=srv FSTAB_FILE=fstab.rusak bash cek-kesiapan.sh > /dev/null 2>&1
assert_eq 2 "$?" "mode salah dan nofail hilang terdeteksi (exit 2)"
chmod 750 srv/berbagi

# --- uji 5: skrip status pada fixture sehat ---------------------------------
# Snapshot ss diambil saat layanan masih jalan (uji 3), lalu layanan
# dihentikan supaya suite tidak meninggalkan proses.
mkdir -p data
ss -tln > data/ss.txt
kill "$server_pid"
wait "$server_pid" 2> /dev/null
server_pid=""
ss -tln | awk -v port="8765" '$1 == "LISTEN" && $4 ~ ":" port "$"' \
    | wc -l | grep -qx 0
assert_eq 0 "$?" "port 8765 bebas setelah layanan berhenti"

cat > data/df.txt <<'EOF'
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1         50G   20G   28G  41% /
/dev/sdb1       200G  143G   47G  72% /srv/data
EOF
mkdir -p srv/backup/run/backup-20260214
HARI_INI=2026-02-15 bash status-home-server.sh > laporan-sehat.txt
assert_eq 0 "$?" "fixture sehat keluar 0"
grep -q '^layanan berbagi : JALAN' laporan-sehat.txt
assert_eq 0 "$?" "laporan menyatakan layanan JALAN"
grep -q '72% (ambang 85%)' laporan-sehat.txt
assert_eq 0 "$?" "disk data terbaca 72% dari snapshot"
grep -q 'backup-20260214 (umur 1 hari, maksimal 2)' laporan-sehat.txt
assert_eq 0 "$?" "umur backup terhitung dari tanggal acuan"

# --- uji 6: setiap temuan menyalakan bit kode keluar sendiri -----------------
# Tanggal acuan 2026-02-15: backup-20260214 berumur 1 hari (segar),
# backup-20260120 berumur 26 hari (terlalu tua).
mv data/ss.txt data/ss-jalan.txt
ss -tln | grep -v ':8765 ' > data/ss.txt || true
HARI_INI=2026-02-15 bash status-home-server.sh > /dev/null 2>&1
assert_eq 2 "$?" "layanan mati menyalakan bit 2"
mv data/ss-jalan.txt data/ss.txt

sed 's/ 72% / 91% /' data/df.txt > data/df-91.txt
mv data/df-91.txt data/df.txt
HARI_INI=2026-02-15 bash status-home-server.sh > /dev/null 2>&1
assert_eq 1 "$?" "disk 91% melewati ambang menyalakan bit 1"

rm -rf srv/backup/run/backup-20260214
mkdir -p srv/backup/run/backup-20260120
HARI_INI=2026-02-15 bash status-home-server.sh > /dev/null 2>&1
assert_eq 5 "$?" "disk 91% dan backup tua menyalakan bit 1+4"

ss -tln | grep -v ':8765 ' > data/ss.txt || true
HARI_INI=2026-02-15 bash status-home-server.sh > laporan-temuan.txt 2>&1
assert_eq 7 "$?" "tiga temuan menyalakan bit 1+2+4"
grep -Eq '^temuan[[:space:]]+: 3$' laporan-temuan.txt
assert_eq 0 "$?" "laporan menyebut tiga temuan"
grep -q 'backup terlalu tua: 26 hari' laporan-temuan.txt
assert_eq 0 "$?" "umur backup tua dihitung benar"
grep -q 'TEMUAN: layanan berbagi tidak jalan' \
    srv/backup/status-home-server.log
assert_eq 0 "$?" "temuan tercatat ke berkas log"

# --- ringkasan --------------------------------------------------------------
if [ "$FAIL" -eq 0 ]; then
    printf 'PASSED: %d/%d test lulus\n' "$PASS" "$PASS"
else
    printf 'FAILED: %d/%d test lulus\n' "$PASS" "$((PASS + FAIL))"
fi
cd / && rm -rf "$WORK"
[ "$FAIL" -eq 0 ]
TESTS
bash proyek/tests/run-tests.sh

Output yang Dihasilkan:

text
OK    berbagi-file.service lolos systemd-analyze verify
OK    unit backup lolos systemd-analyze verify
OK    unit berbagi berjalan sebagai user layanan non-root
OK    WorkingDirectory mengarah ke folder berbagi
OK    ExecStart menjalankan layanan pada port 8765
OK    hardening ProtectSystem dan PrivateTmp aktif
OK    timer memuat OnCalendar
OK    timer Persistent mengejar jadwal yang terlewat
OK    layanan mini siap menerima permintaan
OK    berkas dijawab HTTP 200
OK    isi berkas utuh sampai klien
OK    ss melihat port 8765 dalam keadaan LISTEN
OK    layout siap lolos pemeriksaan
OK    laporan kesiapan menyatakan siap
OK    mode salah dan nofail hilang terdeteksi (exit 2)
OK    port 8765 bebas setelah layanan berhenti
OK    fixture sehat keluar 0
OK    laporan menyatakan layanan JALAN
OK    disk data terbaca 72% dari snapshot
OK    umur backup terhitung dari tanggal acuan
OK    layanan mati menyalakan bit 2
OK    disk 91% melewati ambang menyalakan bit 1
OK    disk 91% dan backup tua menyalakan bit 1+4
OK    tiga temuan menyalakan bit 1+2+4
OK    laporan menyebut tiga temuan
OK    umur backup tua dihitung benar
OK    temuan tercatat ke berkas log
PASSED: 27/27 test lulus

Pola pentingnya:

  1. systemd-analyze verify pada berkas sungguhan: suite memvalidasi persis unit yang akan dipasang, bukan salinan yang disederhanakan — kesalahan ketik pada unit tertangkap di sini, sebelum menyentuh /etc/systemd/system.
  2. grep '^User=berbagi$' dan kawan-kawan: directive penting diperiksa sebagai baris utuh. Unit yang “lolos verify” tapi kehilangan User= akan tertangkap di lapis kedua ini — verify memeriksa kevalidan, grep memeriksa maksud kita.
  3. Layanan sungguhan, bukan tiruan: suite menyalakan http.server sungguhan, menunggu siap lewat loop curl, memeriksa kode 200 dan isi berkas, melihat LISTEN lewat ss, lalu memastikan port bebas setelah berhenti — persis bukti manual dari Bagian 2, kini berjalan otomatis.
  4. Fixture mtime beku: run backup dibuat dengan nama bertanggal sehingga umurnya pasti; ss.txt dibekukan dari kondisi jalan dan mati; df.txt diuji pada 72% dan 91%. Tanggal acuan HARI_INI=2026-02-15 membuat seluruh perhitungan deterministik.
  5. Bitmask diuji satu per satu: suite memastikan tiap temuan menyala bitnya sendiri (exit 1, 2, 4), lalu kombinasinya (5, 7) — kontrak kode keluar yang dijanjikan skrip terbukti untuk setiap cabangnya.

Home server kita selesai: fondasi yang siap diperiksa, layanan yang tervalidasi dan terbukti menjawab, jadwal backup yang mengejar yang terlewat, dan pengawas yang melapor dengan kode yang bisa dibaca mesin. Tetapi kita baru menyentuh satu pola penjadwalan — dunia cron dan systemd timer masih punya banyak bentuk lain, dari @reboot sampai timer dengan RandomizedDelaySec, dan itulah jalan belajar berikutnya di Otomasi dengan cron dan systemd .

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

deploy/cek-kesiapan.sh

Unduh berkas
bash
#!/usr/bin/env bash
# cek-kesiapan.sh — periksa kesiapan struktur home server.
#
# Memeriksa folder layanan di bawah SRV_ROOT, permission dan
# kepemilikan folder berbagi, serta entri fstab untuk disk data.
# Semua target bisa digeser lewat variabel lingkungan sehingga
# pemeriksaan bisa dilatih pada fixture:
#   SRV_ROOT     akar struktur layanan (default: /srv)
#   FSTAB_FILE   berkas fstab yang diperiksa (default: /etc/fstab)
#   LAYANAN_USER user pemilik folder berbagi (default: user saat ini)
#   DATA_MOUNT   mount point disk data (default: /srv/data)
#
# Kode keluar: 0 = siap dijalankan; selain itu = jumlah temuan.

set -u

SRV_ROOT="${SRV_ROOT:-/srv}"
FSTAB_FILE="${FSTAB_FILE:-/etc/fstab}"
LAYANAN_USER="${LAYANAN_USER:-$(id -un)}"
DATA_MOUNT="${DATA_MOUNT:-/srv/data}"

temuan=0

lolos() {
    printf 'OK    %s\n' "$1"
}

gagal() {
    printf 'GAGAL %s\n' "$1"
    temuan=$((temuan + 1))
}

# --- folder berbagi: ada, mode 750, dimiliki user layanan --------------
if [ -d "$SRV_ROOT/berbagi" ]; then
    mode=$(stat -c %a "$SRV_ROOT/berbagi")
    if [ "$mode" = "750" ]; then
        lolos "folder $SRV_ROOT/berbagi (mode $mode)"
    else
        gagal "folder $SRV_ROOT/berbagi mode $mode, harapan 750"
    fi
    pemilik=$(stat -c %U "$SRV_ROOT/berbagi")
    if [ "$pemilik" = "$LAYANAN_USER" ]; then
        lolos "pemilik $SRV_ROOT/berbagi sesuai user layanan"
    else
        gagal "pemilik $SRV_ROOT/berbagi bukan user layanan"
    fi
else
    gagal "folder $SRV_ROOT/berbagi tidak ditemukan"
    # permission dan kepemilikan tidak bisa diperiksa bila foldernya
    # pun tidak ada; dua temuan cukup mewakili satu folder hilang.
fi

# --- folder data keluarga -------------------------------------------------
for d in foto-keluarga backup; do
    if [ -d "$SRV_ROOT/$d" ]; then
        lolos "folder $SRV_ROOT/$d"
    else
        gagal "folder $SRV_ROOT/$d tidak ditemukan"
    fi
done

# --- entri fstab disk data: nofail wajib ----------------------------------
# $2 mount point, $4 opsi; nofail dicocokkan sebagai satu opsi utuh
# sehingga opsi seperti "mynofailx" tidak lolos mengaku nofail.
if awk -v mnt="$DATA_MOUNT" '
        $0 !~ /^[[:space:]]*#/ && $2 == mnt &&
        $4 ~ /(^|,)nofail(,|$)/ { found = 1 }
        END { exit found ? 0 : 1 }' "$FSTAB_FILE" 2> /dev/null; then
    lolos "entri fstab untuk $DATA_MOUNT memuat nofail"
else
    gagal "fstab belum memuat entri nofail untuk $DATA_MOUNT"
fi

# --- ringkasan ------------------------------------------------------------
if [ "$temuan" -eq 0 ]; then
    printf 'server siap dijalankan: 0 temuan\n'
else
    printf 'server belum siap: %d temuan\n' "$temuan"
fi
exit "$temuan"

deploy/status-home-server.sh

Unduh berkas
bash
#!/usr/bin/env bash
# status-home-server.sh — dashboard status home server.
#
# Membaca snapshot beku dari folder data/ (ss.txt: hasil `ss -tln`,
# df.txt: hasil `df -h`) dan daftar run backup (backup-YYYYMMDD) di
# folder BACKUP_DIR, lalu menulis laporan status ke stdout sekaligus
# baris temuan ke berkas log. Pola snapshot membuat hasilnya bisa
# diulang dan diuji — pola yang sama dipakai Proyek Otomasi
# Pemeliharaan Sistem.
#
# Variabel lingkungan (berguna untuk pengujian):
#   CONF      berkas konfigurasi yang dibaca (default: home-server.conf)
#   DATA_DIR  folder snapshot (default: data)
#   HARI_INI  tanggal acuan umur backup (default: hari ini, YYYY-MM-DD)
#
# Kode keluar (temuan bisa gabungan — jumlahkan nilainya):
#   0 sehat                        1 disk data melewati ambang
#   2 layanan berbagi tidak jalan  4 backup terlalu tua / belum ada

set -u

CONF="${CONF:-home-server.conf}"
DATA_DIR="${DATA_DIR:-data}"
HARI_INI="${HARI_INI:-$(date +%F)}"

if [ -f "$CONF" ]; then
    # shellcheck source=/dev/null
    . "$CONF"
fi

PORT_BERBAGI="${PORT_BERBAGI:-8765}"
DATA_MOUNT="${DATA_MOUNT:-/srv/data}"
BACKUP_DIR="${BACKUP_DIR:-/srv/backup/run}"
BACKUP_MAX_UMUR_HARI="${BACKUP_MAX_UMUR_HARI:-2}"
DISK_AMBANG_PCT="${DISK_AMBANG_PCT:-85}"
LOG_FILE="${LOG_FILE:-status-home-server.log}"

GARIS='----------------------------------------------------------------'

# --- 1. Layanan berbagi: cari baris LISTEN pada port dari snapshot ----
# Kolom ke-4 output ss adalah "alamat:port", jadi port dicocokkan
# sebagai akhiran kolom — bentuk [::]:8765 pun lolos pola yang sama.
layanan="MATI"
if awk -v port="$PORT_BERBAGI" '
        $1 == "LISTEN" && $4 ~ ":" port "$" { ketemu = 1 }
        END { exit ketemu ? 0 : 1 }' "$DATA_DIR/ss.txt" 2> /dev/null; then
    layanan="JALAN"
fi

# --- 2. Disk data: persentase pemakaian dari snapshot df --------------
# Kolom ke-5 df adalah Use% ("+0" membuang tanda %), kolom ke-6
# mount point. Mount yang tidak ada berarti disk data gagal termuat.
disk_pct=$(awk -v mnt="$DATA_MOUNT" '
    $6 == mnt { print $5 + 0; exit }' "$DATA_DIR/df.txt" 2> /dev/null)

# --- 3. Backup terakhir: umur dihitung dari nama folder run -----------
backup_terakhir=$(ls -1 "$BACKUP_DIR" 2> /dev/null |
    grep -E '^backup-[0-9]{8}$' | sort | tail -n 1)

umur=""
if [ -n "$backup_terakhir" ]; then
    tgl="${backup_terakhir#backup-}"
    tgl_iso="${tgl:0:4}-${tgl:4:2}-${tgl:6:2}"
    umur=$(( ( $(date -d "$HARI_INI" +%s) -
               $(date -d "$tgl_iso" +%s) ) / 86400 ))
fi

# --- 4. Kumpulkan temuan ------------------------------------------------
temuan=0
n_temuan=0
daftar_temuan=""

catat() {
    n_temuan=$((n_temuan + 1))
    daftar_temuan="$daftar_temuan
- $1"
}

if [ "$layanan" = "MATI" ]; then
    temuan=$((temuan + 2))
    catat "layanan berbagi tidak jalan (port $PORT_BERBAGI tidak mendengarkan)"
fi

if [ -z "$disk_pct" ]; then
    temuan=$((temuan + 1))
    catat "disk data $DATA_MOUNT tidak termuat — periksa fstab"
elif [ "$disk_pct" -ge "$DISK_AMBANG_PCT" ]; then
    temuan=$((temuan + 1))
    catat "disk data melewati ambang: ${disk_pct}% dari ${DISK_AMBANG_PCT}%"
fi

if [ -z "$backup_terakhir" ]; then
    temuan=$((temuan + 4))
    catat "belum ada run backup di $BACKUP_DIR"
elif [ "$umur" -gt "$BACKUP_MAX_UMUR_HARI" ]; then
    temuan=$((temuan + 4))
    catat "backup terlalu tua: $umur hari (maksimal $BACKUP_MAX_UMUR_HARI)"
fi

# --- 5. Tulis laporan ke stdout dan temuan ke log -----------------------
printf '================================================================\n'
printf 'STATUS HOME SERVER\n'
printf 'Tanggal acuan : %s\n' "$HARI_INI"
printf '================================================================\n'
if [ "$layanan" = "JALAN" ]; then
    printf 'layanan berbagi : JALAN (port %s)\n' "$PORT_BERBAGI"
else
    printf 'layanan berbagi : MATI (port %s tidak mendengarkan)\n' "$PORT_BERBAGI"
fi
if [ -n "$disk_pct" ]; then
    if [ "$disk_pct" -ge "$DISK_AMBANG_PCT" ]; then
        printf 'disk data       : %s %s%% MELEWATI AMBANG %s%%\n' \
            "$DATA_MOUNT" "$disk_pct" "$DISK_AMBANG_PCT"
    else
        printf 'disk data       : %s %s%% (ambang %s%%)\n' \
            "$DATA_MOUNT" "$disk_pct" "$DISK_AMBANG_PCT"
    fi
else
    printf 'disk data       : %s tidak ada di snapshot df\n' "$DATA_MOUNT"
fi
if [ -n "$backup_terakhir" ]; then
    printf 'backup terakhir : %s (umur %s hari, maksimal %s)\n' \
        "$backup_terakhir" "$umur" "$BACKUP_MAX_UMUR_HARI"
else
    printf 'backup terakhir : belum ada run backup\n'
fi
printf '%s\n' "$GARIS"

if [ "$temuan" -eq 0 ]; then
    printf 'temuan          : tidak ada — server sehat\n'
else
    printf 'temuan          : %d\n' "$n_temuan"
    # ${var#$'\n'} membuang baris kosong pertama dari daftar.
    printf '%s\n' "${daftar_temuan#$'\n'}"
    # Alert sederhana: satu baris per temuan ke berkas log.
    mkdir -p "$(dirname "$LOG_FILE")"
    printf '%s\n' "${daftar_temuan#$'\n'}" |
        while IFS= read -r baris; do
            printf '[%s] TEMUAN: %s\n' "$HARI_INI" "${baris#- }" >> "$LOG_FILE"
        done
    printf 'temuan dicatat ke: %s\n' "$LOG_FILE"
fi

exit "$temuan"

tests/run-tests.sh

Unduh berkas
bash
#!/usr/bin/env bash
# run-tests.sh — test suite untuk paket layanan home server.
#
# Suite menyiapkan folder kerja terisolasi (mktemp), menyalin unit
# systemd dari config/ dan kedua skrip dari deploy/, lalu menjalankan:
#   1. validasi unit lewat systemd-analyze verify,
#   2. pemeriksaan directive penting lewat grep,
#   3. layanan mini yang benar-benar mendengarkan dan menjawab HTTP,
#   4. skrip kesiapan pada fixture siap dan rusak,
#   5. skrip status pada fixture sehat dan kombinasi temuan.
# Jalankan dari mana saja: bash tests/run-tests.sh

set -u

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")

PASS=0
FAIL=0

# assert_eq <harapan> <nyata> <pesan>: bandingkan dua nilai.
assert_eq() {
    if [ "$1" = "$2" ]; then
        PASS=$((PASS + 1))
        printf 'OK    %s\n' "$3"
    else
        FAIL=$((FAIL + 1))
        printf 'GAGAL %s: harapan %s, nyata %s\n' "$3" "$1" "$2"
    fi
}

# --- siapkan folder kerja terisolasi ------------------------------------
WORK=$(mktemp -d)
cp "$PROJ_DIR/config/berbagi-file.service" \
   "$PROJ_DIR/config/backup-keluarga.service" \
   "$PROJ_DIR/config/backup-keluarga.timer" \
   "$PROJ_DIR/config/home-server.conf" \
   "$PROJ_DIR/deploy/status-home-server.sh" \
   "$PROJ_DIR/deploy/cek-kesiapan.sh" "$WORK/"
cd "$WORK" || exit 1

server_pid=""
cleanup() {
    if [ -n "$server_pid" ]; then
        kill "$server_pid" 2> /dev/null
        wait "$server_pid" 2> /dev/null
    fi
}
trap cleanup EXIT

# --- uji 1: unit valid menurut systemd-analyze verify -------------------
systemd-analyze verify berbagi-file.service
assert_eq 0 "$?" "berbagi-file.service lolos systemd-analyze verify"
systemd-analyze verify backup-keluarga.service backup-keluarga.timer
assert_eq 0 "$?" "unit backup lolos systemd-analyze verify"

# --- uji 2: directive penting ada di tempatnya ----------------------------
grep -q '^User=berbagi$' berbagi-file.service
assert_eq 0 "$?" "unit berbagi berjalan sebagai user layanan non-root"
grep -q '^WorkingDirectory=/srv/berbagi$' berbagi-file.service
assert_eq 0 "$?" "WorkingDirectory mengarah ke folder berbagi"
grep -q '^ExecStart=/usr/bin/python3 -m http.server 8765' \
    berbagi-file.service
assert_eq 0 "$?" "ExecStart menjalankan layanan pada port 8765"
grep -q '^ProtectSystem=' berbagi-file.service
grep -q '^PrivateTmp=true$' berbagi-file.service
assert_eq 0 "$?" "hardening ProtectSystem dan PrivateTmp aktif"
grep -q '^OnCalendar=' backup-keluarga.timer
assert_eq 0 "$?" "timer memuat OnCalendar"
grep -q '^Persistent=true$' backup-keluarga.timer
assert_eq 0 "$?" "timer Persistent mengejar jadwal yang terlewat"

# --- uji 3: layanan mini benar-benar menjawab ------------------------------
mkdir -p srv/berbagi
printf 'jadwal piknik keluarga\n' > srv/berbagi/jadwal-piknik.txt
python3 -m http.server 8765 --directory srv/berbagi >/dev/null 2>&1 &
server_pid=$!
siap=1
for i in 1 2 3 4 5 6 7 8 9 10; do
    curl -s -o /dev/null http://127.0.0.1:8765/ && { siap=0; break; }
    sleep 0.3
done
assert_eq 0 "$siap" "layanan mini siap menerima permintaan"
kode=$(curl -s -o /dev/null -w '%{http_code}' \
    http://127.0.0.1:8765/jadwal-piknik.txt)
assert_eq 200 "$kode" "berkas dijawab HTTP 200"
isi=$(curl -s http://127.0.0.1:8765/jadwal-piknik.txt)
assert_eq "jadwal piknik keluarga" "$isi" "isi berkas utuh sampai klien"
ss -tln | awk -v port="8765" '$1 == "LISTEN" && $4 ~ ":" port "$"' \
    | grep -q .
assert_eq 0 "$?" "ss melihat port 8765 dalam keadaan LISTEN"

# --- uji 4: skrip kesiapan pada fixture -------------------------------------
mkdir -p srv/foto-keluarga srv/backup srv/backup/run
chmod 750 srv/berbagi
printf 'UUID=5c1b2a3d-1111-4222-8333-444455556666 /srv/data ext4 defaults,nofail 0 2\n' > fstab.data
SRV_ROOT=srv FSTAB_FILE=fstab.data bash cek-kesiapan.sh > cek-siap.txt 2>&1
assert_eq 0 "$?" "layout siap lolos pemeriksaan"
grep -q 'server siap dijalankan: 0 temuan' cek-siap.txt
assert_eq 0 "$?" "laporan kesiapan menyatakan siap"
chmod 755 srv/berbagi
sed 's/,nofail//' fstab.data > fstab.rusak
SRV_ROOT=srv FSTAB_FILE=fstab.rusak bash cek-kesiapan.sh > /dev/null 2>&1
assert_eq 2 "$?" "mode salah dan nofail hilang terdeteksi (exit 2)"
chmod 750 srv/berbagi

# --- uji 5: skrip status pada fixture sehat ---------------------------------
# Snapshot ss diambil saat layanan masih jalan (uji 3), lalu layanan
# dihentikan supaya suite tidak meninggalkan proses.
mkdir -p data
ss -tln > data/ss.txt
kill "$server_pid"
wait "$server_pid" 2> /dev/null
server_pid=""
ss -tln | awk -v port="8765" '$1 == "LISTEN" && $4 ~ ":" port "$"' \
    | wc -l | grep -qx 0
assert_eq 0 "$?" "port 8765 bebas setelah layanan berhenti"

cat > data/df.txt <<'EOF'
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1         50G   20G   28G  41% /
/dev/sdb1       200G  143G   47G  72% /srv/data
EOF
mkdir -p srv/backup/run/backup-20260214
HARI_INI=2026-02-15 bash status-home-server.sh > laporan-sehat.txt
assert_eq 0 "$?" "fixture sehat keluar 0"
grep -q '^layanan berbagi : JALAN' laporan-sehat.txt
assert_eq 0 "$?" "laporan menyatakan layanan JALAN"
grep -q '72% (ambang 85%)' laporan-sehat.txt
assert_eq 0 "$?" "disk data terbaca 72% dari snapshot"
grep -q 'backup-20260214 (umur 1 hari, maksimal 2)' laporan-sehat.txt
assert_eq 0 "$?" "umur backup terhitung dari tanggal acuan"

# --- uji 6: setiap temuan menyalakan bit kode keluar sendiri -----------------
# Tanggal acuan 2026-02-15: backup-20260214 berumur 1 hari (segar),
# backup-20260120 berumur 26 hari (terlalu tua).
mv data/ss.txt data/ss-jalan.txt
ss -tln | grep -v ':8765 ' > data/ss.txt || true
HARI_INI=2026-02-15 bash status-home-server.sh > /dev/null 2>&1
assert_eq 2 "$?" "layanan mati menyalakan bit 2"
mv data/ss-jalan.txt data/ss.txt

sed 's/ 72% / 91% /' data/df.txt > data/df-91.txt
mv data/df-91.txt data/df.txt
HARI_INI=2026-02-15 bash status-home-server.sh > /dev/null 2>&1
assert_eq 1 "$?" "disk 91% melewati ambang menyalakan bit 1"

rm -rf srv/backup/run/backup-20260214
mkdir -p srv/backup/run/backup-20260120
HARI_INI=2026-02-15 bash status-home-server.sh > /dev/null 2>&1
assert_eq 5 "$?" "disk 91% dan backup tua menyalakan bit 1+4"

ss -tln | grep -v ':8765 ' > data/ss.txt || true
HARI_INI=2026-02-15 bash status-home-server.sh > laporan-temuan.txt 2>&1
assert_eq 7 "$?" "tiga temuan menyalakan bit 1+2+4"
grep -Eq '^temuan[[:space:]]+: 3$' laporan-temuan.txt
assert_eq 0 "$?" "laporan menyebut tiga temuan"
grep -q 'backup terlalu tua: 26 hari' laporan-temuan.txt
assert_eq 0 "$?" "umur backup tua dihitung benar"
grep -q 'TEMUAN: layanan berbagi tidak jalan' \
    srv/backup/status-home-server.log
assert_eq 0 "$?" "temuan tercatat ke berkas log"

# --- ringkasan --------------------------------------------------------------
if [ "$FAIL" -eq 0 ]; then
    printf 'PASSED: %d/%d test lulus\n' "$PASS" "$PASS"
else
    printf 'FAILED: %d/%d test lulus\n' "$PASS" "$((PASS + FAIL))"
fi
cd / && rm -rf "$WORK"
[ "$FAIL" -eq 0 ]