Fondasi dari Bagian 1 hanya menyiapkan panggungnya; bagian ini menghidupkan pemainnya. Layanan di server yang baik punya tiga ciri: dijalankan oleh systemd (bukan & di terminal yang mati saat SSH terputus), berjalan sebagai user khusus non-root, dan punya jadwal yang jelas kalau memang ia pekerja berkala. Kita menulis unit file sungguhan untuk dua layanan — berbagi file yang berjalan terus dan backup yang berjalan sekali sehari — memvalidasi strukturnya dengan systemd-analyze verify, lalu membuktikan layanan mini-nya benar-benar mendengarkan dan menjawab.

Layanan Mini, Unit Sungguhan #

Layanan berbagi file di proyek ini adalah python3 -m http.server — server HTTP statis bawaan Python yang menyajikan isi folder kerjanya. Ia dipilih bukan karena fiturnya (tidak ada autentikasi, tidak ada enkripsi), melainkan karena ia kecil, aman untuk dicoba, dan yang terpenting: cara memasangnya ke systemd sama persis dengan memasang layanan sesungguh apa pun. Unit yang kita tulis hari ini untuk http.server hanya perlu diganti ExecStart-nya untuk menjadi unit Syncthing atau Jellyfin besok — struktur section, user, hardening, dan timernya tidak berubah.

Unit pertama kita tampak seperti ini — versi lengkap tersedia sebagai config/berbagi-file.service pada bagian Berkas Sumber di bawah halaman ini:

bash
mkdir -p units
cat > units/berbagi-file.service <<'EOF'
[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

[Install]
WantedBy=multi-user.target
EOF
systemd-analyze verify units/berbagi-file.service && echo "berbagi-file.service lolos verifikasi"

Output yang Dihasilkan:

text
berbagi-file.service lolos verifikasi

systemd-analyze verify yang tidak mencetak apa-apa berarti tidak ada satu pun keluhan — itulah bentuk keberhasilannya. Bedah directive-nya satu per satu:

  1. After=network-online.target dan Wants=network-online.target: layanan menunggu jaringan siap sebelum menyala. After= hanya mengatur urutan, sedangkan Wants= yang benar-benar menarik target itu aktif — pasangan keduanya adalah pola standar untuk layanan yang butuh jaringan.
  2. Type=simple: proses di ExecStart berjalan di foreground — bentuk paling sederhana dan cocok untuk http.server yang tidak melepaskan diri ke background.
  3. User= dan Group=: inilah wajah dari keputusan Bagian 1 — layanan mewarisi hak user berbagi yang hanya menguasai /srv, bukan root.
  4. WorkingDirectory=/srv/berbagi: http.server menyajikan isi direktori kerjanya, jadi directive ini menentukan folder apa yang dibagikan — satu baris yang menghubungkan unit ke struktur folder yang kita siapkan.
  5. ExecStart=/usr/bin/python3 -m http.server 8765 --bind 0.0.0.0: perintah pertama ditulis jalur absolut (wajib), port 8765 kita pilih jauh dari port layanan sistem, dan --bind 0.0.0.0 membuatnya terjangkau dari perangkat lain di jaringan rumah. Seluruh opsi section Service terdokumentasi di systemd.service(5) .
  6. Restart=on-failure dan RestartSec=5: bila proses mati karena error, systemd menyalakannya ulang setelah 5 detik — kelebihan terbesar systemd dibanding & di terminal.
  7. WantedBy=multi-user.target: memberi tahu systemctl enable harus membuat symlink ke target mana agar layanan ikut menyala saat boot normal.

Menjalankan dan Membuktikan #

Sebelum unit dipasang, kita buktikan dulu bahwa perintahnya bekerja — menjalankan persis proses yang akan systemd jalankan, lalu memeriksanya dari luar lewat curl dan ss:

bash
mkdir -p latihan/srv/berbagi
printf 'jadwal piknik keluarga\n' > latihan/srv/berbagi/jadwal-piknik.txt
printf 'resep rendang\n' > latihan/srv/berbagi/resep-rendang.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
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8765/jadwal-piknik.txt
curl -s http://127.0.0.1:8765/resep-rendang.txt
ss -tln | awk 'NR == 1 || /:8765 /'
kill "$server_pid"
wait "$server_pid" 2>/dev/null
echo "== setelah layanan berhenti =="
ss -tln | awk '/:8765 /' | wc -l

Output yang Dihasilkan:

text
HTTP 200
resep rendang
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port
LISTEN 0      5            0.0.0.0:8765       0.0.0.0:*
== setelah layanan berhenti ==
0

Yang terjadi di dalamnya:

  1. python3 -m http.server 8765 --directory ... >/dev/null 2>&1 &: layanan dinyalakan di background dengan log dibuang — kita sengaja meniru apa yang systemd lakukan, termasuk menjalankan proses dengan --directory yang berperan sama seperti WorkingDirectory= pada unit.
  2. Loop curl + sleep 0.3: server butuh sekejap untuk mulai mendengarkan. Loop ini diam-diam mencoba hingga sepuluh kali; curl -s -o /dev/null tidak mencetak apa pun baik saat gagal maupun berhasil, jadi output blok tetap bersih.
  3. curl -w 'HTTP %{http_code}': kode status HTTP adalah bukti paling ringkas bahwa layanan menjawab — 200 berarti permintaan berkas berhasil dilayani.
  4. curl -s <berkas>: isi berkas yang sampai ke klien — bukti yang benar-benar dibaca mata, bukan sekadar angka.
  5. ss -tln: melihat dari sisi kernel — socket dalam keadaan LISTEN pada 0.0.0.0:8765. Ini penggunaan nyata dari alat yang dibahas di ss dan netstat .
  6. kill lalu ss lagi: proses dihentikan dan penghitungan baris port 8765 yang 0 membuktikan portnya benar-benar bebas — kebiasaan menutup sesi yang akan kita uji lagi di test suite.

Mendaftarkan ke Boot #

Di server sungguhan, urutan pemasangan unit selalu tiga langkah: salin berkasnya ke /etc/systemd/system/, minta systemd membaca ulang, lalu aktifkan:

bash
sudo cp config/berbagi-file.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now berbagi-file.service

enable membuat symlink dari unit ke multi-user.target sehingga layanan ikut menyala saat boot, dan --now juga langsung menjalankannya saat ini tanpa menunggu reboot. systemctl status kemudian menampilkan keadaannya; berikut contoh tampilannya di server sungguhan:

text
● berbagi-file.service - Berbagi file keluarga via HTTP sederhana
     Loaded: loaded (/etc/systemd/system/berbagi-file.service; enabled)
     Active: active (running) since Sun 2026-02-15 06:02:11 WIB; 1 day ago
   Main PID: 842 (python3)
      Tasks: 1 (limit: 4915)
     Memory: 9.6M
        CPU: 1.234s
     CGroup: /system.slice/berbagi-file.service
             └─842 /usr/bin/python3 -m http.server 8765 --bind 0.0.0.0

Baris active (running) dengan PID yang hidup adalah keadaan normal; enabled pada baris Loaded berarti symlink boot sudah terpasang. Bila kelak layanan ini mati karena error, Restart=on-failure akan menyalakannya kembali — dan jurnalnya bisa dibaca lewat journalctl -u berbagi-file, persis seperti dibahas di journalctl .

Timer Backup Harian #

Layanan kedua adalah kebalikan yang pertama: bukan berjalan terus, melainkan sekali sehari lalu pulang. Type=oneshot adalah bentuk unit untuk pekerjaan sekali jalan, dan pasangannya adalah unit timer yang memicu jadwalnya:

bash
mkdir -p units
cat > units/backup-keluarga.service <<'EOF'
[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 > units/backup-keluarga.timer <<'EOF'
[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
systemd-analyze verify units/backup-keluarga.service units/backup-keluarga.timer &&
    echo "unit backup lolos verifikasi"
systemd-analyze calendar '*-*-* 02:30:00' | grep 'Normalized form'

Output yang Dihasilkan:

text
unit backup lolos verifikasi
Normalized form: *-*-* 02:30:00

Bedahnya:

  1. Type=oneshot: proses dijalankan sampai selesai lalu unit dianggap selesai — cocok untuk backup, bukan untuk layanan yang harus selalu hidup.
  2. ExecStart=/bin/bash /usr/local/bin/backup-keluarga.sh: skrip backup dipanggil lewat /bin/bash. verify memeriksa jalur perintah pertamanya — /bin/bash pasti ada — sedangkan berkas skrip yang menjadi argumennya cukup bisa dibaca, tidak wajib executable. Skrip yang dijalankan adalah pola rsync+tar dari proyek Skrip Backup Otomatis , dipasang di /usr/local/bin pada server.
  3. Nice=10 dan IOSchedulingClass=best-effort: backup adalah warga kelas dua — ia menurunkan prioritas CPU dan I/O-nya sendiri supaya pekerjaan malam tidak bikin server terasa berat saat ada yang memakai file.
  4. Service ini sengaja tanpa [Install]: ia ditarik oleh timernya, bukan oleh boot — enable cukup untuk timernya saja, mencegah backup ikut jalan ganda setiap boot.
  5. OnCalendar=*-*-* 02:30:00: setiap hari jam 02:30. Format kalender ini punya kamus lengkap (weekly, Mon *-*-* 03:00, sampai rentang) di systemd.time(7) ; systemd-analyze calendar menerjemahkan spesifikasi kita ke bentuk ternormalisasi — baris Normalized form di output itu — dan menampilkan jadwal berikutnya.
  6. Persistent=true: bila server mati listrik pukul 01:00 dan hidup jam 07:00, jadwal 02:30 yang terlewat langsung dieksekusi saat boot — fitur yang membuat timer unggul dibanding cron untuk mesin yang tidak selalu menyala, seperti dibahas di Scheduling Scripts . Seluruh opsi unit timer ada di systemd.timer(5) .

Layanan Nyata yang Populer #

Sewaktu waktu kebutuhan keluarga melebihi layanan mini kita — dan di titik itu naik kelas bukan berarti membuang yang sudah dibangun. Tiga layanan open source paling sering dipasang di home server:

LayananFungsiPort bawaanKebutuhan
Syncthingsinkronisasi folder antar perangkat, tanpa cloud22000/tcp transfer, 8384/tcp web UIringan — cukup untuk Pi
Jellyfinserver media: film, musik, foto8096/tcpRAM sedang; transcode video butuh CPU
pi-holeDNS filter iklan untuk seluruh jaringan53 dan 80sangat ringan

Ketiganya dipasang dengan pola yang sama dengan yang barusan kita kerjakan: user khusus non-root, folder data di bawah /srv, unit service yang di-enable, dan — bila berkala — timer. Yang berubah hanya ExecStart dan portnya; Syncthing bahkan menyediakan unit file resminya sendiri. Untuk berbagi file yang kompatibel dengan Windows (protokol SMB), proyek Server File Lokal membangunnya tuntas dengan Samba — pasangan alami dari layanan mini kita yang berbasis HTTP.

Hardening Unit #

Terakhir, kita rapikan unit berbagi file dengan directive hardening. Prinsipnya sederhana: layanan hanya butuh membaca /srv/berbagi — beri dia itu saja, dan tutup pintu yang lain:

bash
mkdir -p units
cat > units/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
systemd-analyze verify units/berbagi-file.service && echo "berbagi-file.service (hardened) lolos verifikasi"
grep -E '^(User|NoNewPrivileges|ProtectSystem|ProtectHome|PrivateTmp)=' units/berbagi-file.service

Output yang Dihasilkan:

text
berbagi-file.service (hardened) lolos verifikasi
User=berbagi
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true

Empat directive hardening itu bekerja seperti ini:

  1. NoNewPrivileges=true: proses dan turunannya dilarang menaikkan hak lewat mekanisme seperti setuid — jebakan klasik “program kecil yang diam-diam memanggil su” terputus di sini.
  2. ProtectSystem=full: filesystem sistem — /usr, /boot, /etc — dipasang hanya-baca untuk proses ini. Layanan file sharing tidak punya alasan menulis ke sana, jadi tidak diberi kesempatan.
  3. ProtectHome=true: /home, /root, dan /run/user tidak terlihat sama sekali — folder pribadi keluarga tidak bisa diintip lewat bug di layanan berbagi.
  4. PrivateTmp=true: proses mendapat /tmp miliknya sendiri — berkas sementara tidak bertabrakan (dan tidak bisa disusupi lewat) proses lain yang memakai /tmp bersama.

Daftar lengkap puluhan directive sejenis — dari CapabilityBoundingSet sampai SystemCallFilter — ada di systemd.exec(5) . Perhatikan bahwa unit yang sudah di-hardening tetap lolos verify tanpa keluhan: hardening bukan tambahan yang saling bertabrakan, melainkan lapisan yang menumpuk rapi.

Dua unit kini tertulis, tervalidasi, dan layanan mininya terbukti menjawab permintaan. Tetapi server yang menyala tanpa diawasi hanya menunda kejutan — pada Bagian 3 kita memasang pengawasnya: skrip status yang membaca snapshot, melaporkan temuan, dan mengunci semuanya dengan test suite.

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 ]