Skip to content

Commit 3851e38

Browse files
committed
io shaping: detect missing subcommand and surface friendly message
On EOS instances that ship without the io shaping subcommand (e.g. eosaliceo2-ns-02), `eos io shaping ls` exits 22 and dumps the generic `io stat` / `io enable` usage block on stdout. We were wrapping that as an error and rendering it verbatim in the IO Traffic view, which buried the actual cause under a wall of unrelated help text. Detect the pattern (exit status 22 + usage block that omits any mention of shaping) and return a sentinel ErrIOShapingUnsupported. The view checks for it via errors.Is and renders a short, actionable message instead of the raw output.
1 parent 4a12be1 commit 3851e38

3 files changed

Lines changed: 77 additions & 1 deletion

File tree

eos/fetch_ioshaping.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,38 @@ package eos
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"strconv"
89
"strings"
910
)
1011

12+
// ErrIOShapingUnsupported is returned when the connected EOS instance does
13+
// not implement the `io shaping` subcommand at all (older builds expose only
14+
// `io stat` / `io enable`). Callers can surface a friendly message instead
15+
// of dumping the raw usage text.
16+
var ErrIOShapingUnsupported = errors.New("io shaping is not supported by this EOS version")
17+
18+
// looksUnsupported recognises the response that EOS gives when an unknown
19+
// `io` subcommand is supplied: exit status 22 plus a usage block that lists
20+
// the supported `io` subcommands but does not mention `shaping`.
21+
func looksUnsupported(err error, output []byte) bool {
22+
if err == nil {
23+
return false
24+
}
25+
if !strings.Contains(err.Error(), "exit status 22") {
26+
return false
27+
}
28+
text := string(output)
29+
if !strings.Contains(text, "usage:") {
30+
return false
31+
}
32+
// The genuine `io shaping` usage text starts with `io shaping`. The
33+
// fallback usage that EOS prints for an unknown subcommand starts with
34+
// `io stat` / `io enable` and never names `shaping`.
35+
return !strings.Contains(text, "io shaping") && !strings.Contains(text, "io_shaping")
36+
}
37+
1138
func (c *Client) IOShaping(ctx context.Context, mode IOShapingMode) ([]IOShapingRecord, error) {
1239
flag := "--apps"
1340
switch mode {
@@ -18,6 +45,9 @@ func (c *Client) IOShaping(ctx context.Context, mode IOShapingMode) ([]IOShaping
1845
}
1946
output, err := c.runCommandContext(ctx, "eos", "io", "shaping", "ls", flag, "--json", "--window", "5")
2047
if err != nil {
48+
if looksUnsupported(err, output) {
49+
return nil, ErrIOShapingUnsupported
50+
}
2151
return nil, fmt.Errorf("io shaping ls: %w: %s", err, strings.TrimSpace(string(output)))
2252
}
2353

@@ -52,6 +82,9 @@ func (c *Client) IOShaping(ctx context.Context, mode IOShapingMode) ([]IOShaping
5282
func (c *Client) IOShapingPolicies(ctx context.Context) ([]IOShapingPolicyRecord, error) {
5383
output, err := c.runCommandContext(ctx, "eos", "io", "shaping", "policy", "ls", "--json")
5484
if err != nil {
85+
if looksUnsupported(err, output) {
86+
return nil, ErrIOShapingUnsupported
87+
}
5588
return nil, fmt.Errorf("io shaping policy ls: %w: %s", err, strings.TrimSpace(string(output)))
5689
}
5790

eos/fetch_ioshaping_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package eos
2+
3+
import (
4+
"errors"
5+
"testing"
6+
)
7+
8+
func TestLooksUnsupported(t *testing.T) {
9+
// Real-world output from eosaliceo2-ns-02 when running `eos io shaping ls`:
10+
// EOS doesn't recognise `shaping` so it dumps the help text for the
11+
// supported `io` subcommands (`stat`, `enable`, ...) and exits 22.
12+
unknownSubcmdHelp := []byte(`usage:
13+
io stat [-l] [-a] [-m] [-n] [-t] [-d] [-x] [--ss] [--sa] [--si] : print io statistics
14+
-l : show summary information (this is the default if -a,-t,-d,-x is not selected)
15+
io enable [-r] [-p] [-n] [--udp <address>] : enable collection of io statistics
16+
io disable [-r] [-p] [-n] [--udp <address>] : disable collection of io statistics
17+
`)
18+
19+
cases := []struct {
20+
name string
21+
err error
22+
output []byte
23+
want bool
24+
}{
25+
{"nil error", nil, unknownSubcmdHelp, false},
26+
{"exit 22 + unknown subcommand help", errors.New("exit status 22"), unknownSubcmdHelp, true},
27+
{"exit 22 wrapping help with shaping mention", errors.New("exit status 22"), []byte("usage: io shaping ls --json"), false},
28+
{"exit 1 + same help text", errors.New("exit status 1"), unknownSubcmdHelp, false},
29+
{"exit 22 + unrelated message", errors.New("exit status 22"), []byte("permission denied"), false},
30+
}
31+
for _, tt := range cases {
32+
t.Run(tt.name, func(t *testing.T) {
33+
if got := looksUnsupported(tt.err, tt.output); got != tt.want {
34+
t.Fatalf("looksUnsupported = %v, want %v", got, tt.want)
35+
}
36+
})
37+
}
38+
}

ui/view_io_shaping.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ui
22

33
import (
4+
"errors"
45
"fmt"
56
"sort"
67
"strings"
@@ -66,10 +67,14 @@ func (m model) renderIOShapingView(height int) string {
6667
}
6768

6869
if m.ioShapingErr != nil {
70+
message := m.ioShapingErr.Error()
71+
if errors.Is(m.ioShapingErr, eos.ErrIOShapingUnsupported) {
72+
message = "IO traffic shaping is not available on this EOS instance.\nThe `io shaping` subcommand is missing — check `eos io --help` on the MGM."
73+
}
6974
lines := []string{
7075
m.styles.label.Render("IO Traffic Shaping") + indicator,
7176
"",
72-
m.styles.error.Render(m.ioShapingErr.Error()),
77+
m.styles.error.Render(message),
7378
}
7479
return m.styles.panelDim.Width(width).Render(fitLines(lines, height))
7580
}

0 commit comments

Comments
 (0)