webui: fix more wrappers decoding keyed GETs as bare nodes

Three more RESTCONF wrappers assume the response to a keyed GET is the
bare node, when the server nests it under the full parent path: the
container resource-usage stats always read zero, the WireGuard listen
port never populated, and resetting advertised link modes silently
re-enabled auto-negotiation on links where it was forced off.

Same class of bug as the firewall zone reset fix: the decode matches
nothing and the zero value is used as if valid.  Model the full nesting
in all three wrappers, reusing the existing container list wrapper.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-07-04 09:32:02 +02:00
parent 7291b812a6
commit 4c6aa115a3
3 changed files with 26 additions and 15 deletions
@@ -1369,15 +1369,22 @@ func (h *ConfigureInterfacesHandler) ResetEthernetAdvertised(w http.ResponseWrit
name := r.PathValue("name") name := r.PathValue("name")
base := ifacePath(name) + "/ieee802-ethernet-interface:ethernet" base := ifacePath(name) + "/ieee802-ethernet-interface:ethernet"
// Keyed GETs nest the requested node under its full parent path.
var resp struct { var resp struct {
AN struct { Interfaces struct {
Enable *bool `json:"enable"` Interface []struct {
} `json:"ieee802-ethernet-interface:auto-negotiation"` Ethernet struct {
AN struct {
Enable *bool `json:"enable"`
} `json:"auto-negotiation"`
} `json:"ieee802-ethernet-interface:ethernet"`
} `json:"interface"`
} `json:"ietf-interfaces:interfaces"`
} }
enable := true // YANG default enable := true // YANG default
if err := h.RC.Get(r.Context(), base+"/auto-negotiation", &resp); err == nil { if err := h.RC.Get(r.Context(), base+"/auto-negotiation", &resp); err == nil {
if resp.AN.Enable != nil { if ifs := resp.Interfaces.Interface; len(ifs) > 0 && ifs[0].Ethernet.AN.Enable != nil {
enable = *resp.AN.Enable enable = *ifs[0].Ethernet.AN.Enable
} }
} else if !restconf.IsNotFound(err) { } else if !restconf.IsNotFound(err) {
log.Printf("configure interfaces %s reset advertised get: %v", name, err) log.Printf("configure interfaces %s reset advertised get: %v", name, err)
+5 -7
View File
@@ -46,11 +46,6 @@ type containerListWrapper struct {
} `json:"infix-containers:containers"` } `json:"infix-containers:containers"`
} }
// containerResourceUsageWrapper wraps the RESTCONF resource-usage response.
type containerResourceUsageWrapper struct {
ResourceUsage containerResourceUsageJSON `json:"infix-containers:resource-usage"`
}
// ContainerEntry holds display-ready data for a single container row. // ContainerEntry holds display-ready data for a single container row.
type ContainerEntry struct { type ContainerEntry struct {
Name string Name string
@@ -106,13 +101,16 @@ func (h *ContainersHandler) Overview(w http.ResponseWriter, r *http.Request) {
defer wg.Done() defer wg.Done()
path := fmt.Sprintf("/data/infix-containers:containers/container=%s/resource-usage", path := fmt.Sprintf("/data/infix-containers:containers/container=%s/resource-usage",
url.PathEscape(name)) url.PathEscape(name))
var w containerResourceUsageWrapper var w containerListWrapper // keyed GETs nest under the full parent path
if err := h.RC.Get(ctx, path, &w); err != nil { if err := h.RC.Get(ctx, path, &w); err != nil {
log.Printf("restconf resource-usage %s: %v", name, err) log.Printf("restconf resource-usage %s: %v", name, err)
return return
} }
if len(w.Containers.Container) == 0 {
return
}
mu.Lock() mu.Lock()
usages[idx] = w.ResourceUsage usages[idx] = w.Containers.Container[0].ResourceUsage
mu.Unlock() mu.Unlock()
}(i, c.Name) }(i, c.Name)
} }
+9 -3
View File
@@ -21,8 +21,13 @@ type wgConfigJSON struct {
} }
// wgIfaceConfigWrapper is used to fetch per-interface WireGuard config. // wgIfaceConfigWrapper is used to fetch per-interface WireGuard config.
// Keyed GETs nest the requested node under its full parent path.
type wgIfaceConfigWrapper struct { type wgIfaceConfigWrapper struct {
WireGuard *wgConfigJSON `json:"infix-interfaces:wireguard"` Interfaces struct {
Interface []struct {
WireGuard *wgConfigJSON `json:"infix-interfaces:wireguard"`
} `json:"interface"`
} `json:"ietf-interfaces:interfaces"`
} }
// WGPeer holds display-ready data for a single WireGuard peer. // WGPeer holds display-ready data for a single WireGuard peer.
@@ -128,8 +133,9 @@ func buildWGTunnel(ctx context.Context, rc *restconf.Client, iface ifaceJSON) WG
// Fetch ListenPort from config endpoint (separate from oper-state). // Fetch ListenPort from config endpoint (separate from oper-state).
var cfgWrap wgIfaceConfigWrapper var cfgWrap wgIfaceConfigWrapper
path := fmt.Sprintf("/data/ietf-interfaces:interfaces/interface=%s/infix-interfaces:wireguard", iface.Name) path := fmt.Sprintf("/data/ietf-interfaces:interfaces/interface=%s/infix-interfaces:wireguard", iface.Name)
if err := rc.Get(ctx, path, &cfgWrap); err == nil && cfgWrap.WireGuard != nil { if err := rc.Get(ctx, path, &cfgWrap); err == nil &&
tunnel.ListenPort = cfgWrap.WireGuard.ListenPort len(cfgWrap.Interfaces.Interface) > 0 && cfgWrap.Interfaces.Interface[0].WireGuard != nil {
tunnel.ListenPort = cfgWrap.Interfaces.Interface[0].WireGuard.ListenPort
} }
// Build peers from embedded peer-status. // Build peers from embedded peer-status.