Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions src/webui/internal/handlers/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,6 @@ type systemConfigWrapper struct {
}

type systemConfig struct {
Hostname string `json:"hostname"`
Contact string `json:"contact"`
Location string `json:"location"`
Software swConfig `json:"infix-system:software"`
Schedules struct {
Schedule []scheduleEntry `json:"schedule"`
Expand Down Expand Up @@ -464,28 +461,42 @@ func (h *DashboardHandler) Index(w http.ResponseWriter, r *http.Request) {
// The RESTCONF client's own 10 s timeout still bounds each call.
ctx := context.WithoutCancel(r.Context())
var (
state systemStateWrapper
hw hardwareWrapper
sysConf systemConfigWrapper
state systemStateWrapper
hw hardwareWrapper
sysConf systemConfigWrapper
ident struct {
System struct {
Hostname string `json:"hostname"`
Contact string `json:"contact"`
Location string `json:"location"`
} `json:"ietf-system:system"`
}
ifaces interfacesWrapper
routes ribWrapper
stateErr, hwErr, confErr error
wg sync.WaitGroup
)

wg.Add(5)
wg.Add(6)
go func() {
defer wg.Done()
stateErr = h.RC.Get(ctx, "/data/ietf-system:system-state", &state)
}()
// The operational view resolves hostname templates like "%m".
go func() {
defer wg.Done()
if err := h.RC.Get(ctx, "/data/ietf-system:system", &ident); err != nil {
log.Printf("restconf system identity: %v", err)
}
}()
go func() {
defer wg.Done()
hwErr = h.RC.Get(ctx, "/data/ietf-hardware:hardware", &hw)
}()
go func() {
defer wg.Done()
// Running, not /data: statd serves operational data for the system
// container and that answer hides running config it does not emit.
// Running, not /data, for the update card: the operational view
// hides config under the system container that statd does not emit.
confErr = h.RC.Get(ctx, "/ds/ietf-datastores:running/ietf-system:system", &sysConf)
}()
// Connectivity/Addresses cards are best-effort: a failure here logs but
Expand Down Expand Up @@ -614,12 +625,13 @@ func (h *DashboardHandler) Index(w http.ResponseWriter, r *http.Request) {

disambiguateVitals(data.KeyVitals)

data.Hostname = ident.System.Hostname
data.Contact = ident.System.Contact
data.Location = ident.System.Location

if confErr != nil {
log.Printf("restconf system config: %v", confErr)
} else {
data.Hostname = sysConf.System.Hostname
data.Contact = sysConf.System.Contact
data.Location = sysConf.System.Location
u := newUpdateEntry(sysConf.System.Software, sysConf.System.Schedules.Schedule,
state.SystemState.Software.Update)
data.Update = &u
Expand Down
40 changes: 39 additions & 1 deletion src/webui/internal/handlers/software_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package handlers

import "testing"
import (
"bytes"
"io"
"os"
"testing"
)

func TestOtherSlots(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -40,3 +45,36 @@ func TestOtherSlots(t *testing.T) {
})
}
}

type memPart struct{ *bytes.Reader }

func (memPart) Close() error { return nil }

func TestKeepUpload(t *testing.T) {
t.Setenv("TMPDIR", t.TempDir())

spilled, err := os.CreateTemp("", "multipart-")
if err != nil {
t.Fatal(err)
}
spilled.WriteString("bundle")
spilled.Seek(0, io.SeekStart)
path, err := keepUpload(spilled)
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(spilled.Name()); !os.IsNotExist(err) {
t.Errorf("spilled part still present, was copied rather than renamed")
}
if b, _ := os.ReadFile(path); string(b) != "bundle" {
t.Errorf("kept bundle = %q", b)
}

path, err = keepUpload(memPart{bytes.NewReader([]byte("small"))})
if err != nil {
t.Fatal(err)
}
if b, _ := os.ReadFile(path); string(b) != "small" {
t.Errorf("kept small bundle = %q", b)
}
}
57 changes: 45 additions & 12 deletions src/webui/internal/handlers/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"html/template"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"os/exec"
Expand Down Expand Up @@ -687,6 +688,17 @@ func (h *SystemHandler) SoftwareUpload(w http.ResponseWriter, r *http.Request) {
return
}

// nginx spools the whole upload before handing it over, and reading
// a bundle into the temp file takes longer than the server's 15 s
// read and write timeouts on a slow card. Push both out.
rc := http.NewResponseController(w)
if err := rc.SetReadDeadline(time.Now().Add(10 * time.Minute)); err != nil {
log.Printf("software upload: extend read deadline: %v", err)
}
if err := rc.SetWriteDeadline(time.Now().Add(10 * time.Minute)); err != nil {
log.Printf("software upload: extend write deadline: %v", err)
}

// 1 MiB in-RAM threshold; larger parts spill to $TMPDIR (/var/tmp on
// the target, eMMC-backed) instead of the RAM-backed /tmp.
if err := r.ParseMultipartForm(1 << 20); err != nil {
Expand All @@ -706,22 +718,12 @@ func (h *SystemHandler) SoftwareUpload(w http.ResponseWriter, r *http.Request) {
}
defer file.Close()

tmp, err := os.CreateTemp("", "webui-bundle-*.pkg")
tmpPath, err := keepUpload(file)
if err != nil {
log.Printf("software upload: create temp: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
tmpPath := tmp.Name()

if _, err := io.Copy(tmp, file); err != nil {
tmp.Close()
os.Remove(tmpPath)
log.Printf("software upload: write: %v", err)
log.Printf("software upload: keep bundle: %v", err)
http.Error(w, "failed to save bundle", http.StatusInternalServerError)
return
}
tmp.Close()

body := map[string]map[string]string{
"infix-system:input": {"url": tmpPath},
Expand All @@ -737,6 +739,37 @@ func (h *SystemHandler) SoftwareUpload(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, target)
}

// keepUpload moves the uploaded part to a file of its own that outlives
// the request. A part larger than the in-RAM threshold already sits in a
// temp file, which is renamed rather than copied, sparing the card a
// second write of the whole bundle. Smaller parts are written out.
func keepUpload(file multipart.File) (string, error) {
tmp, err := os.CreateTemp("", "webui-bundle-*.pkg")
if err != nil {
return "", err
}
tmpPath := tmp.Name()
tmp.Close()

if spilled, ok := file.(*os.File); ok {
if err := os.Rename(spilled.Name(), tmpPath); err == nil {
return tmpPath, nil
}
}

tmp, err = os.OpenFile(tmpPath, os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
os.Remove(tmpPath)
return "", err
}
defer tmp.Close()
if _, err := io.Copy(tmp, file); err != nil {
os.Remove(tmpPath)
return "", err
}
return tmpPath, nil
}

// runInstall fires the install-bundle RPC and then waits for RAUC to finish
// before deleting the uploaded .pkg. The 30-minute outer timeout is the
// safety net for a stuck RAUC.
Expand Down
Loading