Skip to content

Commit dd02ad4

Browse files
committed
fix(configurer): fall back to ip(8) when hostname lacks --all-ip-addresses
LinuxConfigurer.LocalAddresses() ran 'hostname --all-ip-addresses' unconditionally. SLES 12 SP5's hostname (net-tools, ~2018) does not support this GNU extension and exits status 4, which ValidateHosts surfaces as a hard failure before any MCR/MKE install work starts. Fall back to parsing 'ip -4 -o addr show scope global' when the hostname command fails. Also switch strings.Split -> strings.Fields on the hostname path so a trailing space no longer produces an empty address element. Re-add the WrkSles12 worker to TestLegacyCluster now that the Validate Hosts blocker is fixed. PRODENG-3588
1 parent 36bac03 commit dd02ad4

3 files changed

Lines changed: 147 additions & 6 deletions

File tree

pkg/configurer/linux.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,41 @@ func (c LinuxConfigurer) CheckPrivilege(_ os.Host) error {
130130
return nil
131131
}
132132

133-
// LocalAddresses returns a list of local addresses.
133+
// LocalAddresses returns a list of local IP addresses for the host.
134+
//
135+
// It first tries "hostname --all-ip-addresses" (GNU net-tools). Older toolchains
136+
// such as SLES 12 SP5 ship a hostname(1) that does not support this flag and
137+
// exit with status 4. In that case it falls back to parsing "ip -4 -o addr show
138+
// scope global", which is available on all supported Linux platforms.
134139
func (c LinuxConfigurer) LocalAddresses(h os.Host) ([]string, error) {
135140
output, err := h.ExecOutput("hostname --all-ip-addresses")
141+
if err == nil {
142+
// hostname emits addresses separated by spaces with a trailing space;
143+
// use Fields so the trailing space does not produce an empty element.
144+
return strings.Fields(output), nil
145+
}
146+
147+
// Fallback: "ip -4 -o addr show scope global" produces one line per
148+
// address in the form:
149+
// 2: eth0 inet 10.0.0.5/24 brd 10.0.0.255 scope global eth0\
150+
// Field index 3 is "addr/prefix".
151+
output, err = h.ExecOutput("ip -4 -o addr show scope global")
136152
if err != nil {
137153
return nil, fmt.Errorf("failed to get local addresses: %w", err)
138154
}
139155

140-
return strings.Split(output, " "), nil
156+
var addrs []string
157+
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
158+
fields := strings.Fields(line)
159+
if len(fields) < 4 {
160+
continue
161+
}
162+
addr, _, _ := strings.Cut(fields[3], "/")
163+
if iputil.IsValidAddress(addr) {
164+
addrs = append(addrs, addr)
165+
}
166+
}
167+
return addrs, nil
141168
}
142169

