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.
Menulis berbagi-file.service #
Unit pertama kita tampak seperti ini — versi lengkap tersedia sebagai config/berbagi-file.service pada bagian Berkas Sumber di bawah halaman ini:
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:
berbagi-file.service lolos verifikasisystemd-analyze verify yang tidak mencetak apa-apa berarti tidak ada satu pun keluhan — itulah bentuk keberhasilannya. Bedah directive-nya satu per satu:
After=network-online.targetdanWants=network-online.target: layanan menunggu jaringan siap sebelum menyala.After=hanya mengatur urutan, sedangkanWants=yang benar-benar menarik target itu aktif — pasangan keduanya adalah pola standar untuk layanan yang butuh jaringan.Type=simple: proses diExecStartberjalan di foreground — bentuk paling sederhana dan cocok untukhttp.serveryang tidak melepaskan diri ke background.User=danGroup=: inilah wajah dari keputusan Bagian 1 — layanan mewarisi hak userberbagiyang hanya menguasai/srv, bukan root.WorkingDirectory=/srv/berbagi:http.servermenyajikan isi direktori kerjanya, jadi directive ini menentukan folder apa yang dibagikan — satu baris yang menghubungkan unit ke struktur folder yang kita siapkan.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.0membuatnya terjangkau dari perangkat lain di jaringan rumah. Seluruh opsi section Service terdokumentasi di systemd.service(5) .Restart=on-failuredanRestartSec=5: bila proses mati karena error, systemd menyalakannya ulang setelah 5 detik — kelebihan terbesar systemd dibanding&di terminal.WantedBy=multi-user.target: memberi tahusystemctl enableharus 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:
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 -lOutput yang Dihasilkan:
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 ==
0Yang terjadi di dalamnya:
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--directoryyang berperan sama sepertiWorkingDirectory=pada unit.- Loop
curl+sleep 0.3: server butuh sekejap untuk mulai mendengarkan. Loop ini diam-diam mencoba hingga sepuluh kali;curl -s -o /dev/nulltidak mencetak apa pun baik saat gagal maupun berhasil, jadi output blok tetap bersih. curl -w 'HTTP %{http_code}': kode status HTTP adalah bukti paling ringkas bahwa layanan menjawab —200berarti permintaan berkas berhasil dilayani.curl -s <berkas>: isi berkas yang sampai ke klien — bukti yang benar-benar dibaca mata, bukan sekadar angka.ss -tln: melihat dari sisi kernel — socket dalam keadaanLISTENpada0.0.0.0:8765. Ini penggunaan nyata dari alat yang dibahas di ss dan netstat .killlalusslagi: proses dihentikan dan penghitungan baris port 8765 yang0membuktikan 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:
sudo cp config/berbagi-file.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now berbagi-file.serviceenable 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:
● 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.0Baris 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:
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:
unit backup lolos verifikasi
Normalized form: *-*-* 02:30:00Bedahnya:
Type=oneshot: proses dijalankan sampai selesai lalu unit dianggap selesai — cocok untuk backup, bukan untuk layanan yang harus selalu hidup.ExecStart=/bin/bash /usr/local/bin/backup-keluarga.sh: skrip backup dipanggil lewat/bin/bash.verifymemeriksa jalur perintah pertamanya —/bin/bashpasti 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/binpada server.Nice=10danIOSchedulingClass=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.- Service ini sengaja tanpa
[Install]: ia ditarik oleh timernya, bukan oleh boot —enablecukup untuk timernya saja, mencegah backup ikut jalan ganda setiap boot. 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 calendarmenerjemahkan spesifikasi kita ke bentuk ternormalisasi — barisNormalized formdi output itu — dan menampilkan jadwal berikutnya.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:
| Layanan | Fungsi | Port bawaan | Kebutuhan |
|---|---|---|---|
| Syncthing | sinkronisasi folder antar perangkat, tanpa cloud | 22000/tcp transfer, 8384/tcp web UI | ringan — cukup untuk Pi |
| Jellyfin | server media: film, musik, foto | 8096/tcp | RAM sedang; transcode video butuh CPU |
| pi-hole | DNS filter iklan untuk seluruh jaringan | 53 dan 80 | sangat 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:
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.serviceOutput yang Dihasilkan:
berbagi-file.service (hardened) lolos verifikasi
User=berbagi
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=trueEmpat directive hardening itu bekerja seperti ini:
NoNewPrivileges=true: proses dan turunannya dilarang menaikkan hak lewat mekanisme seperti setuid — jebakan klasik “program kecil yang diam-diam memanggilsu” terputus di sini.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.ProtectHome=true:/home,/root, dan/run/usertidak terlihat sama sekali — folder pribadi keluarga tidak bisa diintip lewat bug di layanan berbagi.PrivateTmp=true: proses mendapat/tmpmiliknya sendiri — berkas sementara tidak bertabrakan (dan tidak bisa disusupi lewat) proses lain yang memakai/tmpbersama.
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#!/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#!/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#!/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 ]