summaryrefslogtreecommitdiff
path: root/pkg/machine/qemu/machine.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/machine/qemu/machine.go')
-rw-r--r--pkg/machine/qemu/machine.go512
1 files changed, 512 insertions, 0 deletions
diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go
new file mode 100644
index 000000000..2652ebc10
--- /dev/null
+++ b/pkg/machine/qemu/machine.go
@@ -0,0 +1,512 @@
+package qemu
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/containers/podman/v3/pkg/machine"
+ "github.com/containers/podman/v3/utils"
+ "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"
+)
+
+// NewMachine initializes an instance of a virtual machine based on the qemu
+// virtualization.
+func NewMachine(opts machine.InitOptions) (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
+ }
+ 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 := utils.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
+ cmd = append(cmd, []string{"-smp", strconv.Itoa(int(vm.CPUs))}...)
+ // 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=on,wait=off"}...)
+
+ // Add network
+ cmd = append(cmd, "-nic", "user,model=virtio,hostfwd=tcp::"+strconv.Itoa(vm.Port)+"-:22")
+
+ socketPath, err := getSocketDir()
+ if err != nil {
+ return nil, err
+ }
+ virtualSocketPath := filepath.Join(socketPath, "podman", vm.Name+"_ready.sock")
+ // Add serial port for readiness
+ cmd = append(cmd, []string{
+ "-device", "virtio-serial",
+ "-chardev", "socket,path=" + virtualSocketPath + ",server=on,wait=off,id=" + vm.Name + "_ready",
+ "-device", "virtserialport,chardev=" + vm.Name + "_ready" + ",name=org.fedoraproject.port.0"}...)
+ vm.CmdLine = cmd
+ 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) {
+ vm := new(MachineVM)
+ vmConfigDir, err := machine.GetConfDir(vmtype)
+ if err != nil {
+ return nil, err
+ }
+ b, err := ioutil.ReadFile(filepath.Join(vmConfigDir, name+".json"))
+ if os.IsNotExist(err) {
+ return nil, errors.Wrap(machine.ErrNoSuchVM, name)
+ }
+ if err != nil {
+ return nil, err
+ }
+ err = json.Unmarshal(b, vm)
+ logrus.Debug(vm.CmdLine)
+ return vm, err
+}
+
+// Init writes the json configuration file to the filesystem for
+// other verbs (start, stop)
+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
+ vmConfigDir, err := machine.GetConfDir(vmtype)
+ if err != nil {
+ return err
+ }
+ jsonFile := filepath.Join(vmConfigDir, v.Name) + ".json"
+ v.IdentityPath = filepath.Join(sshDir, v.Name)
+
+ // The user has provided an alternate image which can be a file path
+ // or URL.
+ if len(opts.ImagePath) > 0 {
+ g, err := machine.NewGenericDownloader(vmtype, v.Name, opts.ImagePath)
+ if err != nil {
+ return err
+ }
+ v.ImagePath = g.Get().LocalUncompressedFile
+ if err := g.DownloadImage(); err != nil {
+ return err
+ }
+ } else {
+ // Get the image as usual
+ 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
+ if len(opts.IgnitionPath) < 1 {
+ 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
+ }
+ } else {
+ fmt.Println("An ignition path was provided. No SSH connection was added to Podman")
+ }
+ // 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
+ }
+
+ // User has provided ignition file so keygen
+ // will be skipped.
+ if len(opts.IgnitionPath) < 1 {
+ 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
+ }
+
+ originalDiskSize, err := getDiskSize(v.ImagePath)
+ if err != nil {
+ return err
+ }
+ // Resize the disk image to input disk size
+ // only if the virtualdisk size is less than
+ // the given disk size
+ if opts.DiskSize<<(10*3) > originalDiskSize {
+ resize := exec.Command("qemu-img", []string{"resize", v.ImagePath, strconv.Itoa(int(opts.DiskSize)) + "G"}...)
+ resize.Stdout = os.Stdout
+ resize.Stderr = os.Stderr
+ if err := resize.Run(); err != nil {
+ return errors.Errorf("error resizing image: %q", err)
+ }
+ }
+ // If the user provides an ignition file, we need to
+ // copy it into the conf dir
+ if len(opts.IgnitionPath) > 0 {
+ inputIgnition, err := ioutil.ReadFile(opts.IgnitionPath)
+ if err != nil {
+ return err
+ }
+ return ioutil.WriteFile(v.IgnitionFilePath, inputIgnition, 0644)
+ }
+ // Write the ignition file
+ ign := machine.DynamicIgnition{
+ Name: opts.Username,
+ Key: key,
+ VMName: v.Name,
+ WritePath: v.IgnitionFilePath,
+ }
+ return machine.NewIgnitionFile(ign)
+}
+
+// Start executes the qemu command line and forks it
+func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
+ var (
+ conn net.Conn
+ err error
+ wait time.Duration = time.Millisecond * 500
+ )
+ attr := new(os.ProcAttr)
+ files := []*os.File{os.Stdin, os.Stdout, os.Stderr}
+ attr.Files = files
+ logrus.Debug(v.CmdLine)
+ cmd := v.CmdLine
+
+ // Disable graphic window when not in debug mode
+ // Done in start, so we're not suck with the debug level we used on init
+ if logrus.GetLevel() != logrus.DebugLevel {
+ cmd = append(cmd, "-display", "none")
+ }
+
+ _, err = os.StartProcess(v.CmdLine[0], cmd, attr)
+ if err != nil {
+ return err
+ }
+ fmt.Println("Waiting for VM ...")
+ socketPath, err := getSocketDir()
+ if err != nil {
+ return err
+ }
+
+ // 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"))
+ if err == nil {
+ break
+ }
+ time.Sleep(wait)
+ wait++
+ }
+ if err != nil {
+ return err
+ }
+ _, err = bufio.NewReader(conn).ReadString('\n')
+ 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 := getSocketDir()
+ if err != nil {
+ return Monitor{}, err
+ }
+ rtDir = filepath.Join(rtDir, "podman")
+ if _, err := os.Stat(filepath.Join(rtDir)); os.IsNotExist(err) {
+ // TODO 0644 is fine on linux but macos is weird
+ if err := os.MkdirAll(rtDir, 0755); err != nil {
+ return Monitor{}, err
+ }
+ }
+ if timeout == 0 {
+ timeout = defaultQMPTimeout
+ }
+ monitor := Monitor{
+ Network: network,
+ Address: filepath.Join(rtDir, "qmp_"+name+".sock"),
+ Timeout: timeout,
+ }
+ return monitor, nil
+}
+
+func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (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)
+
+ args := []string{"-i", v.IdentityPath, "-p", port, sshDestination}
+ if len(opts.Args) > 0 {
+ args = append(args, opts.Args...)
+ } else {
+ fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", v.Name)
+ }
+
+ cmd := exec.Command("ssh", args...)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ cmd.Stdin = os.Stdin
+
+ return cmd.Run()
+}
+
+// executes qemu-image info to get the virtual disk size
+// of the diskimage
+func getDiskSize(path string) (uint64, error) {
+ diskInfo := exec.Command("qemu-img", "info", "--output", "json", path)
+ stdout, err := diskInfo.StdoutPipe()
+ if err != nil {
+ return 0, err
+ }
+ if err := diskInfo.Start(); err != nil {
+ return 0, err
+ }
+ tmpInfo := struct {
+ VirtualSize uint64 `json:"virtual-size"`
+ Filename string `json:"filename"`
+ ClusterSize int64 `json:"cluster-size"`
+ Format string `json:"format"`
+ FormatSpecific struct {
+ Type string `json:"type"`
+ Data map[string]string `json:"data"`
+ }
+ DirtyFlag bool `json:"dirty-flag"`
+ }{}
+ if err := json.NewDecoder(stdout).Decode(&tmpInfo); err != nil {
+ return 0, err
+ }
+ if err := diskInfo.Wait(); err != nil {
+ return 0, err
+ }
+ return tmpInfo.VirtualSize, nil
+}
+
+// List lists all vm's that use qemu virtualization
+func List(_ machine.ListOptions) ([]*machine.ListResponse, error) {
+ return GetVMInfos()
+}
+
+func GetVMInfos() ([]*machine.ListResponse, error) {
+ vmConfigDir, err := machine.GetConfDir(vmtype)
+ if err != nil {
+ return nil, err
+ }
+
+ var listed []*machine.ListResponse
+
+ if err = filepath.Walk(vmConfigDir, func(path string, info os.FileInfo, err error) error {
+ vm := new(MachineVM)
+ if strings.HasSuffix(info.Name(), ".json") {
+ fullPath := filepath.Join(vmConfigDir, info.Name())
+ b, err := ioutil.ReadFile(fullPath)
+ if err != nil {
+ return err
+ }
+ err = json.Unmarshal(b, vm)
+ if err != nil {
+ return err
+ }
+ listEntry := new(machine.ListResponse)
+
+ listEntry.Name = vm.Name
+ listEntry.VMType = "qemu"
+ fi, err := os.Stat(fullPath)
+ if err != nil {
+ return err
+ }
+ listEntry.CreatedAt = fi.ModTime()
+
+ fi, err = os.Stat(vm.ImagePath)
+ if err != nil {
+ return err
+ }
+ listEntry.LastUp = fi.ModTime()
+ if vm.isRunning() {
+ listEntry.Running = true
+ }
+
+ listed = append(listed, listEntry)
+ }
+ return nil
+ }); err != nil {
+ return nil, err
+ }
+ return listed, err
+}
+
+func IsValidVMName(name string) (bool, error) {
+ infos, err := GetVMInfos()
+ if err != nil {
+ return false, err
+ }
+ for _, vm := range infos {
+ if vm.Name == name {
+ return true, nil
+ }
+ }
+ return false, nil
+}