Home server yang gagal biasanya bukan gagal karena layanannya jelek, melainkan karena fondasinya amburadul: IP yang berubah tiap router restart sehingga keluarga kehilangan akses, disk data yang lupa dikonfigurasi sehingga boot tersangkut, dan layanan yang berjalan sebagai root karena “biar gampang”. Pada bagian ini kita menata fondasinya dulu — peran server, sistem operasi, alamat IP yang stabil, akses SSH, disk data terpisah, struktur /srv, dan user khusus untuk layanan. Setiap keputusan ditutup dengan pemeriksaan yang bisa dijalankan di lingkungan latihan: fixture fstab diperiksa dengan findmnt, permission diperiksa dengan stat, dan seluruhnya dirangkum oleh skrip cek kesiapan.

Peran Server dan Perangkatnya #

Satu keputusan pertama menentukan semuanya: server ini untuk apa? Kita tetapkan perannya sederhana — penyimpanan file keluarga dan backup otomatis, bukan hosting publik. Dengan peran sesempit itu, kebutuhan perangkatnya rendah, dan justru perangkat “sampah” di rumah yang menang:

PerangkatKenapa cocokCatatan
mini PC bekas kantorhemat daya, kecil, diampilihan paling seimbang
laptop tua berlayar rusakgratis, punya baterai = UPS bawaanlayar tidak diperlukan
Raspberry Pihemat daya, mudah didapathindarkan dari media transcode

Prinsip memilih dan menghidupkan perangkat semacam ini — dari mengukur pemakaian RAM sampai memilih distro ringan — sudah kita bahas tuntas di Memilih Distro Ringan . Yang perlu diingat di sini: server ini menyala terus, jadi daya dan kebisingan ikut menentukan kenyamanan rumah, bukan hanya angka benchmark.

Sistem Operasi Tanpa GUI #

Untuk server, kita pakai Ubuntu Server atau Debian tanpa desktop environment. Alasannya bukan tradisi, melainkan hitungan: GUI memakan ratusan MB RAM yang tidak membeli apa pun untuk server yang diakses lewat SSH, menambah permukaan serangan karena komponen yang terpasang lebih banyak, dan menambah pekerjaan update. Instalasinya dari ISO resmi, dan setelah sistem pertama kali hidup, dua perintah ini memastikan paketnya mutakhir:

bash
sudo apt update && sudo apt full-upgrade

Satu-satunya paket tambahan yang kita butuhkan di awal adalah server SSH, karena seluruh pengelolaan selanjutnya kita lakukan dari laptop:

bash
sudo apt install openssh-server

IP Stabil Lewat DHCP Reservation #

Server yang alamatnya berubah-ubah adalah server yang tidak bisa diandalkan keluarganya: bookmark http://192.0.2.10:8765/ mati diam-diam tiap beberapa hari. Kita mengunci alamat itu lewat fitur router bernama DHCP reservation — router tetap memakai DHCP, tetapi untuk MAC address tertentu selalu membagikan IP yang sama. Contoh tabelnya di halaman admin router:

PerangkatMAC addressIP yang dikunci
home-serverdc:a6:32:1a:2b:3c192.0.2.10
laptop-budia4:5e:60:d1:8f:02192.0.2.21

Akses dari Jauh: SSH #

Seluruh sisa proyek ini dikerjakan lewat SSH dari laptop. Kuncinya dibuat sekali di laptop, lalu ditempelkan ke server — password login kemudian bisa dimatikan:

bash
ssh-keygen -t ed25519 -C "budi@laptop-budi"
ssh-copy-id [email protected]
ssh [email protected]

Perintah pertama membuat pasangan kunci di ~/.ssh/, yang kedua menempelkan kunci publik ke server, dan yang ketiga adalah cara kita bekerja mulai sekarang: terminal yang sama seperti biasa, hanya saja berjalan di mesin di ruang sebelah. Mekanisme kunci dan opsi sshd yang lebih ketat dibahas di ssh dan scp ; untuk proyek ini login kunci saja sudah cukup.

Diska Data Terpisah dan fstab nofail #

Sistem operasinya boleh memakai disk apa pun, tetapi data keluarga harus hidup di disk terpisah — foto dan backup membesar tanpa batas, sedangkan partisi sistem butuh ruang napasnya sendiri. Disk data kita pasang di /srv/data lewat /etc/fstab, dan opsi kuncinya adalah nofail: bila disk itu suatu hari mati, server tetap boot dan kita masih bisa masuk lewat SSH untuk memperbaiki — tanpa nofail, boot tersangkut menunggu disk yang tidak akan pernah datang. Struktur lengkap berkas fstab dan arti tiap kolomnya sudah dibedah di fstab ; di sini kita fokus pada cara memvalidasinya. Karena lingkungan latihan tidak punya disk sungguhan, kita bekerja pada fixture berkas — tetapi pemeriksanya, findmnt --verify, adalah alat yang sama yang akan Anda jalankan di server:

