NAS rakentelut

  • Keskustelun aloittaja Keskustelun aloittaja dun
  • Aloitettu Aloitettu
Mitenkäs työ ootte kiinnitelly noita adaptereita koteloon, kun ei taida yhenkään adapterin reijät sattua koteloissa olevien standoffien kohalle?
Pitää koittaa nyt parempaa onnea CEACENT CNS41CX16I adapterilla. Tosin tuostakin on olemassa kahden SlimSAS liittimen malli (CNS42CX16R) ja noihin myös myydään erillistä PCIe korttia.
Tuo tosin on raportoitu toimivaksi niin on ainakin jotain takeita saada toimimaan.
 
Jaa, se näköjään vaatii boottamisen käyttöjärjestelmään (pelkkä BIOS ei riitä), että tohon SlimSAS PCIe adapterin tulee eloa ja siihen kytketty näytönohjain antaa kuvaa pihalle ja HBA/RAID ohjaimen konfigurointi menuun tolla ei ilmeisesti pääse mitenkään.

Olin jo valmis haukkumaan myös tuon Ceacentin adapterin, että eihän sekään toimi. Pitää vielä toi toinenkin adapteri testata, että onko se sittenkään siinä vika vai onko vika pelkästään käyttäjässä.
 
Uh oh, Toshiban N300 levyt käy kuumana nykysessä kopassa ja laukesi yksi aika nopeasti vrt. Seagaten ironwolffit. Tulisikos jollain mieleen joku parempi kotelo.

Nyt on Silverstone DS380 (https://www.silverstonetek.com/en/product/info/server-nas/DS380/) koppa ja mietin että jos vaihtaisi CS381:een. Vielä on hyllyssä pari kappaletta HPe Microserver 10:stä jääneitä Toshiban levyjä, mutta nyt kun katsoin uusia levyjä, niin aikaisemmin n. 100€ arvoiset 4tb Seagate Ironwolffit alkaa olla 215€ hintaisia halvimmillaan. Yksi levy näyttää scriptin mukaan vetelmään viimeisiä, niin pitää varmana vaihtaa se kun sanoo itsensä irti jossain vaiheessa. Vielä ei tarvii vaihtaa kun ZFS ilmoittaa että kaikki kunnossa.

Tein tämmöisen scriptin joka kertoo tärkeät tiedot.

Koodi:
KERNEL   MODEL                      SERIAL               SIZE     HEALTH     POH        TEMP     AGE          LIFE    REALLOC  UNCORR
------   -----                      ------               ----     ------     ---        ----     ---          ----    -------  ------
sdj      Samsung SSD 840 EVO 1TB    -                    931,5G   PASSED     105159     33C      12y 0m 1d    94%     -        -
sdi      Samsung SSD 850 PRO 256GB  -                    238,5G   PASSED     79239      34C      9y 0m 16d    94%     -        -
sdh      TOSHIBA HDWG480            -                    7,3T     PASSED     16021      51C      1y 10m 2d    -       0        0
sdf      ST4000VN008-2DR166         -                    3,6T     PASSED     74879      46C      8y 6m 19d    -       0        0
sdg      ST4000VN008-2DR166         -                    3,6T     PASSED     74895      45C      8y 6m 20d    -       0        0
sde      ST4000VN008-2DR166         -                    3,6T     PASSED     74872      43C      8y 6m 19d    -       0        0
sdd      ST4000VN008-2DR166         -                    3,6T     PASSED     56387      36C      6y 5m 9d     -       0        0
sdc      ST4000VN008-2DR166         -                    3,6T     PASSED     56389      39C      6y 5m 9d     -       184      328
sdb      ST4000VN008-2DR166         -                    3,6T     PASSED     46681      41C      5y 4m 0d     -       0        0
sda      WDC WD40EFRX-68N32N0       -                    3,6T     PASSED     39996      41C      4y 6m 26d    -       0        0

Edit: Jos jotain muuten kiinnostaa n. 6kk käytössä ollut HPe Microserver 10, niin olisi yksi ylimääräinen jolle ei ole tällä hetkellä mitään käyttöä.Ei ole vieläkään käyttöä, mutta ei ole mitään järkeä näköjään myöskään myydä pois kun tolle voi tulla jossain vaiheessa käyttöä ja uutena joutuu nyt 5 vuotta vanhasta raudata maksamaan 760€. Eli enpä myykään.
 
Viimeksi muokattu:
Uh oh, Toshiban N300 levyt käy kuumana nykysessä kopassa ja laukesi yksi aika nopeasti vrt. Seagaten ironwolffit. Tulisikos jollain mieleen joku parempi kotelo.

Nyt on Silverstone DS380 (https://www.silverstonetek.com/en/product/info/server-nas/DS380/) koppa ja mietin että jos vaihtaisi CS381:een. Vielä on hyllyssä pari kappaletta HPe Microserver 10:stä jääneitä Toshiban levyjä, mutta nyt kun katsoin uusia levyjä, niin aikaisemmin n. 100€ arvoiset 4tb Seagate Ironwolffit alkaa olla 215€ hintaisia halvimmillaan. Yksi levy näyttää scriptin mukaan vetelmään viimeisiä, niin pitää varmana vaihtaa se kun sanoo itsensä irti jossain vaiheessa. Vielä ei tarvii vaihtaa kun ZFS ilmoittaa että kaikki kunnossa.

Tein tämmöisen scriptin joka kertoo tärkeät tiedot.

Koodi:
KERNEL   MODEL                      SERIAL               SIZE     HEALTH     POH        TEMP     AGE          LIFE    REALLOC  UNCORR
------   -----                      ------               ----     ------     ---        ----     ---          ----    -------  ------
sdj      Samsung SSD 840 EVO 1TB    -                    931,5G   PASSED     105159     33C      12y 0m 1d    94%     -        -
sdi      Samsung SSD 850 PRO 256GB  -                    238,5G   PASSED     79239      34C      9y 0m 16d    94%     -        -
sdh      TOSHIBA HDWG480            -                    7,3T     PASSED     16021      51C      1y 10m 2d    -       0        0
sdf      ST4000VN008-2DR166         -                    3,6T     PASSED     74879      46C      8y 6m 19d    -       0        0
sdg      ST4000VN008-2DR166         -                    3,6T     PASSED     74895      45C      8y 6m 20d    -       0        0
sde      ST4000VN008-2DR166         -                    3,6T     PASSED     74872      43C      8y 6m 19d    -       0        0
sdd      ST4000VN008-2DR166         -                    3,6T     PASSED     56387      36C      6y 5m 9d     -       0        0
sdc      ST4000VN008-2DR166         -                    3,6T     PASSED     56389      39C      6y 5m 9d     -       184      328
sdb      ST4000VN008-2DR166         -                    3,6T     PASSED     46681      41C      5y 4m 0d     -       0        0
sda      WDC WD40EFRX-68N32N0       -                    3,6T     PASSED     39996      41C      4y 6m 26d    -       0        0

Edit: Jos jotain muuten kiinnostaa n. 6kk käytössä ollut HPe Microserver 10, niin olisi yksi ylimääräinen jolle ei ole tällä hetkellä mitään käyttöä.

Kehtaisitko jakaa tuon skriptin ketjuun? Itsekin miettinyt että pitäisi levyjen tarkkailuun kehitellä joku vastaava työkalu mistä voisi yhdellä silmäyksellä seurata lämpötilaa ym. elonmerkkejä, mutta en ole vielä saanut aikaiseksi.

Omassa nassissa tällä hetkellä levyt idlaa vielä melkein siedettävissä lämpötiloissa, mutta aina kun zfs aloittaa scrubin, lämmöt meinaa äkkiä karata 50 väärälle puolelle. Jotain aktiivijäähdytystä tuokin vaatisi.
 
Kehtaisitko jakaa tuon skriptin ketjuun? Itsekin miettinyt että pitäisi levyjen tarkkailuun kehitellä joku vastaava työkalu mistä voisi yhdellä silmäyksellä seurata lämpötilaa ym. elonmerkkejä, mutta en ole vielä saanut aikaiseksi.

Omassa nassissa tällä hetkellä levyt idlaa vielä melkein siedettävissä lämpötiloissa, mutta aina kun zfs aloittaa scrubin, lämmöt meinaa äkkiä karata 50 väärälle puolelle. Jotain aktiivijäähdytystä tuokin vaatisi.
Ollos hyvä, laitoin MIT lisenssin alle, eli antaa mennä vaan ja muokkailkaa miten lystää.

disk_inventory.sh
Koodi:
#!/usr/bin/env bash
# Copyright 2026 Dun
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

set -euo pipefail

# disk_inventory.sh
#
# Prints one row per physical disk using /dev/disk/by-id and smartctl.
#
# Features:
# - skips partition aliases: *-part1, *-part2, *-part9, etc.
# - deduplicates multiple aliases for the same disk
# - prefers stable /dev/disk/by-id paths for disk discovery
# - prints kernel device, model, serial, size, SMART health,
#   power-on hours, temperature and estimated age
# - prints remaining SSD life when available
# - prints reallocated and uncorrectable sectors for HDDs
#
# AGE format:
# 1y 2m 10d
#
# AGE is calculated from Power_On_Hours.
# Example:
# 1y 2m 10d = approximately 1 year, 2 months, 10 days powered on
#
# TEMP format:
# 32C
#
# LIFE format:
# 95%
#
# LIFE is only shown for SSDs when a supported SMART lifetime
# indicator is available.
#
# REALLOC:
# Reallocated_Sector_Ct for HDDs.
#
# UNCORR:
# Offline_Uncorrectable for HDDs.
#
# Requirements:
# sudo apt install smartmontools util-linux
#
# Usage:
# chmod +x disk_inventory.sh
# ./disk_inventory.sh
#
# Optional:
# SUDO="" ./disk_inventory.sh
# BY_ID_GLOB="/dev/disk/by-id/scsi-SATA_*" ./disk_inventory.sh

SUDO="${SUDO:-sudo}"
BY_ID_GLOB="${BY_ID_GLOB:-/dev/disk/by-id/scsi-*}"

command -v smartctl >/dev/null 2>&1 || {
    echo "ERROR: smartctl not found. Install with: sudo apt install smartmontools" >&2
    exit 1
}

command -v lsblk >/dev/null 2>&1 || {
    echo "ERROR: lsblk not found." >&2
    exit 1
}

hours_to_age() {
    local hours="$1"

    if [[ ! "$hours" =~ ^[0-9]+$ ]]; then
        echo "-"
        return
    fi

    local total_days=$((hours / 24))
    local years=$((total_days / 365))
    local remaining_days=$((total_days % 365))
    local months=$((remaining_days / 30))
    local days=$((remaining_days % 30))

    printf "%dy %dm %dd" "$years" "$months" "$days"
}

get_temperature() {
    local smart_data="$1"
    local temp=""

    # ATA/SATA drives:
    #
    # Example:
    # 194 Temperature_Celsius ... 33
    #
    # Some drives instead expose attribute 190:
    # 190 Airflow_Temperature_Cel ... 31
    temp="$(
        awk '
        /Temperature_Celsius/ {
            print $10
            exit
        }
        /Airflow_Temperature_Cel/ {
            print $10
            exit
        }
        ' <<< "$smart_data"
    )"

    # SAS/SCSI drives:
    #
    # Example:
    # Current Drive Temperature:     34 C
    if [[ -z "$temp" ]]; then
        temp="$(
            awk '
            /Current Drive Temperature:/ {
                for (i = 1; i <= NF; i++) {
                    if ($i ~ /^[0-9]+$/) {
                        print $i
                        exit
                    }
                }
            }
            ' <<< "$smart_data"
        )"
    fi

    # NVMe and some newer smartctl output:
    #
    # Example:
    # Temperature:                        35 Celsius
    if [[ -z "$temp" ]]; then
        temp="$(
            awk '
            /^[[:space:]]*Temperature:/ {
                for (i = 1; i <= NF; i++) {
                    if ($i ~ /^[0-9]+$/) {
                        print $i
                        exit
                    }
                }
            }
            ' <<< "$smart_data"
        )"
    fi

    if [[ "$temp" =~ ^-?[0-9]+$ ]]; then
        printf "%sC" "$temp"
    else
        printf "-"
    fi
}

