diff --git a/board/common/rootfs/usr/sbin/infix-check-update b/board/common/rootfs/usr/sbin/infix-check-update index 288c35ce..7c6a7fad 100755 --- a/board/common/rootfs/usr/sbin/infix-check-update +++ b/board/common/rootfs/usr/sbin/infix-check-update @@ -1,6 +1,6 @@ #!/bin/sh -# Check for available firmware updates and notify on login if one exists. -# Called by the scheduler when predefined-action infix-schedule:check-update fires. +# Check for available software updates and notify on login if one exists. +# Called by the scheduler. NOTIFY_FILE=/run/infix-update TAG=infix-update @@ -46,7 +46,7 @@ newer() { if [ "$IS_RELEASE" = false ] || newer "$LATEST" "$VERSION"; then RELEASE_URL="${UPDATE_URL}/releases/${LATEST_TAG}" - MSG="Firmware update available: ${LATEST_TAG}, running ${VERSION} (see ${RELEASE_URL})" + MSG="Software update available: ${LATEST_TAG}, running ${VERSION} (see ${RELEASE_URL})" logger -t "$TAG" "$MSG" printf '%s\n' "$MSG" > "$NOTIFY_FILE" else diff --git a/src/confd/yang/confd/infix-system-software.yang b/src/confd/yang/confd/infix-system-software.yang index 154a80b3..29afb29f 100644 --- a/src/confd/yang/confd/infix-system-software.yang +++ b/src/confd/yang/confd/infix-system-software.yang @@ -95,7 +95,7 @@ submodule infix-system-software { container check-update { description - "Policy for automatic firmware update checks. + "Policy for automatic software update checks. When 'enabled' and 'schedule' references a schedule, the system checks the configured URL for a newer release on each occurrence diff --git a/src/webui/internal/handlers/dashboard.go b/src/webui/internal/handlers/dashboard.go index 5c1c0e08..53953e91 100644 --- a/src/webui/internal/handlers/dashboard.go +++ b/src/webui/internal/handlers/dashboard.go @@ -11,7 +11,9 @@ import ( "math" "net" "net/http" + "os" "os/exec" + "regexp" "strconv" "strings" "sync" @@ -199,15 +201,15 @@ type hardwareWrapper struct { } type hwComponentJSON struct { - Name string `json:"name"` - Class string `json:"class"` - Description string `json:"description"` - Parent string `json:"parent"` - MfgName string `json:"mfg-name"` - ModelName string `json:"model-name"` - SerialNum string `json:"serial-num"` - HardwareRev string `json:"hardware-rev"` - PhysAddress string `json:"infix-hardware:phys-address"` + Name string `json:"name"` + Class string `json:"class"` + Description string `json:"description"` + Parent string `json:"parent"` + MfgName string `json:"mfg-name"` + ModelName string `json:"model-name"` + SerialNum string `json:"serial-num"` + HardwareRev string `json:"hardware-rev"` + PhysAddress string `json:"infix-hardware:phys-address"` WiFiRadio *wifiRadioHWJSON `json:"infix-hardware:wifi-radio"` GPSReceiver *struct{} `json:"infix-hardware:gps-receiver"` SensorData *struct { @@ -226,26 +228,26 @@ type hwComponentJSON struct { type dashboardData struct { PageData - Hostname string - Contact string - Location string - OSName string - OSVersion string - Machine string - CurrentTime string - Software string - Uptime string - MemTotal int64 - MemUsed int64 - MemPercent int - MemClass string - Load1 string - Load5 string - Load15 string - CPUClass string - Disks []diskEntry - Board boardInfo - KeyVitals []sensorEntry // Overview's at-a-glance subset: CPU/SoC + wifi-radio temperatures and fan RPMs. Status > Hardware has the full inventory. + Hostname string + Contact string + Location string + OSName string + OSVersion string + Machine string + CurrentTime string + Software string + Uptime string + MemTotal int64 + MemUsed int64 + MemPercent int + MemClass string + Load1 string + Load5 string + Load15 string + CPUClass string + Disks []diskEntry + Board boardInfo + KeyVitals []sensorEntry // Overview's at-a-glance subset: CPU/SoC + wifi-radio temperatures and fan RPMs. Status > Hardware has the full inventory. // Connectivity card. Gateways []gatewayEntry InternetProbe string // address pinged for the Internet reachability row @@ -254,7 +256,11 @@ type dashboardData struct { NTPSync string // "" / the selected NTP source address // Addresses card. Addresses []ifaceAddrEntry - Error string + // Software-update banner — shown only when an update is available. + UpdateAvailable bool + UpdateMessage string // verbatim CLI/login-banner notice + UpdateURL string // release URL extracted from the notice + Error string } // gatewayEntry is a default route's next-hop. @@ -305,6 +311,31 @@ type diskEntry struct { // reachability row — a well-known, stable anycast resolver. const internetProbe = "1.1.1.1" +// updateNoticeFile holds the software-update notice written by +// /usr/sbin/infix-check-update — the same text the CLI shows at login. +// A package var so tests can point it elsewhere. +var updateNoticeFile = "/run/infix-update" + +var updateURLRe = regexp.MustCompile(`https?://\S+`) + +// readUpdateNotice returns the software-update message verbatim (as shown in +// the CLI login banner) and the release URL extracted from it. Both are empty +// when no update is pending or the notice file is absent/empty. +func readUpdateNotice() (msg, url string) { + b, err := os.ReadFile(updateNoticeFile) + if err != nil { + return "", "" + } + msg = strings.TrimSpace(string(b)) + if msg == "" { + return "", "" + } + if u := updateURLRe.FindString(msg); u != "" { + url = strings.TrimRight(u, ").,;") + } + return msg, url +} + // DashboardHandler serves the main dashboard page. type DashboardHandler struct { Template *template.Template @@ -501,6 +532,14 @@ func (h *DashboardHandler) Index(w http.ResponseWriter, r *http.Request) { } data.Addresses = ifaceAddresses(ifaces) + // Software-update banner: surfaces the same notice as the CLI login + // banner (written by infix-check-update to the notice file). + if msg, url := readUpdateNotice(); msg != "" { + data.UpdateAvailable = true + data.UpdateMessage = msg + data.UpdateURL = url + } + tmplName := "dashboard.html" if r.Header.Get("HX-Request") == "true" { tmplName = "content" diff --git a/src/webui/static/css/style.css b/src/webui/static/css/style.css index 3c060ea0..aafcf2c4 100644 --- a/src/webui/static/css/style.css +++ b/src/webui/static/css/style.css @@ -579,6 +579,43 @@ details[open].nav-group-top > summary.nav-group-summary-top::before { padding: 1.25rem; } +/* Dashboard software-update banner — amber "attention" treatment, bright + and theme-aware, but built from the same card primitives as everything + else. Spans the full info-grid width and sits at the top. */ +.update-card { + border-color: var(--warning-border); + background: var(--warning-bg); + box-shadow: 0 0 0 1px var(--warning-border), var(--shadow-sm); +} +.update-card .update-card-header { + background: var(--warning); + color: #fff; + border-bottom-color: var(--warning-border); + display: flex; + align-items: center; + gap: 0.5rem; +} +.update-card .update-card-icon { + font-size: 1rem; + line-height: 1; +} +.update-card .update-card-body { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem 1.5rem; + flex-wrap: wrap; +} +.update-card .update-card-msg { + margin: 0; + color: var(--warning-fg); + font-size: 0.95rem; + font-weight: 500; +} +.update-card .update-card-action { + flex: none; +} + /* ========================================================================== Tables diff --git a/src/webui/templates/pages/dashboard.html b/src/webui/templates/pages/dashboard.html index 7f70c8b7..dafd87d8 100644 --- a/src/webui/templates/pages/dashboard.html +++ b/src/webui/templates/pages/dashboard.html @@ -8,6 +8,21 @@ {{end}}
+ {{if .UpdateAvailable}} +
+
+ Software Update Available +
+
+

{{.UpdateMessage}}

+ {{if .UpdateURL}} + View release → + {{end}} +
+
+ {{end}} +
System Information
diff --git a/test/case/system/schedule_reboot/test.py b/test/case/system/schedule_reboot/test.py index 54e8260f..c04d1413 100755 --- a/test/case/system/schedule_reboot/test.py +++ b/test/case/system/schedule_reboot/test.py @@ -12,7 +12,7 @@ with infamy.Test() as test: env = infamy.Env() target = env.attach("target", "mgmt", "netconf") - with test.step("Define a schedule and point scheduled-reboot at it"): + with test.step("Schedule a reboot"): target.put_config_dicts({ "ietf-system": { "system": {