mirror of
https://github.com/containers/podman.git
synced 2025-05-21 00:56:36 +08:00

podman machine list lists all virtual machines & indicates the default VM connection, if it exists. it also can take a --format flag arg as a go template. [NO TESTS NEEDED] Signed-off-by: Ashley Cui <acui@redhat.com>
479 lines
13 KiB
Go
479 lines
13 KiB
Go
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
|
|
}
|
|
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 := 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,nowait"}...)
|
|
|
|
// 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,nowait,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 {
|
|
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
|
|
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
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
// 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 opts.Execute {
|
|
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(opts machine.ListOptions) ([]*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
|
|
}
|