is_ssd() {
    local device="$1"
    local rotational=""

    rotational="$(
        lsblk -dn -o ROTA "$device" 2>/dev/null |
            awk '{$1=$1; print}'
    )"

    [[ "$rotational" == "0" ]]
}

get_ssd_life() {
    local smart_data="$1"
    local life=""
    local used=""
    local value=""

    # NVMe:
    #
    # Percentage Used: 12%
    #
    # Convert endurance used into estimated life remaining.
    used="$(
        awk '
        /^[[:space:]]*Percentage Used:/ {
            for (i = 1; i <= NF; i++) {
                if ($i ~ /^[0-9]+%$/) {
                    gsub(/%/, "", $i)
                    print $i
                    exit
                }
            }
        }
        ' <<< "$smart_data"
    )"

    if [[ "$used" =~ ^[0-9]+$ ]]; then
        value=$((10#$used))

        if (( value >= 100 )); then
            printf "0%%"
        else
            printf "%d%%" "$((100 - value))"
        fi

        return
    fi

    # SATA SSDs that directly report percentage life remaining.
    life="$(
        awk '
        /Percent_Lifetime_Remain/ ||
        /Percent_Lifetime_Remaining/ ||
        /SSD_Life_Left/ {
            if ($4 ~ /^[0-9]+$/) {
                print $4
                exit
            }
        }
        ' <<< "$smart_data"
    )"

    if [[ "$life" =~ ^[0-9]+$ ]]; then
        value=$((10#$life))

        if (( value >= 0 && value <= 100 )); then
            printf "%d%%" "$value"
            return
        fi
    fi

    # Samsung SATA SSDs commonly expose SMART attribute 177:
    #
    # Wear_Leveling_Count
    #
    # The normalized VALUE field normally starts at 100 and decreases
    # as NAND endurance is consumed.
    life="$(
        awk '
        $1 == "177" && $2 == "Wear_Leveling_Count" {
            if ($4 ~ /^[0-9]+$/) {
                print $4
                exit
            }
        }
        ' <<< "$smart_data"
    )"

    if [[ "$life" =~ ^[0-9]+$ ]]; then
        value=$((10#$life))

        if (( value >= 0 && value <= 100 )); then
            printf "%d%%" "$value"
            return
        fi
    fi

    printf "-"
}

get_reallocated_sectors() {
    local smart_data="$1"
    local value=""

    value="$(
        awk '
        $2 == "Reallocated_Sector_Ct" {
            print $10
            exit
        }
        ' <<< "$smart_data"
    )"

    if [[ "$value" =~ ^[0-9]+$ ]]; then
        printf "%s" "$value"
    else
        printf "-"
    fi
}

get_uncorrectable_sectors() {
    local smart_data="$1"
    local value=""

    value="$(
        awk '
        $2 == "Offline_Uncorrectable" {
            print $10
            exit
        }
        ' <<< "$smart_data"
    )"

    if [[ "$value" =~ ^[0-9]+$ ]]; then
        printf "%s" "$value"
    else
        printf "-"
    fi
}

printf "%-8s %-26s %-20s %-8s %-10s %-10s %-8s %-12s %-7s %-8s %-8s\n" \
    "KERNEL" "MODEL" "SERIAL" "SIZE" "HEALTH" "POH" "TEMP" "AGE" "LIFE" "REALLOC" "UNCORR"

printf "%-8s %-26s %-20s %-8s %-10s %-10s %-8s %-12s %-7s %-8s %-8s\n" \
    "------" "-----" "------" "----" "------" "---" "----" "---" "----" "-------" "------"

declare -A seen

shopt -s nullglob

for byid in $BY_ID_GLOB; do
    [[ -e "$byid" ]] || continue

    [[ "$byid" =~ -part[0-9]+$ ]] && continue

    realdev="$(readlink -f "$byid" 2>/dev/null || true)"
    [[ -n "$realdev" ]] || continue
    [[ -b "$realdev" ]] || continue

    kernel="$(basename "$realdev")"

    info="$($SUDO smartctl -i "$byid" 2>/dev/null || true)"
    all="$($SUDO smartctl -a "$byid" 2>/dev/null || true)"
    extra="$($SUDO smartctl -x "$byid" 2>/dev/null || true)"

    model="$(
        awk -F: '
        /Device Model:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        /Product:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        /Model Number:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        ' <<< "$info"
    )"

    serial="$(
        awk -F: '
        /Serial Number:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        ' <<< "$info"
    )"

    [[ -n "$model" ]] || model="-"
    [[ -n "$serial" ]] || serial="-"

    if [[ "$serial" != "-" ]]; then
        key="serial:$serial"
    else
        key="kernel:$kernel"
    fi

    [[ -n "${seen[$key]:-}" ]] && continue
    seen[$key]=1

    size="$(
        lsblk -dn -o SIZE "$realdev" 2>/dev/null |
            awk '{$1=$1; print}' || true
    )"
    [[ -n "$size" ]] || size="-"

    health="$(
        awk -F: '
        /SMART overall-health self-assessment test result:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        /SMART Health Status:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        ' <<< "$all"
    )"
    [[ -n "$health" ]] || health="-"

    poh="$(
        awk '
        /Power_On_Hours/ {
            print $10
            exit
        }
        /Power-on Hours/ {
            print $NF
            exit
        }
        /Accumulated power on time/ {
            print $0
            exit
        }
        ' <<< "$all"
    )"
    [[ -n "$poh" ]] || poh="-"

    temp="$(get_temperature "$all")"

    if [[ "$temp" == "-" ]]; then
        temp="$(get_temperature "$extra")"
    fi

    age="$(hours_to_age "$poh")"

    if is_ssd "$realdev"; then
        life="$(get_ssd_life "$all")"

        if [[ "$life" == "-" ]]; then
            life="$(get_ssd_life "$extra")"
        fi

        realloc="-"
        uncorr="-"
    else
        life="-"
        realloc="$(get_reallocated_sectors "$all")"
        uncorr="$(get_uncorrectable_sectors "$all")"
    fi

    printf "%-8s %-26.26s %-20.20s %-8s %-10.10s %-10.10s %-8.8s %-12.12s %-7.7s %-8.8s %-8.8s\n" \
        "$kernel" \
        "$model" \
        "$serial" \
        "$size" \
        "$health" \
        "$poh" \
        "$temp" \
        "$age" \
        "$life" \
        "$realloc" \
        "$uncorr"
