Skip to content

qemu: experimental support for S390X #3319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 18, 2025
Merged
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
6 changes: 6 additions & 0 deletions Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ config GUESTAGENT_ARCH_RISCV64
Build lima-guestagent for "riscv64" Arch
default y

config GUESTAGENT_ARCH_S390X
bool "guestagent Arch: s390x"
help
Build lima-guestagent for "s390x" Arch
default y

config GUESTAGENT_COMPRESS
bool "guestagent compress"
help
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ LINUX_GUESTAGENT_PATH_COMMON = _output/share/lima/lima-guestagent.Linux-
# How to add architecture specific guestagent:
# 1. Add the architecture to GUESTAGENT_ARCHS
# 2. Add ENVS_$(*_GUESTAGENT_PATH_COMMON)<arch> to set GOOS, GOARCH, and other necessary environment variables
LINUX_GUESTAGENT_ARCHS = aarch64 armv7l riscv64 x86_64
LINUX_GUESTAGENT_ARCHS = aarch64 armv7l riscv64 s390x x86_64

ifeq ($(CONFIG_GUESTAGENT_OS_LINUX),y)
ALL_GUESTAGENTS_NOT_COMPRESSED += $(addprefix $(LINUX_GUESTAGENT_PATH_COMMON),$(LINUX_GUESTAGENT_ARCHS))
Expand Down Expand Up @@ -321,6 +321,7 @@ additional-guestagents: $(ADDITIONAL_GUESTAGENTS)
ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)aarch64 = CGO_ENABLED=0 GOOS=linux GOARCH=arm64
ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)armv7l = CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7
ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)riscv64 = CGO_ENABLED=0 GOOS=linux GOARCH=riscv64
ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)s390x = CGO_ENABLED=0 GOOS=linux GOARCH=s390x
ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)x86_64 = CGO_ENABLED=0 GOOS=linux GOARCH=amd64
$(ALL_GUESTAGENTS_NOT_COMPRESSED): $(call dependencies_for_cmd,lima-guestagent) $$(call force_build_with_gunzip,$$@) | _output/share/lima
$(ENVS_$@) $(GO_BUILD) -o $@ ./cmd/lima-guestagent
Expand Down
4 changes: 2 additions & 2 deletions cmd/limactl/editflags/editflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ func RegisterCreate(cmd *cobra.Command, commentPrefix string) {
registerEdit(cmd, commentPrefix)
flags := cmd.Flags()

flags.String("arch", "", commentPrefix+"machine architecture (x86_64, aarch64, riscv64)") // colima-compatible
flags.String("arch", "", commentPrefix+"machine architecture (x86_64, aarch64, riscv64, armv7l, s390x)") // colima-compatible
_ = cmd.RegisterFlagCompletionFunc("arch", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return []string{"x86_64", "aarch64", "riscv64"}, cobra.ShellCompDirectiveNoFileComp
return []string{"x86_64", "aarch64", "riscv64", "armv7l", "s390x"}, cobra.ShellCompDirectiveNoFileComp
})

flags.String("containerd", "", commentPrefix+"containerd mode (user, system, user+system, none)")
Expand Down
1 change: 1 addition & 0 deletions config.mk
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ CONFIG_GUESTAGENT_ARCH_X8664=y
CONFIG_GUESTAGENT_ARCH_AARCH64=y
CONFIG_GUESTAGENT_ARCH_ARMV7L=y
CONFIG_GUESTAGENT_ARCH_RISCV64=y
CONFIG_GUESTAGENT_ARCH_S390X=y
CONFIG_GUESTAGENT_COMPRESS=n
4 changes: 0 additions & 4 deletions pkg/guestagent/guestagent_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"github.com/lima-vm/lima/pkg/guestagent/procnettcp"
"github.com/lima-vm/lima/pkg/guestagent/timesync"
"github.com/sirupsen/logrus"
"golang.org/x/sys/cpu"
"google.golang.org/protobuf/types/known/timestamppb"
)

