mirror of
https://github.com/henrygd/beszel.git
synced 2026-08-07 19:11:49 +02:00
Compare commits
2 Commits
a2b6c7f5e6
...
afcae025ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afcae025ae | ||
|
|
1de36625a4 |
@@ -515,10 +515,12 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
|
|||||||
// smartctlArgs returns the arguments for the smartctl command
|
// smartctlArgs returns the arguments for the smartctl command
|
||||||
// based on the device type and whether to include standby mode
|
// based on the device type and whether to include standby mode
|
||||||
func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool) []string {
|
func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool) []string {
|
||||||
args := make([]string, 0, 7)
|
args := make([]string, 0, 9)
|
||||||
|
var deviceType, parserType string
|
||||||
|
|
||||||
if deviceInfo != nil {
|
if deviceInfo != nil {
|
||||||
deviceType := strings.ToLower(deviceInfo.Type)
|
deviceType = strings.ToLower(deviceInfo.Type)
|
||||||
|
parserType = strings.ToLower(deviceInfo.parserType)
|
||||||
// types sometimes misidentified in scan; see github.com/henrygd/beszel/issues/1345
|
// types sometimes misidentified in scan; see github.com/henrygd/beszel/issues/1345
|
||||||
if deviceType != "" && deviceType != "scsi" && deviceType != "ata" {
|
if deviceType != "" && deviceType != "scsi" && deviceType != "ata" {
|
||||||
args = append(args, "-d", deviceInfo.Type)
|
args = append(args, "-d", deviceInfo.Type)
|
||||||
@@ -526,6 +528,13 @@ func (sm *SmartManager) smartctlArgs(deviceInfo *DeviceInfo, includeStandby bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, "-a", "--json=c")
|
args = append(args, "-a", "--json=c")
|
||||||
|
effectiveType := parserType
|
||||||
|
if effectiveType == "" {
|
||||||
|
effectiveType = deviceType
|
||||||
|
}
|
||||||
|
if effectiveType == "sat" || effectiveType == "ata" {
|
||||||
|
args = append(args, "-l", "devstat")
|
||||||
|
}
|
||||||
|
|
||||||
if includeStandby {
|
if includeStandby {
|
||||||
args = append(args, "-n", "standby")
|
args = append(args, "-n", "standby")
|
||||||
@@ -829,6 +838,11 @@ func (sm *SmartManager) parseSmartForSata(output []byte) (bool, int) {
|
|||||||
smartData.FirmwareVersion = data.FirmwareVersion
|
smartData.FirmwareVersion = data.FirmwareVersion
|
||||||
smartData.Capacity = data.UserCapacity.Bytes
|
smartData.Capacity = data.UserCapacity.Bytes
|
||||||
smartData.Temperature = data.Temperature.Current
|
smartData.Temperature = data.Temperature.Current
|
||||||
|
if smartData.Temperature == 0 {
|
||||||
|
if temp, ok := temperatureFromAtaDeviceStatistics(data.AtaDeviceStatistics); ok {
|
||||||
|
smartData.Temperature = temp
|
||||||
|
}
|
||||||
|
}
|
||||||
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
smartData.SmartStatus = getSmartStatus(smartData.Temperature, data.SmartStatus.Passed)
|
||||||
smartData.DiskName = data.Device.Name
|
smartData.DiskName = data.Device.Name
|
||||||
smartData.DiskType = data.Device.Type
|
smartData.DiskType = data.Device.Type
|
||||||
@@ -867,6 +881,36 @@ func getSmartStatus(temperature uint8, passed bool) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func temperatureFromAtaDeviceStatistics(stats smart.AtaDeviceStatistics) (uint8, bool) {
|
||||||
|
entry := findAtaDeviceStatisticsEntry(stats, 5, "Current Temperature")
|
||||||
|
if entry == nil || entry.Value == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
if *entry.Value > 255 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return uint8(*entry.Value), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// findAtaDeviceStatisticsEntry centralizes ATA devstat lookups so additional
|
||||||
|
// metrics can be pulled from the same structure in the future.
|
||||||
|
func findAtaDeviceStatisticsEntry(stats smart.AtaDeviceStatistics, pageNumber uint8, entryName string) *smart.AtaDeviceStatisticsEntry {
|
||||||
|
for pageIdx := range stats.Pages {
|
||||||
|
page := &stats.Pages[pageIdx]
|
||||||
|
if page.Number != pageNumber {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for entryIdx := range page.Table {
|
||||||
|
entry := &page.Table[entryIdx]
|
||||||
|
if !strings.EqualFold(entry.Name, entryName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) {
|
func (sm *SmartManager) parseSmartForScsi(output []byte) (bool, int) {
|
||||||
var data smart.SmartInfoForScsi
|
var data smart.SmartInfoForScsi
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,39 @@ func TestParseSmartForSata(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSmartForSataDeviceStatisticsTemperature(t *testing.T) {
|
||||||
|
jsonPayload := []byte(`{
|
||||||
|
"smartctl": {"exit_status": 0},
|
||||||
|
"device": {"name": "/dev/sdb", "type": "sat"},
|
||||||
|
"model_name": "SanDisk SSD U110 16GB",
|
||||||
|
"serial_number": "DEVSTAT123",
|
||||||
|
"firmware_version": "U21B001",
|
||||||
|
"user_capacity": {"bytes": 16013942784},
|
||||||
|
"smart_status": {"passed": true},
|
||||||
|
"ata_smart_attributes": {"table": []},
|
||||||
|
"ata_device_statistics": {
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"number": 5,
|
||||||
|
"name": "Temperature Statistics",
|
||||||
|
"table": [
|
||||||
|
{"name": "Current Temperature", "value": 22, "flags": {"valid": true}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||||
|
hasData, exitStatus := sm.parseSmartForSata(jsonPayload)
|
||||||
|
require.True(t, hasData)
|
||||||
|
assert.Equal(t, 0, exitStatus)
|
||||||
|
|
||||||
|
deviceData, ok := sm.SmartDataMap["DEVSTAT123"]
|
||||||
|
require.True(t, ok, "expected smart data entry for serial DEVSTAT123")
|
||||||
|
assert.Equal(t, uint8(22), deviceData.Temperature)
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseSmartForSataParentheticalRawValue(t *testing.T) {
|
func TestParseSmartForSataParentheticalRawValue(t *testing.T) {
|
||||||
jsonPayload := []byte(`{
|
jsonPayload := []byte(`{
|
||||||
"smartctl": {"exit_status": 0},
|
"smartctl": {"exit_status": 0},
|
||||||
@@ -267,15 +300,21 @@ func TestSmartctlArgs(t *testing.T) {
|
|||||||
|
|
||||||
sataDevice := &DeviceInfo{Name: "/dev/sda", Type: "sat"}
|
sataDevice := &DeviceInfo{Name: "/dev/sda", Type: "sat"}
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]string{"-d", "sat", "-a", "--json=c", "-n", "standby", "/dev/sda"},
|
[]string{"-d", "sat", "-a", "--json=c", "-l", "devstat", "-n", "standby", "/dev/sda"},
|
||||||
sm.smartctlArgs(sataDevice, true),
|
sm.smartctlArgs(sataDevice, true),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]string{"-d", "sat", "-a", "--json=c", "/dev/sda"},
|
[]string{"-d", "sat", "-a", "--json=c", "-l", "devstat", "/dev/sda"},
|
||||||
sm.smartctlArgs(sataDevice, false),
|
sm.smartctlArgs(sataDevice, false),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
nvmeDevice := &DeviceInfo{Name: "/dev/nvme0", Type: "nvme"}
|
||||||
|
assert.Equal(t,
|
||||||
|
[]string{"-d", "nvme", "-a", "--json=c", "-n", "standby", "/dev/nvme0"},
|
||||||
|
sm.smartctlArgs(nvmeDevice, true),
|
||||||
|
)
|
||||||
|
|
||||||
assert.Equal(t,
|
assert.Equal(t,
|
||||||
[]string{"-a", "--json=c", "-n", "standby"},
|
[]string{"-a", "--json=c", "-n", "standby"},
|
||||||
sm.smartctlArgs(nil, true),
|
sm.smartctlArgs(nil, true),
|
||||||
@@ -516,18 +555,18 @@ func TestUpdateSmartDevicesPreservesRAIDDrives(t *testing.T) {
|
|||||||
},
|
},
|
||||||
SmartDataMap: map[string]*smart.SmartData{
|
SmartDataMap: map[string]*smart.SmartData{
|
||||||
"serial-0": {
|
"serial-0": {
|
||||||
DiskName: "/dev/sda",
|
DiskName: "/dev/sda",
|
||||||
DiskType: "megaraid,0",
|
DiskType: "megaraid,0",
|
||||||
SerialNumber: "serial-0",
|
SerialNumber: "serial-0",
|
||||||
},
|
},
|
||||||
"serial-1": {
|
"serial-1": {
|
||||||
DiskName: "/dev/sda",
|
DiskName: "/dev/sda",
|
||||||
DiskType: "megaraid,1",
|
DiskType: "megaraid,1",
|
||||||
SerialNumber: "serial-1",
|
SerialNumber: "serial-1",
|
||||||
},
|
},
|
||||||
"serial-stale": {
|
"serial-stale": {
|
||||||
DiskName: "/dev/sda",
|
DiskName: "/dev/sda",
|
||||||
DiskType: "megaraid,2",
|
DiskType: "megaraid,2",
|
||||||
SerialNumber: "serial-stale",
|
SerialNumber: "serial-stale",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -130,10 +130,23 @@ type SummaryInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AtaSmartAttributes struct {
|
type AtaSmartAttributes struct {
|
||||||
// Revision int `json:"revision"`
|
|
||||||
Table []AtaSmartAttribute `json:"table"`
|
Table []AtaSmartAttribute `json:"table"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AtaDeviceStatistics struct {
|
||||||
|
Pages []AtaDeviceStatisticsPage `json:"pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtaDeviceStatisticsPage struct {
|
||||||
|
Number uint8 `json:"number"`
|
||||||
|
Table []AtaDeviceStatisticsEntry `json:"table"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AtaDeviceStatisticsEntry struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value *uint64 `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type AtaSmartAttribute struct {
|
type AtaSmartAttribute struct {
|
||||||
ID uint16 `json:"id"`
|
ID uint16 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -343,7 +356,8 @@ type SmartInfoForSata struct {
|
|||||||
SmartStatus SmartStatusInfo `json:"smart_status"`
|
SmartStatus SmartStatusInfo `json:"smart_status"`
|
||||||
// AtaSmartData AtaSmartData `json:"ata_smart_data"`
|
// AtaSmartData AtaSmartData `json:"ata_smart_data"`
|
||||||
// AtaSctCapabilities AtaSctCapabilities `json:"ata_sct_capabilities"`
|
// AtaSctCapabilities AtaSctCapabilities `json:"ata_sct_capabilities"`
|
||||||
AtaSmartAttributes AtaSmartAttributes `json:"ata_smart_attributes"`
|
AtaSmartAttributes AtaSmartAttributes `json:"ata_smart_attributes"`
|
||||||
|
AtaDeviceStatistics AtaDeviceStatistics `json:"ata_device_statistics"`
|
||||||
// PowerOnTime PowerOnTimeInfo `json:"power_on_time"`
|
// PowerOnTime PowerOnTimeInfo `json:"power_on_time"`
|
||||||
// PowerCycleCount uint16 `json:"power_cycle_count"`
|
// PowerCycleCount uint16 `json:"power_cycle_count"`
|
||||||
Temperature TemperatureInfo `json:"temperature"`
|
Temperature TemperatureInfo `json:"temperature"`
|
||||||
|
|||||||
@@ -49,10 +49,12 @@ export function AddSystemButton({ className }: { className?: string }) {
|
|||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" className={cn("flex gap-1 max-xs:h-[2.4rem]", className)}>
|
<Button variant="outline" className={cn("flex gap-1 max-xs:h-[2.4rem]", className)}>
|
||||||
<PlusIcon className="h-4 w-4 -ms-1" />
|
<PlusIcon className="h-4 w-4 450:-ms-1" />
|
||||||
<Trans>
|
<span className="hidden 450:inline">
|
||||||
Add <span className="hidden sm:inline">System</span>
|
<Trans>
|
||||||
</Trans>
|
Add <span className="hidden sm:inline">System</span>
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
{opened.current && <SystemDialog setOpen={setOpen} />}
|
{opened.current && <SystemDialog setOpen={setOpen} />}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export default function Navbar() {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<AddSystemButton className="ms-2 hidden 450:flex" />
|
<AddSystemButton className="ms-2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user