Skip to content
Open
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
31 changes: 29 additions & 2 deletions pkg/configurer/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,41 @@ func (c LinuxConfigurer) CheckPrivilege(_ os.Host) error {
return nil
}

// LocalAddresses returns a list of local addresses.
// LocalAddresses returns a list of local IP addresses for the host.
//
// It first tries "hostname --all-ip-addresses" (GNU net-tools). Older toolchains
// such as SLES 12 SP5 ship a hostname(1) that does not support this flag and
// exit with status 4. In that case it falls back to parsing "ip -4 -o addr show
// scope global", which is available on all supported Linux platforms.
func (c LinuxConfigurer) LocalAddresses(h os.Host) ([]string, error) {
output, err := h.ExecOutput("hostname --all-ip-addresses")
if err == nil {
// hostname emits addresses separated by spaces with a trailing space;
// use Fields so the trailing space does not produce an empty element.
return strings.Fields(output), nil
}

// Fallback: "ip -4 -o addr show scope global" produces one line per
// address in the form:
// 2: eth0 inet 10.0.0.5/24 brd 10.0.0.255 scope global eth0\
// Field index 3 is "addr/prefix".
output, err = h.ExecOutput("ip -4 -o addr show scope global")
if err != nil {
return nil, fmt.Errorf("failed to get local addresses: %w", err)
}

return strings.Split(output, " "), nil
var addrs []string
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
fields := strings.Fields(line)
if len(fields) < 4 {
continue
}
addr, _, _ := strings.Cut(fields[3], "/")
if iputil.IsValidAddress(addr) {
addrs = append(addrs, addr)
}
}
return addrs, nil
}

type reconnectable interface {
Expand Down
113 changes: 113 additions & 0 deletions pkg/configurer/linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package configurer

import (
"fmt"
"io"
"io/fs"
"testing"

"github.com/k0sproject/rig/exec"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// mockHost stubs os.Host for unit-testing configurer methods that run remote
// commands. Command responses are keyed by the full command string; a missing
// key returns an error to simulate command-not-found / non-zero exit.
type mockHost struct {
outputs map[string]string
errors map[string]error
}

func (m *mockHost) String() string { return "mockHost" }

func (m *mockHost) ExecOutput(cmd string, _ ...exec.Option) (string, error) {
if err, ok := m.errors[cmd]; ok {
return "", err
}
if out, ok := m.outputs[cmd]; ok {
return out, nil
}
return "", fmt.Errorf("unexpected command: %q", cmd)
}

func (m *mockHost) Exec(cmd string, opts ...exec.Option) error {
_, err := m.ExecOutput(cmd, opts...)
return err
}

func (m *mockHost) ExecOutputf(cmd string, argsOrOpts ...any) (string, error) {
// Separate format args from exec.Option values.
var args []any
var opts []exec.Option
for _, a := range argsOrOpts {
if o, ok := a.(exec.Option); ok {
opts = append(opts, o)
} else {
args = append(args, a)
}
}
return m.ExecOutput(fmt.Sprintf(cmd, args...), opts...)
}

func (m *mockHost) Execf(cmd string, argsOrOpts ...any) error {
_, err := m.ExecOutputf(cmd, argsOrOpts...)
return err
}

func (m *mockHost) Upload(_ string, _ string, _ fs.FileMode, _ ...exec.Option) error {
return nil
}

func (m *mockHost) ExecStreams(_ string, _ io.ReadCloser, _ io.Writer, _ io.Writer, _ ...exec.Option) (exec.Waiter, error) {
return nil, nil
}

func (m *mockHost) Sudo(cmd string) (string, error) {
return cmd, nil
}

// TestLocalAddresses_Hostname verifies the happy path: hostname --all-ip-addresses
// returns a space-separated list (with trailing space, as real hostname emits).
func TestLocalAddresses_Hostname(t *testing.T) {
h := &mockHost{
outputs: map[string]string{
"hostname --all-ip-addresses": "10.0.0.5 172.31.0.10 ",
},
}
addrs, err := LinuxConfigurer{}.LocalAddresses(h)
require.NoError(t, err)
assert.Equal(t, []string{"10.0.0.5", "172.31.0.10"}, addrs)
}

// TestLocalAddresses_IPFallback verifies the SLES 12 SP5 scenario: hostname
// --all-ip-addresses fails (exit 4), so LocalAddresses falls back to
// "ip -4 -o addr show scope global" and parses the addr/prefix fields.
func TestLocalAddresses_IPFallback(t *testing.T) {
ipOutput := "2: eth0 inet 10.0.0.5/24 brd 10.0.0.255 scope global eth0\\\n" +
"3: eth1 inet 172.31.0.10/20 brd 172.31.15.255 scope global eth1\\\n"
h := &mockHost{
outputs: map[string]string{
"ip -4 -o addr show scope global": ipOutput,
},
errors: map[string]error{
"hostname --all-ip-addresses": fmt.Errorf("exit status 4"),
},
}
addrs, err := LinuxConfigurer{}.LocalAddresses(h)
require.NoError(t, err)
assert.Equal(t, []string{"10.0.0.5", "172.31.0.10"}, addrs)
}

// TestLocalAddresses_BothFail verifies that an error is returned when both
// commands are unavailable (neither hostname nor ip work).
func TestLocalAddresses_BothFail(t *testing.T) {
h := &mockHost{
errors: map[string]error{
"hostname --all-ip-addresses": fmt.Errorf("exit status 4"),
"ip -4 -o addr show scope global": fmt.Errorf("exit status 127"),
},
}
_, err := LinuxConfigurer{}.LocalAddresses(h)
assert.Error(t, err)
}
9 changes: 5 additions & 4 deletions test/smoke/smoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func runSmokeTest(t *testing.T, cfg smokeConfig) {
"nodegroups": cfg.Nodegroups,
"ssh_key_algorithm": cfg.SSHKeyAlgorithm,
"extra_tags": map[string]string{
"launchpad-smoke-test": "true",
"launchpad-smoke-test": "true",
"launchpad-smoke-test-name": cfg.Name,
},
}
Expand Down Expand Up @@ -191,8 +191,8 @@ func TestModernCluster(t *testing.T) {
})
}

// TestLegacyCluster exercises rhel8/rocky8/ubuntu22 managers and workers
// with MCR stable-25.0 and MKE 3.8.8.
// TestLegacyCluster exercises rhel8/rocky8/ubuntu22 managers and
// rhel8/rocky8/ubuntu22/sles12 workers with MCR stable-25.0 and MKE 3.8.8.
func TestLegacyCluster(t *testing.T) {
runSmokeTest(t, smokeConfig{
Name: "legacy",
Expand All @@ -207,6 +207,7 @@ func TestLegacyCluster(t *testing.T) {
"WrkRhel8": test.Platforms["Rhel8"].GetWorker(),
"WrkRocky8": test.Platforms["Rocky8"].GetWorker(),
"WrkUbuntu22": test.Platforms["Ubuntu22"].GetWorker(),
"WrkSles12": test.Platforms["Sles12"].GetWorker(),
},
})
}
Expand Down Expand Up @@ -243,7 +244,7 @@ func TestFIPSCluster(t *testing.T) {
SSHKeyAlgorithm: "rsa",
Nodegroups: map[string]interface{}{
"MngrUbuntu22FIPS": test.Platforms["Ubuntu22FIPS"].GetManager(),
"WrkWin2025": test.Platforms["Windows2025"].GetWorker(),
"WrkWin2025": test.Platforms["Windows2025"].GetWorker(),
},
})
}
Loading