done

Edit: Bitbucket linkki: Bitbucket
 
Viimeksi muokattu:
Ollos hyvä, laitoin MIT lisenssin alle, eli antaa mennä vaan ja muokkailkaa miten lystää.

disk_inventory.sh
Koodi:
#!/usr/bin/env bash
# Copyright 2026 Dun
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

set -euo pipefail

# disk_inventory.sh
#
# Prints one row per physical disk using /dev/disk/by-id and smartctl.
#
# Features:
# - skips partition aliases: *-part1, *-part2, *-part9, etc.
# - deduplicates multiple aliases for the same disk
# - prefers stable /dev/disk/by-id paths for disk discovery
# - prints kernel device, model, serial, size, SMART health,
#   power-on hours, temperature and estimated age
# - prints remaining SSD life when available
# - prints reallocated and uncorrectable sectors for HDDs
#
# AGE format:
# 1y 2m 10d
#
# AGE is calculated from Power_On_Hours.
# Example:
# 1y 2m 10d = approximately 1 year, 2 months, 10 days powered on
#
# TEMP format:
# 32C
#
# LIFE format:
# 95%
#
# LIFE is only shown for SSDs when a supported SMART lifetime
# indicator is available.
#
# REALLOC:
# Reallocated_Sector_Ct for HDDs.
#
# UNCORR:
# Offline_Uncorrectable for HDDs.
#
# Requirements:
# sudo apt install smartmontools util-linux
#
# Usage:
# chmod +x disk_inventory.sh
# ./disk_inventory.sh
#
# Optional:
# SUDO="" ./disk_inventory.sh
# BY_ID_GLOB="/dev/disk/by-id/scsi-SATA_*" ./disk_inventory.sh

SUDO="${SUDO:-sudo}"
BY_ID_GLOB="${BY_ID_GLOB:-/dev/disk/by-id/scsi-*}"

command -v smartctl >/dev/null 2>&1 || {
    echo "ERROR: smartctl not found. Install with: sudo apt install smartmontools" >&2
    exit 1
}

command -v lsblk >/dev/null 2>&1 || {
    echo "ERROR: lsblk not found." >&2
    exit 1
}

hours_to_age() {
    local hours="$1"

    if [[ ! "$hours" =~ ^[0-9]+$ ]]; then
        echo "-"
        return
    fi

    local total_days=$((hours / 24))
    local years=$((total_days / 365))
    local remaining_days=$((total_days % 365))
    local months=$((remaining_days / 30))
    local days=$((remaining_days % 30))

    printf "%dy %dm %dd" "$years" "$months" "$days"
}

get_temperature() {
    local smart_data="$1"
    local temp=""

    # ATA/SATA drives:
    #
    # Example:
    # 194 Temperature_Celsius ... 33
    #
    # Some drives instead expose attribute 190:
    # 190 Airflow_Temperature_Cel ... 31
    temp="$(
        awk '
        /Temperature_Celsius/ {
            print $10
            exit
        }
        /Airflow_Temperature_Cel/ {
            print $10
            exit
        }
        ' <<< "$smart_data"
    )"

    # SAS/SCSI drives:
    #
    # Example:
    # Current Drive Temperature:     34 C
    if [[ -z "$temp" ]]; then
        temp="$(
            awk '
            /Current Drive Temperature:/ {
                for (i = 1; i <= NF; i++) {
                    if ($i ~ /^[0-9]+$/) {
                        print $i
                        exit
                    }
                }
            }
            ' <<< "$smart_data"
        )"
    fi

    # NVMe and some newer smartctl output:
    #
    # Example:
    # Temperature:                        35 Celsius
    if [[ -z "$temp" ]]; then
        temp="$(
            awk '
            /^[[:space:]]*Temperature:/ {
                for (i = 1; i <= NF; i++) {
                    if ($i ~ /^[0-9]+$/) {
                        print $i
                        exit
                    }
                }
            }
            ' <<< "$smart_data"
        )"
    fi

    if [[ "$temp" =~ ^-?[0-9]+$ ]]; then
        printf "%sC" "$temp"
    else
        printf "-"
    fi
}

is_ssd() {
    local device="$1"
    local rotational=""

    rotational="$(
        lsblk -dn -o ROTA "$device" 2>/dev/null |
            awk '{$1=$1; print}'
    )"

    [[ "$rotational" == "0" ]]
}

