Skip to content

Commit 8949137

Browse files
committed
Add a VM monitor for macOS
In order to switch from QEMU to krunkit we need to use gvproxy to provide network support to the VM. We need to bound gvproxy lifetime to krunkit lifetime, so there is not an "orphan" daemon after krunkit finish. We are using an "internal" command to monitor, in the future this should be a different binary, but right now is the easiest way. We save the pid fo krunkit instead of the monitor because the `stop` command sends a SIGINT to that pid, and we currently are capturing the signal at podman-bootc. Signed-off-by: German Maglione <[email protected]>
1 parent 03a3054 commit 8949137

File tree

5 files changed

+388
-3
lines changed

5 files changed

+388
-3
lines changed

cmd/vmmon.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//go:build darwin
2+
3+
package cmd
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"strconv"
11+
12+
"github.com/containers/podman-bootc/pkg/user"
13+
"github.com/containers/podman-bootc/pkg/vm"
14+
15+
"github.com/spf13/cobra"
16+
)
17+
18+
// We treat this command as internal, we don't expect the user to call
19+
// this directly. In the future this will be replaced by an external binary
20+
var (
21+
monCmd = &cobra.Command{
22+
Use: "vmmon <ID> <username> <ssh identity> <ssh port>",
23+
Hidden: true,
24+
Args: cobra.ExactArgs(4),
25+
RunE: doMon,
26+
}
27+
console bool
28+
)
29+
30+
func init() {
31+
RootCmd.AddCommand(monCmd)
32+
runCmd.Flags().BoolVar(&console, "console", false, "Show boot console")
33+
}
34+
35+
func doMon(_ *cobra.Command, args []string) error {
36+
usr, err := user.NewUser()
37+
if err != nil {
38+
return err
39+
}
40+
41+
ctx := context.Background()
42+
fullImageId := args[0]
43+
username := args[1]
44+
sshIdentity := args[2]
45+
46+
sshPort, err := strconv.Atoi(args[3])
47+
if err != nil {
48+
return fmt.Errorf("invalid ssh port: %w", err)
49+
}
50+
51+
cacheDir := filepath.Join(usr.CacheDir(), fullImageId)
52+
53+
// MacOS has a 104 bytes limit for a unix socket path
54+
runDir := filepath.Join(usr.RunDir(), fullImageId[0:12])
55+
if err := os.MkdirAll(runDir, os.ModePerm); err != nil {
56+
return err
57+
}
58+
59+
params := vm.MonitorParmeters{
60+
CacheDir: cacheDir,
61+
RunDir: runDir,
62+
Username: username,
63+
SshIdentity: sshIdentity,
64+
SshPort: sshPort,
65+
}
66+
67+
return vm.StartMonitor(ctx, params)
68+
}

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@ go 1.20
55
require (
66
github.com/adrg/xdg v0.4.0
77
github.com/containers/common v0.58.1
8+
github.com/containers/gvisor-tap-vsock v0.7.3
89
github.com/containers/podman/v5 v5.0.1
910
github.com/docker/go-units v0.5.0
1011
github.com/gofrs/flock v0.8.1
1112
github.com/onsi/ginkgo/v2 v2.17.1
1213
github.com/onsi/gomega v1.32.0
13-
github.com/opencontainers/runtime-spec v1.2.0
1414
github.com/sirupsen/logrus v1.9.3
1515
github.com/spf13/cobra v1.8.0
1616
golang.org/x/crypto v0.22.0
1717
golang.org/x/sys v0.19.0
18+
golang.org/x/term v0.19.0
1819
libvirt.org/go/libvirt v1.10002.0
1920
)
2021

