Merge pull request #9836 from baude/vmcreateresize

Podman machine enhancements
This commit is contained in:
OpenShift Merge Robot
2021-03-28 10:55:45 +00:00
committed by GitHub
15 changed files with 180 additions and 83 deletions

View File

@ -8,6 +8,7 @@ import (
"github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/domain/entities"
"github.com/containers/podman/v3/pkg/machine" "github.com/containers/podman/v3/pkg/machine"
"github.com/containers/podman/v3/pkg/machine/qemu" "github.com/containers/podman/v3/pkg/machine/qemu"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -23,17 +24,8 @@ var (
} }
) )
type InitCLIOptions struct {
CPUS uint64
Memory uint64
Devices []string
ImagePath string
IgnitionPath string
Name string
}
var ( var (
initOpts = InitCLIOptions{} initOpts = machine.InitOptions{}
defaultMachineName string = "podman-machine-default" defaultMachineName string = "podman-machine-default"
) )
@ -53,6 +45,15 @@ func init() {
) )
_ = initCmd.RegisterFlagCompletionFunc(cpusFlagName, completion.AutocompleteNone) _ = initCmd.RegisterFlagCompletionFunc(cpusFlagName, completion.AutocompleteNone)
diskSizeFlagName := "disk-size"
flags.Uint64Var(
&initOpts.DiskSize,
diskSizeFlagName, 10,
"Disk size in GB",
)
_ = initCmd.RegisterFlagCompletionFunc(diskSizeFlagName, completion.AutocompleteNone)
memoryFlagName := "memory" memoryFlagName := "memory"
flags.Uint64VarP( flags.Uint64VarP(
&initOpts.Memory, &initOpts.Memory,
@ -72,28 +73,24 @@ func init() {
// TODO should we allow for a users to append to the qemu cmdline? // TODO should we allow for a users to append to the qemu cmdline?
func initMachine(cmd *cobra.Command, args []string) error { func initMachine(cmd *cobra.Command, args []string) error {
initOpts.Name = defaultMachineName
if len(args) > 0 {
initOpts.Name = args[0]
}
vmOpts := machine.InitOptions{
CPUS: initOpts.CPUS,
Memory: initOpts.Memory,
IgnitionPath: initOpts.IgnitionPath,
ImagePath: initOpts.ImagePath,
Name: initOpts.Name,
}
var ( var (
vm machine.VM vm machine.VM
vmType string vmType string
err error err error
) )
initOpts.Name = defaultMachineName
if len(args) > 0 {
initOpts.Name = args[0]
}
switch vmType { switch vmType {
default: // qemu is the default default: // qemu is the default
vm, err = qemu.NewMachine(vmOpts) if _, err := qemu.LoadVMByName(initOpts.Name); err == nil {
return errors.Wrap(machine.ErrVMAlreadyExists, initOpts.Name)
}
vm, err = qemu.NewMachine(initOpts)
} }
if err != nil { if err != nil {
return err return err
} }
return vm.Init(vmOpts) return vm.Init(initOpts)
} }

View File

@ -17,13 +17,13 @@ import (
) )
var ( var (
removeCmd = &cobra.Command{ rmCmd = &cobra.Command{
Use: "remove [options] NAME", Use: "rm [options] [NAME]",
Short: "Remove an existing machine", Short: "Remove an existing machine",
Long: "Remove an existing machine ", Long: "Remove an existing machine ",
RunE: remove, RunE: rm,
Args: cobra.ExactArgs(1), Args: cobra.MaximumNArgs(1),
Example: `podman machine remove myvm`, Example: `podman machine rm myvm`,
ValidArgsFunction: completion.AutocompleteNone, ValidArgsFunction: completion.AutocompleteNone,
} }
) )
@ -35,13 +35,13 @@ var (
func init() { func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{ registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: removeCmd, Command: rmCmd,
Parent: machineCmd, Parent: machineCmd,
}) })
flags := removeCmd.Flags() flags := rmCmd.Flags()
formatFlagName := "force" formatFlagName := "force"
flags.BoolVar(&destoryOptions.Force, formatFlagName, false, "Do not prompt before removeing") flags.BoolVar(&destoryOptions.Force, formatFlagName, false, "Do not prompt before rming")
keysFlagName := "save-keys" keysFlagName := "save-keys"
flags.BoolVar(&destoryOptions.SaveKeys, keysFlagName, false, "Do not delete SSH keys") flags.BoolVar(&destoryOptions.SaveKeys, keysFlagName, false, "Do not delete SSH keys")
@ -53,20 +53,24 @@ func init() {
flags.BoolVar(&destoryOptions.SaveImage, imageFlagName, false, "Do not delete the image file") flags.BoolVar(&destoryOptions.SaveImage, imageFlagName, false, "Do not delete the image file")
} }
func remove(cmd *cobra.Command, args []string) error { func rm(cmd *cobra.Command, args []string) error {
var ( var (
err error err error
vm machine.VM vm machine.VM
vmType string vmType string
) )
vmName := defaultMachineName
if len(args) > 0 && len(args[0]) > 0 {
vmName = args[0]
}
switch vmType { switch vmType {
default: default:
vm, err = qemu.LoadVMByName(args[0]) vm, err = qemu.LoadVMByName(vmName)
} }
if err != nil { if err != nil {
return err return err
} }
confirmationMessage, doIt, err := vm.Remove(args[0], machine.RemoveOptions{}) confirmationMessage, remove, err := vm.Remove(vmName, machine.RemoveOptions{})
if err != nil { if err != nil {
return err return err
} }
@ -84,5 +88,5 @@ func remove(cmd *cobra.Command, args []string) error {
return nil return nil
} }
} }
return doIt() return remove()
} }

