summaryrefslogtreecommitdiff
path: root/pkg/machine/qemu
diff options
context:
space:
mode:
authorbaude <bbaude@redhat.com>2021-03-15 14:52:43 -0500
committerbaude <bbaude@redhat.com>2021-03-25 08:43:51 -0500
commitb5f54a9b23e8d9418700494da9aa78d8db354c43 (patch)
tree59dfb9edf3faf6d184f6af40522f71968948133a /pkg/machine/qemu
parenta861f6fd3ebe4fe0b63a1b550e6b99d7525228c0 (diff)
downloadpodman-b5f54a9b23e8d9418700494da9aa78d8db354c43.tar.gz
podman-b5f54a9b23e8d9418700494da9aa78d8db354c43.tar.bz2
podman-b5f54a9b23e8d9418700494da9aa78d8db354c43.zip
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>
Diffstat (limited to 'pkg/machine/qemu')
-rw-r--r--pkg/machine/qemu/config.go43
-rw-r--r--pkg/machine/qemu/machine.go304
-rw-r--r--pkg/machine/qemu/options_amd64.go26
-rw-r--r--pkg/machine/qemu/options_arm64.go40
4 files changed, 413 insertions, 0 deletions
diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go
new file mode 100644
index 000000000..e4687914d
--- /dev/null
+++ b/pkg/machine/qemu/config.go
@@ -0,0 +1,43 @@
+package qemu
+
+import "time"
+
+type MachineVM struct {
+ // CPUs to be assigned to the VM
+ CPUs uint64
+ // The command line representation of the qemu command
+ CmdLine []string
+ // IdentityPath is the fq path to the ssh priv key
+ IdentityPath string
+ // IgnitionFilePath is the fq path to the .ign file
+ IgnitionFilePath string
+ // ImagePath is the fq path to
+ ImagePath string
+ // Memory in megabytes assigned to the vm
+ Memory uint64
+ // Name of the vm
+ Name string
+ // SSH port for user networking
+ Port int
+ // QMPMonitor is the qemu monitor object for sending commands
+ QMPMonitor Monitor
+ // RemoteUsername of the vm user
+ RemoteUsername string
+}
+
+type Monitor struct {
+ // Address portion of the qmp monitor (/tmp/tmp.sock)
+ Address string
+ // Network portion of the qmp monitor (unix)
+ Network string
+ // Timeout in seconds for qmp monitor transactions
+ Timeout time.Duration
+}
+
+var (
+ // defaultQMPTimeout is the timeout duration for the
+ // qmp monitor interactions
+ defaultQMPTimeout time.Duration = 2 * time.Second
+ // defaultRemoteUser describes the ssh username default
+ defaultRemoteUser = "core"
+)
diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go
new file mode 100644
index 000000000..92a16dda7
--- /dev/null
+++ b/pkg/machine/qemu/machine.go
@@ -0,0 +1,304 @@
+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()
+}
diff --git a/pkg/machine/qemu/options_amd64.go b/pkg/machine/qemu/options_amd64.go
new file mode 100644
index 000000000..85a2c4b3e
--- /dev/null
+++ b/pkg/machine/qemu/options_amd64.go
@@ -0,0 +1,26 @@
+package qemu
+
+import (
+ "github.com/containers/podman/v3/pkg/util"
+)
+
+var (
+ QemuCommand = "qemu-kvm"
+)
+
+func (v *MachineVM) addArchOptions() []string {
+ opts := []string{"-cpu", "host"}
+ return opts
+}
+
+func (v *MachineVM) prepare() error {
+ return nil
+}
+
+func (v *MachineVM) archRemovalFiles() []string {
+ return []string{}
+}
+
+func getDataDir() (string, error) {
+ return util.GetRuntimeDir()
+}
diff --git a/pkg/machine/qemu/options_arm64.go b/pkg/machine/qemu/options_arm64.go
new file mode 100644
index 000000000..c5b0ea16b
--- /dev/null
+++ b/pkg/machine/qemu/options_arm64.go
@@ -0,0 +1,40 @@
+package qemu
+
+import (
+ "os/exec"
+ "path/filepath"
+)
+
+var (
+ QemuCommand = "qemu-system-aarch64"
+)
+
+func (v *MachineVM) addArchOptions() []string {
+ ovmfDir := getOvmfDir(v.ImagePath, v.Name)
+ opts := []string{
+ "-accel", "hvf",
+ "-cpu", "cortex-a57",
+ "-M", "virt,highmem=off",
+ "-drive", "file=/usr/local/share/qemu/edk2-aarch64-code.fd,if=pflash,format=raw,readonly=on",
+ "-drive", "file=" + ovmfDir + ",if=pflash,format=raw"}
+ return opts
+}
+
+func (v *MachineVM) prepare() error {
+ ovmfDir := getOvmfDir(v.ImagePath, v.Name)
+ cmd := []string{"dd", "if=/dev/zero", "conv=sync", "bs=1m", "count=64", "of=" + ovmfDir}
+ return exec.Command(cmd[0], cmd[1:]...).Run()
+}
+
+func (v *MachineVM) archRemovalFiles() []string {
+ ovmDir := getOvmfDir(v.ImagePath, v.Name)
+ return []string{ovmDir}
+}
+
+func getDataDir() (string, error) {
+ return "/tmp", nil
+}
+
+func getOvmfDir(imagePath, vmName string) string {
+ return filepath.Join(filepath.Dir(imagePath), vmName+"_ovmf_vars.fd")
+}