@@ -40,7 +41,6 @@ require (
4041
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
4142
github.com/containerd/typeurl/v2 v2.1.1 // indirect
4243
github.com/containers/buildah v1.35.3 // indirect
43-
github.com/containers/gvisor-tap-vsock v0.7.3 // indirect
4444
github.com/containers/image/v5 v5.30.0 // indirect
4545
github.com/containers/libhvee v0.7.0 // indirect
4646
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
@@ -134,6 +134,7 @@ require (
134134
github.com/opencontainers/go-digest v1.0.0 // indirect
135135
github.com/opencontainers/image-spec v1.1.0 // indirect
136136
github.com/opencontainers/runc v1.1.12 // indirect
137+
github.com/opencontainers/runtime-spec v1.2.0 // indirect
137138
github.com/opencontainers/runtime-tools v0.9.1-0.20230914150019-408c51e934dc // indirect
138139
github.com/opencontainers/selinux v1.11.0 // indirect
139140
github.com/openshift/imagebuilder v1.2.6 // indirect
@@ -176,7 +177,6 @@ require (
176177
golang.org/x/mod v0.15.0 // indirect
177178
golang.org/x/net v0.22.0 // indirect
178179
golang.org/x/sync v0.6.0 // indirect
179-
golang.org/x/term v0.19.0 // indirect
180180
golang.org/x/text v0.14.0 // indirect
181181
golang.org/x/time v0.3.0 // indirect
182182
golang.org/x/tools v0.18.0 // indirect

pkg/vm/gvproxy.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package vm
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
"time"
11+
12+
"github.com/containers/podman-bootc/pkg/utils"
13+
14+
gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types"
15+
"github.com/sirupsen/logrus"
16+
)
17+
18+
const gvproxyBinaryName = "gvproxy"
19+
20+
type Vmm int
21+
22+
const (
23+
pidFileName = "gvproxy.pid"
24+
socketFile = "net.sock"
25+
// How log we should wait for gvproxy to be ready
26+
maxBackoffs = 5
27+
backoff = time.Millisecond * 200
28+
)
29+
30+
type gvproxyParams struct {
31+
SshPort int
32+
}
33+
34+
type gvproxyDaemon struct {
35+
socketPath string
36+
pidFile string
37+
cmd *exec.Cmd
38+
}
39+
40+
func newGvproxy(ctx context.Context, binaryPath, rundir string, param gvproxyParams) *gvproxyDaemon {
41+
socketPath := filepath.Join(rundir, socketFile)
42+
pidFile := filepath.Join(rundir, pidFileName)
43+
44+
gvpCmd := gvproxy.NewGvproxyCommand()
45+
gvpCmd.SSHPort = param.SshPort
46+
gvpCmd.PidFile = pidFile
47+
gvpCmd.AddVfkitSocket(fmt.Sprintf("unixgram://%s", socketPath))
48+
49+
cmdLine := gvpCmd.ToCmdline()
50+
cmd := exec.CommandContext(ctx, binaryPath, cmdLine...)
51+
logrus.Debugf("gvproxy command-line: %s %s", binaryPath, strings.Join(cmdLine, " "))
52+
53+
return &gvproxyDaemon{socketPath: socketPath, pidFile: pidFile, cmd: cmd}
54+
}
55+
56+
// Start spawn the gvproxy daemon, killing any running daemon using the same unix socket file.
57+
// It blocks until the unix socket file exists, this does not guarantee that the socket will be
58+
// on listen state
59+
func (d *gvproxyDaemon) start() error {
60+
cleanup(d.pidFile, d.socketPath)
61+
62+
if err := d.cmd.Start(); err != nil {
63+
return fmt.Errorf("unable to start gvproxy: %w", err)
64+
}
65+
66+
// this is racy, the socket file could exist but not be in the listen state yet
67+
if err := utils.WaitForFileWithBackoffs(maxBackoffs, backoff, d.socketPath); err != nil {
68+
return fmt.Errorf("waiting for gvproxy socket: %w", err)
69+
}
70+
return nil
71+
}
72+
73+
func (d *gvproxyDaemon) stop() error {
74+
return d.cmd.Cancel()
75+
}
76+
77+
func (d *gvproxyDaemon) wait() error {
78+
return d.cmd.Wait()
79+
}
80+
81+
func cleanup(pidFile, socketPath string) {
82+
// Let's kill any possible running daemon
83+
pid, err := utils.ReadPidFile(pidFile)
84+
if err == nil {
85+
_ = utils.SendInterrupt(pid)
86+
}
87+
88+
_ = os.Remove(pidFile)
89+
_ = os.Remove(socketPath)
90+
}

pkg/vm/krunkit.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package vm
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os/exec"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/containers/podman-bootc/pkg/utils"
11+
12+
"github.com/sirupsen/logrus"
13+
)
14+
15+
const krunkitBinaryName = "krunkit"
16+
17+
const defaultCpus = 4
18+
const defaultMemory = 2048
19+
20+
type krunkitParams struct {
21+
disk string
22+
netSocket string
23+
oemString string
24+
pidFile string
25+
}
26+
27+
type krunkit struct {
28+
pidFile string
29+
cmd *exec.Cmd
30+
}
31+
32+
func newKrunkit(ctx context.Context, binaryPath string, params krunkitParams) *krunkit {
33+
cmdLine := newKrunkitCmdLine(defaultCpus, defaultMemory)
34+
cmdLine.addRngDevice()
35+
cmdLine.addBlockDevice(params.disk)
36+
cmdLine.addNetworkDevice(params.netSocket)
37+
cmdLine.addOemString(params.oemString)
38+
39+
cmdLineSlice := cmdLine.asSlice()
40+
cmd := exec.CommandContext(ctx, binaryPath, cmdLineSlice...)
41+
logrus.Debugf("krunkit command-line: %s %s", binaryPath, strings.Join(cmdLineSlice, " "))
42+
43+
return &krunkit{cmd: cmd, pidFile: params.pidFile}
44+
}
45+
46+
func (k *krunkit) start() error {
47+
if err := k.cmd.Start(); err != nil {
48+
return fmt.Errorf("unable to start krunkit: %w", err)
49+
}
50+
51+
if err := utils.WritePidFile(k.pidFile, k.cmd.Process.Pid); err != nil {
52+
if err := k.cmd.Cancel(); err != nil {
53+
logrus.Debugf("stopping krunkit: %v", err)
54+
}
55+
return fmt.Errorf("writing pid file %s: %w", k.pidFile, err)
56+
}
57+
58+
return nil
59+
}
60+
61+
func (k *krunkit) wait() error {
62+
return k.cmd.Wait()
63+
}
64+
65+
type krunkitCmdLine struct {
66+
cpus int
67+
memory int
68+
devices []string
69+
oemString []string
70+
}
71+
72+
func newKrunkitCmdLine(cpus int, memory int) *krunkitCmdLine {
73+
return &krunkitCmdLine{cpus: cpus, memory: memory}
74+
}
75+
76+
func (kc *krunkitCmdLine) addDevice(device string) {
77+
kc.devices = append(kc.devices, device)
78+
}
79+
80+
func (kc *krunkitCmdLine) addRngDevice() {
81+
kc.addDevice("virtio-rng")
82+
}
83+
84+
func (kc *krunkitCmdLine) addBlockDevice(diskAbsPath string) {
85+
kc.addDevice(fmt.Sprintf("virtio-blk,path=%s", diskAbsPath))
86+
}
87+
88+
func (kc *krunkitCmdLine) addNetworkDevice(socketAbsPath string) {
89+
kc.addDevice(fmt.Sprintf("virtio-net,unixSocketPath=%s,mac=5a:94:ef:e4:0c:ee", socketAbsPath))
90+
}
91+
92+
func (kc *krunkitCmdLine) addOemString(oemStr string) {
93+
kc.oemString = append(kc.oemString, oemStr)
94+
}
95+
96+
func (kc *krunkitCmdLine) asSlice() []string {
97+
args := []string{}
98+
99+
args = append(args, "--cpus", strconv.Itoa(kc.cpus))
100+
args = append(args, "--memory", strconv.Itoa(kc.memory))
101+
102+
for _, device := range kc.devices {
103+
args = append(args, "--device", device)
104+
}
105+
106+
for _, oemStr := range kc.oemString {
107+
args = append(args, "--oem-string", oemStr)
108+
}
109+
return args
110+
}

0 commit comments

Comments
 (0)