#!/usr/bin/env bash
#
# node_check.sh
# ------------------------------------------------------------------------
# 목적: 인프라팀이 OS 설치까지 완료해 인도한 물리 노드에 대해, DevOps 인수
# 테스트 관점에서 기본 HW(NIC 포함) / OS / 커널 상태를 점검하고
# 결과를 파일로 남긴다. 이후 aggregate_report.py 로 여러 노드 결과를
# 한번에 통계/이상치 분석한다.
#
# 설계 원칙:
# 1) 카테고리별로 함수 하나 = 점검 하나. 필요없으면 상단 RUN_* 값을 0으로
# 바꾸거나, 함수 자체를 지우고 main()의 호출부만 지우면 됨 (추가/삭제 용이).
# 2) 모든 점검 결과는 raw 로그(원본 명령 출력) + summary.csv(기계 판독용
# 1줄 요약) 두 군데에 남긴다. 통계 스크립트는 summary.csv만 본다.
# 3) 실제 서버를 훼손/재부팅 하는 위험한 명령은 넣지 않는다. 부하성 테스트
# (stress-ng, fio, iperf3)는 시간/강도를 상단 변수로 제한하고 기본값은
# "짧고 안전하게" 잡는다. 필요하면 값만 늘리면 됨.
# 4) status 값은 PASS / WARN / FAIL / INFO 4종.
# - PASS/FAIL: 스크립트가 자체 판단 가능한 절대 기준(에러 카운터=0 등)
# - WARN : 기준선 근처거나 확인이 필요한 값
# - INFO : 절대 기준이 없고, 여러 노드를 모아봐야 이상치를 알 수
# 있는 값(예: iperf3 처리량, fio IOPS). aggregate 스크립트가
# 노드간 편차로 이상치를 잡아준다.
# 판단 기준의 근거는 README.md 참고.
#
# 사용법:
# sudo ./node_check.sh
# 결과: /var/log/node-accept/<hostname>_<timestamp>/ 아래
#
set -u
# 주의: set -e 는 의도적으로 사용하지 않음. 점검 스크립트 특성상 개별 명령
# 실패(예: 특정 툴 미설치)가 스크립트 전체를 중단시키면 안 되기 때문.
# =========================================================================
# 0. 전역 설정 (여기서 on/off 스위치와 기준값을 관리한다)
# =========================================================================
NODE_NAME="$(hostname)"
TS="$(date +%Y%m%d_%H%M%S)"
RESULT_ROOT="${RESULT_ROOT:-/var/log/node-accept}"
OUTDIR="${RESULT_ROOT}/${NODE_NAME}_${TS}"
RAWDIR="${OUTDIR}/raw"
SUMMARY_CSV="${OUTDIR}/summary.csv"
# --- 카테고리 on/off 스위치 (1=실행, 0=skip) -----------------------------
# 필요 없는 점검은 여기서 0으로 끄면 된다. 새 점검을 추가할 때도
# 스위치부터 하나 만들고 시작할 것.
RUN_INVENTORY=1 # 시스템/BIOS/재고 정보
RUN_CPU_INFO=1 # CPU 스펙/거버너 확인
RUN_CPU_STRESS=1 # CPU 부하 테스트 (stress-ng)
RUN_MEM_INFO=1 # 메모리 슬롯/NUMA/ECC 확인
RUN_MEM_STRESS=1 # 메모리 부하 테스트 (stress-ng --vm)
RUN_STORAGE_INFO=1 # 디스크 SMART/헬스
RUN_STORAGE_FIO=0 # fio 성능 테스트 (기본 OFF: 대상 경로를 반드시 지정해야 함, 아래 FIO_TESTDIR 확인)
RUN_NIC_INFO=1 # NIC 링크/드라이버/오프로드/에러카운터
RUN_NIC_IPERF=0 # iperf3 처리량 (기본 OFF: 상대 서버(IPERF_TARGET) 지정 필요)
RUN_OS_KERNEL=1 # 커널/모듈/sysctl/swap 등 OS 기본 상태
RUN_TIME_SYNC=1 # NTP/chrony 동기화 상태
# --- 부하 테스트 파라미터 (기본은 짧고 가볍게) ----------------------------
CPU_STRESS_DURATION=60 # 초
MEM_STRESS_DURATION=60 # 초
MEM_STRESS_PERCENT=70 # 전체 메모리 대비 사용 비율(%)
FIO_RUNTIME=30 # 초
FIO_SIZE="2G"
FIO_TESTDIR="${FIO_TESTDIR:-/data/fio-test}" # 실제 대상 디스크 마운트 경로로 변경할 것
IPERF_TARGET="${IPERF_TARGET:-}" # 예: IPERF_TARGET=10.0.0.5 ./node_check.sh
IPERF_DURATION=15
# --- 판단 기준값 (환경에 맞게 조정) --------------------------------------
THRESH_NIC_MIN_SPEED_MBPS=10000 # 기대 최소 링크 속도(Mb/s). 10G 이하 노드면 낮출 것
THRESH_MEM_MIN_GB=0 # 0이면 스펙 비교 생략(로그로만 기록)
THRESH_SWAP_MUST_BE_ZERO=1 # K8s 노드는 swap 0이어야 함
# =========================================================================
# 1. 공통 유틸
# =========================================================================
mkdir -p "$RAWDIR"
echo "category|check|value|unit|status|note" > "$SUMMARY_CSV"
log() { echo "[$(date +%H:%M:%S)] $*"; }
have() { command -v "$1" >/dev/null 2>&1; }
# record <category> <check> <value> <unit> <status> <note>
# summary.csv 에 한 줄 남긴다. 값에 파이프(|)가 들어가지 않도록 주의.
record() {
local category="$1" check="$2" value="$3" unit="$4" status="$5" note="${6:-}"
echo "${category}|${check}|${value}|${unit}|${status}|${note}" >> "$SUMMARY_CSV"
}
# 결과를 raw 로그 파일로 저장하며 화면에도 출력
run_and_log() {
local logfile="$1"; shift
{ echo "### CMD: $*"; echo "### TIME: $(date)"; echo; "$@"; } >> "${RAWDIR}/${logfile}" 2>&1
}
# =========================================================================
# 2. 사전 요구 도구 체크 (없으면 WARN만 남기고 계속 진행)
# =========================================================================
check_prereqs() {
log "필수 도구 확인 중..."
local tools=(dmidecode lscpu lspci lsblk ethtool smartctl nvme sensors \
stress-ng fio iperf3 chronyc numactl lldpctl ipmitool)
for t in "${tools[@]}"; do
if have "$t"; then
record "prereq" "$t" "installed" "" "PASS" ""
else
record "prereq" "$t" "missing" "" "WARN" "관련 점검이 스킵되거나 정보가 누락될 수 있음. 설치 필요시: apt/yum install"
fi
done
}
# =========================================================================
# 3. 인벤토리/BIOS
# =========================================================================
check_inventory() {
log "인벤토리/BIOS 정보 수집 중..."
run_and_log "inventory_dmidecode.log" dmidecode
run_and_log "inventory_lspci.log" lspci -vvv
local sys_vendor sys_model bios_ver
sys_vendor=$(dmidecode -s system-manufacturer 2>/dev/null | head -1)
sys_model=$(dmidecode -s system-product-name 2>/dev/null | head -1)
bios_ver=$(dmidecode -s bios-version 2>/dev/null | head -1)
record "inventory" "system_vendor" "${sys_vendor:-unknown}" "" "INFO" "발주 스펙과 육안 대조"
record "inventory" "system_model" "${sys_model:-unknown}" "" "INFO" "발주 스펙과 육안 대조"
record "inventory" "bios_version" "${bios_ver:-unknown}" "" "INFO" "동일 랙/클러스터 노드간 버전 일치 여부 확인"
# BMC(iDRAC/iLO 등) 연결 확인 (있는 경우만)
if have ipmitool; then
if ipmitool mc info >/dev/null 2>>"${RAWDIR}/inventory_bmc.log"; then
record "inventory" "bmc_reachable" "yes" "" "PASS" ""
else
record "inventory" "bmc_reachable" "no" "" "WARN" "BMC 접근 불가 - 로컬에서 IPMI 미지원 환경일 수 있음, 별도 확인 필요"
fi
fi
}
# =========================================================================
# 4. CPU
# =========================================================================
check_cpu_info() {
log "CPU 정보 수집 중..."
run_and_log "cpu_lscpu.log" lscpu
run_and_log "cpu_numa.log" numactl -H
local model cores threads governor
model=$(lscpu | awk -F: '/Model name/{gsub(/^ +/,"",$2); print $2; exit}')
cores=$(lscpu | awk -F: '/^CPU\(s\):/{gsub(/^ +/,"",$2); print $2; exit}')
governor=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo "unknown")
record "cpu" "model" "${model:-unknown}" "" "INFO" ""
record "cpu" "logical_cores" "${cores:-unknown}" "count" "INFO" "발주 스펙과 대조"
if [[ "$governor" == "performance" ]]; then
record "cpu" "scaling_governor" "$governor" "" "PASS" ""
elif [[ "$governor" == "unknown" ]]; then
record "cpu" "scaling_governor" "$governor" "" "WARN" "cpufreq 인터페이스 없음(가상화/일부 플랫폼) - 확인 필요"
else
record "cpu" "scaling_governor" "$governor" "" "WARN" "K8s 노드는 performance governor 권장. 현재 $governor"
fi
}
check_cpu_stress() {
log "CPU 부하 테스트 (${CPU_STRESS_DURATION}s) 중..."
if ! have stress-ng; then
record "cpu" "stress_test" "skipped" "" "WARN" "stress-ng 미설치"
return
fi
local ncpu; ncpu=$(nproc)
run_and_log "cpu_stress.log" stress-ng --cpu "$ncpu" --timeout "${CPU_STRESS_DURATION}s" --metrics-brief
if grep -q "successful run completed" "${RAWDIR}/cpu_stress.log" 2>/dev/null; then
record "cpu" "stress_test" "completed" "" "PASS" "${CPU_STRESS_DURATION}s 전체 코어 부하 정상 완료"
else
record "cpu" "stress_test" "failed_or_incomplete" "" "FAIL" "raw 로그(cpu_stress.log) 확인 필요, 크래시/OOM/throttle 의심"
fi
# 온도/쓰로틀링 (있는 경우)
if have sensors; then
run_and_log "cpu_sensors_after_stress.log" sensors
record "cpu" "sensors_snapshot" "collected" "" "INFO" "raw 로그에서 부하 직후 온도 확인, throttle 여부는 turbostat 권장"
fi
}
# =========================================================================
# 5. 메모리
# =========================================================================
check_mem_info() {
log "메모리 정보 수집 중..."
run_and_log "mem_dmidecode.log" dmidecode -t memory
run_and_log "mem_free.log" free -h
local total_gb populated empty
total_gb=$(free -g | awk '/Mem:/{print $2}')
populated=$(dmidecode -t memory 2>/dev/null | grep -c "Size: [0-9]")
empty=$(dmidecode -t memory 2>/dev/null | grep -c "Size: No Module Installed")
record "memory" "total_memory" "${total_gb:-unknown}" "GB" "INFO" "발주 스펙과 대조"
record "memory" "populated_dimm_slots" "${populated:-unknown}" "count" "INFO" "채널 밸런스 확인 - 슬롯 배치가 균등해야 성능 저하 없음"
record "memory" "empty_dimm_slots" "${empty:-unknown}" "count" "INFO" ""
# ECC 에러 체크
if have ras-mc-ctl; then
run_and_log "mem_ecc.log" ras-mc-ctl --summary
if grep -qE "Corrected|Uncorrected" "${RAWDIR}/mem_ecc.log" 2>/dev/null && \
! grep -q "0 Corrected" "${RAWDIR}/mem_ecc.log" 2>/dev/null; then
record "memory" "ecc_errors" "detected" "" "WARN" "raw 로그(mem_ecc.log) 확인, 반복 발생시 FAIL로 격상 권장"
else
record "memory" "ecc_errors" "none_detected" "" "PASS" ""
fi
else
record "memory" "ecc_errors" "not_checked" "" "WARN" "ras-mc-ctl(edac-utils) 미설치, dmesg 내 EDAC/MCE 로그 수동 확인 권장"
fi
run_and_log "mem_dmesg_mce.log" bash -c "dmesg | grep -iE 'mce|edac' || echo 'no mce/edac lines found'"
}
check_mem_stress() {
log "메모리 부하 테스트 (${MEM_STRESS_DURATION}s, ${MEM_STRESS_PERCENT}%) 중..."
if ! have stress-ng; then
record "memory" "stress_test" "skipped" "" "WARN" "stress-ng 미설치"
return
fi
run_and_log "mem_stress.log" stress-ng --vm 2 --vm-bytes "${MEM_STRESS_PERCENT}%" \
--timeout "${MEM_STRESS_DURATION}s" --metrics-brief --verify
if grep -q "successful run completed" "${RAWDIR}/mem_stress.log" 2>/dev/null; then
record "memory" "stress_test" "completed" "" "PASS" "--verify 옵션으로 메모리 비교검증 포함"
else
record "memory" "stress_test" "failed_or_incomplete" "" "FAIL" "raw 로그(mem_stress.log) 확인 필요, 비트 오류/OOM 의심"
fi
}
# =========================================================================
# 6. 스토리지
# =========================================================================
check_storage_info() {
log "스토리지 정보 수집 중..."
run_and_log "storage_lsblk.log" lsblk -o NAME,SIZE,MODEL,ROTA,TYPE,MOUNTPOINT
for dev in /dev/sd? /dev/nvme?n1; do
[[ -e "$dev" ]] || continue
local base; base=$(basename "$dev")
if have smartctl; then
run_and_log "storage_smart_${base}.log" smartctl -a "$dev"
local health
health=$(smartctl -H "$dev" 2>/dev/null | grep -iE "overall-health|SMART overall-health")
if echo "$health" | grep -qi "PASSED"; then
record "storage" "smart_health_${base}" "PASSED" "" "PASS" ""
elif [[ -n "$health" ]]; then
record "storage" "smart_health_${base}" "$health" "" "FAIL" "SMART 헬스 이상, 즉시 확인 필요"
else
record "storage" "smart_health_${base}" "unknown" "" "WARN" "smartctl 결과 파싱 실패, raw 로그 확인"
fi
fi
done
if have nvme; then
for dev in /dev/nvme?; do
[[ -e "$dev" ]] || continue
run_and_log "storage_nvme_smartlog_$(basename "$dev").log" nvme smart-log "$dev"
done
fi
}
check_storage_fio() {
log "fio 성능 테스트 중... (대상: ${FIO_TESTDIR})"
if ! have fio; then
record "storage" "fio_test" "skipped" "" "WARN" "fio 미설치"
return
fi
mkdir -p "$FIO_TESTDIR"
run_and_log "storage_fio.log" fio --name=accept_randrw --directory="$FIO_TESTDIR" \
--rw=randrw --bs=4k --iodepth=32 --numjobs=4 --size="$FIO_SIZE" \
--runtime="$FIO_RUNTIME" --time_based --group_reporting
local read_iops write_iops
read_iops=$(grep -A1 "read:" "${RAWDIR}/storage_fio.log" | grep "IOPS=" | head -1 | sed -E 's/.*IOPS=([0-9.k]+).*/\1/')
write_iops=$(grep -A1 "write:" "${RAWDIR}/storage_fio.log" | grep "IOPS=" | head -1 | sed -E 's/.*IOPS=([0-9.k]+).*/\1/')
# 절대 임계값이 없으므로 INFO로 기록 -> aggregate 스크립트가 노드간 편차로 이상치 판단
record "storage" "fio_randread_iops" "${read_iops:-unknown}" "IOPS" "INFO" "노드간 편차는 aggregate 리포트 참고"
record "storage" "fio_randwrite_iops" "${write_iops:-unknown}" "IOPS" "INFO" "노드간 편차는 aggregate 리포트 참고"
# 테스트 파일 정리
rm -f "${FIO_TESTDIR}"/accept_randrw.* 2>/dev/null || true
}
# =========================================================================
# 7. NIC
# =========================================================================
check_nic_info() {
log "NIC 정보 수집 중..."
local ifaces
ifaces=$(ls /sys/class/net | grep -vE '^(lo|docker|veth|cni|cilium|virbr)' )
for ifc in $ifaces; do
run_and_log "nic_${ifc}_ethtool.log" ethtool "$ifc"
run_and_log "nic_${ifc}_ethtool_i.log" ethtool -i "$ifc"
run_and_log "nic_${ifc}_ethtool_stats.log" ethtool -S "$ifc"
run_and_log "nic_${ifc}_ethtool_offload.log" ethtool -k "$ifc"
run_and_log "nic_${ifc}_ethtool_channels.log" ethtool -l "$ifc"
local speed link driver
speed=$(ethtool "$ifc" 2>/dev/null | awk -F: '/Speed/{gsub(/[^0-9]/,"",$2); print $2}')
link=$(ethtool "$ifc" 2>/dev/null | awk -F: '/Link detected/{gsub(/^ +/,"",$2); print $2}')
driver=$(ethtool -i "$ifc" 2>/dev/null | awk -F: '/^driver/{gsub(/^ +/,"",$2); print $2}')
record "nic" "${ifc}_link_detected" "${link:-unknown}" "" \
"$([[ "$link" == "yes" ]] && echo PASS || echo WARN)" \
"$([[ "$link" == "yes" ]] || echo '링크 다운 상태 - 케이블/스위치 포트 확인')"
if [[ -n "${speed:-}" ]]; then
if (( speed >= THRESH_NIC_MIN_SPEED_MBPS )); then
record "nic" "${ifc}_speed" "$speed" "Mb/s" "PASS" ""
else
record "nic" "${ifc}_speed" "$speed" "Mb/s" "WARN" "기대 최소 속도(${THRESH_NIC_MIN_SPEED_MBPS}Mb/s) 미만 - 스위치 포트 협상 설정 확인"
fi
else
record "nic" "${ifc}_speed" "unknown" "Mb/s" "WARN" "속도 조회 실패(링크다운 등)"
fi
record "nic" "${ifc}_driver" "${driver:-unknown}" "" "INFO" ""
# 에러/드랍 카운터 합산 체크
local errsum
errsum=$(ethtool -S "$ifc" 2>/dev/null | awk -F: '/err|drop|discard/I{gsub(/[^0-9]/,"",$2); if($2+0>0) sum+=$2} END{print sum+0}')
if [[ "$errsum" == "0" ]]; then
record "nic" "${ifc}_error_counters" "0" "count" "PASS" ""
else
record "nic" "${ifc}_error_counters" "$errsum" "count" "FAIL" "raw 로그(nic_${ifc}_ethtool_stats.log)에서 어떤 카운터인지 확인 필요"
fi
done
# LLDP로 실제 연결된 스위치/포트 확인 (있는 경우)
if have lldpctl; then
run_and_log "nic_lldp.log" lldpctl
record "nic" "lldp_neighbors" "collected" "" "INFO" "raw 로그와 배선도(스위치/포트) 대조 필요"
else
record "nic" "lldp_neighbors" "not_checked" "" "WARN" "lldpd/lldpad 미설치 - 배선 오결선 확인 불가"
fi
}
check_nic_iperf() {
if [[ -z "$IPERF_TARGET" ]]; then
record "nic" "iperf3_throughput" "skipped" "" "WARN" "IPERF_TARGET 환경변수 미지정 (상대 서버 IP 필요)"
return
fi
log "iperf3 처리량 테스트 -> ${IPERF_TARGET} (${IPERF_DURATION}s) 중..."
if ! have iperf3; then
record "nic" "iperf3_throughput" "skipped" "" "WARN" "iperf3 미설치"
return
fi
run_and_log "nic_iperf3.log" iperf3 -c "$IPERF_TARGET" -t "$IPERF_DURATION" -J
local gbps
gbps=$(grep -o '"bits_per_second":[0-9.]*' "${RAWDIR}/nic_iperf3.log" | tail -1 | cut -d: -f2)
if [[ -n "$gbps" ]]; then
local mbps; mbps=$(awk -v b="$gbps" 'BEGIN{printf "%.0f", b/1000000}')
record "nic" "iperf3_throughput" "$mbps" "Mb/s" "INFO" "이론 링크속도 대비/노드간 편차는 aggregate 리포트 참고"
else
record "nic" "iperf3_throughput" "unknown" "Mb/s" "WARN" "결과 파싱 실패, raw 로그 확인"
fi
}
# =========================================================================
# 8. OS / 커널 기본 상태
# =========================================================================
check_os_kernel() {
log "OS/커널 상태 점검 중..."
run_and_log "os_uname.log" uname -a
run_and_log "os_sysctl_all.log" sysctl -a
local kver; kver=$(uname -r)
record "os" "kernel_version" "$kver" "" "INFO" "K8s/CNI(Cilium) 호환 매트릭스와 대조"
# swap
local swaptotal
swaptotal=$(free -m | awk '/Swap:/{print $2}')
if [[ "$THRESH_SWAP_MUST_BE_ZERO" == "1" ]]; then
if [[ "$swaptotal" == "0" ]]; then
record "os" "swap_total" "0" "MB" "PASS" ""
else
record "os" "swap_total" "$swaptotal" "MB" "FAIL" "K8s 노드는 swap off 필요 (swapoff -a, fstab 수정)"
fi
fi
# 필수 커널 모듈
local mods=(overlay br_netfilter)
for m in "${mods[@]}"; do
if lsmod | grep -q "^${m}"; then
record "os" "kmod_${m}" "loaded" "" "PASS" ""
else
record "os" "kmod_${m}" "not_loaded" "" "FAIL" "modprobe ${m} 필요 (K8s 필수 모듈)"
fi
done
# 필수 sysctl (README의 baseline 값과 비교하고 싶으면 여기 임계값 추가)
local ip_forward
ip_forward=$(sysctl -n net.ipv4.ip_forward 2>/dev/null)
record "os" "net.ipv4.ip_forward" "${ip_forward:-unknown}" "" \
"$([[ "$ip_forward" == "1" ]] && echo PASS || echo FAIL)" \
"$([[ "$ip_forward" == "1" ]] || echo 'K8s 필수: sysctl -w net.ipv4.ip_forward=1')"
local max_map
max_map=$(sysctl -n vm.max_map_count 2>/dev/null)
record "os" "vm.max_map_count" "${max_map:-unknown}" "" \
"$([[ "${max_map:-0}" -ge 262144 ]] 2>/dev/null && echo PASS || echo WARN)" \
"권장 262144 이상 (README 참고)"
# SELinux/AppArmor 상태
if have getenforce; then
record "os" "selinux_status" "$(getenforce)" "" "INFO" "설계된 정책과 일치하는지 확인"
fi
if have aa-status; then
run_and_log "os_apparmor.log" aa-status
record "os" "apparmor_status" "collected" "" "INFO" "raw 로그 확인"
fi
# ulimit
record "os" "ulimit_nofile" "$(ulimit -n)" "" "INFO" "권장 baseline과 비교 (README)"
record "os" "ulimit_nproc" "$(ulimit -u)" "" "INFO" "권장 baseline과 비교 (README)"
}
# =========================================================================
# 9. 시간 동기화
# =========================================================================
check_time_sync() {
log "시간 동기화 상태 확인 중..."
if have chronyc; then
run_and_log "time_chrony.log" chronyc tracking
local offset
offset=$(chronyc tracking 2>/dev/null | awk -F: '/System time/{gsub(/[^0-9.]/,"",$2); print $2}')
if [[ -n "$offset" ]]; then
# 100ms 이상 벗어나면 WARN
if awk -v o="$offset" 'BEGIN{exit !(o<0.1)}'; then
record "time" "chrony_offset_sec" "$offset" "sec" "PASS" ""
else
record "time" "chrony_offset_sec" "$offset" "sec" "WARN" "오프셋 100ms 이상, NTP 서버 접근성 확인"
fi
else
record "time" "chrony_offset_sec" "unknown" "sec" "WARN" "raw 로그(time_chrony.log) 확인"
fi
else
record "time" "chrony_status" "not_checked" "" "WARN" "chrony 미설치 - 다른 NTP 클라이언트 확인 필요"
fi
}
# =========================================================================
# 10. main
# =========================================================================
main() {
log "노드 인수 점검 시작: ${NODE_NAME} -> ${OUTDIR}"
check_prereqs
[[ "$RUN_INVENTORY" == "1" ]] && check_inventory
[[ "$RUN_CPU_INFO" == "1" ]] && check_cpu_info
[[ "$RUN_CPU_STRESS" == "1" ]] && check_cpu_stress
[[ "$RUN_MEM_INFO" == "1" ]] && check_mem_info
[[ "$RUN_MEM_STRESS" == "1" ]] && check_mem_stress
[[ "$RUN_STORAGE_INFO" == "1" ]] && check_storage_info
[[ "$RUN_STORAGE_FIO" == "1" ]] && check_storage_fio
[[ "$RUN_NIC_INFO" == "1" ]] && check_nic_info
[[ "$RUN_NIC_IPERF" == "1" ]] && check_nic_iperf
[[ "$RUN_OS_KERNEL" == "1" ]] && check_os_kernel
[[ "$RUN_TIME_SYNC" == "1" ]] && check_time_sync
local fail warn
fail=$(awk -F'|' '$5=="FAIL"' "$SUMMARY_CSV" | wc -l)
warn=$(awk -F'|' '$5=="WARN"' "$SUMMARY_CSV" | wc -l)
log "점검 완료. FAIL=${fail}, WARN=${warn}"
log "결과 디렉터리: ${OUTDIR}"
log "요약 파일: ${SUMMARY_CSV}"
echo
echo "=== FAIL 항목 ==="
awk -F'|' '$5=="FAIL"{print}' "$SUMMARY_CSV"
echo
echo "=== WARN 항목 ==="
awk -F'|' '$5=="WARN"{print}' "$SUMMARY_CSV"
}
main "$@"