Linux gir02.nhanhoa.com 4.18.0-513.9.1.lve.el8.x86_64 #1 SMP Mon Dec 4 15:01:22 UTC 2023 x86_64
LiteSpeed
Server IP : 103.124.95.33 & Your IP : 216.73.217.104
Domains :
Cant Read [ /etc/named.conf ]
User : nhsacmvr
Terminal
Auto Root
Create File
Create Folder
Localroot Suggester
Backdoor Destroyer
Readme
/
opt /
nhanhoa /
checkdisk2 /
Delete
Unzip
Name
Size
Permission
Date
Action
json
[ DIR ]
drwxr-xr-x
2026-07-26 09:09
check.sh
38.53
KB
-rwxr-xr-x
2026-07-26 09:09
disks.html
58.16
KB
-rw-r--r--
2026-07-26 09:09
history.db
5
KB
-rw-r--r--
2026-07-26 09:09
storcli.log
1.2
MB
-rw-r--r--
2026-08-07 02:04
storcli64
8.18
MB
-rwxr-xr-x
2026-07-26 08:56
Save
Rename
#!/bin/bash # ============================================================================== # checkdisk2 - check.sh # Thu thập SMART mọi loại ổ (SATA/SAS/NVMe, trực tiếp hoặc sau RAID/HBA) # Output: /var/log/checkdisk2.json # Audit log: /var/log/checkdisk2_debug.log (mỗi lần chạy ghi đè) # - Muốn xem log realtime trên màn hình: DEBUG=1 ./check.sh # - Khi chạy trực tiếp trên terminal (tty), log cũng tự hiện ra stderr # ============================================================================== LOG_FILE="${CHECKDISK_LOG:-/var/log/checkdisk2_debug.log}" OUT_FILE="${CHECKDISK_OUT:-/var/log/checkdisk2.json}" : > "$LOG_FILE" 2>/dev/null || LOG_FILE="/tmp/checkdisk2_debug.log" && : > "$LOG_FILE" log() { local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*" echo "$msg" >> "$LOG_FILE" # Hiện ra màn hình khi DEBUG=1 hoặc đang chạy trên terminal if [ "$DEBUG" = "1" ] || [ -t 2 ]; then echo "$msg" >&2 fi } # Trích model + serial từ output smartctl (JSON hoặc TEXT) để ghi log (không cần python) log_disk() { # $1 = prefix (buoc), $2 = device mô tả, $3 = RAW output (json hoặc text) local mdl srl mdl=$(echo "$3" | grep -o '"model_name": *"[^"]*"' | head -1 | cut -d'"' -f4) [ -z "$mdl" ] && mdl=$(echo "$3" | grep -o '"scsi_model_name": *"[^"]*"' | head -1 | cut -d'"' -f4) [ -z "$mdl" ] && mdl=$(echo "$3" | grep -o '"scsi_product": *"[^"]*"' | head -1 | cut -d'"' -f4) srl=$(echo "$3" | grep -o '"serial_number": *"[^"]*"' | head -1 | cut -d'"' -f4) # Fallback cho smartctl text mode (bản cũ không có -j) [ -z "$mdl" ] && mdl=$(echo "$3" | grep -E "^(Device Model|Model Number|Product):" | head -1 | cut -d: -f2- | sed 's/^ *//') [ -z "$srl" ] && srl=$(echo "$3" | grep -iE "^Serial [Nn]umber:" | head -1 | cut -d: -f2- | sed 's/^ *//') log "$1 -> TIM THAY o: $2 | model: ${mdl:-?} | serial: ${srl:-?}" } log "===== BAT DAU checkdisk2 =====" # ============================================================================== # BƯỚC 1: THU THẬP THÔNG TIN HỆ THỐNG (IP, HOSTNAME, STORCLI, LSBLK) # ============================================================================== log "[BUOC1] Lay Public IP (curl -4 ifconfig.me, timeout 5s)..." PUBLIC_IP=$(curl -4 -s -m 5 ifconfig.me) # Validate: chi chap nhan dung dang IPv4 (tranh trang loi/HTML cua proxy lot vao lam key IP) echo "$PUBLIC_IP" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}$' || PUBLIC_IP="" # Fallback: lay IPv4 dau tien tu hostname -I (loc bo IPv6) [ -z "$PUBLIC_IP" ] && PUBLIC_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]{1,3}(\.[0-9]{1,3}){3}$' | head -1) && log "[BUOC1] curl fail -> fallback hostname -I (IPv4 dau tien)" [ -z "$PUBLIC_IP" ] && PUBLIC_IP=$(hostname) && log "[BUOC1] khong tim duoc IPv4 -> fallback hostname" HOST_NAME=$(hostname) log "[BUOC1] Public IP: $PUBLIC_IP | Hostname: $HOST_NAME" # Thu thập lsblk - Encode Base64 # Lấy TOÀN BỘ block device (kể cả LVM/crypt/md) kèm PKNAME (parent) để Python # đi ngược cây: lv_var -> nvme0n1p1 -> nvme0n1, map mountpoint về ổ VẬT LÝ gốc. # Fallback nhiều tầng cho util-linux cũ: # 1. NAME,MOUNTPOINTS,WWN,PKNAME,TYPE (mới nhất - MOUNTPOINTS = nhiều mountpoint/dòng) # 2. NAME,MOUNTPOINT,WWN,PKNAME,TYPE (không có cột MOUNTPOINTS) # 3. NAME,MOUNTPOINT,WWN (không có PKNAME - mất khả năng dò LVM) # 4. NAME,MOUNTPOINT (không có WWN) # 5. /proc/mounts (không hỗ trợ -P) LSBLK_RAW=$(lsblk -P -o NAME,MOUNTPOINTS,WWN,PKNAME,TYPE 2>/dev/null) if [ -z "$LSBLK_RAW" ]; then LSBLK_RAW=$(lsblk -P -o NAME,MOUNTPOINT,WWN,PKNAME,TYPE 2>/dev/null) [ -n "$LSBLK_RAW" ] && log "[BUOC1] lsblk: khong co cot MOUNTPOINTS -> dung MOUNTPOINT (don)" fi if [ -z "$LSBLK_RAW" ]; then log "[BUOC1] lsblk: khong co PKNAME (util-linux cu) -> mat kha nang do nguoc LVM, dung mapping truc tiep" LSBLK_RAW=$(lsblk -P -o NAME,MOUNTPOINT,WWN 2>/dev/null | grep -E "NAME=\"(sd|nvme)") fi if [ -z "$LSBLK_RAW" ]; then log "[BUOC1] lsblk voi cot WWN that bai -> thu lai khong co WWN" LSBLK_RAW=$(lsblk -P -o NAME,MOUNTPOINT 2>/dev/null | grep -E "NAME=\"(sd|nvme)" | sed 's/$/ WWN=""/') fi if [ -z "$LSBLK_RAW" ]; then log "[BUOC1] lsblk -P khong ho tro -> fallback dung /proc/mounts de map mountpoint" LSBLK_RAW=$(awk '$1 ~ /^\/dev\/(sd|nvme)/ {n=$1; sub("^/dev/","",n); print "NAME=\"" n "\" MOUNTPOINT=\"" $2 "\" WWN=\"\""}' /proc/mounts 2>/dev/null) fi LSBLK_BASE64=$(echo "$LSBLK_RAW" | base64 -w 0) log "[BUOC1] lsblk: thu duoc $(echo "$LSBLK_RAW" | grep -c .) dong block device" # Tự dò storcli64 (luôn dò, không phụ thuộc phát hiện controller) STORCLI_BIN="" for candidate in "/opt/nhanhoa/checkdisk2/storcli64" "/opt/checkdisk2/storcli64" "/opt/MegaRAID/storcli/storcli64" "$(command -v storcli64 2>/dev/null)"; do if [ -n "$candidate" ] && [ -x "$candidate" ]; then STORCLI_BIN="$candidate" break fi done STORCLI_JSON="" STORCLI_BASE64="" if [ -n "$STORCLI_BIN" ]; then log "[BUOC1] storcli64: dung $STORCLI_BIN" STORCLI_JSON=$("$STORCLI_BIN" /c0/vall show all J 2>/dev/null) if [ -n "$STORCLI_JSON" ]; then STORCLI_BASE64=$(echo "$STORCLI_JSON" | base64 -w 0) log "[BUOC1] storcli64: lay VD info thanh cong ($(echo "$STORCLI_JSON" | wc -c) bytes)" else log "[BUOC1] storcli64: chay duoc nhung /c0/vall khong tra du lieu (co the khong co controller /c0)" fi else log "[BUOC1] storcli64: KHONG tim thay -> bo qua mapping VD (binh thuong neu server khong co PERC)" fi # Danh sách ổ ảo (Virtual Drive) của PERC -> phải BỎ QUA khi quét trực tiếp/lsscsi VD_DEVS="" [ -n "$STORCLI_JSON" ] && VD_DEVS=$(echo "$STORCLI_JSON" | grep -o '"OS Drive Name"[^,}]*' | grep -o '/dev/[a-z0-9]*' | sort -u) [ -n "$VD_DEVS" ] && log "[BUOC1] Danh sach o AO (Virtual Drive) se bo qua khi quet truc tiep: $(echo $VD_DEVS)" # Hàm kiểm tra JSON smartctl có hợp lệ không (phải có serial_number thật) is_valid_smart() { [ -n "$1" ] && [[ "$1" == \{* ]] && echo "$1" | grep -q '"serial_number"' } # ------------------------------------------------------------------------------ # ROUTE SMARTCTL CŨ: smartmontools < 7.0 KHÔNG có cờ -j (JSON output). # Phát hiện khả năng -j; nếu không có thì chạy chế độ TEXT: lấy nguyên output # `smartctl -a` dạng text, encode base64, Python engine sẽ tự parse text -> JSON. # ------------------------------------------------------------------------------ SMARTCTL_VER=$(smartctl --version 2>/dev/null | head -1) if smartctl -j --version >/dev/null 2>&1; then SMARTCTL_HAS_JSON=1 log "[BUOC1] smartctl: $SMARTCTL_VER | ho tro JSON (-j): CO" else SMARTCTL_HAS_JSON=0 log "[BUOC1] smartctl: ${SMARTCTL_VER:-khong xac dinh} | ho tro JSON (-j): KHONG -> chuyen sang TEXT MODE (tu parse output text)" fi # run_smartctl <smartctl device args...> # Thành công -> return 0, set: # RAW_PAYLOAD = fragment JSON để append ("smart_data": {...} hoặc "smart_text_b64": "...") # RAW_OUT = output gốc (để log_disk) run_smartctl() { RAW_PAYLOAD=""; RAW_OUT="" if [ "$SMARTCTL_HAS_JSON" = "1" ]; then RAW_OUT=$(smartctl "$@" -a -j 2>/dev/null) if is_valid_smart "$RAW_OUT"; then RAW_PAYLOAD="\"smart_data\": $RAW_OUT" return 0 fi else RAW_OUT=$(smartctl "$@" -a 2>/dev/null) if echo "$RAW_OUT" | grep -qiE "^Serial [Nn]umber:"; then RAW_PAYLOAD="\"smart_text_b64\": \"$(echo "$RAW_OUT" | base64 -w 0)\"" return 0 fi fi return 1 } JSON_ARRAY="[" DISK_COUNT=0 append_json() { # $1 = chuỗi JSON của 1 ổ if [ "$JSON_ARRAY" != "[" ]; then JSON_ARRAY="$JSON_ARRAY,"; fi JSON_ARRAY="$JSON_ARRAY $1" DISK_COUNT=$((DISK_COUNT + 1)) } # ============================================================================== # BƯỚC 2: QUÉT HỢP NHẤT - CHẠY TẤT CẢ CÁC LUỒNG, DEDUP THEO SERIAL Ở PYTHON # Thứ tự quét = thứ tự ưu tiên khi trùng serial: # 2a. megaraid (PERC) -> có slot/DID chính xác nhất # 2b. HBA/sg (lsscsi) -> ổ sau HBA/IT-mode, kể cả ổ không có node sd # 2c. sdX trực tiếp -> SATA/SAS cắm thẳng mainboard (AHCI onboard), USB... # 2d. NVMe (PCIe) -> luôn độc lập với RAID/HBA # ============================================================================== # --- 2a. LUỒNG RAID/megaraid (Dell PERC H310/H710/H730...) --- MEGARAID_SCAN=$(smartctl --scan 2>/dev/null | grep "megaraid") log "[BUOC2a-megaraid] smartctl --scan phat hien $(echo "$MEGARAID_SCAN" | grep -c .) device megaraid" while read -r line; do [ -z "$line" ] && continue device_path=$(echo "$line" | awk '{print $1}') driver_args=$(echo "$line" | awk '{print $2 " " $3}') disk_id=$(echo "$driver_args" | cut -d',' -f2) log "[BUOC2a-megaraid] Dang check: smartctl $driver_args $device_path (slot/DID $disk_id)" if run_smartctl $driver_args $device_path; then log_disk "[BUOC2a-megaraid]" "$device_path megaraid,$disk_id" "$RAW_OUT" append_json "{\"slot\": \"$disk_id\", $RAW_PAYLOAD}" else log "[BUOC2a-megaraid] -> slot $disk_id: khong co SMART hop le, BO QUA" fi done <<< "$MEGARAID_SCAN" # --- 2b. LUỒNG HBA/sg (lsscsi) - ổ sau HBA/IT-mode hoặc sau Logical Volume --- if command -v lsscsi >/dev/null 2>&1; then LOGICAL_VOL=$(lsscsi 2>/dev/null | grep "Logical Volume" | awk '{print $NF}' | head -n 1) [ -n "$LOGICAL_VOL" ] && log "[BUOC2b-hba] Phat hien Logical Volume: $LOGICAL_VOL" LSSCSI_DISKS=$(lsscsi -g 2>/dev/null | grep "disk" | grep -v "Logical Volume") log "[BUOC2b-hba] lsscsi: phat hien $(echo "$LSSCSI_DISKS" | grep -c .) dong 'disk'" while read -r line; do [ -z "$line" ] && continue addr=$(echo "$line" | awk '{print $1}' | tr -d '[]') sd_dev=$(echo "$line" | awk '{print $(NF-1)}') sg_dev=$(echo "$line" | awk '{print $NF}') final_os_dev="$sd_dev" is_raid_member="false" if [ "$sd_dev" == "-" ]; then final_os_dev="$LOGICAL_VOL" is_raid_member="true" else # Bỏ qua nếu sd device là Virtual Drive của PERC (ổ ảo, không phải đĩa thật) case " $VD_DEVS " in *" $sd_dev "*) log "[BUOC2b-hba] BO QUA $sd_dev ($addr): la Virtual Drive cua RAID controller" continue;; esac fi log "[BUOC2b-hba] Dang check: $sg_dev (addr $addr, os_dev $final_os_dev)" # Thử auto trước, fallback -d sat (một số HBA cần SAT translation) if run_smartctl "$sg_dev" || { log "[BUOC2b-hba] auto fail -> thu lai voi -d sat"; run_smartctl -d sat "$sg_dev"; }; then log_disk "[BUOC2b-hba]" "$sg_dev ($final_os_dev)" "$RAW_OUT" append_json "{\"slot\": \"$addr\", \"os_dev_map\": \"$final_os_dev\", \"is_raid_member\": $is_raid_member, $RAW_PAYLOAD}" else log "[BUOC2b-hba] -> $sg_dev: khong co SMART hop le, BO QUA" fi done <<< "$LSSCSI_DISKS" else log "[BUOC2b-hba] lsscsi khong duoc cai -> bo qua luong nay (o van duoc quet o luong khac)" fi # --- 2c. LUỒNG DISK CẮM TRỰC TIẾP (/dev/sdX qua AHCI onboard, không controller) --- # Bỏ qua các sdX là Virtual Drive của PERC; ổ trùng với luồng 2a/2b sẽ bị dedup theo serial log "[BUOC2c-direct] Quet cac /dev/sdX (type=disk) tu lsblk..." while read -r name dtype; do [ "$dtype" != "disk" ] && continue dev="/dev/$name" case " $VD_DEVS " in *" $dev "*) log "[BUOC2c-direct] BO QUA $dev: la Virtual Drive cua RAID controller" continue;; esac log "[BUOC2c-direct] Dang check: $dev" if run_smartctl "$dev" || { log "[BUOC2c-direct] auto fail -> thu lai voi -d sat"; run_smartctl -d sat "$dev"; }; then log_disk "[BUOC2c-direct]" "$dev" "$RAW_OUT" append_json "{\"slot\": \"$name\", \"os_dev_map\": \"$dev\", \"is_raid_member\": false, $RAW_PAYLOAD}" else log "[BUOC2c-direct] -> $dev: khong co SMART hop le (co the la o ao/VD), BO QUA" fi done < <(lsblk -dn -o NAME,TYPE 2>/dev/null | grep -E "^sd") # --- 2d. LUỒNG NVME (PCIe trực tiếp - không qua RAID/HBA controller) --- # Gộp 2 nguồn: smartctl --scan + /dev/nvmeN (phòng smartctl cũ scan sót), dedup path NVME_CTRLS=$( { smartctl --scan 2>/dev/null | grep -i "nvme" | awk '{print $1}'; \ ls /dev/nvme[0-9]* 2>/dev/null | grep -E '^/dev/nvme[0-9]+$'; } | sort -u ) log "[BUOC2d-nvme] Phat hien $(echo "$NVME_CTRLS" | grep -c .) NVMe controller: $(echo $NVME_CTRLS)" for device_path in $NVME_CTRLS; do slot_name=$(basename "$device_path") # smartctl scan ra controller (/dev/nvme0), block device thực tế thường là namespace 1 ns_dev="${device_path}n1" log "[BUOC2d-nvme] Dang check: $device_path (namespace: $ns_dev)" if run_smartctl -d nvme "$device_path"; then log_disk "[BUOC2d-nvme]" "$device_path" "$RAW_OUT" append_json "{\"slot\": \"$slot_name\", \"os_dev_map\": \"$ns_dev\", \"is_raid_member\": false, $RAW_PAYLOAD}" else log "[BUOC2d-nvme] -> $device_path: khong co SMART hop le, BO QUA" fi done JSON_ARRAY="$JSON_ARRAY]" log "[BUOC2] HOAN TAT quet: tong $DISK_COUNT ban ghi SMART (truoc dedup)" # ============================================================================== # BƯỚC 3: PYTHON ENGINE - DEDUP, MAP MOUNTPOINT, PHÂN NHÓM, TÓM TẮT SỨC KHOẺ # # ROUTE CHỌN PYTHON: # 1. python3 trong PATH (mọi bản 3.x) # 2. CloudLinux alt: /opt/alt/python3XX/bin/python3 (ưu tiên bản mới nhất) # 3. python2 / python2.7 / python (bản 2.x) - code inline đã viết tương thích cả 2 # 4. KHÔNG có python nào -> BASH FALLBACK: vẫn xuất raw JSON (không mapping/summary) # để không mất dữ liệu; disk_report.py sẽ báo host này thiếu health_summary. # # NGUYÊN TẮC OUTPUT (khi có python): # - "health_summary": tóm tắt + cảnh báo, thông số riêng theo từng loại ổ # - "ata_sata"/"sas_scsi"/"nvme": giữ nguyên 100% JSON smartctl trong "smartctl_full" # ============================================================================== PY_BIN="" PY_MAJOR="" log "[BUOC3] Do tim Python..." for candidate in python3 python3.12 python3.11 python3.10 python3.9 python3.8 python3.6; do if command -v "$candidate" >/dev/null 2>&1; then ver=$("$candidate" -c 'import sys; print(sys.version_info[0])' 2>/dev/null) if [ "$ver" = "3" ]; then PY_BIN="$candidate"; PY_MAJOR="3" break fi fi done if [ -z "$PY_BIN" ] && [ -d /opt/alt ]; then log "[BUOC3] Khong co python3 trong PATH -> do CloudLinux alt (/opt/alt/python3*)..." for altdir in $(ls -d /opt/alt/python3* 2>/dev/null | sort -rV); do if [ -x "$altdir/bin/python3" ]; then PY_BIN="$altdir/bin/python3"; PY_MAJOR="3" break fi done fi if [ -z "$PY_BIN" ]; then log "[BUOC3] Khong co python3 -> ROUTE FALLBACK: do python2..." for candidate in python2 python2.7 python2.6 python; do if command -v "$candidate" >/dev/null 2>&1; then ver=$("$candidate" -c 'import sys; print(sys.version_info[0])' 2>/dev/null) if [ "$ver" = "2" ] || [ "$ver" = "3" ]; then PY_BIN="$candidate"; PY_MAJOR="$ver" break fi fi done fi if [ -n "$PY_BIN" ]; then log "[BUOC3] Python engine: $PY_BIN (Python $PY_MAJOR)" # Log cua Python (stderr) duoc append vao cung file audit log FINAL_RESULT=$(echo "$JSON_ARRAY" | "$PY_BIN" -c ' import sys, json, collections, re, base64 def dbg(msg): sys.stderr.write("[PY-ENGINE] " + msg + "\n") def get_system_maps(): # Map: /dev/<disk vat ly> -> [mountpoints], va WWN -> /dev/<disk vat ly> # Ho tro LVM/partition/crypt/md: dung PKNAME de di nguoc cay ve o goc # (vd: vg_nvme-lv_var -> nvme0n1p1 -> nvme0n1) try: b64_lsblk = "'"$LSBLK_BASE64"'" if not b64_lsblk: return {}, {} lsblk_raw = base64.b64decode(b64_lsblk).decode("utf-8", "ignore") entries = [] for line in lsblk_raw.splitlines(): kv = dict(re.findall(r"(\w+)=\"(.*?)\"", line)) if kv.get("NAME"): entries.append(kv) disk_pat = re.compile(r"^(sd[a-z]+|vd[a-z]+|hd[a-z]+|nvme\d+n\d+)$") # name -> set(parent names) tu cot PKNAME (1 name co the xuat hien nhieu dong, # vi du LV thuoc VG trai tren 2 o NVMe -> 2 parent) parents = {} has_pkname = False for kv in entries: pk = kv.get("PKNAME", "") if pk: has_pkname = True parents.setdefault(kv["NAME"], set()).add(pk) def roots_of(name, depth=0): # Tra ve set ten o vat ly goc cua 1 block device if depth > 8: return set() if disk_pat.match(name): return set([name]) out = set() for p in parents.get(name, set()): out |= roots_of(p, depth + 1) if not out: # Fallback heuristic khi khong co PKNAME: sda1 -> sda, nvme0n1p2 -> nvme0n1 m = re.match(r"^(nvme\d+n\d+)", name) if m: return set([m.group(1)]) m = re.match(r"^((?:sd|vd|hd)[a-z]+)", name) if m: return set([m.group(1)]) return out dev_to_mounts, wwn_to_dev = {}, {} for kv in entries: name = kv["NAME"] wwn = kv.get("WWN", "") # MOUNTPOINTS (moi, nhieu gia tri phan tach bang \x0a) hoac MOUNTPOINT (don) mfield = kv.get("MOUNTPOINTS", kv.get("MOUNTPOINT", "")) mounts = [m for m in re.split(r"\\\\x0a|\\x0a", mfield) if m and m.strip()] rset = roots_of(name) if wwn: for r in rset: if wwn.lower() not in wwn_to_dev: wwn_to_dev[wwn.lower()] = "/dev/" + r for m in mounts: # Bo noise: hang tram bind-mount cagefs-skeleton cua CloudLinux if "/cagefs-skeleton" in m: continue for r in rset: key = "/dev/" + r if key not in dev_to_mounts: dev_to_mounts[key] = [] if m not in dev_to_mounts[key]: dev_to_mounts[key].append(m) # Sap xep mountpoint: duong ngan/goc len truoc (/, /var, /home...), gioi han 12/o for key in dev_to_mounts: lst = dev_to_mounts[key] lst.sort(key=lambda x: (len(x), x)) dev_to_mounts[key] = lst[:12] return dev_to_mounts, wwn_to_dev except: return {}, {} def get_storcli_mapping(): try: b64_data = "'"${STORCLI_BASE64:-}"'" if not b64_data: return {} data = json.loads(base64.b64decode(b64_data).decode("utf-8", "ignore")) resp = data["Controllers"][0]["Response Data"] mapping = {} for k in [key for key in resp.keys() if key.startswith("/c0/v")]: vid = k.split("/")[-1][1:] os_drive = resp.get("VD" + vid + " Properties", {}).get("OS Drive Name") for pd in resp.get("PDs for VD " + vid, []): did = str(pd.get("DID")) if did and os_drive: mapping[did] = os_drive return mapping except: return {} def parse_smart_text(txt): # ROUTE SMARTCTL CU (< 7.0, khong co -j): parse output TEXT cua smartctl -a # thanh dict CUNG SCHEMA voi output -j de toan bo pipeline phia sau dung chung. d = {} attrs = [] err_log = {} in_attr = False for ln in txt.splitlines(): s = ln.strip() if s.startswith("Device Model:") or s.startswith("Model Number:"): d["model_name"] = s.split(":", 1)[1].strip() elif s.startswith("Vendor:"): d["scsi_vendor"] = s.split(":", 1)[1].strip() elif s.startswith("Product:"): d["scsi_product"] = s.split(":", 1)[1].strip() elif s.startswith("Serial Number:") or s.startswith("Serial number:"): d["serial_number"] = s.split(":", 1)[1].strip() elif s.startswith("LU WWN Device Id:"): d["logical_unit_id"] = "0x" + s.split(":", 1)[1].strip().replace(" ", "").lower() elif s.startswith("Logical Unit id:"): d["logical_unit_id"] = s.split(":", 1)[1].strip().lower() elif s.startswith("User Capacity:") or s.startswith("Total NVM Capacity:") or s.startswith("Namespace 1 Size/Capacity:"): m = re.search(r"([\d][\d,\.]*)\s*bytes", s) if m and "user_capacity" not in d: try: d["user_capacity"] = {"bytes": int(re.sub(r"[,\.]", "", m.group(1)))} except: pass elif s.startswith("Rotation Rate:"): v = s.split(":", 1)[1] if "Solid State" in v: d["rotation_rate"] = 0 else: m = re.search(r"(\d+)", v) if m: d["rotation_rate"] = int(m.group(1)) elif "overall-health self-assessment" in s: d["smart_status"] = {"passed": "PASSED" in s} elif s.startswith("SMART Health Status:"): d["smart_status"] = {"passed": "OK" in s} elif s.startswith("Current Drive Temperature:") or s.startswith("Temperature:"): m = re.search(r"(\d+)\s*C", s) if m and "temperature" not in d: d["temperature"] = {"current": int(m.group(1))} elif s.startswith("Accumulated power on time"): m = re.search(r"(\d+):", s) if m: d["power_on_time"] = {"hours": int(m.group(1))} elif s.startswith("Power On Hours:"): m = re.search(r"([\d,\.]+)", s.split(":", 1)[1]) if m: d["power_on_time"] = {"hours": int(re.sub(r"[,\.]", "", m.group(1)))} elif s.startswith("Elements in grown defect list:"): m = re.search(r"(\d+)", s) if m: d["scsi_grown_defect_list"] = int(m.group(1)) elif s.startswith("Percentage Used:"): m = re.search(r"(\d+)", s) if m: d.setdefault("nvme_smart_health_information_log", {})["percentage_used"] = int(m.group(1)) elif s.startswith("Available Spare:"): m = re.search(r"(\d+)", s) if m: d.setdefault("nvme_smart_health_information_log", {})["available_spare"] = int(m.group(1)) elif s.startswith("Media and Data Integrity Errors:"): m = re.search(r"(\d+)", s) if m: d.setdefault("nvme_smart_health_information_log", {})["media_errors"] = int(m.group(1)) elif s.startswith("Critical Warning:"): m = re.search(r"0x([0-9a-fA-F]+)", s) if m: d.setdefault("nvme_smart_health_information_log", {})["critical_warning"] = int(m.group(1), 16) # Bang ATA attributes: sau header "ID# ATTRIBUTE_NAME ..." if s.startswith("ID#"): in_attr = True continue if in_attr: m = re.match(r"^(\d+)\s+([\w\-]+)\s+(?:\S+\s+){7}(\S+)", s) if m: mr = re.match(r"\d+", m.group(3)) attrs.append({"id": int(m.group(1)), "name": m.group(2), "raw": {"value": int(mr.group(0)) if mr else 0}}) elif not s: in_attr = False # Bang SCSI error counter log: "read: ... <total_uncorrected>" (cot cuoi) m = re.match(r"^(read|write|verify):\s+(.+)$", s) if m: cols = m.group(2).split() if cols: try: err_log[m.group(1)] = {"total_uncorrected_errors": int(cols[-1])} except: pass if attrs: d["ata_smart_attributes"] = {"table": attrs} if err_log: d["scsi_error_counter_log"] = err_log # Suy ra protocol if "nvme_smart_health_information_log" in d: d["device"] = {"protocol": "NVMe"} elif ("scsi_vendor" in d or "scsi_grown_defect_list" in d) and "ata_smart_attributes" not in d: d["device"] = {"protocol": "SCSI"} else: d["device"] = {"protocol": "ATA"} return d try: mount_map, wwn_map = get_system_maps() stor_map = get_storcli_mapping() vd_devs = set(stor_map.values()) # cac /dev/sdX la Virtual Drive cua RAID controller dbg("Nap map: %d device co mountpoint, %d WWN, %d slot->VD (storcli)" % (len(mount_map), len(wwn_map), len(stor_map))) raw_input_data = sys.stdin.read().strip() disks = json.loads(raw_input_data) if raw_input_data and raw_input_data != "[]" else [] dbg("Nhan %d ban ghi SMART tu bash, bat dau dedup + phan loai..." % len(disks)) # Output chia nhom theo loai o - moi loai field khac nhau, KHONG ep chung schema grouped = collections.OrderedDict() grouped["ata_sata"] = [] # SATA SSD/HDD: ata_smart_attributes.table[] grouped["sas_scsi"] = [] # SAS/SCSI: scsi_grown_defect_list, scsi_error_counter_log grouped["nvme"] = [] # NVMe: nvme_smart_health_information_log # Tom tat suc khoe: 1 entry/o, thong so quan trong RIENG theo tung loai o health_summary = [] seen_serials = set() for item in disks: data = item.get("smart_data", {}) slot_id = str(item.get("slot")) # TEXT MODE: entry tu smartctl cu (khong co -j) -> decode base64 + parse text from_text = False if not data and item.get("smart_text_b64"): try: _txt = base64.b64decode(item.get("smart_text_b64")).decode("utf-8", "ignore") except: _txt = "" data = parse_smart_text(_txt) item["_raw_text"] = _txt from_text = True dbg("TEXT MODE: parse text -> slot %s | serial %s | protocol %s" % ( slot_id, data.get("serial_number"), (data.get("device") or {}).get("protocol"))) # DEDUP: cung 1 o vat ly co the bi quet boi nhieu luong (megaraid + sg + sdX). # Thu tu quet trong bash da uu tien megaraid -> HBA -> direct -> nvme, # nen ban dau tien gap (nhieu thong tin slot nhat) duoc giu lai. serial = data.get("serial_number") if serial: if serial in seen_serials: dbg("DEDUP: bo ban trung serial %s (slot %s)" % (serial, slot_id)) continue seen_serials.add(serial) # --- LOC O AO (Virtual Drive) - luoi loc thu 2 o tang Python --- # Entry tu luong megaraid (khong co os_dev_map) luon la o VAT LY -> giu. # Entry tu luong lsscsi/direct: neu device la VD cua storcli, hoac model mang # chu ky RAID controller (PERC/MegaRAID/Virtual disk/Logical Volume) -> bo. if "os_dev_map" in item: _mn = " ".join([x for x in [ data.get("model_name"), data.get("scsi_model_name"), data.get("scsi_vendor"), data.get("scsi_product")] if x]).upper() if item.get("os_dev_map") in vd_devs: dbg("LOC VD: bo %s (slot %s) - device nam trong danh sach Virtual Drive" % (item.get("os_dev_map"), slot_id)) continue _is_virtual = False for sig in ("PERC", "MEGARAID", "VIRTUAL DISK", "LOGICAL VOLUME", "RAID CONTROLLER"): if sig in _mn: _is_virtual = True if _is_virtual: dbg("LOC VD: bo slot %s - model mang chu ky RAID controller (%s)" % (slot_id, _mn)) continue # --- Phan loai protocol (quyet dinh nhom output) --- protocol = (data.get("device", {}) or {}).get("protocol", "") if protocol == "NVMe" or "nvme_smart_health_information_log" in data: group_key = "nvme" elif protocol == "SCSI" or "scsi_error_counter_log" in data or "scsi_grown_defect_list" in data: group_key = "sas_scsi" else: group_key = "ata_sata" # --- Tinh WWN chi de phuc vu map slot -> device (khong ep vao output) --- w_o = data.get("wwn") if isinstance(w_o, dict) and w_o: wwn_val = "0x%x%06x%09x" % (w_o.get("naa", 0), w_o.get("oui", 0), w_o.get("id", 0)) else: wwn_val = data.get("logical_unit_id") if not wwn_val and data.get("nvme_namespaces"): eui = data.get("nvme_namespaces", [{}])[0].get("eui64") if isinstance(eui, dict): wwn_val = "0x%06x%010x" % (eui.get("oui", 0), eui.get("ext_id", 0)) # --- Map ra device OS + mountpoints --- dev_name = item.get("os_dev_map") if not dev_name or dev_name == "-": dev_name = stor_map.get(slot_id) if not dev_name and wwn_val: dev_name = wwn_map.get(str(wwn_val).lower()) mounts = mount_map.get(dev_name, []) if dev_name else [] # is_raid_member: nhanh HBA/direct/nvme gan tuong minh trong bash; # nhanh megaraid suy ra tu viec slot (DID) co thuoc 1 Virtual Drive khong if "is_raid_member" in item: raid_member = item.get("is_raid_member") else: raid_member = slot_id in stor_map dbg("O %s (slot %s) -> nhom %s | device %s | mount %s | raid_member %s" % ( serial, slot_id, group_key, dev_name, ",".join(sorted(set(mounts))) or "-", raid_member)) # --- Dong goi: meta mapping + FULL smartctl JSON nguyen ban --- entry = collections.OrderedDict() entry["slot"] = slot_id entry["device_name"] = dev_name entry["mountpoints"] = sorted(set(mounts)) entry["is_raid_member"] = raid_member if from_text: # smartctl cu: smartctl_full la ban parse tu text (schema giong -j), # kem nguyen van output text de doi chieu khi can entry["smartctl_parse_mode"] = "text_fallback" entry["smartctl_full"] = data entry["smartctl_text_raw"] = item.get("_raw_text") else: entry["smartctl_full"] = data # <-- toan bo output smartctl -a -j, khong cat got grouped[group_key].append(entry) # ====================================================================== # TOM TAT SUC KHOE (health_summary) # - Thong tin co ban: model, serial, capacity, mountpoint, nhiet do... # - Thong so suc khoe QUAN TRONG rieng theo tung loai o (khong ep chung): # ATA HDD : reallocated / pending / offline_uncorrectable / CRC / spin_retry # ATA SSD : reallocated / pending / wear_leveling / life_left / CRC # SAS/SCSI: grown_defect_list / uncorrected read-write-verify # NVMe : critical_warning / percentage_used / available_spare / media_errors # ====================================================================== def get_attr(name): for attr in (data.get("ata_smart_attributes", {}) or {}).get("table", []): if attr.get("name") == name: return (attr.get("raw", {}) or {}).get("value") return None def get_attr_by_id(aid): for attr in (data.get("ata_smart_attributes", {}) or {}).get("table", []): if attr.get("id") == aid: return (attr.get("raw", {}) or {}).get("value") return None model_name = data.get("model_name") or data.get("scsi_model_name") if not model_name: model_name = ((data.get("scsi_vendor") or "").strip() + " " + (data.get("scsi_product") or "").strip()).strip() or None cap = (data.get("user_capacity") or {}).get("bytes") if not cap: cap = data.get("nvme_total_capacity") if not cap and data.get("nvme_namespaces"): ns0 = (data.get("nvme_namespaces") or [{}])[0] cap = ((ns0.get("size") or {}).get("bytes")) or ((ns0.get("capacity") or {}).get("bytes")) cap_human = None if cap: cap_human = "%.2f TB" % (cap / 1e12) if cap >= 1e12 else "%.1f GB" % (cap / 1e9) nvme_log = data.get("nvme_smart_health_information_log") or {} temp = (data.get("temperature") or {}).get("current") if temp is None: temp = nvme_log.get("temperature") if temp is None: temp = get_attr("Temperature_Celsius") poh = (data.get("power_on_time") or {}).get("hours") or nvme_log.get("power_on_hours") or get_attr("Power_On_Hours") smart_passed = (data.get("smart_status") or {}).get("passed") rotation = data.get("rotation_rate") if group_key == "nvme": dtype = "NVMe" elif rotation == 0: dtype = "SSD" elif rotation: dtype = "HDD" else: dtype = None summary = collections.OrderedDict() summary["slot"] = slot_id summary["device_name"] = dev_name summary["mountpoints"] = sorted(set(mounts)) summary["model_name"] = model_name summary["serial_number"] = serial summary["protocol"] = "NVMe" if group_key == "nvme" else ("SCSI/SAS" if group_key == "sas_scsi" else "ATA/SATA") summary["type"] = dtype summary["is_raid_member"] = raid_member summary["capacity_bytes"] = cap summary["capacity_human"] = cap_human summary["temperature_c"] = temp summary["power_on_hours"] = poh summary["smart_overall_passed"] = smart_passed warnings = [] health = collections.OrderedDict() if group_key == "nvme": health["critical_warning"] = nvme_log.get("critical_warning") health["percentage_used"] = nvme_log.get("percentage_used") health["available_spare"] = nvme_log.get("available_spare") health["available_spare_threshold"] = nvme_log.get("available_spare_threshold") health["media_errors"] = nvme_log.get("media_errors") health["unsafe_shutdowns"] = nvme_log.get("unsafe_shutdowns") health["num_err_log_entries"] = nvme_log.get("num_err_log_entries") if nvme_log.get("critical_warning"): warnings.append("NVMe critical_warning != 0") if (nvme_log.get("media_errors") or 0) > 0: warnings.append("media_errors > 0") sp, spt = nvme_log.get("available_spare"), nvme_log.get("available_spare_threshold") if sp is not None and spt is not None and sp <= spt: warnings.append("available_spare <= threshold") if (nvme_log.get("percentage_used") or 0) >= 90: warnings.append("percentage_used >= 90%") elif group_key == "sas_scsi": err_log = data.get("scsi_error_counter_log") or {} unc = {} for k in ("read", "write", "verify"): unc[k] = (err_log.get(k) or {}).get("total_uncorrected_errors") or 0 health["grown_defect_list"] = data.get("scsi_grown_defect_list") health["uncorrected_errors_read"] = unc["read"] health["uncorrected_errors_write"] = unc["write"] health["uncorrected_errors_verify"] = unc["verify"] health["uncorrected_errors_total"] = unc["read"] + unc["write"] + unc["verify"] health["non_medium_error_count"] = data.get("scsi_non_medium_error_count") if (data.get("scsi_grown_defect_list") or 0) > 0: warnings.append("grown_defect_list > 0") if health["uncorrected_errors_total"] > 0: warnings.append("uncorrected_errors > 0") else: health["reallocated_sector_ct"] = get_attr("Reallocated_Sector_Ct") health["current_pending_sector"] = get_attr("Current_Pending_Sector") health["offline_uncorrectable"] = get_attr("Offline_Uncorrectable") _crc = get_attr("UDMA_CRC_Error_Count") if _crc is None: _crc = get_attr("CRC_Error_Count") health["udma_crc_error_count"] = _crc if dtype == "SSD": _wl = get_attr("Wear_Leveling_Count") if _wl is None: _wl = get_attr("Media_Wearout_Indicator") if _wl is None: _wl = get_attr_by_id(173) health["wear_leveling_count"] = _wl _ll = get_attr("SSD_Life_Left") if _ll is None: _ll = get_attr("Percent_Lifetime_Remain") health["ssd_life_left_pct"] = _ll else: health["spin_retry_count"] = get_attr("Spin_Retry_Count") health["reported_uncorrect"] = get_attr("Reported_Uncorrect") for hk in ("reallocated_sector_ct", "current_pending_sector", "offline_uncorrectable"): if (health.get(hk) or 0) > 0: warnings.append(hk + " > 0") if (health.get("udma_crc_error_count") or 0) > 0: warnings.append("udma_crc_error_count > 0 (kiem tra cap/backplane)") if smart_passed is False: warnings.append("SMART overall-health: FAILED") if temp is not None and isinstance(temp, (int, float)) and temp >= 60: warnings.append("nhiet do >= 60C") summary["health"] = health summary["warnings"] = warnings if smart_passed is False or (group_key == "nvme" and nvme_log.get("critical_warning")): summary["status"] = "CRITICAL" elif warnings: summary["status"] = "WARNING" else: summary["status"] = "OK" if warnings: dbg(" -> %s: %s | %s" % (serial, summary["status"], "; ".join(warnings))) health_summary.append(summary) dbg("HOAN TAT: %d o sau dedup/loc (ata_sata=%d, sas_scsi=%d, nvme=%d)" % ( len(health_summary), len(grouped["ata_sata"]), len(grouped["sas_scsi"]), len(grouped["nvme"]))) out = collections.OrderedDict() out["health_summary"] = health_summary for gk in grouped: out[gk] = grouped[gk] print(json.dumps({"'"$PUBLIC_IP"'": out}, indent=2)) except Exception as e: dbg("LOI PYTHON ENGINE: " + str(e)) print(json.dumps({"error": "Python error: " + str(e)})) ' 2>>"$LOG_FILE") else # ========================================================================== # ROUTE: KHÔNG CÓ PYTHON NÀO TRÊN SERVER (kể cả python2) # -> BASH FALLBACK: vẫn xuất raw JSON để KHÔNG MẤT DỮ LIỆU đã quét. # Không có health_summary/mapping mountpoint; raw_disks chứa nguyên # smartctl JSON từng ổ + lsblk/storcli base64 để xử lý offline nếu cần. # ========================================================================== log "[BUOC3] CANH BAO: Khong tim thay python3 LAN python2 -> dung BASH FALLBACK (raw output, khong co health_summary)" FINAL_RESULT="{\"$PUBLIC_IP\": {\"engine\": \"bash_fallback_no_python\", \"note\": \"Server khong co python -> chi xuat raw. Cai python3 de co day du health_summary/mapping.\", \"hostname\": \"$HOST_NAME\", \"raw_disks\": $JSON_ARRAY, \"lsblk_base64\": \"$LSBLK_BASE64\", \"storcli_base64\": \"${STORCLI_BASE64:-}\"}}" fi # ============================================================================== # BƯỚC 4: LƯU TRỮ VÀ ĐỒNG BỘ HÓA # ============================================================================== echo "$FINAL_RESULT" > "$OUT_FILE" if echo "$FINAL_RESULT" | grep -q '"error"'; then log "[BUOC4] Da ghi $OUT_FILE NHUNG output chua loi - xem chi tiet [PY-ENGINE] o tren" else log "[BUOC4] Da ghi $OUT_FILE ($(echo "$FINAL_RESULT" | wc -c) bytes)" fi log "===== KET THUC checkdisk2 ====="