Files
podman/pkg/machine/qemu/machine.go
baude b5f54a9b23 introduce podman machine
podman machine allows podman to create, manage, and interact with a vm
running some form of linux (default is fcos).  podman is then configured
to be able to interact with the vm automatically.

while this is usable on linux, the real push is to get this working on
both current apple architectures in macos.

Ashley Cui contributed to this PR and was a great help.

[NO TESTS NEEDED]

Signed-off-by: baude <bbaude@redhat.com>
2021-03-25 08:43:51 -05:00

305 lines
8.2 KiB
Go

package qemu
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"time"
"github.com/containers/podman/v3/pkg/machine"
"github.com/containers/podman/v3/pkg/specgen"
"github.com/containers/storage/pkg/homedir"
"github.com/digitalocean/go-qemu/qmp"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
var (
// vmtype refers to qemu (vs libvirt, krun, etc)
vmtype = "qemu"
// qemuCommon are the common command line arguments between the arches
//qemuCommon = []string{"-cpu", "host", "-qmp", "unix://tmp/qmp.sock,server,nowait"}
//qemuCommon = []string{"-cpu", "host", "-qmp", "tcp:localhost:4444,server,nowait"}
)
// NewMachine creates an instance of a virtual machine based on the qemu
// virtualization.
func NewMachine(opts machine.CreateOptions) (machine.VM, error) {
vmConfigDir, err := machine.GetConfDir(vmtype)
if err != nil {
return nil, err
}
vm := new(MachineVM)
if len(opts.Name) > 0 {
vm.Name = opts.Name
}
vm.IgnitionFilePath = opts.IgnitionPath
// If no ignitionfilepath was provided, use defaults
if len(vm.IgnitionFilePath) < 1 {
ignitionFile := filepath.Join(vmConfigDir, vm.Name+".ign")
vm.IgnitionFilePath = ignitionFile
}
// An image was specified
if len(opts.ImagePath) > 0 {
vm.ImagePath = opts.ImagePath
}
// Assign remote user name. if not provided, use default
vm.RemoteUsername = opts.Username
if len(vm.RemoteUsername) < 1 {
vm.RemoteUsername = defaultRemoteUser
}
// Add a random port for ssh
port, err := specgen.GetRandomPort()
if err != nil {
return nil, err
}
vm.Port = port
vm.CPUs = opts.CPUS
vm.Memory = opts.Memory
// Look up the executable
execPath, err := exec.LookPath(QemuCommand)
if err != nil {
return nil, err
}
cmd := append([]string{execPath})
// Add memory
cmd = append(cmd, []string{"-m", strconv.Itoa(int(vm.Memory))}...)
// Add cpus
// TODO
// Add ignition file
cmd = append(cmd, []string{"-fw_cfg", "name=opt/com.coreos/config,file=" + vm.IgnitionFilePath}...)
// Add qmp socket
monitor, err := NewQMPMonitor("unix", vm.Name, defaultQMPTimeout)
if err != nil {
return nil, err
}
vm.QMPMonitor = monitor
cmd = append(cmd, []string{"-qmp", monitor.Network + ":/" + monitor.Address + ",server,nowait"}...)
// Add network
cmd = append(cmd, "-nic", "user,model=virtio,hostfwd=tcp::"+strconv.Itoa(vm.Port)+"-:22")
vm.CmdLine = cmd
fmt.Println("///")
return vm, nil
}
// LoadByName reads a json file that describes a known qemu vm
// and returns a vm instance
func LoadVMByName(name string) (machine.VM, error) {
// TODO need to define an error relating to ErrMachineNotFound
vm := new(MachineVM)
vmConfigDir, err := machine.GetConfDir(vmtype)
if err != nil {
return nil, err
}
b, err := ioutil.ReadFile(filepath.Join(vmConfigDir, name+".json"))
if err != nil {
return nil, err
}
err = json.Unmarshal(b, vm)
logrus.Debug(vm.CmdLine)
return vm, err
}
// Create writes the json configuration file to the filesystem for
// other verbs (start, stop)
func (v *MachineVM) Create(opts machine.CreateOptions) error {
sshDir := filepath.Join(homedir.Get(), ".ssh")
// GetConfDir creates the directory so no need to check for
// its existence
vmConfigDir, err := machine.GetConfDir(vmtype)
if err != nil {
return err
}
jsonFile := filepath.Join(vmConfigDir, v.Name) + ".json"
v.IdentityPath = filepath.Join(sshDir, v.Name)
dd, err := machine.NewFcosDownloader(vmtype, v.Name)
if err != nil {
return err
}
v.ImagePath = dd.Get().LocalUncompressedFile
if err := dd.DownloadImage(); err != nil {
return err
}
// Add arch specific options including image location
v.CmdLine = append(v.CmdLine, v.addArchOptions()...)
// Add location of bootable image
v.CmdLine = append(v.CmdLine, "-drive", "if=virtio,file="+v.ImagePath)
// This kind of stinks but no other way around this r/n
uri := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/user/1000/podman/podman.sock", strconv.Itoa(v.Port), v.RemoteUsername)
if err := machine.AddConnection(&uri, v.Name, filepath.Join(sshDir, v.Name), opts.IsDefault); err != nil {
return err
}
// Write the JSON file
b, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
if err := ioutil.WriteFile(jsonFile, b, 0644); err != nil {
return err
}
key, err := machine.CreateSSHKeys(v.IdentityPath)
if err != nil {
return err
}
// Run arch specific things that need to be done
if err := v.prepare(); err != nil {
return err
}
// Write the ignition file
return machine.NewIgnitionFile(opts.Username, key, v.IgnitionFilePath)
}
// Start executes the qemu command line and forks it
func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
var (
err error
)
attr := new(os.ProcAttr)
files := []*os.File{os.Stdin, os.Stdout, os.Stderr}
attr.Files = files
fmt.Print(v.CmdLine)
_, err = os.StartProcess(v.CmdLine[0], v.CmdLine, attr)
return err
}
// Stop uses the qmp monitor to call a system_powerdown
func (v *MachineVM) Stop(name string, _ machine.StopOptions) error {
// check if the qmp socket is there. if not, qemu instance is gone
if _, err := os.Stat(v.QMPMonitor.Address); os.IsNotExist(err) {
// Right now it is NOT an error to stop a stopped machine
logrus.Debugf("QMP monitor socket %v does not exist", v.QMPMonitor.Address)
return nil
}
qmpMonitor, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address, v.QMPMonitor.Timeout)
if err != nil {
return err
}
// Simple JSON formation for the QAPI
stopCommand := struct {
Execute string `json:"execute"`
}{
Execute: "system_powerdown",
}
input, err := json.Marshal(stopCommand)
if err != nil {
return err
}
if err := qmpMonitor.Connect(); err != nil {
return err
}
defer func() {
if err := qmpMonitor.Disconnect(); err != nil {
logrus.Error(err)
}
}()
_, err = qmpMonitor.Run(input)
return err
}
// NewQMPMonitor creates the monitor subsection of our vm
func NewQMPMonitor(network, name string, timeout time.Duration) (Monitor, error) {
rtDir, err := getDataDir()
if err != nil {
return Monitor{}, err
}
if timeout == 0 {
timeout = defaultQMPTimeout
}
monitor := Monitor{
Network: network,
Address: filepath.Join(rtDir, "podman", "qmp_"+name+".sock"),
Timeout: timeout,
}
return monitor, nil
}
func (v *MachineVM) Destroy(name string, opts machine.DestroyOptions) (string, func() error, error) {
var (
files []string
)
// cannot remove a running vm
if v.isRunning() {
return "", nil, errors.Errorf("running vm %q cannot be destroyed", v.Name)
}
// Collect all the files that need to be destroyed
if !opts.SaveKeys {
files = append(files, v.IdentityPath, v.IdentityPath+".pub")
}
if !opts.SaveIgnition {
files = append(files, v.IgnitionFilePath)
}
if !opts.SaveImage {
files = append(files, v.ImagePath)
}
files = append(files, v.archRemovalFiles()...)
if err := machine.RemoveConnection(v.Name); err != nil {
logrus.Error(err)
}
vmConfigDir, err := machine.GetConfDir(vmtype)
if err != nil {
return "", nil, err
}
files = append(files, filepath.Join(vmConfigDir, v.Name+".json"))
confirmationMessage := "\nThe following files will be deleted:\n\n"
for _, msg := range files {
confirmationMessage += msg + "\n"
}
confirmationMessage += "\n"
return confirmationMessage, func() error {
for _, f := range files {
if err := os.Remove(f); err != nil {
logrus.Error(err)
}
}
return nil
}, nil
}
func (v *MachineVM) isRunning() bool {
// Check if qmp socket path exists
if _, err := os.Stat(v.QMPMonitor.Address); os.IsNotExist(err) {
return false
}
// Check if we can dial it
if _, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address, v.QMPMonitor.Timeout); err != nil {
return false
}
return true
}
// SSH opens an interactive SSH session to the vm specified.
// Added ssh function to VM interface: pkg/machine/config/go : line 58
func (v *MachineVM) SSH(name string, opts machine.SSHOptions) error {
if !v.isRunning() {
return errors.Errorf("vm %q is not running.", v.Name)
}
sshDestination := v.RemoteUsername + "@localhost"
port := strconv.Itoa(v.Port)
fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", v.Name)
cmd := exec.Command("ssh", "-i", v.IdentityPath, "-p", port, sshDestination)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
return cmd.Run()
}