diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index 19f31d1a62..4b0e109fc0 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -29,6 +29,7 @@ var ( initOpts = machine.InitOptions{} defaultMachineName = "podman-machine-default" now bool + nocompat bool ) func init() { @@ -70,6 +71,18 @@ func init() { "Start machine now", ) + flags.BoolVar( + &nocompat, + "nocompat", false, + "Disable compatibility socket proxies when starting", + ) + + flags.BoolVar( + &initOpts.NoLink, + "nolink", false, + "Do not link /var/run/docker.sock", + ) + ImagePathFlagName := "image-path" flags.StringVar(&initOpts.ImagePath, ImagePathFlagName, cfg.Engine.MachineImage, "Path to qcow image") _ = initCmd.RegisterFlagCompletionFunc(ImagePathFlagName, completion.AutocompleteDefault) @@ -105,7 +118,7 @@ func initMachine(cmd *cobra.Command, args []string) error { return err } if now { - err = vm.Start(initOpts.Name, machine.StartOptions{}) + err = vm.Start(initOpts.Name, machine.StartOptions{NoCompat: nocompat, NoLink: initOpts.NoLink}) if err == nil { fmt.Printf("Machine %q started successfully\n", initOpts.Name) } diff --git a/cmd/podman/machine/start.go b/cmd/podman/machine/start.go index 4ae31e6def..faddc9ce84 100644 --- a/cmd/podman/machine/start.go +++ b/cmd/podman/machine/start.go @@ -3,8 +3,6 @@ package machine import ( - "fmt" - "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/machine" "github.com/containers/podman/v3/pkg/machine/qemu" @@ -14,7 +12,7 @@ import ( var ( startCmd = &cobra.Command{ - Use: "start [MACHINE]", + Use: "start [options] [MACHINE]", Short: "Start an existing machine", Long: "Start a managed virtual machine ", RunE: start, @@ -22,6 +20,8 @@ var ( Example: `podman machine start myvm`, ValidArgsFunction: autocompleteMachine, } + + startOptions machine.StartOptions ) func init() { @@ -29,6 +29,9 @@ func init() { Command: startCmd, Parent: machineCmd, }) + flags := startCmd.Flags() + flags.BoolVar(&startOptions.NoCompat, "nocompat", false, "Disable Docker API compatibility socket proxies") + flags.BoolVar(&startOptions.NoLink, "nolink", false, "Do not link /var/run/docker.sock",) } func start(cmd *cobra.Command, args []string) error { @@ -60,9 +63,8 @@ func start(cmd *cobra.Command, args []string) error { if err != nil { return err } - if err := vm.Start(vmName, machine.StartOptions{}); err != nil { + if err := vm.Start(vmName, startOptions); err != nil { return err } - fmt.Printf("Machine %q started successfully\n", vmName) return nil } diff --git a/cmd/podman/system/umask_noop.go b/cmd/podman/system/umask_noop.go new file mode 100644 index 0000000000..b61dabab2f --- /dev/null +++ b/cmd/podman/system/umask_noop.go @@ -0,0 +1,7 @@ +// +build windows + +package system + +func umask(mask int) int { + return 0 +} diff --git a/cmd/podman/system/umask_unix.go b/cmd/podman/system/umask_unix.go new file mode 100644 index 0000000000..5755c560e8 --- /dev/null +++ b/cmd/podman/system/umask_unix.go @@ -0,0 +1,9 @@ +// +build !windows + +package system + +import "syscall" + +func umask(mask int) int { + return syscall.Umask(mask) +} diff --git a/cmd/podman/system/unixproxy.go b/cmd/podman/system/unixproxy.go new file mode 100644 index 0000000000..c8927d9bc2 --- /dev/null +++ b/cmd/podman/system/unixproxy.go @@ -0,0 +1,249 @@ +package system + +import ( + "fmt" + "io" + "net" + "net/url" + "os" + "path/filepath" + "strconv" + "time" + + "github.com/containers/common/pkg/completion" + "github.com/containers/podman/v3/cmd/podman/common" + "github.com/containers/podman/v3/cmd/podman/registry" + "github.com/containers/podman/v3/pkg/bindings" + "github.com/containers/podman/v3/pkg/rootless" + "github.com/containers/podman/v3/pkg/util" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" +) + +var ( + unixProxyDescription = `Proxy a remote podman service + + Proxies a remote podman service over a local unix domain socket. +` + + upCmd = &cobra.Command{ + Use: "unix-proxy [options] [URI]", + Args: cobra.MaximumNArgs(1), + Short: "Proxies a remote podman service over a local unix domain socket", + Long: unixProxyDescription, + RunE: proxy, + ValidArgsFunction: common.AutocompleteDefaultOneArg, + Example: `podman system unix-proxy unix:///tmp/podman.sock`, + } + + upArgs = struct { + PidFile string + Quiet bool + }{} +) + +type CloseWriteStream interface { + io.Reader + io.WriteCloser + CloseWrite() error +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: upCmd, + Parent: systemCmd, + }) + + flags := upCmd.Flags() + + flags.StringVarP(&upArgs.PidFile, "pid-file", "", "", "File to save PID") + _ = upCmd.RegisterFlagCompletionFunc("pid-file", completion.AutocompleteNone) + flags.BoolVarP(&upArgs.Quiet, "quiet", "q", false, "Suppress printed output") + _ = upCmd.RegisterFlagCompletionFunc("quiet", completion.AutocompleteNone) +} + +func proxy(cmd *cobra.Command, args []string) error { + apiURI, err := resolveUnixURI(args) + if err != nil { + return err + } + logrus.Infof("using API endpoint: '%s'", apiURI) + // Clean up any old existing unix domain socket + var uri *url.URL + if len(apiURI) > 0 { + var err error + uri, err = url.Parse(apiURI) + if err != nil { + return err + } + + // socket activation uses a unix:// socket in the shipped unit files but apiURI is coded as "" at this layer. + if uri.Scheme == "unix" { + if err := os.Remove(uri.Path); err != nil && !os.IsNotExist(err) { + return err + } + } else { + return errors.Errorf("Only unix domain sockets are supported as a proxy address: %s", uri) + } + } + + if len(upArgs.PidFile) > 0 { + f, err := os.Create(upArgs.PidFile) + if err != nil { + errors.Wrap(err, "Error creating pid") + } + defer os.Remove(upArgs.PidFile) + pid := os.Getpid() + if _, err := f.WriteString(strconv.Itoa(pid)); err != nil { + errors.Wrap(err, "Error creating pid") + } + } + + return setupProxy(uri) +} + +func connectForward(bastion *bindings.Bastion) (CloseWriteStream, error) { + for retries := 1; ; retries++ { + forward, err := bastion.Client.Dial("unix", bastion.URI.Path) + if err == nil { + return forward.(ssh.Channel), nil + } + // Check if ssh connection is still alive + _, _, err2 := bastion.Client.Conn.SendRequest("alive@podman", true, nil) + if err2 != nil || retries > 2 { + // couldn't reconnect ssh tunnel, or the destination is unreachable + return nil, errors.Wrapf(err, "Couldn't reestablish ssh connection: %s", bastion.URI) + } + + bastion.Reconnect() + } +} + +func listenUnix(socketURI *url.URL) (net.Listener, error) { + oldmask := umask(0177) + defer umask(oldmask) + listener, err := net.Listen("unix", socketURI.Path) + if err != nil { + return listener, errors.Wrapf(err, "Error listening on socket: %s", socketURI.Path) + } + + return listener, nil +} + +func setupProxy(socketURI *url.URL) error { + cfg := registry.PodmanConfig() + + uri, err := url.Parse(cfg.URI) + if err != nil { + return errors.Wrapf(err, "Not a valid url: %s", uri) + } + + if uri.Scheme != "ssh" { + return errors.Errorf("Only ssh is supported, specify another connection: %s", uri) + } + + bastion, err := bindings.CreateBastion(uri, "", cfg.Identity) + defer bastion.Client.Close() + if err != nil { + return err + } + + printfOrQuiet("SSH Bastion connected: %s\n", uri) + + listener, err := listenUnix(socketURI) + if err != nil { + return errors.Wrapf(err, "Error listening on socket: %s", socketURI.Path) + } + defer listener.Close() + + printfOrQuiet("Listening on: %s\n", socketURI) + + for { + acceptConnection(listener, &bastion, socketURI) + } +} + +func printfOrQuiet(format string, a ...interface{}) (n int, err error) { + if !upArgs.Quiet { + return fmt.Printf(format, a...) + } + + return 0, nil +} + +func acceptConnection(listener net.Listener, bastion *bindings.Bastion, socketURI *url.URL) error { + con, err := accept(listener) + if err != nil { + return errors.Wrapf(err, "Error accepting on socket: %s", socketURI.Path) + } + + src, ok := con.(CloseWriteStream) + if !ok { + con.Close() + return errors.Wrapf(err, "Underlying socket does not support half-close %s", socketURI.Path) + } + + dest, err := connectForward(bastion) + if err != nil { + con.Close() + logrus.Error(err) + return nil // eat + } + + go forward(src, dest) + go forward(dest, src) + + return nil +} + +func backOff(delay time.Duration) time.Duration { + if delay == 0 { + delay = 5 * time.Millisecond + } else { + delay *= 2 + } + if delay > time.Second { + delay = time.Second + } + return delay +} + +func accept(listener net.Listener) (net.Conn, error) { + con, err := listener.Accept() + delay := time.Duration(0) + if ne, ok := err.(net.Error); ok && ne.Temporary() { + delay = backOff(delay) + time.Sleep(delay) + } + return con, err +} + +func forward(src io.Reader, dest CloseWriteStream) { + defer dest.CloseWrite() + io.Copy(dest, src) +} + +func resolveUnixURI(_url []string) (string, error) { + socketName := "podman-remote.sock" + + if len(_url) > 0 && _url[0] != "" { + return _url[0], nil + } + + xdg, err := util.GetRuntimeDir() + if rootless.IsRootless() { + xdg = os.TempDir() + } + + if err != nil { + return "", err + } + + socketPath := filepath.Join(xdg, "podman", socketName) + if err := os.MkdirAll(filepath.Dir(socketPath), 0700); err != nil { + return "", err + } + return "unix:" + socketPath, nil +} diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index 4127ad2f00..ed592ccd99 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -32,6 +32,13 @@ type Connection struct { Client *http.Client } +type Bastion struct { + Client *ssh.Client + Config *ssh.ClientConfig + URI *url.URL + Port string +} + type valueKey string const ( @@ -65,10 +72,7 @@ func NewConnection(ctx context.Context, uri string) (context.Context, error) { // or unix:///run/podman/podman.sock // or ssh://@[:port]/run/podman/podman.sock?secure=True func NewConnectionWithIdentity(ctx context.Context, uri string, identity string) (context.Context, error) { - var ( - err error - secure bool - ) + var err error if v, found := os.LookupEnv("CONTAINER_HOST"); found && uri == "" { uri = v } @@ -91,11 +95,7 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, identity string) var connection Connection switch _url.Scheme { case "ssh": - secure, err = strconv.ParseBool(_url.Query().Get("secure")) - if err != nil { - secure = false - } - connection, err = sshClient(_url, secure, passPhrase, identity) + connection, err = sshClient(_url, passPhrase, identity) case "unix": if !strings.HasPrefix(uri, "unix:///") { // autofix unix://path_element vs unix:///path_element @@ -174,7 +174,23 @@ func pingNewConnection(ctx context.Context) error { return errors.Errorf("ping response was %d", response.StatusCode) } -func sshClient(_url *url.URL, secure bool, passPhrase string, identity string) (Connection, error) { +func sshClient(_url *url.URL, passPhrase string, identity string) (Connection, error) { + bastion, err := CreateBastion(_url, passPhrase, identity) + if err != nil { + return Connection{}, err + } + + connection := Connection{URI: _url} + connection.Client = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return bastion.Client.Dial("unix", _url.Path) + }, + }} + return connection, nil +} + +func CreateBastion(_url *url.URL, passPhrase string, identity string) (Bastion, error) { // if you modify the authmethods or their conditionals, you will also need to make similar // changes in the client (currently cmd/podman/system/connection/add getUDS). @@ -183,7 +199,7 @@ func sshClient(_url *url.URL, secure bool, passPhrase string, identity string) ( if len(identity) > 0 { s, err := terminal.PublicKey(identity, []byte(passPhrase)) if err != nil { - return Connection{}, errors.Wrapf(err, "failed to parse identity %q", identity) + return Bastion{}, errors.Wrapf(err, "failed to parse identity %q", identity) } signers = append(signers, s) @@ -195,12 +211,12 @@ func sshClient(_url *url.URL, secure bool, passPhrase string, identity string) ( c, err := net.Dial("unix", sock) if err != nil { - return Connection{}, err + return Bastion{}, err } agentSigners, err := agent.NewClient(c).Signers() if err != nil { - return Connection{}, err + return Bastion{}, err } signers = append(signers, agentSigners...) @@ -249,6 +265,8 @@ func sshClient(_url *url.URL, secure bool, passPhrase string, identity string) ( port = "22" } + secure, _ := strconv.ParseBool(_url.Query().Get("secure")) + callback := ssh.InsecureIgnoreHostKey() if secure { host := _url.Hostname() @@ -261,35 +279,37 @@ func sshClient(_url *url.URL, secure bool, passPhrase string, identity string) ( } } - bastion, err := ssh.Dial("tcp", - net.JoinHostPort(_url.Hostname(), port), - &ssh.ClientConfig{ - User: _url.User.Username(), - Auth: authMethods, - HostKeyCallback: callback, - HostKeyAlgorithms: []string{ - ssh.KeyAlgoRSA, - ssh.KeyAlgoDSA, - ssh.KeyAlgoECDSA256, - ssh.KeyAlgoECDSA384, - ssh.KeyAlgoECDSA521, - ssh.KeyAlgoED25519, - }, - Timeout: 5 * time.Second, + config := &ssh.ClientConfig{ + User: _url.User.Username(), + Auth: authMethods, + HostKeyCallback: callback, + HostKeyAlgorithms: []string{ + ssh.KeyAlgoRSA, + ssh.KeyAlgoDSA, + ssh.KeyAlgoECDSA256, + ssh.KeyAlgoECDSA384, + ssh.KeyAlgoECDSA521, + ssh.KeyAlgoED25519, }, + Timeout: 5 * time.Second, + } + + bastion := Bastion{nil, config, _url, port} + + return bastion, bastion.Reconnect() +} + +func (bastion *Bastion) Reconnect() error { + con, err := ssh.Dial("tcp", + net.JoinHostPort(bastion.URI.Hostname(), bastion.Port), + bastion.Config, ) if err != nil { - return Connection{}, errors.Wrapf(err, "Connection to bastion host (%s) failed.", _url.String()) + return errors.Wrapf(err, "Connection to bastion host (%s) failed.", bastion.URI.String()) } + bastion.Client = con - connection := Connection{URI: _url} - connection.Client = &http.Client{ - Transport: &http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - return bastion.Dial("unix", _url.Path) - }, - }} - return connection, nil + return nil } func unixClient(_url *url.URL) Connection { diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 8db2335aaa..d9ec53fad1 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -1,3 +1,4 @@ +//go:build (amd64 && !windows) || (arm64 && !windows) // +build amd64,!windows arm64,!windows package machine @@ -21,6 +22,7 @@ type InitOptions struct { IsDefault bool Memory uint64 Name string + NoLink bool URI url.URL Username string } @@ -64,7 +66,10 @@ type SSHOptions struct { Username string Args []string } -type StartOptions struct{} +type StartOptions struct { + NoCompat bool + NoLink bool +} type StopOptions struct{} diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index d5f5385940..7f30c803ad 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -1,11 +1,14 @@ +//go:build (amd64 && !windows) || (arm64 && !windows) // +build amd64,!windows arm64,!windows package qemu import ( "bufio" + "context" "encoding/json" "fmt" + "io/fs" "io/ioutil" "net" "os" @@ -16,11 +19,13 @@ import ( "time" "github.com/containers/common/pkg/config" + "github.com/containers/podman/v3/pkg/bindings" "github.com/containers/podman/v3/pkg/machine" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/utils" "github.com/containers/storage/pkg/homedir" "github.com/digitalocean/go-qemu/qmp" + "github.com/moby/term" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -129,6 +134,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { var ( key string ) + sshDir := filepath.Join(homedir.Get(), ".ssh") // GetConfDir creates the directory so no need to check for // its existence @@ -218,6 +224,14 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { return errors.Errorf("error resizing image: %q", err) } } + + if !opts.NoLink { + err = createDockerSock(v) + if err != nil { + return err + } + } + // If the user provides an ignition file, we need to // copy it into the conf dir if len(opts.IgnitionPath) > 0 { @@ -227,6 +241,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { } return ioutil.WriteFile(v.IgnitionFilePath, inputIgnition, 0644) } + // Write the ignition file ign := machine.DynamicIgnition{ Name: opts.Username, @@ -238,12 +253,11 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { } // Start executes the qemu command line and forks it -func (v *MachineVM) Start(name string, _ machine.StartOptions) error { +func (v *MachineVM) Start(name string, startOptions machine.StartOptions) error { var ( conn net.Conn err error qemuSocketConn net.Conn - wait time.Duration = time.Millisecond * 500 ) if err := v.startHostNetworking(); err != nil { @@ -271,14 +285,8 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { if err := os.Remove(qemuSocketPath); err != nil && !errors.Is(err, os.ErrNotExist) { logrus.Warn(err) } - for i := 0; i < 6; i++ { - qemuSocketConn, err = net.Dial("unix", qemuSocketPath) - if err == nil { - break - } - time.Sleep(wait) - wait++ - } + + qemuSocketConn, err = socketWait(qemuSocketPath, false) if err != nil { return err } @@ -313,19 +321,165 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { // The socket is not made until the qemu process is running so here // we do a backoff waiting for it. Once we have a conn, we break and // then wait to read it. - for i := 0; i < 6; i++ { - conn, err = net.Dial("unix", filepath.Join(socketPath, "podman", v.Name+"_ready.sock")) + conn, err = socketWait(filepath.Join(socketPath, "podman", v.Name+"_ready.sock"), false) + if err != nil { + return err + } + _, err = bufio.NewReader(conn).ReadString('\n') + if err != nil { + return err + } + + var ( + rootfullSocket string + rootlessSocket string + ) + if !startOptions.NoCompat { + err = createDockerSock(v) + if err != nil { + return err + } + if !startOptions.NoLink { + rootfullSocket, rootlessSocket, err = createCompatibilityProxies(v) + if err != nil { + return err + } + } + } + + fmt.Printf("Machine %q started successfully!\n\n", v.Name) + fmt.Printf("Podman Clients\n") + fmt.Printf("--------------\n") + fmt.Printf("Podman clients can now access this machine using standard podman commands.\n") + fmt.Printf("For example, to run a date command on a *rootless* container:\n") + fmt.Printf("\n podman run ubi8/ubi-micro date\n") + fmt.Printf("\nTo bind port 80 using a *root* container:\n") + fmt.Printf("\n podman -c podman-machine-default-root run -dt -p 80:80/tcp docker.io/library/httpd\n\n") + + if !startOptions.NoCompat { + fmt.Printf("Docker API Clients\n") + fmt.Printf("------------------\n") + if active, _, _ := verifyDockerSock(rootfullSocket); active { + fmt.Printf("Compatibility socket link is present. Docker API clients require no special environment for *root* containers.\n\n") + } else { + fmt.Printf("Compatibility socket link is not present, rerun 'podman machine init' to create.\n") + fmt.Printf("*Root* containers can still be accessed using:\n\n") + fmt.Printf(" export DOCKER_HOST=unix://%s\n\n", rootfullSocket) + } + fmt.Printf("Docker API clients can also access *rootless* podman with the following environment:\n\n") + fmt.Printf(" export DOCKER_HOST=unix://%s\n\n", rootlessSocket) + } + + return nil +} + +func verifyDockerSock(rootfullSocket string) (bool, bool, string) { + target, err := os.Readlink("/var/run/docker.sock") + return target == rootfullSocket, errors.Is(err, fs.ErrNotExist), target +} + +func createDockerSock(v *MachineVM) error { + var err error + + rootfullSocket, _, err := v.getUnixSocketAndPID(true) + if err != nil { + err = errors.Errorf("Unable to determine runtime env for socket: %q", err) + } + + skip, safe, current := verifyDockerSock(rootfullSocket) + if skip { + return nil + } + + if !term.IsTerminal(os.Stdin.Fd()) { + fmt.Println("No terminal present, can't prompt to create link") + return nil + } + + if !safe { + fmt.Printf("WARNING: Docker socket (/var/run/docker.sock) already exists (points to %q\n)", current) + fmt.Print("Overwrite to point to podman? [y/N] ") + reader := bufio.NewReader(os.Stdin) + answer, err := reader.ReadString('\n') + if err != nil { + return err + } + if strings.ToLower(answer)[0] != 'y' { + return nil + } + } + + fmt.Println("Creating /var/run/docker.sock compatibility link (you might be prompted for your password)") + cmd := exec.Command("/usr/bin/sudo", "/bin/ln", "-fs", rootfullSocket, "/var/run/docker.sock") + if cmd.Run() != nil { + err = errors.Errorf("Sudo failed creating link. %q", err) + } + + return err +} + +func socketWait(socket string, close bool) (net.Conn, error) { + return socketWaitS("unix", socket, close, false) +} + +func socketWaitS(schema string, socket string, close bool, read bool) (net.Conn, error) { + wait := time.Millisecond * 50 + + var ( + err error + conn net.Conn + ) + for i := 0; i < 8; i++ { + conn, err = net.Dial(schema, socket) if err == nil { - break + if read { + buffer := make([]byte, 10) + _, err = conn.Read(buffer) + } + if close { + conn.Close() + } + if err == nil { + break + } } time.Sleep(wait) - wait++ + wait *= 2 } + + return conn, err +} + +func createCompatibilityProxies(v *MachineVM) (string, string, error) { + fmt.Println("Waiting on SSH to come up..") + + // FIXME gvproxy generates some noise, try to wait in advance to mute the proxy failures + time.Sleep(1000 * time.Millisecond) + + _, err := socketWaitS("tcp", fmt.Sprintf("localhost:%d", v.Port), true, true) if err != nil { - return err + return "", "", err } - _, err = bufio.NewReader(conn).ReadString('\n') - return err + + fmt.Println("Waiting on initial proxy connections..") + + for i := 0; i < 2; i++ { + if err := v.startUnixProxy(i == 1); err != nil { + return "", "", errors.Errorf("Unable to start unix socket proxy: %q", err) + } + } + + rootlessSocket, _, _ := v.getUnixSocketAndPID(false) + rootfullSocket, _, _ := v.getUnixSocketAndPID(true) + + for _, s := range []string{rootfullSocket, rootlessSocket} { + err := v.verifyProxyConnection(s) + if err != nil { + return "", "", err + } + } + + return rootfullSocket, rootlessSocket, nil } // Stop uses the qmp monitor to call a system_powerdown @@ -362,9 +516,23 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { return err } qemuSocketFile, pidFile, err := v.getSocketandPid() + cleanupProcess(pidFile, qemuSocketFile) if err != nil { return err } + + for i := 0; i < 2; i++ { + unixSocketFile, pidFile, err := v.getUnixSocketAndPID(i == 1) + cleanupProcess(pidFile, unixSocketFile) + if err != nil { + return err + } + } + + return nil +} + +func cleanupProcess(pidFile string, socketFile string) error { if _, err := os.Stat(pidFile); os.IsNotExist(err) { logrus.Infof("pid file %s does not exist", pidFile) return nil @@ -390,8 +558,14 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { if err := os.Remove(pidFile); err != nil && !errors.Is(err, os.ErrNotExist) { logrus.Warn(err) } + // Remove socket - return os.Remove(qemuSocketFile) + os.Remove(socketFile) + if err != nil { + return nil + } + + return nil } // NewQMPMonitor creates the monitor subsection of our vm @@ -660,6 +834,64 @@ func (v *MachineVM) startHostNetworking() error { return err } +func (v *MachineVM) startUnixProxy(root bool) error { + binary, err := os.Executable() + if err != nil { + return err + } + + unixSocket, pidFile, err := v.getUnixSocketAndPID(root) + if err != nil { + return err + } + attr := new(os.ProcAttr) + // Pass on stdin, stdout, stderr + files := []*os.File{os.Stdin, os.Stdout, os.Stderr} + attr.Files = files + cmd := []string{binary} + if logrus.GetLevel() == logrus.DebugLevel { + cmd = append(cmd, "--debug") + fmt.Println(cmd) + } + suffix := "" + if root { + suffix = "-root" + } + name := fmt.Sprintf("%s%s", v.Name, suffix) + cmd = append(cmd, []string{"-c", name, "system", "unix-proxy", "-q", "--pid-file", pidFile, "unix://" + unixSocket}...) + _, err = os.StartProcess(cmd[0], cmd, attr) + + return err +} + +func (v *MachineVM) verifyProxyConnection(unixSocket string) error { + _, err := socketWait(unixSocket, true) + if err != nil { + return err + } + + _, err = bindings.NewConnection(context.Background(), "unix://"+unixSocket) + return err +} + +func (v *MachineVM) getUnixSocketAndPID(root bool) (string, string, error) { + rtPath, err := getRuntimeDir() + if err != nil { + return "", "", err + } + if !rootless.IsRootless() { + rtPath = "/run" + } + suffix := "" + if root { + suffix = "-root" + } + socketDir := filepath.Join(rtPath, "podman") + pidFile := filepath.Join(socketDir, fmt.Sprintf("%s%s-unix.pid", v.Name, suffix)) + unixSocket := filepath.Join(socketDir, fmt.Sprintf("%s%s-api.sock", v.Name, suffix)) + return unixSocket, pidFile, nil +} + func (v *MachineVM) getSocketandPid() (string, string, error) { rtPath, err := getRuntimeDir() if err != nil {