get_ssd_life() {
    local smart_data="$1"
    local life=""
    local used=""
    local value=""

    # NVMe:
    #
    # Percentage Used: 12%
    #
    # Convert endurance used into estimated life remaining.
    used="$(
        awk '
        /^[[:space:]]*Percentage Used:/ {
            for (i = 1; i <= NF; i++) {
                if ($i ~ /^[0-9]+%$/) {
                    gsub(/%/, "", $i)
                    print $i
                    exit
                }
            }
        }
        ' <<< "$smart_data"
    )"

    if [[ "$used" =~ ^[0-9]+$ ]]; then
        value=$((10#$used))

        if (( value >= 100 )); then
            printf "0%%"
        else
            printf "%d%%" "$((100 - value))"
        fi

        return
    fi

    # SATA SSDs that directly report percentage life remaining.
    life="$(
        awk '
        /Percent_Lifetime_Remain/ ||
        /Percent_Lifetime_Remaining/ ||
        /SSD_Life_Left/ {
            if ($4 ~ /^[0-9]+$/) {
                print $4
                exit
            }
        }
        ' <<< "$smart_data"
    )"

    if [[ "$life" =~ ^[0-9]+$ ]]; then
        value=$((10#$life))

        if (( value >= 0 && value <= 100 )); then
            printf "%d%%" "$value"
            return
        fi
    fi

    # Samsung SATA SSDs commonly expose SMART attribute 177:
    #
    # Wear_Leveling_Count
    #
    # The normalized VALUE field normally starts at 100 and decreases
    # as NAND endurance is consumed.
    life="$(
        awk '
        $1 == "177" && $2 == "Wear_Leveling_Count" {
            if ($4 ~ /^[0-9]+$/) {
                print $4
                exit
            }
        }
        ' <<< "$smart_data"
    )"

    if [[ "$life" =~ ^[0-9]+$ ]]; then
        value=$((10#$life))

        if (( value >= 0 && value <= 100 )); then
            printf "%d%%" "$value"
            return
        fi
    fi

    printf "-"
}

get_reallocated_sectors() {
    local smart_data="$1"
    local value=""

    value="$(
        awk '
        $2 == "Reallocated_Sector_Ct" {
            print $10
            exit
        }
        ' <<< "$smart_data"
    )"

    if [[ "$value" =~ ^[0-9]+$ ]]; then
        printf "%s" "$value"
    else
        printf "-"
    fi
}

get_uncorrectable_sectors() {
    local smart_data="$1"
    local value=""

    value="$(
        awk '
        $2 == "Offline_Uncorrectable" {
            print $10
            exit
        }
        ' <<< "$smart_data"
    )"

    if [[ "$value" =~ ^[0-9]+$ ]]; then
        printf "%s" "$value"
    else
        printf "-"
    fi
}

printf "%-8s %-26s %-20s %-8s %-10s %-10s %-8s %-12s %-7s %-8s %-8s\n" \
    "KERNEL" "MODEL" "SERIAL" "SIZE" "HEALTH" "POH" "TEMP" "AGE" "LIFE" "REALLOC" "UNCORR"

printf "%-8s %-26s %-20s %-8s %-10s %-10s %-8s %-12s %-7s %-8s %-8s\n" \
    "------" "-----" "------" "----" "------" "---" "----" "---" "----" "-------" "------"

declare -A seen

shopt -s nullglob

for byid in $BY_ID_GLOB; do
    [[ -e "$byid" ]] || continue

    [[ "$byid" =~ -part[0-9]+$ ]] && continue

    realdev="$(readlink -f "$byid" 2>/dev/null || true)"
    [[ -n "$realdev" ]] || continue
    [[ -b "$realdev" ]] || continue

    kernel="$(basename "$realdev")"

    info="$($SUDO smartctl -i "$byid" 2>/dev/null || true)"
    all="$($SUDO smartctl -a "$byid" 2>/dev/null || true)"
    extra="$($SUDO smartctl -x "$byid" 2>/dev/null || true)"

    model="$(
        awk -F: '
        /Device Model:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        /Product:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        /Model Number:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        ' <<< "$info"
    )"

    serial="$(
        awk -F: '
        /Serial Number:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        ' <<< "$info"
    )"

    [[ -n "$model" ]] || model="-"
    [[ -n "$serial" ]] || serial="-"

    if [[ "$serial" != "-" ]]; then
        key="serial:$serial"
    else
        key="kernel:$kernel"
    fi

    [[ -n "${seen[$key]:-}" ]] && continue
    seen[$key]=1

    size="$(
        lsblk -dn -o SIZE "$realdev" 2>/dev/null |
            awk '{$1=$1; print}' || true
    )"
    [[ -n "$size" ]] || size="-"

    health="$(
        awk -F: '
        /SMART overall-health self-assessment test result:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        /SMART Health Status:/ {
            gsub(/^[ \t]+/, "", $2)
            print $2
            exit
        }
        ' <<< "$all"
    )"
    [[ -n "$health" ]] || health="-"

    poh="$(
        awk '
        /Power_On_Hours/ {
            print $10
            exit
        }
        /Power-on Hours/ {
            print $NF
            exit
        }
        /Accumulated power on time/ {
            print $0
            exit
        }
        ' <<< "$all"
    )"
    [[ -n "$poh" ]] || poh="-"

    temp="$(get_temperature "$all")"

    if [[ "$temp" == "-" ]]; then
        temp="$(get_temperature "$extra")"
    fi

    age="$(hours_to_age "$poh")"

    if is_ssd "$realdev"; then
        life="$(get_ssd_life "$all")"

        if [[ "$life" == "-" ]]; then
            life="$(get_ssd_life "$extra")"
        fi

        realloc="-"
        uncorr="-"
    else
        life="-"
        realloc="$(get_reallocated_sectors "$all")"
        uncorr="$(get_uncorrectable_sectors "$all")"
    fi

    printf "%-8s %-26.26s %-20.20s %-8s %-10.10s %-10.10s %-8.8s %-12.12s %-7.7s %-8.8s %-8.8s\n" \
        "$kernel" \
        "$model" \
        "$serial" \
        "$size" \
        "$health" \
        "$poh" \
        "$temp" \
        "$age" \
        "$life" \
        "$realloc" \
        "$uncorr"
done
Kiitoksia.

Sen verran pitää kysyä että oliko tuossa mitään muita (distrokohtaisia?) riippuvuuksia kuin smartmontools ja util-linux? Ihan suorilta en saanut tuota vielä toimimaan; pikaisesti 22.04 LTS Ubuntulla kokeiltuna printtaa ainoastaan sarakeotsikot ja niiden alle viivat muttei mitään levyjen tietoja.

Muuten kuitenkin ilmeisen huolella kirjoitettu skripti että tuskin on kovin isosta kiinni, luultavasti multa vaan puuttuu systeemistä joku pikkupaketti tai työkalu. Kunhan tässä on aikaa niin täytyy debugata tarkemmin.
 
Kiitoksia.

Sen verran pitää kysyä että oliko tuossa mitään muita (distrokohtaisia?) riippuvuuksia kuin smartmontools ja util-linux? Ihan suorilta en saanut tuota vielä toimimaan; pikaisesti 22.04 LTS Ubuntulla kokeiltuna printtaa ainoastaan sarakeotsikot ja niiden alle viivat muttei mitään levyjen tietoja.

Muuten kuitenkin ilmeisen huolella kirjoitettu skripti että tuskin on kovin isosta kiinni, luultavasti multa vaan puuttuu systeemistä joku pikkupaketti tai työkalu. Kunhan tässä on aikaa niin täytyy debugata tarkemmin.

Sinäänsä ne ei ole distrokohtaisia sillä ittellä tuo toimii fedoralla ihan suoraan. Sulta varmaan puuttuu awk se ei ole monesti asennettuna distroissa eli koitappas apt-get install awk taikka gawk jospa se sillä lähtisi. Nopeasti en muuta huomaa tuossa.
 
Kiitoksia.

Sen verran pitää kysyä että oliko tuossa mitään muita (distrokohtaisia?) riippuvuuksia kuin smartmontools ja util-linux? Ihan suorilta en saanut tuota vielä toimimaan; pikaisesti 22.04 LTS Ubuntulla kokeiltuna printtaa ainoastaan sarakeotsikot ja niiden alle viivat muttei mitään levyjen tietoja.

Muuten kuitenkin ilmeisen huolella kirjoitettu skripti että tuskin on kovin isosta kiinni, luultavasti multa vaan puuttuu systeemistä joku pikkupaketti tai työkalu. Kunhan tässä on aikaa niin täytyy debugata tarkemmin

Vähän muutoksia tein itselle niin listasi nvme asemat myös.

Koodi:
SUDO="${SUDO:-sudo}"
BY_ID_GLOB="${BY_ID_GLOB:-/dev/disk/by-path/*}"
 
Sinäänsä ne ei ole distrokohtaisia sillä ittellä tuo toimii fedoralla ihan suoraan. Sulta varmaan puuttuu awk se ei ole monesti asennettuna distroissa eli koitappas apt-get install awk taikka gawk jospa se sillä lähtisi. Nopeasti en muuta huomaa tuossa.
Ihan hyvä veikkaus, aluksi epäilin samaa kun ilmeisesti vielä 22.04 Desktop ja Server asentaa oletuksena eri version (mawk vs. gawk).