View File

@ -14,11 +14,11 @@ import (
var ( var (
sshCmd = &cobra.Command{ sshCmd = &cobra.Command{
Use: "ssh [options] NAME [COMMAND [ARG ...]]", Use: "ssh [options] [NAME] [COMMAND [ARG ...]]",
Short: "SSH into a virtual machine", Short: "SSH into a virtual machine",
Long: "SSH into a virtual machine ", Long: "SSH into a virtual machine ",
RunE: ssh, RunE: ssh,
Args: cobra.MinimumNArgs(1), Args: cobra.MaximumNArgs(1),
Example: `podman machine ssh myvm Example: `podman machine ssh myvm
podman machine ssh -e myvm echo hello`, podman machine ssh -e myvm echo hello`,
@ -48,6 +48,10 @@ func ssh(cmd *cobra.Command, args []string) error {
vm machine.VM vm machine.VM
vmType string vmType string
) )
vmName := defaultMachineName
if len(args) > 0 && len(args[0]) > 1 {
vmName = args[0]
}
sshOpts.Args = args[1:] sshOpts.Args = args[1:]
// Error if no execute but args given // Error if no execute but args given
@ -61,10 +65,10 @@ func ssh(cmd *cobra.Command, args []string) error {
switch vmType { switch vmType {
default: default:
vm, err = qemu.LoadVMByName(args[0]) vm, err = qemu.LoadVMByName(vmName)
} }
if err != nil { if err != nil {
return errors.Wrapf(err, "vm %s not found", args[0]) return errors.Wrapf(err, "vm %s not found", args[0])
} }
return vm.SSH(args[0], sshOpts) return vm.SSH(vmName, sshOpts)
} }

View File

@ -13,11 +13,11 @@ import (
var ( var (
startCmd = &cobra.Command{ startCmd = &cobra.Command{
Use: "start NAME", Use: "start [NAME]",
Short: "Start an existing machine", Short: "Start an existing machine",
Long: "Start an existing machine ", Long: "Start an existing machine ",
RunE: start, RunE: start,
Args: cobra.ExactArgs(1), Args: cobra.MaximumNArgs(1),
Example: `podman machine start myvm`, Example: `podman machine start myvm`,
ValidArgsFunction: completion.AutocompleteNone, ValidArgsFunction: completion.AutocompleteNone,
} }
@ -37,12 +37,16 @@ func start(cmd *cobra.Command, args []string) error {
vm machine.VM vm machine.VM
vmType string vmType string
) )
vmName := defaultMachineName
if len(args) > 0 && len(args[0]) > 0 {
vmName = args[0]
}
switch vmType { switch vmType {
default: default:
vm, err = qemu.LoadVMByName(args[0]) vm, err = qemu.LoadVMByName(vmName)
} }
if err != nil { if err != nil {
return err return err
} }
return vm.Start(args[0], machine.StartOptions{}) return vm.Start(vmName, machine.StartOptions{})
} }

View File

@ -13,11 +13,11 @@ import (
var ( var (
stopCmd = &cobra.Command{ stopCmd = &cobra.Command{
Use: "stop NAME", Use: "stop [NAME]",
Short: "Stop an existing machine", Short: "Stop an existing machine",
Long: "Stop an existing machine ", Long: "Stop an existing machine ",
RunE: stop, RunE: stop,
Args: cobra.ExactArgs(1), Args: cobra.MaximumNArgs(1),
Example: `podman machine stop myvm`, Example: `podman machine stop myvm`,
ValidArgsFunction: completion.AutocompleteNone, ValidArgsFunction: completion.AutocompleteNone,
} }
@ -38,12 +38,16 @@ func stop(cmd *cobra.Command, args []string) error {
vm machine.VM vm machine.VM
vmType string vmType string
) )
vmName := defaultMachineName
if len(args) > 0 && len(args[0]) > 0 {
vmName = args[0]
}
switch vmType { switch vmType {
default: default:
vm, err = qemu.LoadVMByName(args[0]) vm, err = qemu.LoadVMByName(vmName)
} }
if err != nil { if err != nil {
return err return err
} }
return vm.Stop(args[0], machine.StopOptions{}) return vm.Stop(vmName, machine.StopOptions{})
} }

View File

@ -3,7 +3,7 @@ Machine
:doc:`init <markdown/podman-machine-init.1>` Initialize a new virtual machine :doc:`init <markdown/podman-machine-init.1>` Initialize a new virtual machine
:doc:`remove <markdown/podman-machine-remove.1>` Remove a virtual machine :doc:`rm <markdown/podman-machine-rm.1>` Remove a virtual machine
:doc:`ssh <markdown/podman-machine-ssh.1>` SSH into a virtual machine :doc:`ssh <markdown/podman-machine-ssh.1>` SSH into a virtual machine
:doc:`start <markdown/podman-machine-start.1>` Start a virtual machine :doc:`start <markdown/podman-machine-start.1>` Start a virtual machine
:doc:`stop <markdown/podman-machine-stop.1>` Stop a virtual machine :doc:`stop <markdown/podman-machine-stop.1>` Stop a virtual machine

View File

@ -22,6 +22,10 @@ tied to the Linux kernel.
Number of CPUs. Number of CPUs.
#### **--disk-size**=*number*
Size of the disk for the guest VM in GB.
#### **--ignition-path** #### **--ignition-path**
Fully qualified path of the ignition file Fully qualified path of the ignition file

View File

@ -1,17 +1,17 @@
% podman-machine-remove(1) % podman-machine-rm(1)
## NAME ## NAME
podman\-machine\-remove - Remove a virtual machine podman\-machine\-rm - Remove a virtual machine
## SYNOPSIS ## SYNOPSIS
**podman machine remove** [*options*] *name* **podman machine rm** [*options*] [*name*]
## DESCRIPTION ## DESCRIPTION
Remove a virtual machine and its related files. What is actually deleted Remove a virtual machine and its related files. What is actually deleted
depends on the virtual machine type. For all virtual machines, the generated depends on the virtual machine type. For all virtual machines, the generated
SSH keys and the podman system connection are deleted. The ignition files SSH keys and the podman system connection are deleted. The ignition files
generated for that VM are also removeed as is its image file on the filesystem. generated for that VM are also removed as is its image file on the filesystem.
Users get a display of what will be deleted and are required to confirm unless the option `--force` Users get a display of what will be deleted and are required to confirm unless the option `--force`
is used. is used.
@ -45,7 +45,7 @@ deleted.
Remove a VM named "test1" Remove a VM named "test1"
``` ```
$ podman machine remove test1 $ podman machine rm test1
The following files will be deleted: The following files will be deleted:

View File

@ -4,7 +4,7 @@
podman\-machine\-ssh - SSH into a virtual machine podman\-machine\-ssh - SSH into a virtual machine
## SYNOPSIS ## SYNOPSIS
**podman machine ssh** [*options*] *name* [*command* [*arg* ...]] **podman machine ssh** [*options*] [*name*] [*command* [*arg* ...]]
## DESCRIPTION ## DESCRIPTION

View File

@ -4,7 +4,7 @@
podman\-machine\-start - Start a virtual machine podman\-machine\-start - Start a virtual machine
## SYNOPSIS ## SYNOPSIS
**podman machine start** *name* **podman machine start** [*name*]
## DESCRIPTION ## DESCRIPTION

View File

@ -4,7 +4,7 @@
podman\-machine\-stop - Stop a virtual machine podman\-machine\-stop - Stop a virtual machine
## SYNOPSIS ## SYNOPSIS
**podman machine stop** *name* **podman machine stop** [*name*]
## DESCRIPTION ## DESCRIPTION

View File

@ -14,7 +14,7 @@ podman\-machine - Manage Podman's virtual machine
| Command | Man Page | Description | | Command | Man Page | Description |
| ------- | ------------------------------------------------------- | --------------------------------- | | ------- | ------------------------------------------------------- | --------------------------------- |
| init | [podman-machine-init(1)](podman-machine-init.1.md) | Initialize a new virtual machine | | init | [podman-machine-init(1)](podman-machine-init.1.md) | Initialize a new virtual machine |
| remove | [podman-machine-remove(1)](podman-machine-remove.1.md) | Remove a virtual machine | | rm | [podman-machine-rm(1)](podman-machine-rm.1.md)| Remove a virtual machine |
| ssh | [podman-machine-ssh(1)](podman-machine-ssh.1.md) | SSH into a virtual machine | | ssh | [podman-machine-ssh(1)](podman-machine-ssh.1.md) | SSH into a virtual machine |
| start | [podman-machine-start(1)](podman-machine-start.1.md) | Start a virtual machine | | start | [podman-machine-start(1)](podman-machine-start.1.md) | Start a virtual machine |
| stop | [podman-machine-stop(1)](podman-machine-stop.1.md) | Stop a virtual machine | | stop | [podman-machine-stop(1)](podman-machine-stop.1.md) | Stop a virtual machine |

View File

@ -7,19 +7,19 @@ import (
"path/filepath" "path/filepath"
"github.com/containers/storage/pkg/homedir" "github.com/containers/storage/pkg/homedir"
"github.com/pkg/errors"
) )
type InitOptions struct { type InitOptions struct {
Name string
CPUS uint64 CPUS uint64
Memory uint64 DiskSize uint64
IgnitionPath string IgnitionPath string
ImagePath string ImagePath string
Username string
URI url.URL
IsDefault bool IsDefault bool
//KernelPath string Memory uint64
//Devices []VMDevices Name string
URI url.URL
Username string
} }
type RemoteConnectionType string type RemoteConnectionType string
@ -27,6 +27,8 @@ type RemoteConnectionType string
var ( var (
SSHRemoteConnection RemoteConnectionType = "ssh" SSHRemoteConnection RemoteConnectionType = "ssh"
DefaultIgnitionUserName = "core" DefaultIgnitionUserName = "core"
ErrNoSuchVM = errors.New("VM does not exist")
ErrVMAlreadyExists = errors.New("VM already exists")
) )
type Download struct { type Download struct {

View File

@ -2,6 +2,7 @@ package machine
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
) )
@ -37,10 +38,17 @@ func getNodeGrp(grpName string) NodeGroup {
return NodeGroup{Name: &grpName} return NodeGroup{Name: &grpName}
} }
type DynamicIgnition struct {
Name string
Key string
VMName string
WritePath string
}
// NewIgnitionFile // NewIgnitionFile
func NewIgnitionFile(name, key, writePath string) error { func NewIgnitionFile(ign DynamicIgnition) error {
if len(name) < 1 { if len(ign.Name) < 1 {
name = DefaultIgnitionUserName ign.Name = DefaultIgnitionUserName
} }
ignVersion := Ignition{ ignVersion := Ignition{
Version: "3.2.0", Version: "3.2.0",
@ -48,23 +56,44 @@ func NewIgnitionFile(name, key, writePath string) error {
ignPassword := Passwd{ ignPassword := Passwd{
Users: []PasswdUser{{ Users: []PasswdUser{{
Name: name, Name: ign.Name,
SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(key)}, SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(ign.Key)},
}}, }},
} }
ignStorage := Storage{ ignStorage := Storage{
Directories: getDirs(name), Directories: getDirs(ign.Name),
Files: getFiles(name), Files: getFiles(ign.Name),
Links: getLinks(name), Links: getLinks(ign.Name),
} }
// ready is a unit file that sets up the virtual serial device
// where when the VM is done configuring, it will send an ack
// so a listening host knows it can being interacting with it
ready := `[Unit]
Requires=dev-virtio\\x2dports-%s.device
OnFailure=emergency.target
OnFailureJobMode=isolate
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c '/usr/bin/echo Ready >/dev/%s'
[Install]
RequiredBy=multi-user.target
`
_ = ready
ignSystemd := Systemd{ ignSystemd := Systemd{
Units: []Unit{ Units: []Unit{
{ {
Enabled: boolToPtr(true), Enabled: boolToPtr(true),
Name: "podman.socket", Name: "podman.socket",
}}} },
{
Enabled: boolToPtr(true),
Name: "ready.service",
Contents: strToPtr(fmt.Sprintf(ready, "vport1p1", "vport1p1")),
},
}}
ignConfig := Config{ ignConfig := Config{
Ignition: ignVersion, Ignition: ignVersion,
Passwd: ignPassword, Passwd: ignPassword,
@ -75,7 +104,7 @@ func NewIgnitionFile(name, key, writePath string) error {
if err != nil { if err != nil {
return err return err
} }
return ioutil.WriteFile(writePath, b, 0644) return ioutil.WriteFile(ign.WritePath, b, 0644)
} }
func getDirs(usrName string) []Directory { func getDirs(usrName string) []Directory {

View File

@ -1,9 +1,11 @@
package qemu package qemu
import ( import (
"bufio"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -22,9 +24,6 @@ import (
var ( var (
// vmtype refers to qemu (vs libvirt, krun, etc) // vmtype refers to qemu (vs libvirt, krun, etc)
vmtype = "qemu" 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 initializes an instance of a virtual machine based on the qemu // NewMachine initializes an instance of a virtual machine based on the qemu
@ -89,6 +88,16 @@ func NewMachine(opts machine.InitOptions) (machine.VM, error) {
// Add network // Add network
cmd = append(cmd, "-nic", "user,model=virtio,hostfwd=tcp::"+strconv.Itoa(vm.Port)+"-:22") 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 vm.CmdLine = cmd
return vm, nil return vm, nil
} }
@ -96,13 +105,15 @@ func NewMachine(opts machine.InitOptions) (machine.VM, error) {
// LoadByName reads a json file that describes a known qemu vm // LoadByName reads a json file that describes a known qemu vm
// and returns a vm instance // and returns a vm instance
func LoadVMByName(name string) (machine.VM, error) { func LoadVMByName(name string) (machine.VM, error) {
// TODO need to define an error relating to ErrMachineNotFound
vm := new(MachineVM) vm := new(MachineVM)
vmConfigDir, err := machine.GetConfDir(vmtype) vmConfigDir, err := machine.GetConfDir(vmtype)
if err != nil { if err != nil {
return nil, err return nil, err
} }
b, err := ioutil.ReadFile(filepath.Join(vmConfigDir, name+".json")) b, err := ioutil.ReadFile(filepath.Join(vmConfigDir, name+".json"))
if os.IsNotExist(err) {
return nil, errors.Wrap(machine.ErrNoSuchVM, name)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -159,14 +170,28 @@ func (v *MachineVM) Init(opts machine.InitOptions) error {
if err := v.prepare(); err != nil { if err := v.prepare(); err != nil {
return err return err
} }
// Resize the disk image to input disk size
resize := exec.Command("qemu-img", []string{"resize", v.ImagePath, strconv.Itoa(int(opts.DiskSize)) + "G"}...)
if err := resize.Run(); err != nil {
return errors.Errorf("error resizing image: %q", err)
}
// Write the ignition file // Write the ignition file
return machine.NewIgnitionFile(opts.Username, key, v.IgnitionFilePath) 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 // Start executes the qemu command line and forks it
func (v *MachineVM) Start(name string, _ machine.StartOptions) error { func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
var ( var (
conn net.Conn
err error err error
wait time.Duration = time.Millisecond * 500
) )
attr := new(os.ProcAttr) attr := new(os.ProcAttr)
files := []*os.File{os.Stdin, os.Stdout, os.Stderr} files := []*os.File{os.Stdin, os.Stdout, os.Stderr}
@ -181,6 +206,30 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
} }
_, err = os.StartProcess(v.CmdLine[0], cmd, attr) _, 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 return err
} }