143170
type reconnectable interface {

pkg/configurer/linux_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package configurer
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"io/fs"
7+
"testing"
8+
9+
"github.com/k0sproject/rig/exec"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// mockHost stubs os.Host for unit-testing configurer methods that run remote
15+
// commands. Command responses are keyed by the full command string; a missing
16+
// key returns an error to simulate command-not-found / non-zero exit.
17+
type mockHost struct {
18+
outputs map[string]string
19+
errors map[string]error
20+
}
21+
22+
func (m *mockHost) String() string { return "mockHost" }
23+
24+
func (m *mockHost) ExecOutput(cmd string, _ ...exec.Option) (string, error) {
25+
if err, ok := m.errors[cmd]; ok {
26+
return "", err
27+
}
28+
if out, ok := m.outputs[cmd]; ok {
29+
return out, nil
30+
}
31+
return "", fmt.Errorf("unexpected command: %q", cmd)
32+
}
33+
34+
func (m *mockHost) Exec(cmd string, opts ...exec.Option) error {
35+
_, err := m.ExecOutput(cmd, opts...)
36+
return err
37+
}
38+
39+
func (m *mockHost) ExecOutputf(cmd string, argsOrOpts ...any) (string, error) {
40+
// Separate format args from exec.Option values.
41+
var args []any
42+
var opts []exec.Option
43+
for _, a := range argsOrOpts {
44+
if o, ok := a.(exec.Option); ok {
45+
opts = append(opts, o)
46+
} else {
47+
args = append(args, a)
48+
}
49+
}
50+
return m.ExecOutput(fmt.Sprintf(cmd, args...), opts...)
51+
}
52+
53+
func (m *mockHost) Execf(cmd string, argsOrOpts ...any) error {
54+
_, err := m.ExecOutputf(cmd, argsOrOpts...)
55+
return err
56+
}
57+
58+
func (m *mockHost) Upload(_ string, _ string, _ fs.FileMode, _ ...exec.Option) error {
59+
return nil
60+
}
61+
62+
func (m *mockHost) ExecStreams(_ string, _ io.ReadCloser, _ io.Writer, _ io.Writer, _ ...exec.Option) (exec.Waiter, error) {
63+
return nil, nil
64+
}
65+
66+
func (m *mockHost) Sudo(cmd string) (string, error) {
67+
return cmd, nil
68+
}
69+
70+
// TestLocalAddresses_Hostname verifies the happy path: hostname --all-ip-addresses
71+
// returns a space-separated list (with trailing space, as real hostname emits).
72+
func TestLocalAddresses_Hostname(t *testing.T) {
73+
h := &mockHost{
74+
outputs: map[string]string{
75+
"hostname --all-ip-addresses": "10.0.0.5 172.31.0.10 ",
76+
},
77+
}
78+
addrs, err := LinuxConfigurer{}.LocalAddresses(h)
79+
require.NoError(t, err)
80+
assert.Equal(t, []string{"10.0.0.5", "172.31.0.10"}, addrs)
81+
}
82+
83+
// TestLocalAddresses_IPFallback verifies the SLES 12 SP5 scenario: hostname
84+
// --all-ip-addresses fails (exit 4), so LocalAddresses falls back to
85+
// "ip -4 -o addr show scope global" and parses the addr/prefix fields.
86+
func TestLocalAddresses_IPFallback(t *testing.T) {
87+
ipOutput := "2: eth0 inet 10.0.0.5/24 brd 10.0.0.255 scope global eth0\\\n" +
88+
"3: eth1 inet 172.31.0.10/20 brd 172.31.15.255 scope global eth1\\\n"
89+
h := &mockHost{
90+
outputs: map[string]string{
91+
"ip -4 -o addr show scope global": ipOutput,
92+
},
93+
errors: map[string]error{
94+
"hostname --all-ip-addresses": fmt.Errorf("exit status 4"),
95+
},
96+
}
97+
addrs, err := LinuxConfigurer{}.LocalAddresses(h)
98+
require.NoError(t, err)
99+
assert.Equal(t, []string{"10.0.0.5", "172.31.0.10"}, addrs)
100+
}
101+
102+
// TestLocalAddresses_BothFail verifies that an error is returned when both
103+
// commands are unavailable (neither hostname nor ip work).
104+
func TestLocalAddresses_BothFail(t *testing.T) {
105+
h := &mockHost{
106+
errors: map[string]error{
107+
"hostname --all-ip-addresses": fmt.Errorf("exit status 4"),
108+
"ip -4 -o addr show scope global": fmt.Errorf("exit status 127"),
109+
},
110+
}
111+
_, err := LinuxConfigurer{}.LocalAddresses(h)
112+
assert.Error(t, err)
113+
}

test/smoke/smoke_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func runSmokeTest(t *testing.T, cfg smokeConfig) {
112112
"nodegroups": cfg.Nodegroups,
113113
"ssh_key_algorithm": cfg.SSHKeyAlgorithm,
114114
"extra_tags": map[string]string{
115-
"launchpad-smoke-test": "true",
115+
"launchpad-smoke-test": "true",
116116
"launchpad-smoke-test-name": cfg.Name,
117117
},
118118
}
@@ -191,8 +191,8 @@ func TestModernCluster(t *testing.T) {
191191
})
192192
}
193193

194-
// TestLegacyCluster exercises rhel8/rocky8/ubuntu22 managers and workers
195-
// with MCR stable-25.0 and MKE 3.8.8.
194+
// TestLegacyCluster exercises rhel8/rocky8/ubuntu22 managers and
195+
// rhel8/rocky8/ubuntu22/sles12 workers with MCR stable-25.0 and MKE 3.8.8.
196196
func TestLegacyCluster(t *testing.T) {
197197
runSmokeTest(t, smokeConfig{
198198
Name: "legacy",
@@ -207,6 +207,7 @@ func TestLegacyCluster(t *testing.T) {
207207
"WrkRhel8": test.Platforms["Rhel8"].GetWorker(),
208208
"WrkRocky8": test.Platforms["Rocky8"].GetWorker(),
209209
"WrkUbuntu22": test.Platforms["Ubuntu22"].GetWorker(),
210+
"WrkSles12": test.Platforms["Sles12"].GetWorker(),
210211
},
211212
})
212213
}
@@ -243,7 +244,7 @@ func TestFIPSCluster(t *testing.T) {
243244
SSHKeyAlgorithm: "rsa",
244245
Nodegroups: map[string]interface{}{
245246
"MngrUbuntu22FIPS": test.Platforms["Ubuntu22FIPS"].GetManager(),
246-
"WrkWin2025": test.Platforms["Windows2025"].GetWorker(),
247+
"WrkWin2025": test.Platforms["Windows2025"].GetWorker(),
247248
},
248249
})
249250
}

0 commit comments

Comments
 (0)