diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 136b7530..f51d1422 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -18,6 +18,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: install qemu + run: | + sudo apt-get update + sudo apt-get -y install qemu-system-x86 + - name: Set up Go uses: actions/setup-go@v3 with: diff --git a/client/client.go b/client/client.go index e2db34f3..7d1655ec 100644 --- a/client/client.go +++ b/client/client.go @@ -61,8 +61,8 @@ type Cmd struct { SessionOut io.Reader SessionErr io.Reader Stdin io.Reader - Stdout io.Writer - Stderr io.Writer + Stdout io.WriteCloser + Stderr io.WriteCloser Row int Col int hasTTY bool // Set if we have a TTY @@ -102,6 +102,13 @@ func (c *Cmd) Listen(n, addr string) (net.Listener, error) { return c.client.Listen(n, addr) } +func sameFD(w io.WriteCloser, std *os.File) bool { + if file, ok := w.(*os.File); ok { + return file.Fd() == std.Fd() + } + return false +} + // Command implements exec.Command. The required parameter is a host. // The args arg args to $SHELL. If there are no args, then starting $SHELL // is assumed. @@ -512,6 +519,7 @@ func (c *Cmd) Start() error { } go c.TTYIn(c.session, c.SessionIn, c.Stdin) } else { + verbose("Setup batch input") go func() { if _, err := io.Copy(c.SessionIn, c.Stdin); err != nil && !errors.Is(err, io.EOF) { log.Printf("copying stdin: %v", err) @@ -522,14 +530,26 @@ func (c *Cmd) Start() error { }() } go func() { + verbose("set up copying to c.Stdout") if _, err := io.Copy(c.Stdout, c.SessionOut); err != nil && !errors.Is(err, io.EOF) { log.Printf("copying stdout: %v", err) } + + // If the file is NOT stdout, close it. + // This is needed when programmers have + // set c.Stdout to be some other WriteCloser, e.g. a pipe. + if !sameFD(c.Stdout, os.Stdout) { + c.Stdout.Close() + } }() go func() { + verbose("set up copying to c.Stderr") if _, err := io.Copy(c.Stderr, c.SessionErr); err != nil && !errors.Is(err, io.EOF) { log.Printf("copying stderr: %v", err) } + if !sameFD(c.Stdout, os.Stderr) { + c.Stderr.Close() + } }() return nil @@ -549,6 +569,21 @@ func (c *Cmd) Run() error { return c.Wait() } +func (c *Cmd) CombinedOutput() ([]byte, error) { + r, w, err := os.Pipe() + if err != nil { + return nil, err + } + + c.Stdout, c.Stderr = w, w + + cpuerr := c.Run() + + b, err := io.ReadAll(r) + + return b, errors.Join(cpuerr, err) +} + // TTYIn manages tty input for a cpu session. // It exists mainly to deal with ~. func (c *Cmd) TTYIn(s *ssh.Session, w io.WriteCloser, r io.Reader) { diff --git a/client/fns.go b/client/fns.go index 2c1bbb8d..8f7754df 100644 --- a/client/fns.go +++ b/client/fns.go @@ -266,15 +266,15 @@ func (c *Cmd) Signal(s ssh.Signal) error { // and an error if either had trouble being read. func (c *Cmd) Outputs() ([]bytes.Buffer, error) { var r [2]bytes.Buffer - var errs []error + var errs error if _, err := io.Copy(&r[0], c.SessionOut); err != nil && err != io.EOF { - errs = append(errs, fmt.Errorf("Stdout: %w", err)) + errs = err } if _, err := io.Copy(&r[1], c.SessionErr); err != nil && err != io.EOF { - errs = append(errs, fmt.Errorf("Stderr: %w", err)) + errs = errors.Join(errs, err) } if errs != nil { - return r[:], fmt.Errorf(fmt.Sprintf("%v", errs)) + return r[:], errs } return r[:], nil } diff --git a/cmds/cpud/serve_test.go b/cmds/cpud/serve_test.go index 7d6edd4f..f9731426 100644 --- a/cmds/cpud/serve_test.go +++ b/cmds/cpud/serve_test.go @@ -60,7 +60,13 @@ func TestListen(t *testing.T) { // e.g. no ipv4 or vsock continue } - t.Errorf("Listen(%v, %v): err != nil", tt.network, tt.port) + // If it is in use, not a lot to do. + if errors.As(err, &sysErr) && sysErr.Err == syscall.EADDRINUSE { + t.Logf("%s:%s is in use, so can not test; continuing", tt.network, tt.port) + // e.g. no ipv4 or vsock + continue + } + t.Errorf("Listen(%v, %v): got %v, want nil", tt.network, tt.port, err) continue } if ln == nil { diff --git a/go.mod b/go.mod index 5250731a..3aed022d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/u-root/cpu -go 1.23.0 +go 1.24 toolchain go1.24.1 @@ -8,14 +8,16 @@ require ( github.com/gliderlabs/ssh v0.3.5 github.com/kevinburke/ssh_config v1.2.0 github.com/u-root/u-root v0.11.1-0.20230913033713-004977728a9d - golang.org/x/crypto v0.36.0 - golang.org/x/sys v0.31.0 + golang.org/x/crypto v0.39.0 + golang.org/x/sys v0.33.0 ) require ( + github.com/apptainer/container-library-client v1.4.12 github.com/brutella/dnssd v1.2.9 github.com/creack/pty v1.1.18 github.com/go-git/go-billy/v5 v5.5.1-0.20240514075308-8f1b719cb6a2 + github.com/google/go-containerregistry v0.20.6 github.com/google/uuid v1.6.0 github.com/hugelgupf/p9 v0.3.0 github.com/mdlayher/vsock v1.2.1 @@ -23,32 +25,46 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible github.com/willscott/go-nfs v0.0.0-20240424173852-04b947a7e58a golang.org/x/exp v0.0.0-20230810033253-352e893a4cad - golang.org/x/term v0.30.0 + golang.org/x/term v0.32.0 ) require ( github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect + github.com/apptainer/sif/v2 v2.21.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/cyphar/filepath-securejoin v0.2.5 // indirect + github.com/docker/cli v28.2.2+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-log/log v0.2.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/miekg/dns v1.1.55 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pierrec/lz4/v4 v4.1.18 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sylabs/json-resp v0.9.4 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect github.com/ulikunitz/xz v0.5.11 // indirect + github.com/vbatts/tar-split v0.12.1 // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sync v0.3.0 // indirect - golang.org/x/tools v0.12.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/tools v0.34.0 // indirect google.golang.org/grpc v1.56.3 // indirect ) diff --git a/go.sum b/go.sum index 2add9c4f..4fae07fc 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,44 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/apptainer/container-library-client v1.4.12 h1:6kqtG7qiwZ20ZCox0ayD9RqTq8akGMIewXKl300bhVk= +github.com/apptainer/container-library-client v1.4.12/go.mod h1:egSrd5HgP7OfZpnqrbZmcr95kihVXEzQv4TqXwwa/4E= +github.com/apptainer/sif/v2 v2.21.1 h1:RPRBhlw5ZOAORbLTCoCCTRVRLuHoebSx1TknrgBd0NM= +github.com/apptainer/sif/v2 v2.21.1/go.mod h1:n9YSqALOT2SOSFXYgYecw8Ne1mwF99wsBKHNOsjXs2I= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/brutella/dnssd v1.2.9 h1:eUqO0qXZAMaFN4W4Ms1AAO/OtAbNoh9U87GAlN+1FCs= github.com/brutella/dnssd v1.2.9/go.mod h1:yZ+GHHbGhtp5yJeKTnppdFGiy6OhiPoxs0WHW1KUcFA= +github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= +github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-git/go-billy/v5 v5.5.1-0.20240514075308-8f1b719cb6a2 h1:vWpyklHBOaIWDj1n3s/6M7rfLFDBXXbT1jrNOtBVo7Y= github.com/go-git/go-billy/v5 v5.5.1-0.20240514075308-8f1b719cb6a2/go.mod h1:7u9KSvLbAYN7/p0U+SsuzbbMuqCAk7MYkPI8l2+71s4= +github.com/go-log/log v0.2.0 h1:z8i91GBudxD5L3RmF0KVpetCbcGWAV7q1Tw1eRwQM9Q= +github.com/go-log/log v0.2.0/go.mod h1:xzCnwajcues/6w7lne3yK2QU7DBPW7kqbgPGG5AF65U= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= +github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= github.com/google/goexpect v0.0.0-20191001010744-5b6988669ffa h1:PMkmJA8ju9DjqAJjIzrBdrmhuuPsoNnNLYgKQBopWL0= github.com/google/goexpect v0.0.0-20191001010744-5b6988669ffa/go.mod h1:qtE5aAEkt0vOSA84DBh8aJsz6riL8ONfqfULY7lBjqc= github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4= @@ -39,8 +58,8 @@ github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Go github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -53,13 +72,21 @@ github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQGC0tEc1+FqiLlvYXY5mQ2f8Wg= @@ -68,10 +95,20 @@ github.com/rekby/gpt v0.0.0-20200219180433-a930afbc6edc h1:goZGTwEEn8mWLcY012Vou github.com/rekby/gpt v0.0.0-20200219180433-a930afbc6edc/go.mod h1:scrOqOnnHVKCHENvFw8k9ajCb88uqLQDA4BvuJNJ2ew= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= +github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/sylabs/json-resp v0.9.4 h1:gFvnPdfrBUQgTAFKcxW8VOTfFdj/eOwBrwSG76BwiCw= +github.com/sylabs/json-resp v0.9.4/go.mod h1:Q9X4wRlZNPv3x76KaL8vTCBO4aC/DP2gh13xdtEqd1g= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= @@ -85,6 +122,8 @@ github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2 github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= +github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= @@ -97,18 +136,18 @@ github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00/go.mod h1: github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20230810033253-352e893a4cad h1:g0bG7Z4uG+OgH2QDODnjp6ggkk1bJDsINcuWmJN1iJU= golang.org/x/exp v0.0.0-20230810033253-352e893a4cad/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -117,29 +156,34 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/vm/build.go b/vm/build.go new file mode 100644 index 00000000..fa8849e1 --- /dev/null +++ b/vm/build.go @@ -0,0 +1,176 @@ +//go:build generate + +package main + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "log" + "os" + "os/exec" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/u-root/u-root/pkg/cpio" + "github.com/u-root/u-root/pkg/tarutil" +) + +type build struct { + os string + arch string + version string + kernel string + container string + env []string + cmd []string +} + +var builds = []build{ + { + os: "linux", + arch: "amd64", + version: "v1", + kernel: "bzImage", + container: "ghcr.io/hugelgupf/vmtest/kernel-amd64:main", + env: []string{"GOARCH=amd64", "GOAMD64=v1"}, + cmd: []string{}, + }, + { + os: "linux", + arch: "arm64", + version: "v1", + kernel: "Image", + container: "ghcr.io/hugelgupf/vmtest/kernel-arm64:main", + 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", + kernel: "Image", + container: "ghcr.io/hugelgupf/vmtest/kernel-riscv64:main", + env: []string{"GOARCH=riscv64"}, + cmd: []string{}, + }, +} + +func main() { + env := []string{"CGO_ENABLED=0"} + for _, b := range builds { + log.Printf("Build %v", b) + n := fmt.Sprintf("initramfs_%s_%s.cpio", b.os, b.arch) + cmd := []string{"-initcmd=/bbin/cpud", "-defaultsh=", "-o=" + n, + "../cmds/cpud", + "../../u-root/cmds/core/dhclient", + } + c := exec.Command("u-root", cmd...) + c.Stdout, c.Stderr = os.Stdout, os.Stderr + c.Env = append(os.Environ(), append(env, b.env...)...) + if err := c.Run(); err != nil { + log.Fatal(err) + } + f, err := os.ReadFile(n) + if err != nil { + log.Fatal(err) + } + + var newcpio bytes.Buffer + rw := cpio.Newc.Writer(&newcpio) + recs := cpio.Newc.Reader(bytes.NewReader(f)) + fixed := 0 + cpio.ForEachRecord(recs, func(r cpio.Record) error { + switch r.Name { + case "bbin/bb": + fixed++ + r.Name = "bbin/cibb" + case "bbin/init", "bbin/dhclient", "bbin/cpud": + fixed++ + r.ReaderAt = bytes.NewReader([]byte("cibb")) + r.Info.FileSize = 4 + } + if err := rw.WriteRecord(r); err != nil { + return fmt.Errorf("writing record %q failed: %w", r.Name, err) + } + return nil + }) + + if fixed < 2 { + log.Fatal("Did not fix any entries in %q", n) + } + + // because we modify the cpio, and one of the tests makes sure the cpio matches compressed, + // write back the modified cpio. + if err := os.WriteFile(n, newcpio.Bytes(), 0644); err != nil { + log.Fatalf("writing back changed cpio %s:%v", n, err) + } + + var out bytes.Buffer + gz := gzip.NewWriter(&out) + if _, err := gz.Write(newcpio.Bytes()); err != nil { + log.Fatal(err) + } + if err := gz.Close(); err != nil { + log.Fatal(err) + } + if err := os.WriteFile(n+".gz", out.Bytes(), 0644); err != nil { + log.Fatal(err) + } + + if len(b.container) == 0 { + continue + } + ref, err := name.ParseReference(b.container) + if err != nil { + log.Fatal(err) + } + + img, err := crane.Pull(ref.Name()) + if err != nil { + log.Fatal(err) + } + + r := mutate.Extract(img) + + opts := &tarutil.Opts{} + opts.Filters = []tarutil.Filter{tarutil.SafeFilter, func(h *tar.Header) bool { + return h.Name == b.kernel + }, + } + + if err := tarutil.ExtractDir(r, ".", opts); err != nil { + log.Fatal(err) + } + + // tarutil does not let us extract a file with a name into a []byte.Unfortunate. + kname := fmt.Sprintf("kernel_%s_%s", b.os, b.arch) + if err := os.Rename(b.kernel, kname); err != nil { + log.Fatal(err) + } + if f, err = os.ReadFile(kname); err != nil { + log.Fatal(err) + } + var kout bytes.Buffer + gz = gzip.NewWriter(&kout) + if _, err := gz.Write(f); err != nil { + log.Fatal(err) + } + if err := gz.Close(); err != nil { + log.Fatal(err) + } + if err := os.WriteFile(kname+".gz", kout.Bytes(), 0644); err != nil { + log.Fatal(err) + } + + } +} diff --git a/vm/doc.go b/vm/doc.go new file mode 100644 index 00000000..dd815ba1 --- /dev/null +++ b/vm/doc.go @@ -0,0 +1,17 @@ +// Package vm provides an exec-like interface to VMs running cpud. It is +// designed to make running commands in a vm as easy as using exec. +// +// For purposes of convenience, this package provides, via embed, a set of +// kernels and initramfs, for several architectures. Currently, it provides +// Linux kernels and initramfs for amd64, arm, arm64, and riscv64. +// +// The kernels are minimal, hence small; but enable enough options to run +// cpud and u-root programs. The initramfs only contain the cpud and +// dhclient commands, and are also small. +// +// The package asssumes that qemu for a target architecture is available on +// the system. +// +// Users can not yet provide their own kernel and initramfs, though +// that is planned. +package vm diff --git a/vm/initramfs_linux_amd64.cpio b/vm/initramfs_linux_amd64.cpio new file mode 100644 index 00000000..e36d81d8 Binary files /dev/null and b/vm/initramfs_linux_amd64.cpio differ diff --git a/vm/initramfs_linux_amd64.cpio.gz b/vm/initramfs_linux_amd64.cpio.gz new file mode 100644 index 00000000..db3af48c Binary files /dev/null and b/vm/initramfs_linux_amd64.cpio.gz differ diff --git a/vm/initramfs_linux_arm.cpio b/vm/initramfs_linux_arm.cpio new file mode 100644 index 00000000..f9e9be88 Binary files /dev/null and b/vm/initramfs_linux_arm.cpio differ diff --git a/vm/initramfs_linux_arm.cpio.gz b/vm/initramfs_linux_arm.cpio.gz new file mode 100644 index 00000000..f6677b01 Binary files /dev/null and b/vm/initramfs_linux_arm.cpio.gz differ diff --git a/vm/initramfs_linux_arm64.cpio b/vm/initramfs_linux_arm64.cpio new file mode 100644 index 00000000..80b80f93 Binary files /dev/null and b/vm/initramfs_linux_arm64.cpio differ diff --git a/vm/initramfs_linux_arm64.cpio.gz b/vm/initramfs_linux_arm64.cpio.gz new file mode 100644 index 00000000..b18424e2 Binary files /dev/null and b/vm/initramfs_linux_arm64.cpio.gz differ diff --git a/vm/initramfs_linux_riscv64.cpio b/vm/initramfs_linux_riscv64.cpio new file mode 100644 index 00000000..b69de183 Binary files /dev/null and b/vm/initramfs_linux_riscv64.cpio differ diff --git a/vm/initramfs_linux_riscv64.cpio.gz b/vm/initramfs_linux_riscv64.cpio.gz new file mode 100644 index 00000000..5956ce3b Binary files /dev/null and b/vm/initramfs_linux_riscv64.cpio.gz differ diff --git a/vm/initramfs_linux_test.go b/vm/initramfs_linux_test.go new file mode 100644 index 00000000..1b6a036f --- /dev/null +++ b/vm/initramfs_linux_test.go @@ -0,0 +1,45 @@ +package vm_test + +import ( + "errors" + "os" + "slices" + "testing" + + "github.com/u-root/cpu/vm" +) + +func TestInitRamfs(t *testing.T) { + if _, err := vm.New("no", "amd64"); !errors.Is(err, os.ErrNotExist) { + t.Errorf("Testing kernel=no: got %v, want %v", err, os.ErrNotExist) + } + + if _, err := vm.New("linux", "no"); !errors.Is(err, os.ErrNotExist) { + t.Errorf("Testing arch=no: got %v, want %v", err, os.ErrNotExist) + } + + b, err := vm.New("linux", "amd64") + if !errors.Is(err, nil) { + t.Fatalf("Testing kernel=linux arch=amd64: got %v, want nil", err) + } + + n := "initramfs_linux_amd64.cpio" + f, err := os.ReadFile(n) + if err != nil { + t.Fatalf("Reading cpio: got %v, want nil", err) + } + + if !slices.Equal(b.InitRAMFS, f) { + t.Fatalf("initramfs: Uncompress %q is not the same as compiled-in initramfs", n) + } + + n = "kernel_linux_amd64" + if f, err = os.ReadFile(n); err != nil { + t.Fatalf("Reading kernel: got %v, want nil", err) + } + + if !slices.Equal(b.Kernel, f) { + t.Fatalf("kernel:uncompress %q is not the same as compiled-in kernel", n) + } + +} diff --git a/vm/kernel_linux_amd64 b/vm/kernel_linux_amd64 new file mode 100644 index 00000000..55c913e3 Binary files /dev/null and b/vm/kernel_linux_amd64 differ diff --git a/vm/kernel_linux_amd64.gz b/vm/kernel_linux_amd64.gz new file mode 100644 index 00000000..53039c4e Binary files /dev/null and b/vm/kernel_linux_amd64.gz differ diff --git a/vm/kernel_linux_arm b/vm/kernel_linux_arm new file mode 100644 index 00000000..81208611 Binary files /dev/null and b/vm/kernel_linux_arm differ diff --git a/vm/kernel_linux_arm.gz b/vm/kernel_linux_arm.gz new file mode 100644 index 00000000..a5e46f2f Binary files /dev/null and b/vm/kernel_linux_arm.gz differ diff --git a/vm/kernel_linux_arm64 b/vm/kernel_linux_arm64 new file mode 100644 index 00000000..f7aec743 Binary files /dev/null and b/vm/kernel_linux_arm64 differ diff --git a/vm/kernel_linux_arm64.gz b/vm/kernel_linux_arm64.gz new file mode 100644 index 00000000..68852cfe Binary files /dev/null and b/vm/kernel_linux_arm64.gz differ diff --git a/vm/kernel_linux_riscv64 b/vm/kernel_linux_riscv64 new file mode 100644 index 00000000..353e818a Binary files /dev/null and b/vm/kernel_linux_riscv64 differ diff --git a/vm/kernel_linux_riscv64.gz b/vm/kernel_linux_riscv64.gz new file mode 100644 index 00000000..81e36f34 Binary files /dev/null and b/vm/kernel_linux_riscv64.gz differ diff --git a/vm/vm.go b/vm/vm.go new file mode 100644 index 00000000..8ff999b3 --- /dev/null +++ b/vm/vm.go @@ -0,0 +1,196 @@ +//go:generate go run build.go + +package vm + +import ( + "bufio" + "bytes" + "compress/gzip" + "context" + _ "embed" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/u-root/cpu/client" +) + +//go:embed initramfs_linux_amd64.cpio.gz +var linux_amd64 []byte + +//go:embed initramfs_linux_arm64.cpio.gz +var linux_arm64 []byte + +//go:embed initramfs_linux_arm.cpio.gz +var linux_arm []byte + +//go:embed initramfs_linux_riscv64.cpio.gz +var linux_riscv64 []byte + +//go:embed kernel_linux_amd64.gz +var kernel_linux_amd64 []byte + +//go:embed kernel_linux_arm64.gz +var kernel_linux_arm64 []byte + +//go:embed kernel_linux_arm.gz +var kernel_linux_arm []byte + +//go:embed kernel_linux_riscv64.gz +var kernel_linux_riscv64 []byte + +// Image defines an image, including []byte for a kernel and initramfs; +// a []string for the Cmd and its args; and its environment; +// and a directory in which to run. +type Image struct { + Kernel []byte + InitRAMFS []byte + Cmd []string + Env []string + dir string +} + +var images = map[string]Image{ + "linux_amd64": {Kernel: kernel_linux_amd64, InitRAMFS: linux_amd64, Cmd: []string{"qemu-system-x86_64", "-m", "1G"}}, + "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_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) { + 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", + // 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. + // To add cpud debugging, you can add -d to the append string. + "--append", "ip=dhcp init=/init -pk \"\"", + } + env := []string{} + n := fmt.Sprintf("%s_%s", kernel, arch) + im, ok := images[n] + if !ok { + return nil, fmt.Errorf("(%q,%q): %w", kernel, arch, os.ErrNotExist) + } + + r, err := gzip.NewReader(bufio.NewReader(bytes.NewBuffer(im.InitRAMFS))) + if err != nil { + return nil, err + } + + i, err := io.ReadAll(r) + if err != nil { + return nil, fmt.Errorf("unzipped %d bytes: %w", len(i), err) + } + + if r, err = gzip.NewReader(bufio.NewReader(bytes.NewBuffer(im.Kernel))); err != nil { + return nil, err + } + + k, err := io.ReadAll(r) + 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 +} + +// 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") + if out, err := c.CombinedOutput(); err != nil { + return "", fmt.Errorf("u-root initramfs:%q:%w", out, err) + } + return filepath.Join(d, "uroot.cpio"), nil + +} + +// CommandContext starts qemu, given a context, directory in which to +// run the command. The variadic arguments are a set of cpios which will +// be merged into Image.InitRAMFS. A typical use of the extra arguments +// will be to extend the initramfs; they are usually not needed. +func (image *Image) CommandContext(ctx context.Context, d string, extra ...string) (*exec.Cmd, error) { + image.dir = d + i, k := filepath.Join(d, "initramfs"), filepath.Join(d, "kernel") + + if err := os.WriteFile(k, image.Kernel, 0644); err != nil { + return nil, err + } + + ir, err := os.OpenFile(i, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return nil, err + } + + defer ir.Close() + for _, n := range extra { + f, err := os.Open(n) + if err != nil { + return nil, err + } + defer f.Close() + if _, err := io.Copy(ir, f); err != nil { + return nil, err + } + } + if _, err := ir.Write(image.InitRAMFS); err != nil { + return nil, err + } + + ir.Close() + c := exec.CommandContext(ctx, image.Cmd[0], append(image.Cmd[1:], "-kernel", k, "-initrd", i)...) + c.Env = append(os.Environ(), c.Env...) + c.Dir = d + return c, nil +} + +// StartVM is used to start a VM (or in fact any exec.Cmd). +// Once cmd.Start is called, StartVM delays for one second. +// This time has been experimentally determined as the minimum +// required for the guest network to be ready. +func (*Image) StartVM(c *exec.Cmd) error { + if err := c.Start(); err != nil { + return fmt.Errorf("starting VM: %w", err) + } + time.Sleep(time.Second) + return nil +} + +// CPUCommand runs a command in a guest running cpud. +// It is similar to exec.Command, in that it accepts an arg and +// a set of optional args. It differs in that it can return an error. +// If there are no errors, it returns a client.Cmd. +// The returned client.Cmd can be called with CombinedOutput, to make +// it easier to scan output from a command for error messages. +func (i *Image) CPUCommand(arg string, args ...string) (*client.Cmd, error) { + cpu := client.Command("127.0.0.1", append([]string{arg}, args...)...) + cpu.Env = os.Environ() + + if err := cpu.SetOptions( + client.WithDisablePrivateKey(true), + client.WithPort("17010"), + client.WithRoot(i.dir), + client.WithNameSpace("/"), + client.With9P(true), + client.WithTimeout("5s"), + ); err != nil { + return nil, err + } + + if err := cpu.Dial(); err != nil { + return nil, err + } + cpu.Env = append(cpu.Env, "PATH=/bbin", "PWD=/", "SHELL=/bbin/x") + return cpu, nil +} diff --git a/vm/vm_linux_test.go b/vm/vm_linux_test.go new file mode 100644 index 00000000..0ddc724d --- /dev/null +++ b/vm/vm_linux_test.go @@ -0,0 +1,111 @@ +// Copyright 2023 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. + +// runvmtest sets VMTEST_QEMU and VMTEST_KERNEL (if not already set) with +// binaries downloaded from Docker images, then executes a command. +package vm_test + +import ( + "context" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/u-root/cpu/client" + "github.com/u-root/cpu/vm" +) + +// TestCPUAMD64 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 TestCPUAMD64(t *testing.T) { + d := t.TempDir() + i, err := vm.New("linux", "amd64") + if !errors.Is(err, nil) { + t.Fatalf("Testing kernel=linux arch=amd64: got %v, want nil", err) + } + + 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 := vm.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) + } + if err := i.StartVM(c); err != nil { + 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 + 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 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") + } + + for _, tt := range []struct { + args string + out string + }{ + {args: "cw 14 1", out: ""}, + {args: "cr 14", out: "0x01\n"}, + {args: "cw 14 0", out: ""}, + {args: "cr 14", out: "0x00\n"}, + } { + cpu, err := i.CPUCommand("/bbin/io", strings.Split(tt.args, " ")...) + if err != nil { + t.Fatalf("CPUCommand: got %v, want nil", err) + } + client.SetVerbose(t.Logf) + + b, err := cpu.CombinedOutput() + if err != nil { + t.Errorf("io %s: got %v, want nil", tt.args, err) + } + if string(b) != tt.out { + t.Errorf("io %s: got %v, want %v", tt.args, string(b), tt.out) + } + t.Logf("io %s = %q", tt.args, string(b)) + } + + // 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. + +}