This commit is contained in:
@@ -1167,6 +1167,16 @@ func (s *Service) ListDisks() ([]map[string]string, error) {
|
||||
disk["status"] = "unavailable"
|
||||
}
|
||||
|
||||
// Get SMART health info
|
||||
healthInfo := s.getDiskHealth(dev.Name)
|
||||
if healthInfo != nil {
|
||||
disk["health_status"] = healthInfo["status"]
|
||||
disk["health_temperature"] = healthInfo["temperature"]
|
||||
disk["health_power_on_hours"] = healthInfo["power_on_hours"]
|
||||
disk["health_reallocated_sectors"] = healthInfo["reallocated_sectors"]
|
||||
disk["health_pending_sectors"] = healthInfo["pending_sectors"]
|
||||
}
|
||||
|
||||
disks = append(disks, disk)
|
||||
}
|
||||
}
|
||||
@@ -1174,6 +1184,116 @@ func (s *Service) ListDisks() ([]map[string]string, error) {
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
// getDiskHealth retrieves SMART health information for a disk
|
||||
func (s *Service) getDiskHealth(diskName string) map[string]string {
|
||||
health := make(map[string]string)
|
||||
diskPath := "/dev/" + diskName
|
||||
|
||||
// Check if smartctl is available
|
||||
smartctlPath := "smartctl"
|
||||
if path, err := exec.LookPath("smartctl"); err != nil {
|
||||
// smartctl not available, skip health check
|
||||
return nil
|
||||
} else {
|
||||
smartctlPath = path
|
||||
}
|
||||
|
||||
// Get overall health status
|
||||
cmd := exec.Command("sudo", "-n", smartctlPath, "-H", diskPath)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
// Check if it's because SMART is not supported (common for virtual disks)
|
||||
// If exit code indicates unsupported, return nil (don't show health info)
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
exitCode := exitError.ExitCode()
|
||||
// Exit code 2 usually means "SMART not supported" or "device doesn't support SMART"
|
||||
if exitCode == 2 {
|
||||
return nil // Don't show health for unsupported devices
|
||||
}
|
||||
}
|
||||
// For other errors, also return nil (don't show unknown)
|
||||
return nil
|
||||
}
|
||||
|
||||
outputStr := string(output)
|
||||
|
||||
// Check if SMART is unsupported (common messages)
|
||||
if strings.Contains(outputStr, "SMART support is: Unavailable") ||
|
||||
strings.Contains(outputStr, "Device does not support SMART") ||
|
||||
strings.Contains(outputStr, "SMART not supported") ||
|
||||
strings.Contains(outputStr, "Unable to detect device type") ||
|
||||
strings.Contains(outputStr, "SMART support is: Disabled") {
|
||||
return nil // Don't show health for unsupported devices
|
||||
}
|
||||
|
||||
// Parse health status - multiple possible formats:
|
||||
// - "SMART overall-health self-assessment test result: PASSED" or "FAILED"
|
||||
// - "SMART Health Status: OK"
|
||||
// - "SMART Status: OK" or "SMART Status: FAILED"
|
||||
if strings.Contains(outputStr, "PASSED") ||
|
||||
strings.Contains(outputStr, "SMART Health Status: OK") ||
|
||||
strings.Contains(outputStr, "SMART Status: OK") {
|
||||
health["status"] = "healthy"
|
||||
} else if strings.Contains(outputStr, "FAILED") ||
|
||||
strings.Contains(outputStr, "SMART Status: FAILED") {
|
||||
health["status"] = "failed"
|
||||
} else {
|
||||
// If we can't determine status but SMART is supported, return nil instead of unknown
|
||||
// This avoids showing "Unknown" for virtual disks or devices with unclear status
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get detailed SMART attributes
|
||||
cmd = exec.Command("sudo", "-n", smartctlPath, "-A", diskPath)
|
||||
attrOutput, err := cmd.Output()
|
||||
if err != nil {
|
||||
// Return what we have
|
||||
return health
|
||||
}
|
||||
|
||||
attrStr := string(attrOutput)
|
||||
lines := strings.Split(attrStr, "\n")
|
||||
|
||||
// Parse key attributes
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 10 {
|
||||
continue
|
||||
}
|
||||
|
||||
// ID 194: Temperature_Celsius
|
||||
if strings.Contains(line, "194") && strings.Contains(line, "Temperature") {
|
||||
if len(fields) >= 9 {
|
||||
health["temperature"] = fields[9] + "°C"
|
||||
}
|
||||
}
|
||||
|
||||
// ID 9: Power_On_Hours
|
||||
if strings.Contains(line, "9") && (strings.Contains(line, "Power_On") || strings.Contains(line, "Power-on")) {
|
||||
if len(fields) >= 9 {
|
||||
hours := fields[9]
|
||||
health["power_on_hours"] = hours
|
||||
}
|
||||
}
|
||||
|
||||
// ID 5: Reallocated_Sector_Ct
|
||||
if strings.Contains(line, "5") && strings.Contains(line, "Reallocated") {
|
||||
if len(fields) >= 9 {
|
||||
health["reallocated_sectors"] = fields[9]
|
||||
}
|
||||
}
|
||||
|
||||
// ID 197: Current_Pending_Sector
|
||||
if strings.Contains(line, "197") && strings.Contains(line, "Pending") {
|
||||
if len(fields) >= 9 {
|
||||
health["pending_sectors"] = fields[9]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return health
|
||||
}
|
||||
|
||||
// parseSize converts human-readable size to bytes
|
||||
func parseSize(s string) (uint64, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
@@ -1328,3 +1448,18 @@ func (s *Service) GetSnapshot(name string) (*models.Snapshot, error) {
|
||||
|
||||
return nil, fmt.Errorf("snapshot %s not found", name)
|
||||
}
|
||||
|
||||
// RestoreSnapshot rolls back a dataset to a snapshot
|
||||
func (s *Service) RestoreSnapshot(snapshotName string, force bool) error {
|
||||
args := []string{"rollback"}
|
||||
if force {
|
||||
args = append(args, "-r") // Recursive rollback for child datasets
|
||||
}
|
||||
args = append(args, snapshotName)
|
||||
|
||||
_, err := s.execCommand(s.zfsPath, args...)
|
||||
if err != nil {
|
||||
return translateZFSError(err, "merestore snapshot", snapshotName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user