Fix TrueNAS system info buildtime decoding

Fixes #1469
This commit is contained in:
rcourtman 2026-05-15 14:21:39 +01:00
parent eac7dfe9ef
commit 2b9b1ec573
3 changed files with 77 additions and 10 deletions

View file

@ -767,6 +767,12 @@ stopping at disconnected toast notifications.
That same runtime owner also defines the feature-default contract for TrueNAS:
the API-backed integration is on by default, and `PULSE_ENABLE_TRUENAS` is an
explicit opt-out switch rather than a required bootstrap toggle.
That same TrueNAS monitoring boundary owns system identity compatibility for
`/system/info`. `internal/truenas/client.go` must tolerate provider-version
drift in non-identity display fields such as `buildtime`, including structured
date/value wrappers, and still preserve the canonical hostname, version,
machine ID, capacity, and poll-health path instead of failing connection tests
or background refreshes during JSON decoding.
That same monitoring boundary now also owns live TrueNAS disk temperatures.
`internal/truenas/client.go` and `internal/truenas/provider.go` must ingest
`disk.temperatures` from the TrueNAS API, fall back to modern

View file

@ -158,7 +158,7 @@ func (c *Client) GetSystemInfo(ctx context.Context) (*SystemInfo, error) {
machineID = strings.TrimSpace(response.Hostname)
}
build := strings.TrimSpace(response.BuildTime)
build := strings.TrimSpace(response.BuildTime.String())
if build == "" {
build = strings.TrimSpace(response.Version)
}
@ -2868,15 +2868,30 @@ func normalizeFingerprint(fingerprint string) (string, error) {
}
type systemInfoResponse struct {
Hostname string `json:"hostname"`
Version string `json:"version"`
BuildTime string `json:"buildtime"`
UptimeSeconds int64 `json:"uptime_seconds"`
SystemSerial string `json:"system_serial"`
SystemVendor string `json:"system_manufacturer"`
Cores int `json:"cores"`
PhysicalCores int `json:"physical_cores"`
Physmem int64 `json:"physmem"`
Hostname string `json:"hostname"`
Version string `json:"version"`
BuildTime textResponseField `json:"buildtime"`
UptimeSeconds int64 `json:"uptime_seconds"`
SystemSerial string `json:"system_serial"`
SystemVendor string `json:"system_manufacturer"`
Cores int `json:"cores"`
PhysicalCores int `json:"physical_cores"`
Physmem int64 `json:"physmem"`
}
type textResponseField string
func (f textResponseField) String() string {
return string(f)
}
func (f *textResponseField) UnmarshalJSON(data []byte) error {
value, err := parseTextResponseField(data)
if err != nil {
return err
}
*f = textResponseField(value)
return nil
}
type poolResponse struct {
@ -3068,6 +3083,34 @@ func readStringAny(record map[string]any, keys ...string) string {
return ""
}
func parseTextResponseField(raw json.RawMessage) (string, error) {
trimmed := bytes.TrimSpace(raw)
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
return "", nil
}
var decoded any
decoder := json.NewDecoder(bytes.NewReader(trimmed))
decoder.UseNumber()
if err := decoder.Decode(&decoded); err != nil {
return "", err
}
return textFromDecodedAny(decoded), nil
}
func textFromDecodedAny(value any) string {
switch typed := value.(type) {
case string:
return strings.TrimSpace(typed)
case json.Number:
return strings.TrimSpace(typed.String())
case map[string]any:
return readStringAny(typed, "rawvalue", "value", "parsed", "$date", "date", "datetime", "text", "string")
default:
return ""
}
}
func readStringSliceAny(record map[string]any, keys ...string) []string {
if record == nil {
return nil

View file

@ -172,6 +172,24 @@ func TestClientAuthHeaderBasic(t *testing.T) {
}
}
func TestGetSystemInfoAcceptsStructuredBuildTime(t *testing.T) {
server := newMockServer(t, map[string]apiResponse{
"/api/v2.0/system/info": {
body: `{"hostname":"nas","version":"TrueNAS-SCALE-25.10.3.1","buildtime":{"$date":"2026-05-14T18:24:01+02:00"},"uptime_seconds":1}`,
},
}, nil)
t.Cleanup(server.Close)
client := mustClientForServer(t, server.URL, ClientConfig{APIKey: "test-key"})
system, err := client.GetSystemInfo(context.Background())
if err != nil {
t.Fatalf("GetSystemInfo() error = %v", err)
}
if system.Build != "2026-05-14T18:24:01+02:00" {
t.Fatalf("Build = %q, want structured buildtime date", system.Build)
}
}
func TestTestConnectionSuccessAndFailure(t *testing.T) {
successServer := newMockServer(t, map[string]apiResponse{
"/api/v2.0/system/info": {body: `{"hostname":"nas","version":"v","buildtime":"b","uptime_seconds":1}`},