bash
mkdir -p latihan/srv/data/tmp
printf 'UUID=5c1b2a3d-1111-4222-8333-444455556666 /srv/data ext4 defaults,nofail 0 2\n' > latihan/fstab.data
cat > latihan/fstab.tmpfs <<EOF
tmpfs $PWD/latihan/srv/data/tmp tmpfs size=256M,nosuid,nodev,noexec 0 0
EOF
echo "== findmnt memeriksa entri tmpfs (targetnya benar-benar ada) =="
findmnt --verify -F latihan/fstab.tmpfs
echo "exit=$?"
echo "== findmnt menolak target yang tidak ada =="
printf 'tmpfs /srv/data/tidak-ada tmpfs defaults 0 0\n' > latihan/fstab.salah
findmnt --verify -F latihan/fstab.salah 2>&1
echo "exit=$?"

Output yang Dihasilkan:

text
== findmnt memeriksa entri tmpfs (targetnya benar-benar ada) ==
Success, no errors or warnings detected
exit=0
== findmnt menolak target yang tidak ada ==

0 parse errors, 1 error, 0 warnings
/srv/data/tidak-ada
   [E] unreachable on boot required target: No such file or directory
exit=1

Bedah blok di atas:

  1. latihan/fstab.data adalah entri disk data yang akan kita tulis ke /etc/fstab di server sungguhan — sumbernya UUID (bukan /dev/sdb1 yang bisa berubah urutan), targetnya /srv/data, opsi defaults,nofail, dan kolom fsck 2 karena bukan root filesystem. Detail tiap kolom ada di fstab(5) .
  2. latihan/fstab.tmpfs sengaja memakai jalur absolut dari folder latihan sebagai target — findmnt --verify memeriksa apakah target itu benar-benar ada, jadi fixture latihan harus menunjuk folder yang bisa dibuat di sini. Entri tmpfs untuk ruang sementaranya disk data itu contoh entri tambahan yang sah di server sungguhan.
  3. findmnt --verify -F <berkas>: memeriksa berkas tabel mount yang kita tunjuk (-F, singkatan --tab-file) tanpa menyentuh /etc/fstab sungguhan. Entri tmpfs lolos karena targetnya ada; opsi lengkapnya di findmnt(8) .
  4. Entri dengan target yang tidak ada ditolak keras: unreachable on boot required target berarti saat boot, mount akan gagal menemukan foldernya — persis kelas kesalahan yang ingin kita tangkap sebelum server dinyalakan, bukan sesudah.
  5. Di server sungguhan, sudo findmnt --verify tanpa -F memeriksa /etc/fstab asli termasuk mengecek apakah UUID benar-benar terpasang — bagian yang tidak bisa kita tiru di fixture karena lingkungan latihan tidak punya block device.

Struktur /srv dan User Layanan Non-Root #

Semua yang dilayankan server ini hidup di bawah /srv — lokasi standar untuk data layanan menurut hierarki filesystem. Kita pakai empat folder: berbagi untuk file yang dibagikan, foto-keluarga untuk foto yang dikumpulkan dari ponsel, backup untuk hasil backup, dan data untuk mount disk data. Yang memilikinya bukan user login kita, melainkan user sistem khusus:

bash
sudo useradd -r -m -d /srv -s /usr/sbin/nologin berbagi
sudo mkdir -p /srv/berbagi /srv/foto-keluarga /srv/backup
sudo chown berbagi:berbagi /srv/berbagi /srv/foto-keluarga
sudo chmod 750 /srv/berbagi

Mengapa repot-repot membuat user khusus? Karena setiap layanan yang berjalan mewarisi hak akses user pemiliknya. Layanan berjalan sebagai root berarti bug atau diretasnya layanan itu langsung menjadi bencana seluruh sistem; berjalan sebagai user berbagi berarti ledakannya terbatas pada folder yang memang ia kelola. Opsi -r membuat user sistem (tanpa masa berlaku password), -m membuat home directory /srv, dan -s /usr/sbin/nologin memastikan user ini tidak bisa dipakai login — layanan meminjam identitasnya, bukan sebaliknya; rincian opsinya di useradd(8) . Di lingkungan latihan kita bangun bentuk yang sama tanpa root — folder di-recreate, permission disetel dengan chmod, dan kepemilikan diperiksa relatif terhadap user saat ini:

bash
mkdir -p latihan/srv/berbagi latihan/srv/foto-keluarga latihan/srv/backup
printf 'jadwal piknik keluarga\n' > latihan/srv/berbagi/jadwal-piknik.txt
chmod 750 latihan/srv/berbagi
chmod 755 latihan/srv latihan/srv/foto-keluarga latihan/srv/backup
tree latihan/srv
stat -c '%a %n' latihan/srv/berbagi latihan/srv/foto-keluarga latihan/srv/backup
[ -O latihan/srv/berbagi ] && echo "folder berbagi dimiliki user saat ini"

Output yang Dihasilkan:

text
latihan/srv
├── backup
├── berbagi
│   └── jadwal-piknik.txt
└── foto-keluarga

4 directories, 1 file
750 latihan/srv/berbagi
755 latihan/srv/foto-keluarga
755 latihan/srv/backup
folder berbagi dimiliki user saat ini

Tiga hal dari output itu yang akan dicek otomatis nanti: mode 750 pada berbagi (pemilik baca-tulis, grup baca, lainnya tidak bisa lihat sama sekali), mode 755 pada folder data lain (perlu dibaca layanan lain), dan kepemilikan oleh user layanan — [ -O path ] menguji kepemilikan tanpa harus mencetak nama user, supaya pemeriksaannya bisa dijalankan oleh siapa pun.

Skrip Cek Kesiapan #

Keputusan-keputusan di atas berguna hanya bila ada cara murah memastikannya terpenuhi. Skrip berikut memeriksa struktur /srv, mode, kepemilikan, dan entri fstab dalam satu eksekusi — semua targetnya bisa digeser lewat variabel lingkungan supaya pemeriksaan bisa dilatih pada fixture. Versi lengkapnya tersedia sebagai deploy/cek-kesiapan.sh pada bagian Berkas Sumber di bawah halaman ini:

bash
mkdir -p latihan/srv/berbagi latihan/srv/foto-keluarga latihan/srv/backup
printf 'jadwal piknik keluarga\n' > latihan/srv/berbagi/jadwal-piknik.txt
chmod 750 latihan/srv/berbagi
printf 'UUID=5c1b2a3d-1111-4222-8333-444455556666 /srv/data ext4 defaults,nofail 0 2\n' > latihan/fstab.data
cat > 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
echo "== layout siap =="
SRV_ROOT=latihan/srv FSTAB_FILE=latihan/fstab.data bash cek-kesiapan.sh
echo "exit=$?"
echo "== layout rusak: mode salah dan nofail hilang =="
chmod 755 latihan/srv/berbagi
sed 's/,nofail//' latihan/fstab.data > latihan/fstab.rusak
SRV_ROOT=latihan/srv FSTAB_FILE=latihan/fstab.rusak bash cek-kesiapan.sh
echo "exit=$?"

Output yang Dihasilkan:

text
== layout siap ==
OK    folder latihan/srv/berbagi (mode 750)
OK    pemilik latihan/srv/berbagi sesuai user layanan
OK    folder latihan/srv/foto-keluarga
OK    folder latihan/srv/backup
OK    entri fstab untuk /srv/data memuat nofail
server siap dijalankan: 0 temuan
exit=0
== layout rusak: mode salah dan nofail hilang ==
GAGAL folder latihan/srv/berbagi mode 755, harapan 750
OK    pemilik latihan/srv/berbagi sesuai user layanan
OK    folder latihan/srv/foto-keluarga
OK    folder latihan/srv/backup
GAGAL fstab belum memuat entri nofail untuk /srv/data
server belum siap: 2 temuan
exit=2

Pola pentingnya:

  1. SRV_ROOT=... FSTAB_FILE=... bash cek-kesiapan.sh: variabel lingkungan ditempel di depan perintah untuk eksekusi itu saja — default /srv dan /etc/fstab tetap berlaku di server sungguhan tanpa menyunting skrip.
  2. stat -c %a dan stat -c %U: permission oktal dan nama pemilik dibaca tanpa waktu modifikasi, sehingga hasilnya stabil dan cocok dibandingkan.
  3. awk ... $4 ~ /(^|,)nofail(,|$)/: opsi nofail dicocokkan sebagai satu token utuh — opsi palsu seperti mynofailx tidak lolos mengaku nofail, dan baris komentar di fstab diabaikan lewat $0 !~ /^[[:space:]]*#/.
  4. exit "$temuan": jumlah temuan menjadi exit code — 0 berarti siap, 2 berarti dua hal harus dibereskan. Pada sesi rusak di atas, chmod 755 dan hilangnya nofail sengaja dibuat supaya bentuk laporan kegagalannya kelihatan.

Fondasi kini berdiri: alamat yang stabil, akses SSH, disk data yang aman dari boot gagal, struktur /srv yang jelas, dan user layanan yang terpisah dari kehidupan root. Folder-folder itu masih kosong dan belum ada yang melayani — pekerjaan Bagian 2 adalah menghidupkan layanan pertama sebagai unit systemd yang sah.

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 ]