diff --git a/vm/build.go b/vm/build.go index fa8849e..cb6d599 100644 --- a/vm/build.go +++ b/vm/build.go @@ -38,6 +38,14 @@ var builds = []build{ env: []string{"GOARCH=amd64", "GOAMD64=v1"}, cmd: []string{}, }, + { + os: "linux", + arch: "arm", + kernel: "zImage", + container: "ghcr.io/hugelgupf/vmtest/kernel-arm:main", + env: []string{"GOARCH=arm", "GOARM=5"}, + cmd: []string{}, + }, { os: "linux", arch: "arm64", @@ -47,14 +55,6 @@ var builds = []build{ env: []string{"GOARCH=arm64"}, cmd: []string{}, }, - { - os: "linux", - arch: "arm", - kernel: "zImage", - container: "ghcr.io/hugelgupf/vmtest/kernel-arm:main", - env: []string{"GOARCH=arm", "GOARM=5"}, - cmd: []string{}, - }, { os: "linux", arch: "riscv64", @@ -130,12 +130,16 @@ func main() { if len(b.container) == 0 { continue } - ref, err := name.ParseReference(b.container) - if err != nil { - log.Fatal(err) - } + if false { + ref, err := name.ParseReference(b.container, name.Insecure) + if err != nil { + log.Fatal(err) + } - img, err := crane.Pull(ref.Name()) + fmt.Printf("parse %s to %s", b.container, ref.Name()) + } + //img, err := crane.Pull(ref.Name()) + img, err := crane.Pull(b.container) if err != nil { log.Fatal(err) } diff --git a/vm/initramfs_linux_amd64.cpio b/vm/initramfs_linux_amd64.cpio index e36d81d..586dbba 100644 Binary files a/vm/initramfs_linux_amd64.cpio and b/vm/initramfs_linux_amd64.cpio differ diff --git a/vm/initramfs_linux_amd64.cpio.gz b/vm/initramfs_linux_amd64.cpio.gz index db3af48..1ae32a5 100644 Binary files a/vm/initramfs_linux_amd64.cpio.gz and b/vm/initramfs_linux_amd64.cpio.gz differ diff --git a/vm/initramfs_linux_arm.cpio b/vm/initramfs_linux_arm.cpio index f9e9be8..0f31ced 100644 Binary files a/vm/initramfs_linux_arm.cpio and b/vm/initramfs_linux_arm.cpio differ diff --git a/vm/initramfs_linux_arm.cpio.gz b/vm/initramfs_linux_arm.cpio.gz index f6677b0..3fbaaf8 100644 Binary files a/vm/initramfs_linux_arm.cpio.gz and b/vm/initramfs_linux_arm.cpio.gz differ diff --git a/vm/initramfs_linux_arm64.cpio b/vm/initramfs_linux_arm64.cpio index 80b80f9..66233cc 100644 Binary files a/vm/initramfs_linux_arm64.cpio and b/vm/initramfs_linux_arm64.cpio differ diff --git a/vm/initramfs_linux_arm64.cpio.gz b/vm/initramfs_linux_arm64.cpio.gz index b18424e..f5dd52b 100644 Binary files a/vm/initramfs_linux_arm64.cpio.gz and b/vm/initramfs_linux_arm64.cpio.gz differ diff --git a/vm/initramfs_linux_riscv64.cpio b/vm/initramfs_linux_riscv64.cpio index b69de18..db702c8 100644 Binary files a/vm/initramfs_linux_riscv64.cpio and b/vm/initramfs_linux_riscv64.cpio differ diff --git a/vm/initramfs_linux_riscv64.cpio.gz b/vm/initramfs_linux_riscv64.cpio.gz index 5956ce3..21804c5 100644 Binary files a/vm/initramfs_linux_riscv64.cpio.gz and b/vm/initramfs_linux_riscv64.cpio.gz differ diff --git a/vm/ip_linux_test.go b/vm/ip_linux_test.go new file mode 100644 index 0000000..aecc973 --- /dev/null +++ b/vm/ip_linux_test.go @@ -0,0 +1,240 @@ +// Copyright 2025 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !race && amd64 + +package vm_test + +import ( + "context" + "os" + "strings" + "testing" + "time" + + "github.com/u-root/cpu/client" + "github.com/u-root/cpu/vm" +) + +func count(s string, t []string) int { + var i int + for _, n := range t { + if strings.Contains(s, n) { + i++ + } + } + return i +} + +func all(s string, t []string) bool { + return count(s, t) == len(t) +} + +func some(s string, t []string) bool { + return count(s, t) > 0 +} + +func none(s string, t []string) bool { + return count(s, t) == 0 +} + +// TestIP tests creation and removal of addresses, tunnels, and +// ARP entries with the u-root ip command. +func TestIP(t *testing.T) { + d := t.TempDir() + for _, arch := range []string{"amd64", "arm", "arm64", "riscv64"} { + i, err := vm.New("linux", arch) + if err != nil { + t.Fatalf("Testing kernel=linux arch=%s: got %v, want nil", arch, err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + t.Logf("image:%s", i) + n, err := i.Uroot(d) + if err != nil { + t.Skipf("skipping this test as we have no uroot command") + } + + c, err := i.CommandContext(ctx, d, n) + if err != nil { + t.Fatalf("starting VM: got %v, want nil", err) + } + t.Logf("Start %v", c.Args) + + // For debug. It's been needed, but we do not want this spew in + // CI logs, so leave it off. + if false { + c.Stdout, c.Stderr = os.Stdout, os.Stderr + } + if err := i.StartVM(c); err != nil { + t.Fatalf("starting VM: got %v, want nil", err) + } + + type iptest struct { + cmd any + delay time.Duration + failok bool + includes []string + excludes []string + } + // This is a slice of iptest slices. + // The intent is that if the first one fails, and it has failok set to false, the rest of the tests + // in that slice are skipped. + for _, iptest := range [][]iptest{ + { + {cmd: "ip link set eth1 down", delay: 3 * time.Second}, + {cmd: "cat /sys/class/net/eth1/operstate", includes: []string{"down"}}, + {cmd: "ip link set eth1 up", delay: 3 * time.Second}, + {cmd: "cat /sys/class/net/eth1/operstate", includes: []string{"up"}}, + {cmd: []string{"ip", "addr", "add", "192.168.241.1/24", "dev", "eth1"}}, + {cmd: []string{"cat", "/proc/net/fib_trie"}, includes: []string{"192.168.241.1"}}, + {cmd: "ip route add 192.168.242.0/24 via 192.168.241.1 dev eth1"}, + {cmd: "cat /proc/net/route", includes: []string{"00F2A8C0", "01F1A8C0"}}, + {cmd: "ip route del 192.168.242.0/24"}, + {cmd: "cat /proc/net/route", excludes: []string{"00F2A8C0", "01F1A8C0"}}, + {cmd: "ip tunnel add my_test_tunnel mode sit remote 192.168.242.1 local 192.168.241.1 ttl 64"}, + {cmd: "cat /proc/net/dev", includes: []string{"my_test_tunnel"}}, + {cmd: "ip tunnel del my_test_tunnel"}, + {cmd: "cat /proc/net/dev", excludes: []string{"my_test_tunnel"}}, + {cmd: "ip tunnel add my_test_tunnel mode sit remote 192.168.242.1 local 192.168.241.1 ttl 64"}, + {cmd: "ip tunnel show my_test_tunnel", includes: []string{"my_test_tunnel", "remote 192.168.242.1", "local 192.168.241.1", "ttl 64"}}, + {cmd: "ip tunnel del my_test_tunnel"}, + {cmd: "cat /proc/net/dev", excludes: []string{"my_test_tunnel"}}, + }, + { + // Various tunnel tests. + // Add a GRE tunnel with key and tos options + {cmd: "ip tunnel add gre_tunnel mode gre remote 192.168.242.2 local 192.168.241.1 ttl 128 key 1234 tos 10", failok: false, delay: time.Second}, + {cmd: "cat /proc/net/dev", includes: []string{"gre_tunnel"}}, + // Verify GRE tunnel parameters + {cmd: "ip tunnel show gre_tunnel", includes: []string{"gre_tunnel:", "remote 192.168.242.2", "local 192.168.241.1", "ttl 128", "key 1234", "tos 0xa"}}, + {cmd: "ip link set gre_tunnel up"}, + {cmd: "ip addr add 10.0.0.1/24 dev gre_tunnel"}, + {cmd: []string{"cat", "/proc/net/fib_trie"}, includes: []string{"10.0.0.1"}}, + {cmd: "ip link set gre_tunnel down"}, + {cmd: "ip tunnel del gre_tunnel"}, + {cmd: "cat /proc/net/dev", excludes: []string{"gre_tunnel"}}, + }, + { + {cmd: "ip tunnel add vti_tunnel mode vti remote 192.168.242.3 local 192.168.241.1 key 5678", failok: false}, + + // Verify VTI tunnel exists in /proc/net/dev + {cmd: "cat /proc/net/dev", includes: []string{"vti_tunnel"}}, + + // Verify VTI tunnel parameters + {cmd: "ip tunnel show vti_tunnel", includes: []string{"vti_tunnel:", "remote 192.168.242.3", "local 192.168.241.1", "key 5678"}}, + {cmd: "ip link set vti_tunnel up"}, + {cmd: "ip addr add 172.16.0.1/30 dev vti_tunnel"}, + {cmd: []string{"cat", "/proc/net/fib_trie"}, includes: []string{"172.16.0.1"}}, + {cmd: "ip link set vti_tunnel down"}, + {cmd: "ip tunnel del vti_tunnel"}, + {cmd: "cat /proc/net/dev", excludes: []string{"vti_tunnel"}}, + }, + { + {cmd: "ip tunnel add ipip_tunnel mode ipip remote 192.168.243.1 local 192.168.241.1 ttl 64", failok: false}, + + // Verify IPIP tunnel exists in /proc/net/dev + {cmd: "cat /proc/net/dev", includes: []string{"ipip_tunnel"}}, + + // Verify IPIP tunnel parameters + {cmd: "ip tunnel show ipip_tunnel", includes: []string{"ipip_tunnel:", "remote 192.168.243.1", "local 192.168.241.1", "ttl 64"}}, + {cmd: "ip link set ipip_tunnel up"}, + {cmd: "ip addr add 172.17.0.1/30 dev ipip_tunnel"}, + {cmd: []string{"cat", "/proc/net/fib_trie"}, includes: []string{"172.17.0.1"}}, + {cmd: "ip link set ipip_tunnel down"}, + {cmd: "ip tunnel del ipip_tunnel"}, + {cmd: "cat /proc/net/dev", excludes: []string{"ipip_tunnel"}}, + }, + { + // ARP tests + {cmd: "ip neigh add 192.168.241.2 lladdr 00:11:22:33:44:55 dev eth1"}, + {cmd: "cat /proc/net/arp", includes: []string{"192.168.241.2"}}, + + // Verify the neighbor entry + {cmd: "ip neigh show dev eth1", includes: []string{"192.168.241.2", "192.168.241.2 dev eth1 lladdr 00:11:22:33:44:55 PERMANENT"}}, + + // Replace the entry with another hwaddress, nud state and router flag + {cmd: "ip neigh replace 192.168.241.2 lladdr 11:22:33:44:55:66 dev eth1 nud stale router", includes: []string{}}, + + // Verify the modified flags + {cmd: "ip neigh show dev eth1", includes: []string{"192.168.241.2", "192.168.241.2 dev eth1 lladdr 11:22:33:44:55:66 router STALE"}}, + + // Delete the neighbor + {cmd: "ip neigh del 192.168.241.2 dev eth1", includes: []string{}}, + {cmd: "cat /proc/net/arp", excludes: []string{"192.168.241.2"}}, + + // Test IP Neighbor flush capability + // Add 3 neighbors + {cmd: "ip neigh add 192.168.241.5 lladdr aa:bb:cc:dd:ee:ff nud stale dev eth1"}, + {cmd: "ip neigh add 192.168.241.6 lladdr aa:bb:cc:11:22:33 nud stale dev eth1"}, + {cmd: "ip neigh add 192.168.241.7 lladdr aa:bb:cc:44:55:66 dev eth1"}, + + // Verify all entries exist + {cmd: "cat /proc/net/arp", includes: []string{"192.168.241.5", "192.168.241.6", "192.168.241.7"}}, + + // Flush the 2 stale neighbors from the table for eth1 + {cmd: "ip neigh flush dev eth1", includes: []string{}}, + + // Verify the 2 stale entries are gone, the permanent one remains + {cmd: "cat /proc/net/arp", includes: []string{"192.168.241.7"}, excludes: []string{"192.168.241.5", "192.168.241.6"}}, + + // Delete the IP address from eth1 + {cmd: "ip addr del 192.168.241.1/24 dev eth1"}, + {cmd: []string{"cat", "/proc/net/fib_trie"}, excludes: []string{"192.168.241.1"}}, + // Bring the eth1 interface down + {cmd: "ip link set eth1 down", delay: 2 * time.Second}, + {cmd: "cat /sys/class/net/eth1/operstate", includes: []string{"down"}}, + }, + } { + + for _, tt := range iptest { + var cmd []string + switch c := tt.cmd.(type) { + case []string: + cmd = c + case string: + cmd = strings.Fields(c) + default: + t.Fatalf("tt.cmd.Type(): type %T, want string or []string", tt.cmd) + } + t.Logf("Run %s", cmd) + cpu, err := i.CPUCommand(cmd[0], cmd[1:]...) + if err != nil { + t.Errorf("CPUCommand: got %v, want nil", err) + continue + } + if false { + client.SetVerbose(t.Logf) + } + + b, err := cpu.CombinedOutput() + + if false { // Only enable this if you want to see console output/errors. + t.Logf("%s %v", string(b), err) + } + + if err != nil { + if tt.failok { + t.Logf("%s: got %v, want nil, skipping rest of tests in slice", cmd, err) + break + } + t.Fatalf("%s: got %v, want nil", cmd, err) + } + //t.Logf("%q, includes %s?", string(b), tt.includes) + if !all(string(b), tt.includes) { + t.Fatalf("%s: got %s, does not contain all of %s", cmd, string(b), tt.includes) + } + //t.Logf("%q, excludes %s?", string(b), tt.includes) + if some(string(b), tt.excludes) { + t.Fatalf("%s:got %s, contains some of %s and should not", cmd, string(b), tt.excludes) + } + if tt.delay > 0 { + time.Sleep(tt.delay) + } + } + } + } +} diff --git a/vm/kernel_linux_amd64 b/vm/kernel_linux_amd64 index 55c913e..1c3671c 100644 Binary files a/vm/kernel_linux_amd64 and b/vm/kernel_linux_amd64 differ diff --git a/vm/kernel_linux_amd64.gz b/vm/kernel_linux_amd64.gz index 53039c4..7041fdc 100644 Binary files a/vm/kernel_linux_amd64.gz and b/vm/kernel_linux_amd64.gz differ diff --git a/vm/vm.go b/vm/vm.go index 8ff999b..7d70b62 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -51,25 +51,34 @@ type Image struct { Cmd []string Env []string dir string + GOOS string + GOARCH string + Opts []string +} + +func (i *Image) String() string { + return fmt.Sprintf("Image for %s:%s(%s): Cmd %s, Env %s", i.GOOS, i.GOARCH, i.Opts, i.Cmd, i.Env) } var images = map[string]Image{ - "linux_amd64": {Kernel: kernel_linux_amd64, InitRAMFS: linux_amd64, Cmd: []string{"qemu-system-x86_64", "-m", "1G"}}, + "linux_amd64": {Kernel: kernel_linux_amd64, InitRAMFS: linux_amd64, Cmd: []string{"qemu-system-x86_64", "-m", "4G"}}, "linux_arm64": {Kernel: kernel_linux_arm64, InitRAMFS: linux_arm64, Cmd: []string{"qemu-system-aarch64", "-machine", "virt", "-cpu", "max", "-m", "1G"}}, - "linux_arm": {Kernel: kernel_linux_arm, InitRAMFS: linux_arm, Cmd: []string{"qemu-system-arm", "-M", "virt,highmem=off"}}, + "linux_arm": {Kernel: kernel_linux_arm, InitRAMFS: linux_arm, Cmd: []string{"qemu-system-arm", "-M", "virt,highmem=off"}, Opts: []string{"GOARM=5"}}, "linux_riscv64": {Kernel: kernel_linux_riscv64, InitRAMFS: linux_riscv64, Cmd: []string{"qemu-system-riscv64", "-M", "virt", "-cpu", "rv64", "-m", "1G"}}, } // New creates an Image, using the kernel and arch to select the Image. // It will return an error if there is a problem uncompressing // the kernel and initramfs. -func New(kernel, arch string) (*Image, error) { +func New(goos, arch string) (*Image, error) { common := []string{ "-nographic", "-netdev", "user,id=net0,ipv4=on,hostfwd=tcp::17010-:17010", // required for mac. No idea why. Should work on linux. If not, we'll need a bit // more logic. "-device", "e1000-82545em,netdev=net0,id=net0,mac=52:54:00:c9:18:27", + "-netdev", "user,id=net1", + "-device", "e1000-82545em,netdev=net1,id=net1,mac=52:54:00:c9:18:28", // No password needed, you're just a guest vm ... // The kernel may not understand ip=dhcp, in which case it just ends up // in init's environment. @@ -77,10 +86,10 @@ func New(kernel, arch string) (*Image, error) { "--append", "ip=dhcp init=/init -pk \"\"", } env := []string{} - n := fmt.Sprintf("%s_%s", kernel, arch) + n := fmt.Sprintf("%s_%s", goos, arch) im, ok := images[n] if !ok { - return nil, fmt.Errorf("(%q,%q): %w", kernel, arch, os.ErrNotExist) + return nil, fmt.Errorf("(%q,%q): %w", goos, arch, os.ErrNotExist) } r, err := gzip.NewReader(bufio.NewReader(bytes.NewBuffer(im.InitRAMFS))) @@ -101,19 +110,25 @@ func New(kernel, arch string) (*Image, error) { if err != nil { return nil, fmt.Errorf("unzipped %d bytes: %w", len(k), err) } - return &Image{Kernel: k, InitRAMFS: i, Cmd: append(im.Cmd, common...), Env: append(env, im.Env...)}, nil + return &Image{Kernel: k, InitRAMFS: i, Cmd: append(im.Cmd, common...), Env: append(env, im.Env...), GOOS: goos, GOARCH: arch, Opts: im.Opts}, nil } // Uroot builds a uroot cpio into the a directory. // It returns the full path of the file, or an error. -func Uroot(d string) (string, error) { - c := exec.Command("u-root", "-o", filepath.Join(d, "uroot.cpio")) - c.Env = append(os.Environ(), "CGO_ENABLED=0") +func Uroot(d, GOOS, GOARCH string, opts ...string) (string, error) { + out := filepath.Join(d, GOOS+"_"+GOARCH+".cpio") + c := exec.Command("u-root", "-o", out) + c.Env = append(append(os.Environ(), "CGO_ENABLED=0", "GOARCH="+GOARCH, "GOOS="+GOOS), opts...) if out, err := c.CombinedOutput(); err != nil { return "", fmt.Errorf("u-root initramfs:%q:%w", out, err) } - return filepath.Join(d, "uroot.cpio"), nil + return out, nil + +} +// Uroot builds a uroot cpio for an Image +func (i *Image) Uroot(d string) (string, error) { + return Uroot(d, i.GOOS, i.GOARCH, i.Opts...) } // CommandContext starts qemu, given a context, directory in which to diff --git a/vm/vm_linux_test.go b/vm/vm_linux_test.go index 0ddc724..6d7da34 100644 --- a/vm/vm_linux_test.go +++ b/vm/vm_linux_test.go @@ -35,7 +35,7 @@ func TestCPUAMD64(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - n, err := vm.Uroot(d) + n, err := vm.Uroot(d, "linux", "amd64") if err != nil { t.Skipf("skipping this test as we have no uroot command") } @@ -48,7 +48,6 @@ func TestCPUAMD64(t *testing.T) { t.Fatalf("starting VM: got %v, want nil", err) } - // TODO: make stuff not appear on stderr/out. for _, tt := range []struct { cmd string args []string @@ -59,7 +58,7 @@ func TestCPUAMD64(t *testing.T) { {cmd: "/bbin/dd", args: []string{"if=/tmp/cpu/a", "of=/tmp/cpu/b"}, ok: true}, } { cpu, err := i.CPUCommand(tt.cmd, tt.args...) - if err != nil { + if !errors.Is(err, nil) { t.Errorf("CPUCommand: got %v, want nil", err) continue } @@ -71,6 +70,7 @@ func TestCPUAMD64(t *testing.T) { } t.Logf("%q", string(b)) } + b, err := os.ReadFile(filepath.Join(d, "b")) if err != nil { t.Fatalf("reading b: got %v, want nil", err) @@ -103,9 +103,68 @@ func TestCPUAMD64(t *testing.T) { } t.Logf("io %s = %q", tt.args, string(b)) } +} + +// TestCPUARM tests both general and specific things. The specific parts are the io and cmos commands. +// It being cheaper to use a single generated initramfs, we use the full u-root for several tests. +func TestCPUARM(t *testing.T) { + d := t.TempDir() + i, err := vm.New("linux", "arm") + if !errors.Is(err, nil) { + t.Fatalf("Testing kernel=linux arch=arm: got %v, want nil", err) + } - // The io integration tests include writing to 3f8. There's no need to do that, - // the cmos write tests all that needs testing, as it uses inb and outb, - // and uart hardware is fickle. The test above is enough. + if err := os.WriteFile(filepath.Join(d, "a"), []byte("hi"), 0644); err != nil { + t.Fatal(err) + } + + // Cancel before wg.Wait(), so goroutine can exit. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + n, err := i.Uroot(d) + if err != nil { + t.Skipf("skipping this test as we have no uroot command") + } + + c, err := i.CommandContext(ctx, d, n) + if err != nil { + t.Fatalf("starting VM: got %v, want nil", err) + } + t.Logf("Start VM: %v", c) + if err := i.StartVM(c); err != nil { + t.Fatalf("starting VM: got %v, want nil", err) + } + + for _, tt := range []struct { + cmd string + args []string + ok bool + }{ + {cmd: "/bbin/dd", args: []string{"if=/dev/x"}, ok: false}, + {cmd: "/bbin/dd", args: []string{"if=/dev/null"}, ok: true}, + {cmd: "/bbin/dd", args: []string{"if=/tmp/cpu/a", "of=/tmp/cpu/b"}, ok: true}, + } { + cpu, err := i.CPUCommand(tt.cmd, tt.args...) + if !errors.Is(err, nil) { + t.Errorf("CPUCommand: got %v, want nil", err) + continue + } + client.SetVerbose(t.Logf) + + b, err := cpu.CombinedOutput() + if err == nil != tt.ok { + t.Errorf("%s %s: got %v, want %v", tt.cmd, tt.args, err == nil != tt.ok, err == nil == tt.ok) + } + t.Logf("%q", string(b)) + } + + b, err := os.ReadFile(filepath.Join(d, "b")) + if err != nil { + t.Fatalf("reading b: got %v, want nil", err) + } + if string(b) != "hi" { + t.Fatalf("file b: got %q, want %q", b, "hi") + } }