mirror of
https://github.com/JustKato/drive-health.git
synced 2026-02-27 06:17:00 +02:00
Rootless Run
+ Debug Tools * ReadME * Optimized config, now it's cached * ReWrote and optimized logic.go * rewrote and optimized + Styling improvements
This commit is contained in:
@@ -20,8 +20,15 @@ type DHConfig struct {
|
||||
DebugMode bool `json:"debugMode"`
|
||||
}
|
||||
|
||||
func GetConfiguration() DHConfig {
|
||||
config := DHConfig{
|
||||
var config *DHConfig = nil
|
||||
|
||||
func GetConfiguration() *DHConfig {
|
||||
|
||||
if config != nil {
|
||||
return config
|
||||
}
|
||||
|
||||
config = &DHConfig{
|
||||
DiskFetchFrequency: 5,
|
||||
CleanupServiceFrequency: 3600,
|
||||
MaxHistoryAge: 2592000,
|
||||
|
||||
@@ -1,70 +1,78 @@
|
||||
package hardware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"tea.chunkbyte.com/kato/drive-health/lib/config"
|
||||
)
|
||||
|
||||
func GetSystemHardDrives(db *gorm.DB, olderThan *time.Time, newerThan *time.Time) ([]*HardDrive, error) {
|
||||
|
||||
// Execute the lsblk command to get detailed block device information
|
||||
cmd := exec.Command("lsblk", "-d", "-o", "NAME,TRAN,SIZE,MODEL,SERIAL,TYPE")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println("Failed to execute command:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var systemHardDrives []*HardDrive
|
||||
|
||||
// Scan the output line by line
|
||||
scanner := bufio.NewScanner(&out)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Skip the header line
|
||||
if strings.Contains(line, "NAME") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Split the line into columns
|
||||
cols := strings.Fields(line)
|
||||
if len(cols) < 6 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter out nvme drives (M.2)
|
||||
if cols[1] != "usb" {
|
||||
hd := &HardDrive{
|
||||
Name: cols[0],
|
||||
Transport: cols[1],
|
||||
Size: cols[2],
|
||||
Model: cols[3],
|
||||
Serial: cols[4],
|
||||
Type: cols[5],
|
||||
}
|
||||
systemHardDrives = append(systemHardDrives, hd)
|
||||
}
|
||||
// List all block devices
|
||||
devices, err := os.ReadDir("/sys/block/")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list block devices: %w", err)
|
||||
}
|
||||
|
||||
// Handle error
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
for _, device := range devices {
|
||||
deviceName := device.Name()
|
||||
|
||||
// Skip non-physical devices (like loop and ram devices)
|
||||
// TODO: Read more about this, there might be some other devices we should or should not skip
|
||||
if strings.HasPrefix(deviceName, "loop") || strings.HasPrefix(deviceName, "ram") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read device details
|
||||
model, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/device/model", deviceName))
|
||||
serial, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/device/serial", deviceName))
|
||||
sizeBytes, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/size", deviceName))
|
||||
|
||||
size := convertSizeToString(sizeBytes)
|
||||
transport := getTransportType(deviceName)
|
||||
|
||||
// TODO: Maybe find a better way?
|
||||
if size == "0 Bytes" {
|
||||
// This looks like an invalid device, skip it.
|
||||
if config.GetConfiguration().DebugMode {
|
||||
fmt.Printf("[🟨] Igoring device:[/dev/%s], reported size of 0\n", deviceName)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
hwid, err := getHardwareID(deviceName)
|
||||
if err != nil {
|
||||
if config.GetConfiguration().DebugMode {
|
||||
fmt.Printf("[🟨] No unique identifier found for device:[/dev/%s] unique identifier\n", deviceName)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
hd := &HardDrive{
|
||||
Name: deviceName,
|
||||
Transport: transport,
|
||||
Model: strings.TrimSpace(string(model)),
|
||||
Serial: strings.TrimSpace(string(serial)),
|
||||
Size: size,
|
||||
Type: getDriveType(deviceName),
|
||||
HWID: hwid,
|
||||
}
|
||||
|
||||
systemHardDrives = append(systemHardDrives, hd)
|
||||
}
|
||||
|
||||
var updatedHardDrives []*HardDrive
|
||||
|
||||
for _, sysHDD := range systemHardDrives {
|
||||
var existingHD HardDrive
|
||||
q := db.Where("serial = ? AND model = ? AND type = ?", sysHDD.Serial, sysHDD.Model, sysHDD.Type)
|
||||
q := db.Where("hw_id = ?", sysHDD.HWID)
|
||||
|
||||
if newerThan != nil && olderThan != nil {
|
||||
q = q.Preload("Temperatures", "time_stamp < ? AND time_stamp > ?", newerThan, olderThan)
|
||||
@@ -90,3 +98,101 @@ func GetSystemHardDrives(db *gorm.DB, olderThan *time.Time, newerThan *time.Time
|
||||
|
||||
return updatedHardDrives, nil
|
||||
}
|
||||
|
||||
func getTransportType(deviceName string) string {
|
||||
transportLink, err := filepath.EvalSymlinks(fmt.Sprintf("/sys/block/%s/device", deviceName))
|
||||
if err != nil {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
if strings.Contains(transportLink, "/usb/") {
|
||||
return "USB"
|
||||
} else if strings.Contains(transportLink, "/ata") {
|
||||
return "SATA"
|
||||
} else if strings.Contains(transportLink, "/nvme/") {
|
||||
return "NVMe"
|
||||
}
|
||||
|
||||
return "Other"
|
||||
}
|
||||
|
||||
func convertSizeToString(sizeBytes []byte) string {
|
||||
// Convert the size from a byte slice to a string, then to an integer
|
||||
sizeStr := strings.TrimSpace(string(sizeBytes))
|
||||
sizeSectors, err := strconv.ParseInt(sizeStr, 10, 64)
|
||||
if err != nil {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// Convert from 512-byte sectors to bytes
|
||||
sizeInBytes := sizeSectors * 512
|
||||
|
||||
// Define size units
|
||||
const (
|
||||
_ = iota // ignore first value by assigning to blank identifier
|
||||
KB float64 = 1 << (10 * iota)
|
||||
MB
|
||||
GB
|
||||
TB
|
||||
)
|
||||
|
||||
var size float64 = float64(sizeInBytes)
|
||||
var unit string
|
||||
|
||||
// Determine the unit to use
|
||||
switch {
|
||||
case size >= TB:
|
||||
size /= TB
|
||||
unit = "TB"
|
||||
case size >= GB:
|
||||
size /= GB
|
||||
unit = "GB"
|
||||
case size >= MB:
|
||||
size /= MB
|
||||
unit = "MB"
|
||||
case size >= KB:
|
||||
size /= KB
|
||||
unit = "KB"
|
||||
default:
|
||||
unit = "Bytes"
|
||||
}
|
||||
|
||||
// Return the formatted size
|
||||
return fmt.Sprintf("%.2f %s", size, unit)
|
||||
}
|
||||
|
||||
// Look throug /sys/block/device/ and try and find the unique identifier of the device.
|
||||
func getHardwareID(deviceName string) (string, error) {
|
||||
// Define potential ID file paths
|
||||
idFilePaths := []string{
|
||||
"/sys/block/" + deviceName + "/device/wwid",
|
||||
"/sys/block/" + deviceName + "/device/wwn",
|
||||
"/sys/block/" + deviceName + "/device/serial",
|
||||
}
|
||||
|
||||
// Try to read each file and return the first successful read
|
||||
for _, path := range idFilePaths {
|
||||
if idBytes, err := os.ReadFile(path); err == nil {
|
||||
return strings.TrimSpace(string(idBytes)), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Return an empty string if no ID is found
|
||||
return "", fmt.Errorf("could not find unique identifier for %s", deviceName)
|
||||
}
|
||||
|
||||
// Figure out what kind of device this is by reading if it's rotational or not
|
||||
func getDriveType(deviceName string) string {
|
||||
// Check if the drive is rotational (HDD)
|
||||
if isRotational, _ := os.ReadFile(fmt.Sprintf("/sys/block/%s/queue/rotational", deviceName)); string(isRotational) == "1\n" {
|
||||
return "HDD"
|
||||
}
|
||||
|
||||
// Check if the drive is NVMe
|
||||
if strings.HasPrefix(deviceName, "nvme") {
|
||||
return "NVMe"
|
||||
}
|
||||
|
||||
// Default to SSD for non-rotational and non-NVMe drives
|
||||
return "SSD"
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ package hardware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anatol/smart.go"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -19,6 +22,7 @@ type HardDrive struct {
|
||||
Model string
|
||||
Serial string
|
||||
Type string
|
||||
HWID string
|
||||
Temperatures []HardDriveTemperature `gorm:"foreignKey:HardDriveID"`
|
||||
}
|
||||
|
||||
@@ -39,26 +43,49 @@ type Snapshots struct {
|
||||
List []*HardwareSnapshot
|
||||
}
|
||||
|
||||
// Fetch the temperature of the device, optinally update the reference object
|
||||
func (h *HardDrive) GetTemperature() int {
|
||||
// Fetch the device by name
|
||||
disk, err := smart.Open("/dev/" + h.Name)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open device %s: %s\n", h.Name, err)
|
||||
return -1
|
||||
}
|
||||
defer disk.Close()
|
||||
|
||||
// Fetch SMART data
|
||||
smartInfo, err := disk.ReadGenericAttributes()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get SMART data for %s: %s\n", h.Name, err)
|
||||
return -1
|
||||
// Try HDD/SSD path
|
||||
temp, found := h.getTemperatureFromPath("/sys/block/" + h.Name + "/device/hwmon/")
|
||||
if found {
|
||||
return temp
|
||||
}
|
||||
|
||||
// Parse the temperature
|
||||
temperature := int(smartInfo.Temperature)
|
||||
// Try NVMe path
|
||||
temp, found = h.getTemperatureFromPath("/sys/block/" + h.Name + "/device/")
|
||||
if found {
|
||||
return temp
|
||||
}
|
||||
|
||||
// Return the found value
|
||||
return temperature
|
||||
fmt.Printf("[🛑] Failed to get temperature for %s\n", h.Name)
|
||||
return -1
|
||||
}
|
||||
|
||||
func (h *HardDrive) getTemperatureFromPath(basePath string) (int, bool) {
|
||||
hwmonDirs, err := os.ReadDir(basePath)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
for _, dir := range hwmonDirs {
|
||||
if strings.HasPrefix(dir.Name(), "hwmon") {
|
||||
tempPath := filepath.Join(basePath, dir.Name(), "temp1_input")
|
||||
if _, err := os.Stat(tempPath); err == nil {
|
||||
tempBytes, err := os.ReadFile(tempPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
tempStr := strings.TrimSpace(string(tempBytes))
|
||||
temperature, err := strconv.Atoi(tempStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert millidegree Celsius to degree Celsius
|
||||
return temperature / 1000, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user