mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-20 01:01:20 +00:00
Fix Unraid agent host profile detection
This commit is contained in:
parent
326196cbec
commit
ac82a28521
15 changed files with 154 additions and 26 deletions
|
|
@ -16,7 +16,9 @@
|
|||
"governance_state": "supported",
|
||||
"readiness_stage": "supported",
|
||||
"host_identity_tokens": [
|
||||
"unraid"
|
||||
"unraid",
|
||||
"unraid-os",
|
||||
"unraid os"
|
||||
],
|
||||
"runtime_platform": "linux",
|
||||
"support_floor": {
|
||||
|
|
|
|||
|
|
@ -194,7 +194,10 @@ system instead of falling back to an all-platform generic agent screen.
|
|||
Agent runtime normalization must use that same governed host-profile manifest:
|
||||
profile identity tokens and runtime platform fallback values such as
|
||||
`unraid` -> `linux` are generated into the runtime resolver instead of being
|
||||
redeclared in host-agent or settings-table branches.
|
||||
redeclared in host-agent or settings-table branches. Host-profile detection is
|
||||
an identity fact and must not depend on optional storage probes succeeding; an
|
||||
Unraid host still reports the governed `unraid` profile and `linux` runtime
|
||||
platform when `mdcmd` or array-topology collection is unavailable.
|
||||
The lifecycle-owned infrastructure source manager also owns platform/system
|
||||
grouping as source-management content, but not its table band presentation:
|
||||
`frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx` must
|
||||
|
|
|
|||
|
|
@ -834,7 +834,10 @@ the canonical monitored-system blocked payload.
|
|||
`agentIdentity.platform` remains the canonical runtime platform such as
|
||||
`linux`, while `agentIdentity.hostProfile` carries the governed profile id
|
||||
such as `unraid`; frontend clients must not re-promote those profile ids
|
||||
into first-class platform types.
|
||||
into first-class platform types. Once a governed host profile is resolved,
|
||||
the runtime platform is the profile's manifest runtime platform even when
|
||||
the host's base distro token is something implementation-specific such as
|
||||
Slackware on Unraid.
|
||||
Unified-resource agent payloads use the same split: `agent.platform` is
|
||||
the normalized runtime platform and `agent.hostProfile` is the optional
|
||||
governed profile id for host/appliance presentation.
|
||||
|
|
|
|||
|
|
@ -652,7 +652,9 @@ prompt explain the same operator-facing priority.
|
|||
frontend primitives must still render host-profile labels through
|
||||
explicit backend profile fields such as `agentIdentity.hostProfile` and
|
||||
unified-resource `platformData.agent.hostProfile` rather than prettifying
|
||||
presentation-only ids as platform values.
|
||||
presentation-only ids as platform values. Raw appliance identity aliases
|
||||
such as `unraid-os` belong only in the generated host-profile token list so
|
||||
shared helpers resolve them to `unraid` before presentation.
|
||||
9. Keep top-of-page summary interaction on shared primitives. Infrastructure, workloads, and storage summary cards must route sticky-shell behavior through `frontend-modern/src/components/shared/StickySummarySection.tsx` and route row-hover or focused-series rendering through shared chart primitives such as `frontend-modern/src/components/shared/InteractiveSparkline.tsx` and `frontend-modern/src/components/shared/DensityMap.tsx`, rather than page-local sticky wrappers or metric-card-specific hover logic. When a page keeps summary charts visible below the desktop breakpoint, it must use the shared `stickyDesktopOnly` mode instead of adding page-local media queries, so wrapped two-column summaries scroll as normal content and only become sticky once the large-screen layout is active. The shared summary-card contract must also own stable summary-card geometry for chart-backed cards so row hover, focus, synchronized readouts, or idle header metadata cannot ratchet the sticky summary taller across rerenders.
|
||||
10. Keep summary chart interaction identity on one shared helper. Summary surfaces that expose row-hover, group-hover, chart-hover, or route-focus-driven chart emphasis must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` and pass that same resolved scope into card-state, sparkline, and density-map primitives, rather than letting cards read `hovered || focused` while charts listen to a different page-local ID source. Hovering one summary chart must promote that series into the shared active entity so sibling cards highlight the same object instead of keeping chart-local hover islands, and hovering or pinning a workload group header, infrastructure cluster header, or storage pool-group header must scope the matching summary cards through that same shared contract instead of forking a page-local summary filter path. Sibling cards should surface that synchronized hover as one compact header readout through the shared summary-card contract, while the chart under the pointer keeps the only floating tooltip. `frontend-modern/src/components/Recovery/RecoverySummary.tsx` is explicitly outside this interaction dialect: recovery posture cards may share summary framing, but they must not silently grow row/group/chart hover behavior without a separate governed product decision.
|
||||
11. Keep page summaries page-scoped when table rows enter contextual focus. Route-backed row selection may add a focused label and shared series emphasis, but infrastructure, workloads, and storage summary cards must continue to render the page-level series set instead of collapsing the summary down to the selected row or replacing the global trend view with row-local empty states.
|
||||
|
|
|
|||
|
|
@ -622,7 +622,10 @@ such as `linux` without making the appliance profile a unified-resource
|
|||
platform. Unified-resource `AgentData` must carry that distinction explicitly:
|
||||
`agent.platform` is the normalized runtime platform, while
|
||||
`agent.hostProfile` carries a governed profile id such as `unraid` for
|
||||
presentation and host/appliance support-floor copy.
|
||||
presentation and host/appliance support-floor copy. Raw appliance identity
|
||||
aliases such as `unraid-os` may be accepted only through the generated
|
||||
host-profile token projection and must resolve to a governed profile id before
|
||||
they reach platform filters, source IDs, or top-level resource identity.
|
||||
That same shared source boundary also applies when unified seeds and
|
||||
supplemental providers coexist. If a canonical unified-resource seed omits an
|
||||
owned supplemental source such as TrueNAS or VMware, the shared resource API
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
AGENT_HOST_PROFILE_IDS,
|
||||
PLATFORM_TYPE_KEYS,
|
||||
PRESENTATION_ONLY_PLATFORM_IDS,
|
||||
SOURCE_AGENT_HOST_PROFILE_HOST_IDENTITY_TOKENS,
|
||||
getAgentHostProfileManifestEntry,
|
||||
getAgentHostProfileFamily,
|
||||
getAgentHostProfileRuntimePlatform,
|
||||
getSourcePlatformCanonicalProjections,
|
||||
|
|
@ -154,6 +156,13 @@ describe('sourcePlatforms', () => {
|
|||
expect(AGENT_HOST_PROFILE_IDS).toEqual(['unraid']);
|
||||
expect(getAgentHostProfileFamily('unraid')).toBe('Unraid');
|
||||
expect(getAgentHostProfileRuntimePlatform('unraid')).toBe('linux');
|
||||
expect(SOURCE_AGENT_HOST_PROFILE_HOST_IDENTITY_TOKENS.unraid).toEqual([
|
||||
'unraid',
|
||||
'unraid-os',
|
||||
'unraid os',
|
||||
]);
|
||||
expect(getAgentHostProfileManifestEntry('unraid-os')?.id).toBe('unraid');
|
||||
expect(getAgentHostProfileManifestEntry('Unraid OS')?.id).toBe('unraid');
|
||||
expect(PLATFORM_TYPE_KEYS).not.toContain('unraid');
|
||||
expect(PRESENTATION_ONLY_PLATFORM_IDS).toContain('unraid');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
// This file is generated by scripts/release_control/generate_platform_support_frontend_module.py.
|
||||
// Do not edit by hand.
|
||||
// Source: docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json
|
||||
// Source SHA256: 769261b515a0bd5baa4d17847a5d04b1f1a93ea0e5a8e0a048e85cd717def2e3
|
||||
// Source SHA256: d9f0b92b967180e558b838714f7bd45e0ba982306ff8bd94866aa4ab60db5320
|
||||
|
||||
export const PLATFORM_SUPPORT_MANIFEST_SOURCE = {
|
||||
path: 'docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json',
|
||||
sha256: '769261b515a0bd5baa4d17847a5d04b1f1a93ea0e5a8e0a048e85cd717def2e3',
|
||||
sha256: 'd9f0b92b967180e558b838714f7bd45e0ba982306ff8bd94866aa4ab60db5320',
|
||||
} as const;
|
||||
export const PLATFORM_SUPPORT_MANIFEST = {
|
||||
schemaVersion: 1,
|
||||
|
|
@ -24,7 +24,7 @@ export const PLATFORM_SUPPORT_MANIFEST = {
|
|||
family: 'Unraid',
|
||||
governanceState: 'supported',
|
||||
readinessStage: 'supported',
|
||||
hostIdentityTokens: ['unraid'],
|
||||
hostIdentityTokens: ['unraid', 'unraid-os', 'unraid os'],
|
||||
runtimePlatform: 'linux',
|
||||
supportFloor: {
|
||||
setup: 'supported',
|
||||
|
|
@ -507,7 +507,7 @@ export const SOURCE_AGENT_HOST_PROFILE_READINESS_STAGE = {
|
|||
unraid: 'supported',
|
||||
} as const;
|
||||
export const SOURCE_AGENT_HOST_PROFILE_HOST_IDENTITY_TOKENS = {
|
||||
unraid: ['unraid'],
|
||||
unraid: ['unraid', 'unraid-os', 'unraid os'],
|
||||
} as const;
|
||||
export const SOURCE_AGENT_HOST_PROFILE_SUPPORT_FLOOR = {
|
||||
unraid: {
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ func TestBuildConnections_AgentHostAliasesIncludeReportedIdentityHints(t *testin
|
|||
{
|
||||
ID: "pi",
|
||||
Hostname: "pi",
|
||||
Platform: "unraid",
|
||||
Platform: "slackware",
|
||||
OSName: "Unraid",
|
||||
OSVersion: "7.1.0",
|
||||
KernelVersion: "6.12.0",
|
||||
|
|
|
|||
|
|
@ -236,6 +236,9 @@ func New(cfg Config) (*Agent, error) {
|
|||
}
|
||||
osVersion := strings.TrimSpace(info.PlatformVersion)
|
||||
osName, osVersion = resolveHostOSIdentity(collector, osName, osVersion)
|
||||
if profile, ok := platformsupport.AgentHostProfileForIdentity(osName, platform); ok {
|
||||
platform = platformsupport.NormalizeRuntimePlatformForAgentHostProfile(profile.ID, platform)
|
||||
}
|
||||
kernelVersion := strings.TrimSpace(info.KernelVersion)
|
||||
arch := strings.TrimSpace(info.KernelArch)
|
||||
if arch == "" {
|
||||
|
|
|
|||
|
|
@ -516,6 +516,87 @@ Platform = QNAP
|
|||
t.Fatalf("normalisePlatform(unraid) = %q, want linux", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unraid normalizes slackware base platform", func(t *testing.T) {
|
||||
mc := &mockCollector{
|
||||
goos: "linux",
|
||||
hostInfoFn: func(context.Context) (*gohost.InfoStat, error) {
|
||||
return &gohost.InfoStat{
|
||||
Hostname: "tower",
|
||||
HostID: "hid",
|
||||
Platform: "slackware",
|
||||
PlatformFamily: "slackware",
|
||||
PlatformVersion: "15.0+",
|
||||
KernelVersion: "6.12.54-Unraid",
|
||||
KernelArch: runtime.GOARCH,
|
||||
}, nil
|
||||
},
|
||||
readFileFn: func(name string) ([]byte, error) {
|
||||
switch name {
|
||||
case "/etc/unraid-version":
|
||||
return []byte(`version="7.2.2"`), nil
|
||||
default:
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
agent, err := New(Config{APIToken: "token", LogLevel: zerolog.InfoLevel, Collector: mc})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if agent.platform != "linux" {
|
||||
t.Fatalf("platform = %q, want linux", agent.platform)
|
||||
}
|
||||
if agent.osName != "Unraid" {
|
||||
t.Fatalf("osName = %q, want Unraid", agent.osName)
|
||||
}
|
||||
if agent.osVersion != "7.2.2" {
|
||||
t.Fatalf("osVersion = %q, want 7.2.2", agent.osVersion)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unraid from os release fallback", func(t *testing.T) {
|
||||
mc := &mockCollector{
|
||||
goos: "linux",
|
||||
hostInfoFn: func(context.Context) (*gohost.InfoStat, error) {
|
||||
return &gohost.InfoStat{
|
||||
Hostname: "tower",
|
||||
HostID: "hid",
|
||||
Platform: "slackware",
|
||||
PlatformFamily: "slackware",
|
||||
PlatformVersion: "15.0+",
|
||||
KernelArch: runtime.GOARCH,
|
||||
}, nil
|
||||
},
|
||||
readFileFn: func(name string) ([]byte, error) {
|
||||
switch name {
|
||||
case "/etc/os-release":
|
||||
return []byte(`NAME="Unraid OS"
|
||||
ID="unraid-os"
|
||||
VERSION_ID="7.2.2"
|
||||
PRETTY_NAME="Unraid OS 7.2 x86_64"
|
||||
`), nil
|
||||
default:
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
agent, err := New(Config{APIToken: "token", LogLevel: zerolog.InfoLevel, Collector: mc})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if agent.platform != "linux" {
|
||||
t.Fatalf("platform = %q, want linux", agent.platform)
|
||||
}
|
||||
if agent.osName != "Unraid" {
|
||||
t.Fatalf("osName = %q, want Unraid", agent.osName)
|
||||
}
|
||||
if agent.osVersion != "7.2.2" {
|
||||
t.Fatalf("osVersion = %q, want 7.2.2", agent.osVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNew_UsesCustomCABundleForHTTPTransport(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -153,16 +153,39 @@ func detectQNAPOSIdentity(collector SystemCollector) (string, string, bool) {
|
|||
func detectUnraidOSIdentity(collector SystemCollector) (string, string, bool) {
|
||||
data, err := collector.ReadFile(hostAgentUnraidVersionPath)
|
||||
if err != nil {
|
||||
if _, statErr := collector.Stat(hostAgentUnraidVersionPath); statErr != nil {
|
||||
return "", "", false
|
||||
if _, statErr := collector.Stat(hostAgentUnraidVersionPath); statErr == nil {
|
||||
return "Unraid", "", true
|
||||
}
|
||||
return "Unraid", "", true
|
||||
return detectUnraidOSReleaseIdentity(collector)
|
||||
}
|
||||
|
||||
version := cleanUnraidVersion(string(data))
|
||||
return "Unraid", version, true
|
||||
}
|
||||
|
||||
func detectUnraidOSReleaseIdentity(collector SystemCollector) (string, string, bool) {
|
||||
data, err := collector.ReadFile("/etc/os-release")
|
||||
if err != nil || len(data) == 0 {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
values := parseAssignmentConfig(string(data))
|
||||
hints := strings.ToLower(strings.Join([]string{
|
||||
values["id"],
|
||||
values["name"],
|
||||
values["pretty_name"],
|
||||
}, " "))
|
||||
if !strings.Contains(hints, "unraid") {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(values["version_id"])
|
||||
if version == "" {
|
||||
version = cleanUnraidVersion(values["version"])
|
||||
}
|
||||
return "Unraid", version, true
|
||||
}
|
||||
|
||||
func cleanUnraidVersion(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Code generated by scripts/release_control/generate_platform_support_backend_module.py.
|
||||
// DO NOT EDIT.
|
||||
// Source: docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json
|
||||
// Source SHA256: 769261b515a0bd5baa4d17847a5d04b1f1a93ea0e5a8e0a048e85cd717def2e3
|
||||
// Source SHA256: d9f0b92b967180e558b838714f7bd45e0ba982306ff8bd94866aa4ab60db5320
|
||||
|
||||
package platformsupport
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ const ManifestSourcePath = "docs/release-control/v6/internal/PLATFORM_SUPPORT_MA
|
|||
|
||||
// ManifestSourceSHA256 is the sha256 of the manifest bytes used to produce
|
||||
// this projection.
|
||||
const ManifestSourceSHA256 = "769261b515a0bd5baa4d17847a5d04b1f1a93ea0e5a8e0a048e85cd717def2e3"
|
||||
const ManifestSourceSHA256 = "d9f0b92b967180e558b838714f7bd45e0ba982306ff8bd94866aa4ab60db5320"
|
||||
|
||||
// ManifestSchemaVersion is the schema_version field of the manifest.
|
||||
const ManifestSchemaVersion = 1
|
||||
|
|
@ -35,7 +35,7 @@ var agentHostProfileEntries = []AgentHostProfileEntry{
|
|||
Family: "Unraid",
|
||||
GovernanceState: "supported",
|
||||
ReadinessStage: "supported",
|
||||
HostIdentityTokens: []string{"unraid"},
|
||||
HostIdentityTokens: []string{"unraid", "unraid-os", "unraid os"},
|
||||
RuntimePlatform: "linux",
|
||||
},
|
||||
}
|
||||
|
|
@ -86,10 +86,7 @@ func NormalizeRuntimePlatformForAgentHostProfile(profileID, platform string) str
|
|||
if !ok || strings.TrimSpace(profile.RuntimePlatform) == "" {
|
||||
return reportedPlatform
|
||||
}
|
||||
if reportedPlatform == "" || agentHostProfileMatchesIdentity(profile, reportedPlatform) {
|
||||
return profile.RuntimePlatform
|
||||
}
|
||||
return reportedPlatform
|
||||
return profile.RuntimePlatform
|
||||
}
|
||||
|
||||
func (profile AgentHostProfileEntry) MatchesIdentity(value string) bool {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ func TestAgentHostProfileResolverUsesManifestTokens(t *testing.T) {
|
|||
if profile.RuntimePlatform != "linux" {
|
||||
t.Fatalf("runtime platform = %q, want linux", profile.RuntimePlatform)
|
||||
}
|
||||
if profile, ok := AgentHostProfileForIdentity("unraid-os"); !ok || profile.ID != "unraid" {
|
||||
t.Fatalf("expected raw os-release Unraid identity token to resolve, got %+v ok=%v", profile, ok)
|
||||
}
|
||||
|
||||
profile.HostIdentityTokens[0] = "mutated"
|
||||
profile, ok = AgentHostProfileForIdentity("unraid")
|
||||
|
|
@ -34,6 +37,9 @@ func TestNormalizeRuntimePlatformForAgentHostProfile(t *testing.T) {
|
|||
if got := NormalizeRuntimePlatformForAgentHostProfile("unraid", "linux"); got != "linux" {
|
||||
t.Fatalf("canonical reported platform = %q, want linux", got)
|
||||
}
|
||||
if got := NormalizeRuntimePlatformForAgentHostProfile("unraid", "slackware"); got != "linux" {
|
||||
t.Fatalf("profiled distro platform = %q, want linux", got)
|
||||
}
|
||||
if got := NormalizeRuntimePlatformForAgentHostProfile("unknown", "unraid"); got != "unraid" {
|
||||
t.Fatalf("unknown host profile platform = %q, want original value", got)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,11 +146,10 @@ func TestResourceFromHostProjectsAgentHostProfile(t *testing.T) {
|
|||
host := models.Host{
|
||||
ID: "tower-host",
|
||||
Hostname: "tower",
|
||||
Platform: "unraid",
|
||||
Platform: "slackware",
|
||||
OSName: "Unraid",
|
||||
OSVersion: "7.1.0",
|
||||
Status: "online",
|
||||
Unraid: &models.HostUnraidStorage{ArrayStarted: true},
|
||||
}
|
||||
|
||||
resource, _ := resourceFromHost(host)
|
||||
|
|
|
|||
|
|
@ -229,10 +229,7 @@ def render_module(normalized: dict[str, Any], manifest_hash: str) -> str:
|
|||
"\tif !ok || strings.TrimSpace(profile.RuntimePlatform) == \"\" {\n"
|
||||
"\t\treturn reportedPlatform\n"
|
||||
"\t}\n"
|
||||
"\tif reportedPlatform == \"\" || agentHostProfileMatchesIdentity(profile, reportedPlatform) {\n"
|
||||
"\t\treturn profile.RuntimePlatform\n"
|
||||
"\t}\n"
|
||||
"\treturn reportedPlatform\n"
|
||||
"\treturn profile.RuntimePlatform\n"
|
||||
"}\n\n"
|
||||
"func (profile AgentHostProfileEntry) MatchesIdentity(value string) bool {\n"
|
||||
"\treturn agentHostProfileMatchesIdentity(profile, value)\n"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue