diff options
Diffstat (limited to 'pkg/machine/qemu/machine.go')
-rw-r--r-- | pkg/machine/qemu/machine.go | 512 |
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 +} |