Expand Down Expand Up @@ -220,9 +219,6 @@ func (a *agent) Events(ctx context.Context, ch chan *api.Event) {
}

func (a *agent) LocalPorts(_ context.Context) ([]*api.IPPort, error) {
if cpu.IsBigEndian {
return nil, errors.New("big endian architecture is unsupported, because I don't know how /proc/net/tcp looks like on big endian hosts")
}
var res []*api.IPPort
tcpParsed, err := procnettcp.ParseFiles()
if err != nil {
Expand Down
37 changes: 28 additions & 9 deletions pkg/guestagent/procnettcp/procnettcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"net"
"strconv"
"strings"

"golang.org/x/sys/cpu"
)

type Kind = string
Expand Down Expand Up @@ -40,6 +42,10 @@ type Entry struct {
}

func Parse(r io.Reader, kind Kind) ([]Entry, error) {
return ParseWithEndian(r, kind, cpu.IsBigEndian)
}

func ParseWithEndian(r io.Reader, kind Kind, isBE bool) ([]Entry, error) {
switch kind {
case TCP, TCP6, UDP, UDP6:
default:
Expand Down Expand Up @@ -72,7 +78,7 @@ func Parse(r io.Reader, kind Kind) ([]Entry, error) {
default:
// localAddress is like "0100007F:053A"
localAddress := fields[fieldNames["local_address"]]
ip, port, err := ParseAddress(localAddress)
ip, port, err := ParseAddressWithEndian(localAddress, isBE)
if err != nil {
return entries, err
}
Expand All @@ -99,17 +105,24 @@ func Parse(r io.Reader, kind Kind) ([]Entry, error) {
return entries, nil
}

// ParseAddress parses a string, e.g.,
// ParseAddress parses a string.
//
// Little endian hosts:
// "0100007F:0050" (127.0.0.1:80)
// "000080FE00000000FF57A6705DC771FE:0050" ([fe80::70a6:57ff:fe71:c75d]:80)
// "00000000000000000000000000000000:0050" (0.0.0.0:80)
//
// See https://serverfault.com/questions/592574/why-does-proc-net-tcp6-represents-1-as-1000
// Big endian hosts:
// "7F000001:0050" (127.0.0.1:80)
// "FE8000000000000070A657FFFE71C75D:0050" ([fe80::70a6:57ff:fe71:c75d]:80)
// "00000000000000000000000000000000:0050" (0.0.0.0:80)
//
// ParseAddress is expected to be used for /proc/net/{tcp,tcp6} entries on
// little endian machines.
// Not sure how those entries look like on big endian machines.
// See https://serverfault.com/questions/592574/why-does-proc-net-tcp6-represents-1-as-1000
func ParseAddress(s string) (net.IP, uint16, error) {
return ParseAddressWithEndian(s, cpu.IsBigEndian)
}

func ParseAddressWithEndian(s string, isBE bool) (net.IP, uint16, error) {
split := strings.SplitN(s, ":", 2)
if len(split) != 2 {
return nil, 0, fmt.Errorf("unparsable address %q", s)
Expand All @@ -124,12 +137,18 @@ func ParseAddress(s string) (net.IP, uint16, error) {
ipBytes := make([]byte, len(split[0])/2) // 4 bytes (8 chars) or 16 bytes (32 chars)
for i := 0; i < len(split[0])/8; i++ {
quartet := split[0][8*i : 8*(i+1)]
quartetLE, err := hex.DecodeString(quartet) // surprisingly little endian, per 4 bytes
quartetB, err := hex.DecodeString(quartet) // surprisingly little endian, per 4 bytes, on little endian hosts
if err != nil {
return nil, 0, fmt.Errorf("unparsable address %q: unparsable quartet %q: %w", s, quartet, err)
}
for j := 0; j < len(quartetLE); j++ {
ipBytes[4*i+len(quartetLE)-1-j] = quartetLE[j]
if isBE {
for j := 0; j < len(quartetB); j++ {
ipBytes[4*i+j] = quartetB[j]
}
} else {
for j := 0; j < len(quartetB); j++ {
ipBytes[4*i+len(quartetB)-1-j] = quartetB[j]
}
}
}
ip := net.IP(ipBytes)
Expand Down
33 changes: 28 additions & 5 deletions pkg/guestagent/procnettcp/procnettcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
)

func TestParseTCP(t *testing.T) {
const isBE = false
procNetTCP := ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:8AEF 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 28152 1 0000000000000000 100 0 0 10 0
1: 0103000A:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 31474 1 0000000000000000 100 0 0 10 5
Expand All @@ -20,7 +21,7 @@ func TestParseTCP(t *testing.T) {
4: 0100007F:053A 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 31430 1 0000000000000000 100 0 0 10 0
5: 0B3CA8C0:0016 690AA8C0:F705 01 00000000:00000000 02:00028D8B 00000000 0 0 32989 4 0000000000000000 20 4 31 10 19
`
entries, err := Parse(strings.NewReader(procNetTCP), TCP)
entries, err := ParseWithEndian(strings.NewReader(procNetTCP), TCP, isBE)
assert.NilError(t, err)
t.Log(entries)

Expand All @@ -34,9 +35,10 @@ func TestParseTCP(t *testing.T) {
}

func TestParseTCP6(t *testing.T) {
const isBE = false
procNetTCP := ` sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 000080FE00000000FF57A6705DC771FE:0050 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 850222 1 0000000000000000 100 0 0 10 0`
entries, err := Parse(strings.NewReader(procNetTCP), TCP6)
entries, err := ParseWithEndian(strings.NewReader(procNetTCP), TCP6, isBE)
assert.NilError(t, err)
t.Log(entries)

Expand All @@ -46,12 +48,13 @@ func TestParseTCP6(t *testing.T) {
}

func TestParseTCP6Zero(t *testing.T) {
const isBE = false
procNetTCP := ` sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000000000000000000000000000:0016 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 33825 1 0000000000000000 100 0 0 10 0
1: 00000000000000000000000000000000:006F 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 26772 1 0000000000000000 100 0 0 10 0
2: 00000000000000000000000000000000:0050 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1210901 1 0000000000000000 100 0 0 10 0
`
entries, err := Parse(strings.NewReader(procNetTCP), TCP6)
entries, err := ParseWithEndian(strings.NewReader(procNetTCP), TCP6, isBE)
assert.NilError(t, err)
t.Log(entries)

Expand All @@ -61,13 +64,14 @@ func TestParseTCP6Zero(t *testing.T) {
}

func TestParseUDP(t *testing.T) {
const isBE = false
procNetTCP := ` sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops
716: 3600007F:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000 991 0 2964 2 0000000000000000 0
716: 3500007F:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000 991 0 2962 2 0000000000000000 0
731: 0369A8C0:0044 00000000:0000 07 00000000:00000000 00:00000000 00000000 998 0 29132 2 0000000000000000 0
731: 0F05A8C0:0044 00000000:0000 07 00000000:00000000 00:00000000 00000000 998 0 4049 2 0000000000000000 0
1768: 00000000:1451 00000000:0000 07 00000000:00000000 00:00000000 00000000 502 0 28364 2 0000000000000000 0 `
entries, err := Parse(strings.NewReader(procNetTCP), UDP)
entries, err := ParseWithEndian(strings.NewReader(procNetTCP), UDP, isBE)
assert.NilError(t, err)
t.Log(entries)

Expand All @@ -79,6 +83,7 @@ func TestParseUDP(t *testing.T) {
func TestParseAddress(t *testing.T) {
tests := []struct {
input string
bigEndian bool
expectedIP net.IP
expectedPort uint16
expectedErrSubstr string
Expand All @@ -88,13 +93,31 @@ func TestParseAddress(t *testing.T) {
expectedIP: net.IPv4(127, 0, 0, 1),
expectedPort: 80,
},
{
input: "7F000001:0050",
bigEndian: true,
expectedIP: net.IPv4(127, 0, 0, 1),
expectedPort: 80,
},
{
input: "000080FE00000000FF57A6705DC771FE:0050",
expectedIP: net.ParseIP("fe80::70a6:57ff:fe71:c75d"),
expectedPort: 80,
},
{
input: "FE8000000000000070A657FFFE71C75D:0050",
bigEndian: true,
expectedIP: net.ParseIP("fe80::70a6:57ff:fe71:c75d"),
expectedPort: 80,
},
{
input: "00000000000000000000000000000000:0050",
expectedIP: net.IPv6zero,
expectedPort: 80,
},
{
input: "00000000000000000000000000000000:0050",
bigEndian: true,
expectedIP: net.IPv6zero,
expectedPort: 80,
},
Expand Down Expand Up @@ -126,7 +149,7 @@ func TestParseAddress(t *testing.T) {

for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
ip, port, err := ParseAddress(test.input)
ip, port, err := ParseAddressWithEndian(test.input, test.bigEndian)
if test.expectedErrSubstr != "" {
assert.ErrorContains(t, err, test.expectedErrSubstr)
} else {
Expand Down
1 change: 1 addition & 0 deletions pkg/limatmpl/locator.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ var archKeywords = map[string]limayaml.Arch{
"armhf": limayaml.ARMV7L,
"armv7l": limayaml.ARMV7L,
"riscv64": limayaml.RISCV64,
"s390x": limayaml.S390X,
"x86_64": limayaml.X8664,
}

Expand Down
6 changes: 5 additions & 1 deletion pkg/limayaml/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func defaultCPUType() CPUType {
// Since https://github.com/lima-vm/lima/pull/494, we use qemu64 cpu for better emulation of x86_64.
X8664: "qemu64",
RISCV64: "rv64", // FIXME: what is the right choice for riscv64?
S390X: "qemu", // FIXME: what is the right choice for s390x?
}
for arch := range cpuType {
if IsNativeArch(arch) && IsAccelOS() {
Expand Down Expand Up @@ -1090,6 +1091,8 @@ func NewArch(arch string) Arch {
return arch
case "riscv64":
return RISCV64
case "s390x":
return S390X
default:
logrus.Warnf("Unknown arch: %s", arch)
return arch
Expand Down Expand Up @@ -1280,7 +1283,8 @@ func IsNativeArch(arch Arch) bool {
nativeAARCH64 := arch == AARCH64 && runtime.GOARCH == "arm64"
nativeARMV7L := arch == ARMV7L && runtime.GOARCH == "arm" && goarm() == 7
nativeRISCV64 := arch == RISCV64 && runtime.GOARCH == "riscv64"
return nativeX8664 || nativeAARCH64 || nativeARMV7L || nativeRISCV64
nativeS390X := arch == S390X && runtime.GOARCH == "s390x"
return nativeX8664 || nativeAARCH64 || nativeARMV7L || nativeRISCV64 || nativeS390X
}

func unique(s []string) []string {
Expand Down
4 changes: 4 additions & 0 deletions pkg/limayaml/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func TestFillDefault(t *testing.T) {
arch = ARMV7L
case "riscv64":
arch = RISCV64
case "s390x":
arch = S390X
default:
t.Skipf("unknown GOARCH: %s", runtime.GOARCH)
}
Expand Down Expand Up @@ -335,6 +337,7 @@ func TestFillDefault(t *testing.T) {
ARMV7L: "armhf",
X8664: "amd64",
RISCV64: "riscv64",
S390X: "s390x",
},
CPUs: ptr.Of(7),
Memory: ptr.Of("5GiB"),
Expand Down Expand Up @@ -547,6 +550,7 @@ func TestFillDefault(t *testing.T) {
ARMV7L: "armv8",
X8664: "pentium",
RISCV64: "sifive-u54",
S390X: "z14",
},
CPUs: ptr.Of(12),
Memory: ptr.Of("7GiB"),
Expand Down
3 changes: 2 additions & 1 deletion pkg/limayaml/limayaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const (
AARCH64 Arch = "aarch64"
ARMV7L Arch = "armv7l"
RISCV64 Arch = "riscv64"
S390X Arch = "s390x"

REVSSHFS MountType = "reverse-sshfs"
NINEP MountType = "9p"
Expand All @@ -90,7 +91,7 @@ const (

var (
OSTypes = []OS{LINUX}
ArchTypes = []Arch{X8664, AARCH64, ARMV7L, RISCV64}
ArchTypes = []Arch{X8664, AARCH64, ARMV7L, RISCV64, S390X}
MountTypes = []MountType{REVSSHFS, NINEP, VIRTIOFS, WSLMount}
VMTypes = []VMType{QEMU, VZ, WSL2}
)
Expand Down
15 changes: 8 additions & 7 deletions pkg/limayaml/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ func validateFileObject(f File, fieldName string) error {
// f.Location does NOT need to be accessible, so we do NOT check os.Stat(f.Location)
}
switch f.Arch {
case X8664, AARCH64, ARMV7L, RISCV64:
case X8664, AARCH64, ARMV7L, RISCV64, S390X:
default:
return fmt.Errorf("field `arch` must be %q, %q, %q, or %q; got %q", X8664, AARCH64, ARMV7L, RISCV64, f.Arch)
return fmt.Errorf("field `arch` must be %q, %q, %q, %q, or %q; got %q", X8664, AARCH64, ARMV7L, RISCV64, S390X, f.Arch)
}
if f.Digest != "" {
if !f.Digest.Algorithm().Available() {
Expand Down Expand Up @@ -77,9 +77,9 @@ func Validate(y *LimaYAML, warn bool) error {
return fmt.Errorf("field `os` must be %q; got %q", LINUX, *y.OS)
}
switch *y.Arch {
case X8664, AARCH64, ARMV7L, RISCV64:
case X8664, AARCH64, ARMV7L, RISCV64, S390X:
default:
return fmt.Errorf("field `arch` must be %q, %q, %q or %q; got %q", X8664, AARCH64, ARMV7L, RISCV64, *y.Arch)
return fmt.Errorf("field `arch` must be %q, %q, %q, %q or %q; got %q", X8664, AARCH64, ARMV7L, RISCV64, S390X, *y.Arch)
}

switch *y.VMType {
Expand Down Expand Up @@ -125,7 +125,7 @@ func Validate(y *LimaYAML, warn bool) error {

for arch := range y.CPUType {
switch arch {
case AARCH64, X8664, ARMV7L, RISCV64:
case AARCH64, X8664, ARMV7L, RISCV64, S390X:
// these are the only supported architectures
default:
return fmt.Errorf("field `cpuType` uses unsupported arch %q", arch)
Expand Down Expand Up @@ -578,8 +578,9 @@ func warnExperimental(y *LimaYAML) {
if *y.MountType == VIRTIOFS && runtime.GOOS == "linux" {
logrus.Warn("`mountType: virtiofs` on Linux is experimental")
}
if *y.Arch == RISCV64 {
logrus.Warn("`arch: riscv64` is experimental")
switch *y.Arch {
case RISCV64, ARMV7L, S390X:
logrus.Warnf("`arch: %s ` is experimental", *y.Arch)
}
if y.Video.Display != nil && strings.Contains(*y.Video.Display, "vnc") {
logrus.Warn("`video.display: vnc` is experimental")
Expand Down
Loading
Loading