Skip to content

Commit ad43569

Browse files
committed
feat(machine): add OS and kernel version information to machine info, show in 'machine ls'
1 parent 9ea30e6 commit ad43569

8 files changed

Lines changed: 574 additions & 230 deletions

File tree

‎cmd/uncloud/machine/ls.go‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ func list(ctx context.Context, uncli *cli.CLI) error {
4040

4141
// Print the list of machines in a table format.
4242
t := tui.NewTable()
43-
t.Headers("NAME", "STATE", "ADDRESS", "PUBLIC IP", "WIREGUARD ENDPOINTS", "ARCH", "VERSION", "DOCKER", "MACHINE ID")
43+
t.Headers("NAME", "STATE", "ADDRESS", "PUBLIC IP", "WIREGUARD ENDPOINTS",
44+
"OS", "KERNEL", "ARCH", "DOCKER", "VERSION", "MACHINE ID")
4445

4546
for _, member := range machines {
4647
m := member.Machine
@@ -64,6 +65,16 @@ func list(ctx context.Context, uncli *cli.CLI) error {
6465
arch = m.Arch
6566
}
6667

68+
osName := "-"
69+
if m.OsPrettyName != "" {
70+
osName = m.OsPrettyName
71+
}
72+
73+
kernel := "-"
74+
if m.KernelVersion != "" {
75+
kernel = m.KernelVersion
76+
}
77+
6778
daemonVersion := "-"
6879
if m.DaemonVersion != "" {
6980
daemonVersion = m.DaemonVersion
@@ -80,9 +91,11 @@ func list(ctx context.Context, uncli *cli.CLI) error {
8091
subnet.String(),
8192
publicIP,
8293
strings.Join(endpoints, tui.Faint.Render(", ")),
94+
osName,
95+
kernel,
8396
arch,
84-
daemonVersion,
8597
dockerVersion,
98+
daemonVersion,
8699
member.Machine.Id,
87100
)
88101
}

‎internal/machine/api/pb/machine.pb.go‎

Lines changed: 243 additions & 220 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎internal/machine/api/pb/machine.proto‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,19 @@ message MachineInfo {
3636
string name = 2;
3737
NetworkConfig network = 3;
3838
IP public_ip = 4;
39-
// Operating system hostname.
40-
string hostname = 5;
41-
// CPU architecture.
42-
string arch = 6;
4339
// Version of the machine daemon (uncloudd).
4440
string daemon_version = 7;
4541
// Version of the Docker engine running on the machine. Could be empty if unable to get.
4642
string docker_version = 8;
43+
44+
// Operating system hostname.
45+
string hostname = 5;
46+
// CPU architecture.
47+
string arch = 6;
48+
// Human-readable operating system name and version, e.g. "Ubuntu 24.04.4 LTS". Empty if cannot be determined.
49+
string os_pretty_name = 9;
50+
// Kernel release version, e.g. "6.8.0-31-generic".
51+
string kernel_version = 10;
4752
}
4853

4954
message NetworkConfig {

‎internal/machine/machine.go‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
3838
"github.com/psviderski/uncloud/internal/machine/metrics"
3939
"github.com/psviderski/uncloud/internal/machine/network"
40+
"github.com/psviderski/uncloud/internal/machine/osinfo"
4041
"github.com/psviderski/uncloud/internal/machine/store"
4142
"github.com/psviderski/uncloud/internal/secret"
4243
"github.com/psviderski/uncloud/internal/version"
@@ -1037,11 +1038,13 @@ func (m *Machine) Info(ctx context.Context) *pb.MachineInfo {
10371038
}
10381039
}
10391040

1041+
hostname, _ := os.Hostname()
1042+
osName := osinfo.PrettyName()
1043+
kernelVersion := osinfo.KernelVersion()
1044+
10401045
m.state.mu.RLock()
10411046
defer m.state.mu.RUnlock()
10421047

1043-
hostname, _ := os.Hostname()
1044-
10451048
endpoints := make([]*pb.IPPort, len(m.state.Network.Endpoints))
10461049
for i, ep := range m.state.Network.Endpoints {
10471050
endpoints[i] = pb.NewIPPort(ep)
@@ -1056,10 +1059,12 @@ func (m *Machine) Info(ctx context.Context) *pb.MachineInfo {
10561059
Endpoints: endpoints,
10571060
PublicKey: m.state.Network.PublicKey,
10581061
},
1059-
Hostname: hostname,
1060-
Arch: runtime.GOARCH,
10611062
DaemonVersion: version.String(),
10621063
DockerVersion: dockerVersion,
1064+
Hostname: hostname,
1065+
Arch: runtime.GOARCH,
1066+
OsPrettyName: osName,
1067+
KernelVersion: kernelVersion,
10631068
}
10641069
if m.state.PublicIP.IsValid() {
10651070
info.PublicIp = pb.NewIP(m.state.PublicIP)

‎internal/machine/osinfo/osinfo.go‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Package osinfo collects the host operating system information.
2+
package osinfo
3+
4+
import (
5+
"bufio"
6+
"io"
7+
"os"
8+
"strings"
9+
)
10+
11+
// Paths to the files describing the OS release. They are package variables so tests can override them.
12+
var (
13+
osReleasePaths = []string{"/etc/os-release", "/usr/lib/os-release"}
14+
debianVersionPath = "/etc/debian_version"
15+
)
16+
17+
// PrettyName returns a human-readable OS name and version derived from os-release, or an empty string
18+
// if it cannot be determined. For example, "Ubuntu 24.04.4 LTS" or "Debian 13.5".
19+
func PrettyName() string {
20+
rel := readOSRelease()
21+
if len(rel) == 0 {
22+
return ""
23+
}
24+
25+
// Debian's os-release only carries the major version (e.g. "13"), while /etc/debian_version
26+
// holds the point release (e.g. "13.5"). Read it to report the precise version.
27+
var debianVersion string
28+
if rel["ID"] == "debian" {
29+
if data, err := os.ReadFile(debianVersionPath); err == nil {
30+
debianVersion = strings.TrimSpace(string(data))
31+
}
32+
}
33+
34+
return buildPrettyName(rel, debianVersion)
35+
}
36+
37+
// readOSRelease reads the first available os-release file and parses it into a key-value map. It
38+
// returns an empty map if no file is found.
39+
func readOSRelease() map[string]string {
40+
for _, path := range osReleasePaths {
41+
f, err := os.Open(path)
42+
if err != nil {
43+
continue
44+
}
45+
rel := parseOSRelease(f)
46+
f.Close()
47+
return rel
48+
}
49+
return nil
50+
}
51+
52+
// parseOSRelease parses an os-release file into a key-value map. Blank lines and comments are
53+
// skipped, and surrounding quotes are stripped from values.
54+
func parseOSRelease(r io.Reader) map[string]string {
55+
rel := make(map[string]string)
56+
scanner := bufio.NewScanner(r)
57+
for scanner.Scan() {
58+
line := strings.TrimSpace(scanner.Text())
59+
if line == "" || strings.HasPrefix(line, "#") {
60+
continue
61+
}
62+
key, value, ok := strings.Cut(line, "=")
63+
if !ok {
64+
continue
65+
}
66+
key = strings.TrimSpace(key)
67+
value = strings.Trim(strings.TrimSpace(value), `"'`)
68+
rel[key] = value
69+
}
70+
return rel
71+
}
72+
73+
// buildPrettyName composes a human-readable OS name and version from the parsed os-release map and the
74+
// contents of /etc/debian_version (empty for non-Debian systems).
75+
func buildPrettyName(rel map[string]string, debianVersion string) string {
76+
// Debian's PRETTY_NAME is verbose ("Debian GNU/Linux 13 (trixie)") and lacks the point release,
77+
// so prefer the precise version or codename (unstable releases) from /etc/debian_version.
78+
if rel["ID"] == "debian" {
79+
return strings.Join([]string{"Debian", debianVersion}, " ")
80+
}
81+
82+
// PRETTY_NAME is the vendor's human-readable string and already includes the point release on
83+
// most distros (e.g. "Ubuntu 24.04.4 LTS").
84+
if pretty := rel["PRETTY_NAME"]; pretty != "" {
85+
return pretty
86+
}
87+
88+
// Fall back to composing the name from the individual fields.
89+
name := rel["NAME"]
90+
if name == "" {
91+
name = rel["ID"]
92+
}
93+
parts := make([]string, 0, 3)
94+
if name != "" {
95+
parts = append(parts, name)
96+
}
97+
if version := rel["VERSION_ID"]; version != "" {
98+
parts = append(parts, version)
99+
}
100+
if codename := rel["VERSION_CODENAME"]; codename != "" {
101+
parts = append(parts, "("+codename+")")
102+
}
103+
return strings.Join(parts, " ")
104+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package osinfo
2+
3+
import "golang.org/x/sys/unix"
4+
5+
// KernelVersion returns the running Linux kernel release, e.g. "6.8.0-31-generic".
6+
func KernelVersion() string {
7+
var un unix.Utsname
8+
if err := unix.Uname(&un); err != nil {
9+
return ""
10+
}
11+
return unix.ByteSliceToString(un.Release[:])
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build !linux
2+
3+
package osinfo
4+
5+
// KernelVersion returns an empty string on non-Linux hosts where the machine daemon does not run.
6+
func KernelVersion() string {
7+
return ""
8+
}

0 commit comments

Comments
 (0)