Ei ollut kuitenkaan siitä kiinni vaan näemmä /disk/by-id/ listaa satalevyt scsi-* sijaan ata-* -prefiksillä, ja tuon muuttamalla alkoi jo levyjä löytymään.

Vähän muutoksia tein itselle niin listasi nvme asemat myös.

Koodi:
SUDO="${SUDO:-sudo}"
BY_ID_GLOB="${BY_ID_GLOB:-/dev/disk/by-path/*}"

Tässä kuitenkin luultavasti vielä astetta tyylipuhtaampi ratkaisu, joten tällä mennään. :thumbsup:
 
Jatketaan vielä hiukan, omassa koneessa on SK hynix BC711 HFM256GD3JX nvme ja smartctl 7.5 2025-04-30 r5714.
Käyttöaika ilmoitetaan muodossa "Power On Hours: 2,918"

eli skriptiin pitää lisätä "Power On Hours":
Bash:
    poh="$(
        awk '
        /Power_On_Hours/ {
            print $10
            exit
        }
        /Power-on Hours/ {
            print $NF
            exit
        }
        /Power On Hours/ {
            print $NF
            exit
        }
        /Accumulated power on time/ {
            print $0
            exit
        }
        ' <<< "$all"
    )"

ja
Bash:
poh=$(echo ${poh} | tr -d ',')

ennen hours_to_age kutsua niin saa siitäkin oikean tuloksen.

Ei varmasti paras ratkaisu mutta saipahan nopeasti toimimaan
 
Jatketaan vielä hiukan, omassa koneessa on SK hynix BC711 HFM256GD3JX nvme ja smartctl 7.5 2025-04-30 r5714.
Käyttöaika ilmoitetaan muodossa "Power On Hours: 2,918"

eli skriptiin pitää lisätä "Power On Hours":
Bash:
    poh="$(
        awk '
        /Power_On_Hours/ {
            print $10
            exit
        }
        /Power-on Hours/ {
            print $NF
            exit
        }
        /Power On Hours/ {
            print $NF
            exit
        }
        /Accumulated power on time/ {
            print $0
            exit
        }
        ' <<< "$all"
    )"

ja
Bash:
poh=$(echo ${poh} | tr -d ',')

ennen hours_to_age kutsua niin saa siitäkin oikean tuloksen.

Ei varmasti paras ratkaisu mutta saipahan nopeasti toimimaan

Koodi:
        /Power On Hours:/ {
                gsub(",","")
                print $NF
                exit
        }

Tuon gsub kanssa saapi sen pilkun pois niin ei tartte erikseen enää tr:n kautta pyöräyttää. Itte oon muokkaamassa tuota alkuperäistä hiukan raskaammin koska siinä on muitakin ongelmia. Ittellä ei näkynyt kuin parista ssd levystä kulutus prosentit koska tuntuu aika mielivaltaisesti eri valmistajat esittävän sen. Onneksi smartctl osaa ulostaa eri formaateissa tuon ulosteensa niin siellä on json formaatissa huomattavasti yhtenäisempiä avaimia joilla hakea arvoja.
 
Koodi:
        /Power On Hours:/ {
                gsub(",","")
                print $NF
                exit
        }

Tuon gsub kanssa saapi sen pilkun pois niin ei tartte erikseen enää tr:n kautta pyöräyttää. Itte oon muokkaamassa tuota alkuperäistä hiukan raskaammin koska siinä on muitakin ongelmia. Ittellä ei näkynyt kuin parista ssd levystä kulutus prosentit koska tuntuu aika mielivaltaisesti eri valmistajat esittävän sen. Onneksi smartctl osaa ulostaa eri formaateissa tuon ulosteensa niin siellä on json formaatissa huomattavasti yhtenäisempiä avaimia joilla hakea arvoja.
Totta, on jäänyt awk sen verran vähäiselle käytölle ettei taas osannut ajatella... ja liian helposti menee helposti ekalla ajatuksella vaikka ei olekaan "oikein".
Eri levyt ja eri smartctl versiot tuottaa kivasti eri tuloksia, toivottavasti json auttaa
 
Juu, ton scriptin ei ollut tarkoituskaan olla mikään yleispätevä, mutta hyvä jos saatte itsellene toimimaaan. Olis ton voinut laittaa jonnekin bitbuckettiin tai githubiinkin, mutta ei jaksanut ja sit olis tullut ehkä vielä Pull requsti kyselyitä, niin helpommin meni näin.

Edit: Sain tossa kopassa lämmötkin kuriin Toshiban levyillä kun huitasin pika-kelkkojen eteen vaan kylmästi 80cm Noctuan tuulettimen ulkoisella virtalähteellä ja 5V jännitteellä. Hyvin tippu 50C => 36C.

Pitää miettiä/katsoa olisko joku tehnyt 3d tulostettavan tuuletinrivaston tolle Silverstone DS380 kopalle jonka voisi vaan tulostaa vai pitääkö pistää Freecad päälle ja suunnitella oma että saisi 2kpl nätisti ettei ruuveilla tarvisi roikkutella ja saisi sormisuojan samalla.

Edit2: Laitoin kuitenkin julkisen repon pystyyn: Bitbucket
 
Viimeksi muokattu:
Juu, ton scriptin ei ollut tarkoituskaan olla mikään yleispätevä, mutta hyvä jos saatte itsellene toimimaaan. Olis ton voinut laittaa jonnekin bitbuckettiin tai githubiinkin, mutta ei jaksanut ja sit olis tullut ehkä vielä Pull requsti kyselyitä, niin helpommin meni näin.

Sinäänsä scriptihän on ainakin omasta mielestäni tosi hyvin kirjoitettu, parempi se on mitä itte saan tuotettua mutta kaiken huomioon ottaminen on se mikä on hankalaa. Huomannu sen ihan omissä kötöstyksissä, toimii hyvin sillä raudalla jossa kötöstyksen teet mutta heti kun vaihtuu rauta niin toiminta onkin puutteellista. :)

Oikeastaan ainoa sellainen suurta ihmetysta hetättänyt seikka oli toi että teet kolmeen kertaan smartctl eli -i -a -x kun -x tarjoaa kaiken edellisen.

Toi toimii oikein hyvänä pohja koska ollut aikomus tuollainen kötöstys rakentaa itsekin mutta en ole laiskuuttani saanut sitä aikaiseksi.
 
No joo, työkseni aikoinaan paljon näitä kirjoittelin, niin silloin tuli eri raudalla esimerkkejä vastaan ja aika nopeasti eri varlmistajien erikoisuudet tuli hanskattua ja implementoitua. Omalla raudalla jos toimii kotona niin riittää :). Mutta laitoin nyt kuitenkin julkiseen repoon jakoon, niin jos tulee hyviä ideoita niin voi PR:n siitä kirjoitella nopsaan.
 
Viimeksi muokattu:
No joo-o. Virittelin aikani tuota scriptiä ja sain ihan mieleisen siittä. Sitten piru istahti olkapäälle ja piti alkaa leikkiä chatgpt:n kanssa. Aika nopeasti huomasin että sen kanssa saan moninverroin paremman kötöstyksen kuin itse värkkäämällä. Joten päätin että rakennetaampas tuo härveli täysin promptaamalla kirjoittamatta itse riviäkään scriptistä. Tarpeeksi kun väänsi jä käänsi niin tässäpä tulos.

1787006645302.png


noin 2sec ottaa tuo ulostus aikaa.

Orkkis scriptillä omalla koneella noin 15sec joka oli ihan alkujaan se syy miksi aloin tuota scriptiä ronkkimaan. Noi Hitachin laikat ottaa useamman sekunnin ajaa smartctl -x ja jos levyt oli rasituksessa niin paljon pidempään.. Tuossa la-su välisenä yönä kun teki raid pakkojen tarkistusta niin meni lähes 45sec.
Ja tämä ei ole mitenkään kritiikkiä @dun suuntaan, vaan kuten jo aiemmin oli puhetta niin se on vaan yksinkertaisesti aika mahdoton tehtävä ottaa kaikki rauta huomioon ja niiden oikun. Ei mulla mitkään muut asemat anna hitaasti infoa kuin nuo Hitachit. Mutta ne pitää jotain lämpötila historiaa sun muuta jonka toi -x palauttaa että sieltä tulee hitosti enemmän dataa.

