DEVICE-002: ARM machine-ID fallback — device-tree model + /etc/machine-id combo, then /proc/cpuinfo Serial (all-zero rejected), before the weak hostname fallback. Hardware-bound IDs on DMI-less devices. DEVICE-001: agent detects device_type (server/desktop/phone/tablet) from /sys signals — system battery (scope=Device peripherals excluded, UPS excluded), DRM connector state, framebuffer min-dimension for phone/tablet split. Reports device_type/device_model/os_distro in registration and system-info paths. SERVER-001: migration 061 — device_type, device_type_manual (operator override, never agent-written), device_model, os_distro on agents. effective_device_type computed into every serialized agent. SERVER-002: PUT /admin/agents/:id/device-type — set/clear override, enum-validated, journaled. WEB-001: device-type icons + fleet filter, device model in list, detail header badge with reclassify dropdown, os_distro surfaced. INSTALL-003: arm64 install path unblocked — helper (required manifest component) now cross-built aarch64-unknown-linux-musl via rust-lld in the server image, signed at boot (helperArches += arm64), listed in the release manifest. Install template already handled uname -m and pacman. Plus in-flight: desktop tray wiring, enrollment page polish, CI workflow updates, RAF session-broker/pacman-scanner docs, native installer scaffold.
243 lines
11 KiB
Go
243 lines
11 KiB
Go
package models
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid/v5"
|
|
)
|
|
|
|
// Agent represents a registered update agent
|
|
type Agent struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
Hostname string `json:"hostname" db:"hostname"`
|
|
OSType string `json:"os_type" db:"os_type"`
|
|
OSVersion string `json:"os_version" db:"os_version"`
|
|
OSArchitecture string `json:"os_architecture" db:"os_architecture"`
|
|
AgentVersion string `json:"agent_version" db:"agent_version"` // Version at registration
|
|
CurrentVersion string `json:"current_version" db:"current_version"` // Current running version
|
|
UpdateAvailable bool `json:"update_available" db:"update_available"` // Whether update is available
|
|
LastVersionCheck time.Time `json:"last_version_check" db:"last_version_check"` // Last time version was checked
|
|
MachineID *string `json:"machine_id,omitempty" db:"machine_id"` // Unique machine identifier
|
|
PublicKeyFingerprint *string `json:"public_key_fingerprint,omitempty" db:"public_key_fingerprint"` // Public key fingerprint
|
|
IsUpdating bool `json:"is_updating" db:"is_updating"` // Whether agent is currently updating
|
|
UpdatingToVersion *string `json:"updating_to_version,omitempty" db:"updating_to_version"` // Target version for ongoing update
|
|
UpdateInitiatedAt *time.Time `json:"update_initiated_at,omitempty" db:"update_initiated_at"` // When update process started
|
|
LastSeen time.Time `json:"last_seen" db:"last_seen"`
|
|
Status string `json:"status" db:"status"`
|
|
Metadata JSONB `json:"metadata" db:"metadata"`
|
|
RebootRequired bool `json:"reboot_required" db:"reboot_required"`
|
|
LastRebootAt *time.Time `json:"last_reboot_at,omitempty" db:"last_reboot_at"`
|
|
RebootReason *string `json:"reboot_reason,omitempty" db:"reboot_reason"`
|
|
DockerVersion string `json:"docker_version" db:"docker_version"`
|
|
DeviceType string `json:"device_type" db:"device_type"` // Agent-detected form factor (DEVICE-001)
|
|
DeviceTypeManual *string `json:"device_type_manual,omitempty" db:"device_type_manual"` // Operator override, nil = use auto (SERVER-002)
|
|
DeviceModel *string `json:"device_model,omitempty" db:"device_model"` // Hardware model string
|
|
OSDistro *string `json:"os_distro,omitempty" db:"os_distro"` // Distro ID from /etc/os-release
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// ValidDeviceType reports whether s is a recognized device form factor.
|
|
// Mirrors the CHECK constraint from migration 061 — validate before writing
|
|
// so a bad agent report degrades to the default instead of failing the row.
|
|
func ValidDeviceType(s string) bool {
|
|
switch s {
|
|
case "server", "desktop", "phone", "tablet":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// EffectiveDeviceType returns the operator override when set, else the
|
|
// agent-detected type. All display and policy decisions use this.
|
|
func (a *Agent) EffectiveDeviceType() string {
|
|
if a.DeviceTypeManual != nil && *a.DeviceTypeManual != "" {
|
|
return *a.DeviceTypeManual
|
|
}
|
|
return a.DeviceType
|
|
}
|
|
|
|
// AgentWithLastScan extends Agent with last scan information
|
|
type AgentWithLastScan struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
Hostname string `json:"hostname" db:"hostname"`
|
|
OSType string `json:"os_type" db:"os_type"`
|
|
OSVersion string `json:"os_version" db:"os_version"`
|
|
OSArchitecture string `json:"os_architecture" db:"os_architecture"`
|
|
AgentVersion string `json:"agent_version" db:"agent_version"` // Version at registration
|
|
CurrentVersion string `json:"current_version" db:"current_version"` // Current running version
|
|
UpdateAvailable bool `json:"update_available" db:"update_available"` // Whether update is available
|
|
LastVersionCheck time.Time `json:"last_version_check" db:"last_version_check"` // Last time version was checked
|
|
MachineID *string `json:"machine_id,omitempty" db:"machine_id"` // Unique machine identifier
|
|
PublicKeyFingerprint *string `json:"public_key_fingerprint,omitempty" db:"public_key_fingerprint"` // Public key fingerprint
|
|
IsUpdating bool `json:"is_updating" db:"is_updating"` // Whether agent is currently updating
|
|
UpdatingToVersion *string `json:"updating_to_version,omitempty" db:"updating_to_version"` // Target version for ongoing update
|
|
UpdateInitiatedAt *time.Time `json:"update_initiated_at,omitempty" db:"update_initiated_at"` // When update process started
|
|
LastSeen time.Time `json:"last_seen" db:"last_seen"`
|
|
Status string `json:"status" db:"status"`
|
|
Metadata JSONB `json:"metadata" db:"metadata"`
|
|
RebootRequired bool `json:"reboot_required" db:"reboot_required"`
|
|
LastRebootAt *time.Time `json:"last_reboot_at,omitempty" db:"last_reboot_at"`
|
|
RebootReason *string `json:"reboot_reason,omitempty" db:"reboot_reason"`
|
|
DockerVersion string `json:"docker_version" db:"docker_version"`
|
|
DeviceType string `json:"device_type" db:"device_type"`
|
|
DeviceTypeManual *string `json:"device_type_manual,omitempty" db:"device_type_manual"`
|
|
DeviceModel *string `json:"device_model,omitempty" db:"device_model"`
|
|
OSDistro *string `json:"os_distro,omitempty" db:"os_distro"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
LastScan *time.Time `json:"last_scan" db:"last_scan"`
|
|
}
|
|
|
|
// EffectiveDeviceType mirrors Agent.EffectiveDeviceType.
|
|
func (a *AgentWithLastScan) EffectiveDeviceType() string {
|
|
if a.DeviceTypeManual != nil && *a.DeviceTypeManual != "" {
|
|
return *a.DeviceTypeManual
|
|
}
|
|
return a.DeviceType
|
|
}
|
|
|
|
// MarshalJSON adds the computed effective_device_type to every serialized
|
|
// agent, so list and detail responses carry it without per-handler wiring.
|
|
func (a Agent) MarshalJSON() ([]byte, error) {
|
|
type alias Agent
|
|
return json.Marshal(struct {
|
|
alias
|
|
EffectiveDeviceType string `json:"effective_device_type"`
|
|
}{alias(a), a.EffectiveDeviceType()})
|
|
}
|
|
|
|
// MarshalJSON mirrors Agent.MarshalJSON.
|
|
func (a AgentWithLastScan) MarshalJSON() ([]byte, error) {
|
|
type alias AgentWithLastScan
|
|
return json.Marshal(struct {
|
|
alias
|
|
EffectiveDeviceType string `json:"effective_device_type"`
|
|
}{alias(a), a.EffectiveDeviceType()})
|
|
}
|
|
|
|
// AgentSpecs represents system specifications for an agent
|
|
type AgentSpecs struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
|
|
CPUModel string `json:"cpu_model" db:"cpu_model"`
|
|
CPUCores int `json:"cpu_cores" db:"cpu_cores"`
|
|
MemoryTotalMB int `json:"memory_total_mb" db:"memory_total_mb"`
|
|
DiskTotalGB int `json:"disk_total_gb" db:"disk_total_gb"`
|
|
DiskFreeGB int `json:"disk_free_gb" db:"disk_free_gb"`
|
|
NetworkInterfaces JSONB `json:"network_interfaces" db:"network_interfaces"`
|
|
DockerInstalled bool `json:"docker_installed" db:"docker_installed"`
|
|
DockerVersion string `json:"docker_version" db:"docker_version"`
|
|
PackageManagers StringArray `json:"package_managers" db:"package_managers"`
|
|
CollectedAt time.Time `json:"collected_at" db:"collected_at"`
|
|
}
|
|
|
|
// AgentRegistrationRequest is the payload for agent registration
|
|
type AgentRegistrationRequest struct {
|
|
Hostname string `json:"hostname" binding:"required"`
|
|
OSType string `json:"os_type" binding:"required"`
|
|
OSVersion string `json:"os_version"`
|
|
OSArchitecture string `json:"os_architecture"`
|
|
AgentVersion string `json:"agent_version" binding:"required"`
|
|
RegistrationToken string `json:"registration_token"` // Optional, for fallback method
|
|
MachineID string `json:"machine_id"` // Unique machine identifier
|
|
PublicKeyFingerprint string `json:"public_key_fingerprint"` // Embedded public key fingerprint
|
|
Metadata map[string]string `json:"metadata"`
|
|
AvailableScanners []string `json:"available_scanners"` // Platform-specific scanners (apt, dnf, winget, windows, docker)
|
|
DeviceType string `json:"device_type"` // Auto-detected form factor (DEVICE-001)
|
|
DeviceModel string `json:"device_model"` // Hardware model string
|
|
OSDistro string `json:"os_distro"` // Distro ID from /etc/os-release
|
|
}
|
|
|
|
// AgentRegistrationResponse is returned after successful registration
|
|
type AgentRegistrationResponse struct {
|
|
AgentID uuid.UUID `json:"agent_id"`
|
|
Token string `json:"token"` // Short-lived access token (24h)
|
|
RefreshToken string `json:"refresh_token"` // Long-lived refresh token (90d)
|
|
Config map[string]interface{} `json:"config"`
|
|
}
|
|
|
|
// TokenRenewalRequest is the payload for token renewal using refresh token
|
|
type TokenRenewalRequest struct {
|
|
AgentID uuid.UUID `json:"agent_id" binding:"required"`
|
|
RefreshToken string `json:"refresh_token" binding:"required"`
|
|
AgentVersion string `json:"agent_version,omitempty"` // Optional: agent's current version for upgrade tracking
|
|
}
|
|
|
|
// TokenRenewalResponse is returned after successful token renewal
|
|
type TokenRenewalResponse struct {
|
|
Token string `json:"token"` // New short-lived access token (24h)
|
|
RefreshToken string `json:"refresh_token"` // Rotated refresh token (migration 045) — agent must persist it
|
|
}
|
|
|
|
// UTCTime is a time.Time that marshals to ISO format with UTC timezone
|
|
type UTCTime time.Time
|
|
|
|
// MarshalJSON implements json.Marshaler for UTCTime
|
|
func (t UTCTime) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(time.Time(t).UTC().Format("2006-01-02T15:04:05.000Z"))
|
|
}
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler for UTCTime
|
|
func (t *UTCTime) UnmarshalJSON(data []byte) error {
|
|
var s string
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return err
|
|
}
|
|
parsed, err := time.Parse("2006-01-02T15:04:05.000Z", s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*t = UTCTime(parsed)
|
|
return nil
|
|
}
|
|
|
|
// JSONB type for PostgreSQL JSONB columns
|
|
type JSONB map[string]interface{}
|
|
|
|
// Value implements driver.Valuer for database storage
|
|
func (j JSONB) Value() (driver.Value, error) {
|
|
if j == nil {
|
|
return nil, nil
|
|
}
|
|
return json.Marshal(j)
|
|
}
|
|
|
|
// Scan implements sql.Scanner for database retrieval
|
|
func (j *JSONB) Scan(value interface{}) error {
|
|
if value == nil {
|
|
*j = nil
|
|
return nil
|
|
}
|
|
bytes, ok := value.([]byte)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return json.Unmarshal(bytes, j)
|
|
}
|
|
|
|
// StringArray type for PostgreSQL text[] columns
|
|
type StringArray []string
|
|
|
|
// Value implements driver.Valuer
|
|
func (s StringArray) Value() (driver.Value, error) {
|
|
if s == nil {
|
|
return nil, nil
|
|
}
|
|
return json.Marshal(s)
|
|
}
|
|
|
|
// Scan implements sql.Scanner
|
|
func (s *StringArray) Scan(value interface{}) error {
|
|
if value == nil {
|
|
*s = nil
|
|
return nil
|
|
}
|
|
bytes, ok := value.([]byte)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return json.Unmarshal(bytes, s)
|
|
}
|