Anyway tässä on tuo ai slop scripti johon en ole siis omin kätösin kirjoittanut riviäkään. Ainoastaan promptiin ohjeistanut. Olen ainakin itse varsin yllättynyt tuloksesta.
Bash:
#!/usr/bin/env bash

# SPDX-License-Identifier: MIT
#
# MIT License
#
# Copyright (c) 2026 JiiPee
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


# =============================================================================
# SMART DRIVE HEALTH REPORT
# =============================================================================
#
# Discovers SMART-capable drives using smartctl --scan-open.
#
# Each drive is queried once with smartctl. The resulting JSON is processed
# with one jq query to extract all required information.
#
# Drive type detection:
#
#   device.type == nvme       -> NVMe
#   device.protocol == nvme   -> NVMe
#   rotation_rate == 0        -> SSD
#   rotation_rate > 0         -> HDD
#   rotation_rate not reported -> HDD
#
# Requirements: bash, smartctl, jq, numfmt, column, sort
#
# =============================================================================

# =============================================================================
# USER CONFIGURATION
# =============================================================================

# -----------------------------------------------------------------------------
# TEMPERATURE
# -----------------------------------------------------------------------------
#
# C = Celsius
# F = Fahrenheit
#
TEMP_UNIT="C"

# Temperature thresholds are always configured in Celsius.
#
# <= TEMP_GREEN_MAX  = green
# <= TEMP_YELLOW_MAX = yellow
# >  TEMP_YELLOW_MAX = red
#
TEMP_GREEN_MAX=50
TEMP_YELLOW_MAX=60

# -----------------------------------------------------------------------------
# DRIVE LIFE
# -----------------------------------------------------------------------------
#
# Remaining life thresholds.
#
# >= LIFE_GREEN_MIN  = green
# >= LIFE_YELLOW_MIN = yellow
# <  LIFE_YELLOW_MIN = red
#
LIFE_GREEN_MIN=50
LIFE_YELLOW_MIN=25

# -----------------------------------------------------------------------------
# MODEL DISPLAY
# -----------------------------------------------------------------------------
#
# Maximum number of characters displayed for MODEL.
#
# Minimum allowed value is 15.
#
MODEL_MAX_LENGTH=30

# -----------------------------------------------------------------------------
# SORTING
# -----------------------------------------------------------------------------
SORT_COLUMN="MODEL"

# ASC = ascending
# DESC = descending
#
SORT_DIRECTION="ASC"

# -----------------------------------------------------------------------------
# DISPLAY
# -----------------------------------------------------------------------------

# yes = colors enabled
# no  = colors disabled
#
USE_COLORS="yes"

# yes = show processing spinner
# no  = don't show spinner
#
SHOW_PROGRESS="yes"

# -----------------------------------------------------------------------------
# SIZE DISPLAY
# -----------------------------------------------------------------------------
#
# SI  = decimal units: MB, GB, TB
# IEC = binary units:  MiB, GiB, TiB
#
SIZE_UNIT="IEC"

# =============================================================================
# INTERNAL VARIABLES
# =============================================================================

DRIVE_DEVICES=()
RESULTS=()
FAILED_DEVICES=()

# =============================================================================
# COLORS
# =============================================================================

if [[ "$USE_COLORS" == "yes" && -t 1 ]]; then

    COLOR_RESET=$'\033[0m'

    COLOR_RED=$'\033[31m'
    COLOR_GREEN=$'\033[32m'
    COLOR_YELLOW=$'\033[33m'

    # Light blue.
    COLOR_LIGHT_BLUE=$'\033[94m'

else

    COLOR_RESET=""
    COLOR_RED=""
    COLOR_GREEN=""
    COLOR_YELLOW=""
    COLOR_LIGHT_BLUE=""
fi

# =============================================================================
# ERROR HANDLING
# =============================================================================

die()
{
    printf 'ERROR: %s\n' "$*" >&2
    exit 1
}


# =============================================================================
# DEPENDENCY CHECK
# =============================================================================

check_dependencies()
{
    local command missing=""

    for command in smartctl jq numfmt column sort; do
        if ! command -v "$command" >/dev/null 2>&1; then missing+="$command "; fi
    done

    if [[ -n "$missing" ]]; then printf 'ERROR: Missing required commands:\n' >&2; printf '  %s\n' "$missing" >&2; exit 1; fi
}

# =============================================================================
# CONFIGURATION VALIDATION
# =============================================================================

validate_integer()
{
    local name="$1" value="$2"

    [[ "$value" =~ ^[0-9]+$ ]] ||
        die "$name must be a non-negative integer: '$value'"
}

validate_config()
{
    # Temperature unit.
    case "$TEMP_UNIT" in C|F) ;; *) die "TEMP_UNIT must be C or F" ;; esac

    # Colors.
    case "$USE_COLORS" in yes|no) ;; *) die "USE_COLORS must be yes or no" ;; esac

    # Progress spinner.
    case "$SHOW_PROGRESS" in yes|no) ;; *) die "SHOW_PROGRESS must be yes or no" ;; esac

    # Size units.
    case "$SIZE_UNIT" in SI|IEC) ;; *) die "SIZE_UNIT must be SI or IEC" ;; esac

    # Sort direction.
    case "$SORT_DIRECTION" in ASC|DESC) ;; *) die "SORT_DIRECTION must be ASC or DESC" ;; esac

    # Sort column.
    case "$SORT_COLUMN" in
        KERNEL|DEVTYPE|MODEL|FIRMWARE|SERIAL|SIZE|HEALTH|POH|TEMP|AGE|LIFE|REALLOC|UNCORR) ;;
        *) die "Invalid SORT_COLUMN: '$SORT_COLUMN'" ;;
    esac

    # Numeric configuration.
    validate_integer "TEMP_GREEN_MAX" "$TEMP_GREEN_MAX"
    validate_integer "TEMP_YELLOW_MAX" "$TEMP_YELLOW_MAX"

    validate_integer "LIFE_GREEN_MIN" "$LIFE_GREEN_MIN"
    validate_integer "LIFE_YELLOW_MIN" "$LIFE_YELLOW_MIN"

    validate_integer "MODEL_MAX_LENGTH" "$MODEL_MAX_LENGTH"

    # Temperature thresholds.
    (( TEMP_GREEN_MAX < TEMP_YELLOW_MAX )) || die "TEMP_GREEN_MAX must be lower than TEMP_YELLOW_MAX"

    # Life thresholds.
    (( LIFE_GREEN_MIN <= 100 )) || die "LIFE_GREEN_MIN cannot exceed 100"

    (( LIFE_YELLOW_MIN <= 100 )) || die "LIFE_YELLOW_MIN cannot exceed 100"

    (( LIFE_GREEN_MIN > LIFE_YELLOW_MIN )) || die "LIFE_GREEN_MIN must be greater than LIFE_YELLOW_MIN"

    # Model length.
    (( MODEL_MAX_LENGTH >= 15 )) || die "MODEL_MAX_LENGTH must be at least 15"
}

# =============================================================================
# SPINNER
# =============================================================================

SPINNER_CHARS=('-' '\' '|' '/')
SPINNER_INDEX=0

print_progress()
{
    local current="$1" total="$2" device="$3"

    [[ "$SHOW_PROGRESS" == "yes" ]] || return

    local spinner="${SPINNER_CHARS[$SPINNER_INDEX]}"

    ((SPINNER_INDEX++))

    if (( SPINNER_INDEX >= ${#SPINNER_CHARS[@]} )); then
        SPINNER_INDEX=0
    fi

    printf '\r\033[K[%s] Checking %d/%d: %s' "$spinner" "$current" "$total" "$device"
}

finish_progress()
{
    [[ "$SHOW_PROGRESS" == "yes" ]] || return

    printf '\r\033[K'
}

# =============================================================================
# DRIVE DISCOVERY
# =============================================================================
#
# Use smartctl JSON output instead of parsing the human-readable scan output.
# We need only the device name and type passed to smartctl -d.
#
# =============================================================================

discover_drives()
{
    local scan_json

    scan_json=$(smartctl -j --scan-open 2>/dev/null) ||
        die "smartctl --scan-open failed."

    [[ -n "$scan_json" ]] ||
        die "smartctl --scan-open returned no data."

    mapfile -t DRIVE_DEVICES < <(jq -r '.devices[] | [.name, .type] | @tsv' <<< "$scan_json")

    ((${#DRIVE_DEVICES[@]} > 0)) ||
        die "smartctl --scan-open did not find any usable devices."
}

# =============================================================================
# DRIVE TYPE DETECTION
# =============================================================================
#
# NVMe:
#   device.type == nvme
#   OR device.protocol == nvme
#
# SSD:
#   rotation_rate == 0
#
# HDD:
#   rotation_rate > 0
#   OR rotation_rate is not reported
#
# =============================================================================

get_drive_type()
{
    local device_type="$1" protocol="$2" rotation_rate="$3"

    if [[ "${device_type,,}" == "nvme" ]]; then
        echo "NVMe"
        return
    fi

    if [[ "${protocol,,}" == "nvme" ]]; then
        echo "NVMe"
        return
    fi

    if [[ "$rotation_rate" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
        rotation_rate=${rotation_rate%%.*}
        if (( rotation_rate > 0 )); then echo "HDD"; else echo "SSD"; fi
        return
    fi

    # No rotation rate reported.
    # According to our detection rules, treat it as HDD.
    echo "HDD"
}


# =============================================================================
# FORMAT AGE
# =============================================================================
#
# Power-on hours are converted to days.
#
# Assumptions:
#
#   365 days = 1 year
#   30 days  = 1 month
#
# If months ever reaches 12, it is normalized into one additional year.
#
# =============================================================================

format_age()
{
    local poh="$1" days years months

    [[ "$poh" =~ ^[0-9]+$ ]] || {
        printf '%10s' "-"
        return
    }

    days=$((poh / 24))
    years=$((days / 365))
    days=$((days % 365))

    months=$((days / 30))
    days=$((days % 30))

    # Never display 12 months.
    if (( months == 12 )); then years=$(( years + 1 )); months=0; fi

    printf '%2dy %2dm %2dd' "$years" "$months" "$days"
}


# =============================================================================
# PROCESS DRIVE
# =============================================================================
#
# One smartctl call per drive.
#
# Extract: MODEL FIRMWARE SERIAL SIZE HEALTH POH TEMP LIFE ROTATION DEVTYPE PROTOCOL REALLOC UNCORR
# Missing values are represented by "-" so Bash field positions remain stable.
#
# =============================================================================

process_drive()
{
    local scan_entry="$1" device scan_type json smart_data

    IFS=$'\t' read -r device scan_type <<< "$scan_entry"

    # -------------------------------------------------------------------------
    # Query the drive once.
    #
    # smartctl exit status is deliberately not used as the success test.
    # SMART warnings can cause a non-zero smartctl status while still returning
    # useful JSON containing the information we want.
    # -------------------------------------------------------------------------

    # -j JSON, -i identity, -H health, -A attributes, -l ssd SSD statistics. Deliberately no -a.
    json=$(smartctl -j -i -H -A -l ssd -d "$scan_type" "$device" 2>/dev/null)

    [[ -n "$json" ]] || {
        FAILED_DEVICES+=("$device")
        return 1
    }

    # Extract all values with one jq query.
    # "-" is used for unavailable values so Bash field positions stay stable.
    # SMART attribute 5   = Reallocated_Sector_Ct
    # SMART attribute 198 = Offline_Uncorrectable

    if ! smart_data=$(jq -r '
        [
            (.model_name // "-"),
            (.firmware_version // "-"),
            (.serial_number // "-"),
            (.user_capacity.bytes // "-"),
            (if .smart_status.passed == true then "PASSED" elif .smart_status.passed == false then "FAILED" else "UNKNOWN" end),
            (.power_on_time.hours // "-"),
            (.temperature.current // "-"),
            (.endurance_used.current_percent // "-"),
            (.rotation_rate // "-"),
            (.device.type // "-"),
            (.device.protocol // "-"),
            ([.ata_smart_attributes.table[]? | select(.id == 5) | .raw.value] | first // "-"),
            ([.ata_smart_attributes.table[]? | select(.id == 198) | .raw.value] | first // "-")
        ]
        | @tsv
    ' <<< "$json")
    then

        FAILED_DEVICES+=("$device")
        return 1
    fi

    [[ -n "$smart_data" ]] || {
        FAILED_DEVICES+=("$device")
        return 1
    }

    # -------------------------------------------------------------------------
    # Read the single jq result.
    # -------------------------------------------------------------------------

    local model firmware serial size_bytes health poh temp_c
    local endurance_used rotation_rate device_type protocol realloc uncorrectable

    IFS=$'\t' read -r model firmware serial size_bytes health poh temp_c \
        endurance_used rotation_rate device_type protocol realloc uncorrectable <<< "$smart_data"

    # -------------------------------------------------------------------------
    # Basic information.
    # -------------------------------------------------------------------------

    if [[ "$model" == "-" || -z "$model" ]]; then FAILED_DEVICES+=("$device"); return 1; fi

    local kernel devtype life

    kernel="${device##*/}"
    devtype=$(get_drive_type "$device_type" "$protocol" "$rotation_rate")
    if (( ${#model} > MODEL_MAX_LENGTH )); then model="${model:0:$((MODEL_MAX_LENGTH - 1))}…"; fi

    # -------------------------------------------------------------------------
    # LIFE
    # -------------------------------------------------------------------------
    #
    # LIFE applies only to SSD/NVMe.
    #
    # endurance_used.current_percent is the percentage already used.
    #
    # Therefore:
    #
    #   remaining life = 100 - endurance used
    #
    # HDDs always get "-".
    # -------------------------------------------------------------------------

    life="-"

    if [[ "$devtype" != "HDD" && "$endurance_used" =~ ^[0-9]+$ ]]; then
        life=$((100 - endurance_used)); (( life < 0 )) && life=0; (( life > 100 )) && life=100
    fi

    # -------------------------------------------------------------------------
    # REALLOC / UNCORR
    #
    # These are HDD-only.
    # -------------------------------------------------------------------------

    if [[ "$devtype" != "HDD" ]]; then realloc="-"; uncorrectable="-"; fi

    # -------------------------------------------------------------------------
    # Store raw row.
    # -------------------------------------------------------------------------

    RESULTS+=("$(printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s' \
        "$kernel" "$devtype" "$model" "$firmware" "$serial" "$size_bytes" "$health" \
        "$poh" "$temp_c" "$life" "$realloc" "$uncorrectable")")
}

# =============================================================================
# SORT RESULTS
# =============================================================================

sort_results()
{
    local sort_column_number sort_options=() sorted_output

    case "$SORT_COLUMN" in
        KERNEL)   sort_column_number=1 ;;
        DEVTYPE)  sort_column_number=2 ;;
        MODEL)    sort_column_number=3 ;;
        FIRMWARE) sort_column_number=4 ;;
        SERIAL)   sort_column_number=5 ;;
        SIZE)     sort_column_number=6; sort_options+=(-n) ;;
        HEALTH)   sort_column_number=7 ;;
        POH)      sort_column_number=8; sort_options+=(-n) ;;
        TEMP)     sort_column_number=9; sort_options+=(-n) ;;
        AGE)      sort_column_number=8; sort_options+=(-n) ;;
        LIFE)     sort_column_number=10; sort_options+=(-n) ;;
        REALLOC)  sort_column_number=11; sort_options+=(-n) ;;
        UNCORR)   sort_column_number=12; sort_options+=(-n) ;;
    esac

    [[ "$SORT_DIRECTION" == "DESC" ]] && sort_options+=(-r)

    sorted_output=$(
        printf '%s\n' "${RESULTS[@]}" |
            sort -t $'\t' -k"${sort_column_number},${sort_column_number}" "${sort_options[@]}"
    )

    RESULTS=()
    while IFS= read -r row; do
        [[ -n "$row" ]] && RESULTS+=("$row")
    done <<< "$sorted_output"
}

# =============================================================================
# PRINT TABLE
# =============================================================================

print_table()
{
    local header underline table_data="" row i

    header=$'KERNEL\tDEVTYPE\tMODEL\tFIRMWARE\tSERIAL\tSIZE\tHEALTH\tPOH\tTEMP\tAGE\tLIFE\tREALLOC\tUNCORR'

    # Make an underline matching the header.
    underline=""
    for ((i = 0; i < ${#header}; i++)); do
        [[ "${header:i:1}" == $'\t' ]] && underline+=$'\t' || underline+='-'
done

    # Header and underline use the same light-blue color.
    table_data+="${COLOR_LIGHT_BLUE}${header}${COLOR_RESET}"$'\n'
    table_data+="${COLOR_LIGHT_BLUE}${underline}${COLOR_RESET}"$'\n'

    for row in "${RESULTS[@]}"; do
        local kernel devtype model firmware serial size_bytes health poh temp_c life realloc uncorrectable
        IFS=$'\t' read -r kernel devtype model firmware serial size_bytes health poh temp_c life realloc uncorrectable <<< "$row"
        local size age display_health display_temp display_life display_realloc display_uncorrectable
        local temp_display temp_value green_max yellow_max temp_color life_color realloc_color uncorrectable_color

        size="-"
        if [[ "$size_bytes" =~ ^[0-9]+$ ]]; then
            if [[ "$SIZE_UNIT" == "SI" ]]; then
                size=$(numfmt --to=si --suffix=B "$size_bytes" 2>/dev/null || echo "-")
            else
                size=$(numfmt --to=iec-i --suffix=B "$size_bytes" 2>/dev/null || echo "-")
            fi
        fi
        age=$(format_age "$poh")

        case "$health" in
            PASSED) display_health="${COLOR_GREEN}${health}${COLOR_RESET}" ;;
            FAILED) display_health="${COLOR_RED}${health}${COLOR_RESET}" ;;
            *) display_health="${COLOR_YELLOW}${health}${COLOR_RESET}" ;;
        esac

        display_temp="-" display_life="-" display_realloc="-" display_uncorrectable="-"

        if [[ "$temp_c" != "-" ]]; then
            temp_c=${temp_c%%.*}
            if [[ "$TEMP_UNIT" == "F" ]]; then
                temp_display="$(( temp_c * 9 / 5 + 32 ))°F"; temp_value="$(( temp_c * 9 / 5 + 32 ))"
                green_max="$TEMP_GREEN_MAX"; yellow_max="$TEMP_YELLOW_MAX"
            else
                temp_display="${temp_c}°C"; temp_value="$temp_c"
                green_max="$TEMP_GREEN_MAX"; yellow_max="$TEMP_YELLOW_MAX"
            fi
            if (( temp_value <= green_max )); then temp_color="$COLOR_GREEN"
            elif (( temp_value <= yellow_max )); then temp_color="$COLOR_YELLOW"
            else temp_color="$COLOR_RED"
            fi
            display_temp="${temp_color}${temp_display}${COLOR_RESET}"
        fi

        if [[ "$life" != "-" ]]; then
            if (( life >= LIFE_GREEN_MIN )); then life_color="$COLOR_GREEN"
            elif (( life >= LIFE_YELLOW_MIN )); then life_color="$COLOR_YELLOW"
            else life_color="$COLOR_RED"
            fi
            display_life="${life_color}${life}%${COLOR_RESET}"
        fi

        if [[ "$realloc" != "-" ]]; then
            if (( realloc == 0 )); then realloc_color="$COLOR_GREEN"; else realloc_color="$COLOR_RED"; fi
            display_realloc="${realloc_color}${realloc}${COLOR_RESET}"
        fi

        if [[ "$uncorrectable" != "-" ]]; then
            if (( uncorrectable == 0 )); then uncorrectable_color="$COLOR_GREEN"; else uncorrectable_color="$COLOR_RED"; fi
            display_uncorrectable="${uncorrectable_color}${uncorrectable}${COLOR_RESET}"
        fi

        table_data+="$(printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s' \
            "$kernel" "$devtype" "$model" "$firmware" "$serial" "$size" "$display_health" \
            "$poh" "$display_temp" "$age" "$display_life" "$display_realloc" "$display_uncorrectable")"$'\n'
    done

    printf '%s' "$table_data" | column -t -s $'\t'
}

# =============================================================================
# FAILED DEVICES
# =============================================================================

print_failed_devices()
{
    ((${#FAILED_DEVICES[@]} > 0)) || return

    echo

    printf '%sFailed SMART queries:%s\n' "$COLOR_RED" "$COLOR_RESET"

    local device
    for device in "${FAILED_DEVICES[@]}"; do printf '  %s\n' "$device"; done
}

# =============================================================================
# MAIN
# =============================================================================

validate_config
check_dependencies

echo
echo "SMART Drive Health Report"
echo "========================="
echo

discover_drives

total=${#DRIVE_DEVICES[@]}
current=0

printf 'Found %d SMART-capable device(s).\n\n' "$total"

for scan_entry in "${DRIVE_DEVICES[@]}"; do
    ((current++))
    device="${scan_entry%%$'\t'*}"
    print_progress "$current" "$total" "$device"
    process_drive "$scan_entry"
done

finish_progress

successful=${#RESULTS[@]}
failed=${#FAILED_DEVICES[@]}

((successful > 0 )) || die "No drives returned usable SMART information."

printf 'SMART processing complete: %d OK, %d failed.\n' "$successful" "$failed"

sort_results

echo
print_table
print_failed_devices
echo

Siitä vaan chatgpt:lle syötätte tuon jos ei omiin tarpeisiin toimi niin eiköhän se saa sen korjattua.
 
Edit: Sain tossa kopassa lämmötkin kuriin Toshiban levyillä kun huitasin pika-kelkkojen eteen vaan kylmästi 80cm Noctuan tuulettimen ulkoisella virtalähteellä ja 5V jännitteellä. Hyvin tippu 50C =>
Joskus ennen ainakin hdd-levyillä oli optimi lämpötila joka on ihmisen mielestä hieman kuuma.
 
osaako joku vinkata mitä 19" räkkiin sopivia levyhyllyjä euroopassa kannattaa metsästää käytettynä?

kiinostaisi lähinnä joku tyhmä lodju ilman mitään vendor sontaa, ihan vaan minisas sff8087 liittimillä
 
Joskus ennen ainakin hdd-levyillä oli optimi lämpötila joka on ihmisen mielestä hieman kuuma.
Vuosia sitten Google julkaisi jonkun paperin missä analysoitiin levyjen vikaantumista datakeskuksissa ja siitä jäi mieleen että datan perusteella optimaalinen lämpötila oli karvan alle 40 astetta. Syy-seuraussuhde ei ollut ihan aukoton, mutta keskimäärin näin.

Edit: Failure Trends in a Large Disk Drive Population, julkaistu 2007, sivut 5 ja 6: https://storage.googleapis.com/gweb-research2023-media/pubtools/4445.pdf

google-disk-afr-vs-temperature-2007.png
 
Viimeksi muokattu:
Vuosia sitten Google julkaisi jonkun paperin missä analysoitiin levyjen vikaantumista datakeskuksissa ja siitä jäi mieleen että datan perusteella optimaalinen lämpötila oli karvan alle 40
Juuri tätä muistelin !

Ja tuulettamalla kovasti omia hdd-levyjään pääsee niistä nopeammin eroon. Toisaalta onneksi on se NAS niin on datat turvassa :)
 
osaako joku vinkata mitä 19" räkkiin sopivia levyhyllyjä euroopassa kannattaa metsästää käytettynä?

kiinostaisi lähinnä joku tyhmä lodju ilman mitään vendor sontaa, ihan vaan minisas sff8087 liittimillä
Kysele kelvollisia malleja joltain AI:lta ja sitten käsket tutkimaan ebayt ja muut läpi?
 

Statistiikka

Viestiketjuista
314 688
Viestejä
5 340 009
Jäsenet
84 873
Uusin jäsen
Eelias

Hinta.fi

Back
Ylös Bottom