diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index 900f67e2f5..05474fd892 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -8,6 +8,7 @@ import ( "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/machine" "github.com/containers/podman/v3/pkg/machine/qemu" + "github.com/pkg/errors" "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 ( - initOpts = InitCLIOptions{} + initOpts = machine.InitOptions{} defaultMachineName string = "podman-machine-default" ) @@ -53,6 +45,15 @@ func init() { ) _ = 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" flags.Uint64VarP( &initOpts.Memory, @@ -72,28 +73,24 @@ func init() { // TODO should we allow for a users to append to the qemu cmdline? 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 ( vm machine.VM vmType string err error ) + initOpts.Name = defaultMachineName + if len(args) > 0 { + initOpts.Name = args[0] + } switch vmType { 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 { return err } - return vm.Init(vmOpts) + return vm.Init(initOpts) } diff --git a/cmd/podman/machine/remove.go b/cmd/podman/machine/rm.go similarity index 76% rename from cmd/podman/machine/remove.go rename to cmd/podman/machine/rm.go index f6ce9e3269..cd2cc84f22 100644 --- a/cmd/podman/machine/remove.go +++ b/cmd/podman/machine/rm.go @@ -17,13 +17,13 @@ import ( ) var ( - removeCmd = &cobra.Command{ - Use: "remove [options] NAME", + rmCmd = &cobra.Command{ + Use: "rm [options] [NAME]", Short: "Remove an existing machine", Long: "Remove an existing machine ", - RunE: remove, - Args: cobra.ExactArgs(1), - Example: `podman machine remove myvm`, + RunE: rm, + Args: cobra.MaximumNArgs(1), + Example: `podman machine rm myvm`, ValidArgsFunction: completion.AutocompleteNone, } ) @@ -35,13 +35,13 @@ var ( func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, - Command: removeCmd, + Command: rmCmd, Parent: machineCmd, }) - flags := removeCmd.Flags() + flags := rmCmd.Flags() 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" 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") } -func remove(cmd *cobra.Command, args []string) error { +func rm(cmd *cobra.Command, args []string) error { var ( err error vm machine.VM vmType string ) + vmName := defaultMachineName + if len(args) > 0 && len(args[0]) > 0 { + vmName = args[0] + } switch vmType { default: - vm, err = qemu.LoadVMByName(args[0]) + vm, err = qemu.LoadVMByName(vmName) } if err != nil { return err } - confirmationMessage, doIt, err := vm.Remove(args[0], machine.RemoveOptions{}) + confirmationMessage, remove, err := vm.Remove(vmName, machine.RemoveOptions{}) if err != nil { return err } @@ -84,5 +88,5 @@ func remove(cmd *cobra.Command, args []string) error { return nil } } - return doIt() + return remove() } diff --git a/cmd/podman/machine/ssh.go b/cmd/podman/machine/ssh.go index a7111a1958..879122a14f 100644 --- a/cmd/podman/machine/ssh.go +++ b/cmd/podman/machine/ssh.go @@ -14,11 +14,11 @@ import ( var ( sshCmd = &cobra.Command{ - Use: "ssh [options] NAME [COMMAND [ARG ...]]", + Use: "ssh [options] [NAME] [COMMAND [ARG ...]]", Short: "SSH into a virtual machine", Long: "SSH into a virtual machine ", RunE: ssh, - Args: cobra.MinimumNArgs(1), + Args: cobra.MaximumNArgs(1), Example: `podman machine ssh myvm podman machine ssh -e myvm echo hello`, @@ -48,6 +48,10 @@ func ssh(cmd *cobra.Command, args []string) error { vm machine.VM vmType string ) + vmName := defaultMachineName + if len(args) > 0 && len(args[0]) > 1 { + vmName = args[0] + } sshOpts.Args = args[1:] // Error if no execute but args given @@ -61,10 +65,10 @@ func ssh(cmd *cobra.Command, args []string) error { switch vmType { default: - vm, err = qemu.LoadVMByName(args[0]) + vm, err = qemu.LoadVMByName(vmName) } if err != nil { return errors.Wrapf(err, "vm %s not found", args[0]) } - return vm.SSH(args[0], sshOpts) + return vm.SSH(vmName, sshOpts) } diff --git a/cmd/podman/machine/start.go b/cmd/podman/machine/start.go index 44ade2850f..80fd771020 100644 --- a/cmd/podman/machine/start.go +++ b/cmd/podman/machine/start.go @@ -13,11 +13,11 @@ import ( var ( startCmd = &cobra.Command{ - Use: "start NAME", + Use: "start [NAME]", Short: "Start an existing machine", Long: "Start an existing machine ", RunE: start, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Example: `podman machine start myvm`, ValidArgsFunction: completion.AutocompleteNone, } @@ -37,12 +37,16 @@ func start(cmd *cobra.Command, args []string) error { vm machine.VM vmType string ) + vmName := defaultMachineName + if len(args) > 0 && len(args[0]) > 0 { + vmName = args[0] + } switch vmType { default: - vm, err = qemu.LoadVMByName(args[0]) + vm, err = qemu.LoadVMByName(vmName) } if err != nil { return err } - return vm.Start(args[0], machine.StartOptions{}) + return vm.Start(vmName, machine.StartOptions{}) } diff --git a/cmd/podman/machine/stop.go b/cmd/podman/machine/stop.go index 35fd4ff95a..4fcb065a39 100644 --- a/cmd/podman/machine/stop.go +++ b/cmd/podman/machine/stop.go @@ -13,11 +13,11 @@ import ( var ( stopCmd = &cobra.Command{ - Use: "stop NAME", + Use: "stop [NAME]", Short: "Stop an existing machine", Long: "Stop an existing machine ", RunE: stop, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), Example: `podman machine stop myvm`, ValidArgsFunction: completion.AutocompleteNone, } @@ -38,12 +38,16 @@ func stop(cmd *cobra.Command, args []string) error { vm machine.VM vmType string ) + vmName := defaultMachineName + if len(args) > 0 && len(args[0]) > 0 { + vmName = args[0] + } switch vmType { default: - vm, err = qemu.LoadVMByName(args[0]) + vm, err = qemu.LoadVMByName(vmName) } if err != nil { return err } - return vm.Stop(args[0], machine.StopOptions{}) + return vm.Stop(vmName, machine.StopOptions{}) } diff --git a/docs/source/machine.rst b/docs/source/machine.rst index 55df296679..be9ef1e95f 100644 --- a/docs/source/machine.rst +++ b/docs/source/machine.rst @@ -3,7 +3,7 @@ Machine :doc:`init ` Initialize a new virtual machine -:doc:`remove ` Remove a virtual machine +:doc:`rm ` Remove a virtual machine :doc:`ssh ` SSH into a virtual machine :doc:`start ` Start a virtual machine :doc:`stop ` Stop a virtual machine diff --git a/docs/source/markdown/podman-machine-init.1.md b/docs/source/markdown/podman-machine-init.1.md index 5ff07de03d..be07a7bd58 100644 --- a/docs/source/markdown/podman-machine-init.1.md +++ b/docs/source/markdown/podman-machine-init.1.md @@ -22,6 +22,10 @@ tied to the Linux kernel. Number of CPUs. +#### **--disk-size**=*number* + +Size of the disk for the guest VM in GB. + #### **--ignition-path** Fully qualified path of the ignition file diff --git a/docs/source/markdown/podman-machine-remove.1.md b/docs/source/markdown/podman-machine-rm.1.md similarity index 83% rename from docs/source/markdown/podman-machine-remove.1.md rename to docs/source/markdown/podman-machine-rm.1.md index 07763741d0..4da17fdcbe 100644 --- a/docs/source/markdown/podman-machine-remove.1.md +++ b/docs/source/markdown/podman-machine-rm.1.md @@ -1,17 +1,17 @@ -% podman-machine-remove(1) +% podman-machine-rm(1) ## NAME -podman\-machine\-remove - Remove a virtual machine +podman\-machine\-rm - Remove a virtual machine ## SYNOPSIS -**podman machine remove** [*options*] *name* +**podman machine rm** [*options*] [*name*] ## DESCRIPTION Remove a virtual machine and its related files. What is actually deleted depends on the virtual machine type. For all virtual machines, the generated 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` is used. @@ -45,7 +45,7 @@ deleted. Remove a VM named "test1" ``` -$ podman machine remove test1 +$ podman machine rm test1 The following files will be deleted: diff --git a/docs/source/markdown/podman-machine-ssh.1.md b/docs/source/markdown/podman-machine-ssh.1.md index bcecd1010f..01cec1f57c 100644 --- a/docs/source/markdown/podman-machine-ssh.1.md +++ b/docs/source/markdown/podman-machine-ssh.1.md @@ -4,7 +4,7 @@ podman\-machine\-ssh - SSH into a virtual machine ## SYNOPSIS -**podman machine ssh** [*options*] *name* [*command* [*arg* ...]] +**podman machine ssh** [*options*] [*name*] [*command* [*arg* ...]] ## DESCRIPTION diff --git a/docs/source/markdown/podman-machine-start.1.md b/docs/source/markdown/podman-machine-start.1.md index 511296b116..7f3a9f5920 100644 --- a/docs/source/markdown/podman-machine-start.1.md +++ b/docs/source/markdown/podman-machine-start.1.md @@ -4,7 +4,7 @@ podman\-machine\-start - Start a virtual machine ## SYNOPSIS -**podman machine start** *name* +**podman machine start** [*name*] ## DESCRIPTION diff --git a/docs/source/markdown/podman-machine-stop.1.md b/docs/source/markdown/podman-machine-stop.1.md index 62439cbb15..f4be54511a 100644 --- a/docs/source/markdown/podman-machine-stop.1.md +++ b/docs/source/markdown/podman-machine-stop.1.md @@ -4,7 +4,7 @@ podman\-machine\-stop - Stop a virtual machine ## SYNOPSIS -**podman machine stop** *name* +**podman machine stop** [*name*] ## DESCRIPTION diff --git a/docs/source/markdown/podman-machine.1.md b/docs/source/markdown/podman-machine.1.md index 0e3c1ca340..a5d3b78df0 100644 --- a/docs/source/markdown/podman-machine.1.md +++ b/docs/source/markdown/podman-machine.1.md @@ -14,10 +14,10 @@ podman\-machine - Manage Podman's virtual machine | Command | Man Page | Description | | ------- | ------------------------------------------------------- | --------------------------------- | | 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 | -| 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 | -| stop | [podman-machine-stop(1)](podman-machine-stop.1.md) | Stop 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 | +| 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 | ## SEE ALSO podman(1) diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 4933deee84..273deca00f 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -7,19 +7,19 @@ import ( "path/filepath" "github.com/containers/storage/pkg/homedir" + "github.com/pkg/errors" ) type InitOptions struct { - Name string CPUS uint64 - Memory uint64 + DiskSize uint64 IgnitionPath string ImagePath string - Username string - URI url.URL IsDefault bool - //KernelPath string - //Devices []VMDevices + Memory uint64 + Name string + URI url.URL + Username string } type RemoteConnectionType string @@ -27,6 +27,8 @@ type RemoteConnectionType string var ( SSHRemoteConnection RemoteConnectionType = "ssh" DefaultIgnitionUserName = "core" + ErrNoSuchVM = errors.New("VM does not exist") + ErrVMAlreadyExists = errors.New("VM already exists") ) type Download struct { diff --git a/pkg/machine/ignition.go b/pkg/machine/ignition.go index ff79d5afb6..a68d68ac3f 100644 --- a/pkg/machine/ignition.go +++ b/pkg/machine/ignition.go @@ -2,6 +2,7 @@ package machine import ( "encoding/json" + "fmt" "io/ioutil" ) @@ -37,10 +38,17 @@ func getNodeGrp(grpName string) NodeGroup { return NodeGroup{Name: &grpName} } +type DynamicIgnition struct { + Name string + Key string + VMName string + WritePath string +} + // NewIgnitionFile -func NewIgnitionFile(name, key, writePath string) error { - if len(name) < 1 { - name = DefaultIgnitionUserName +func NewIgnitionFile(ign DynamicIgnition) error { + if len(ign.Name) < 1 { + ign.Name = DefaultIgnitionUserName } ignVersion := Ignition{ Version: "3.2.0", @@ -48,23 +56,44 @@ func NewIgnitionFile(name, key, writePath string) error { ignPassword := Passwd{ Users: []PasswdUser{{ - Name: name, - SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(key)}, + Name: ign.Name, + SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(ign.Key)}, }}, } ignStorage := Storage{ - Directories: getDirs(name), - Files: getFiles(name), - Links: getLinks(name), + Directories: getDirs(ign.Name), + Files: getFiles(ign.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{ Units: []Unit{ { Enabled: boolToPtr(true), Name: "podman.socket", - }}} - + }, + { + Enabled: boolToPtr(true), + Name: "ready.service", + Contents: strToPtr(fmt.Sprintf(ready, "vport1p1", "vport1p1")), + }, + }} ignConfig := Config{ Ignition: ignVersion, Passwd: ignPassword, @@ -75,7 +104,7 @@ func NewIgnitionFile(name, key, writePath string) error { if err != nil { return err } - return ioutil.WriteFile(writePath, b, 0644) + return ioutil.WriteFile(ign.WritePath, b, 0644) } func getDirs(usrName string) []Directory { diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index b97eb991a0..fe155750f9 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -1,9 +1,11 @@ package qemu import ( + "bufio" "encoding/json" "fmt" "io/ioutil" + "net" "os" "os/exec" "path/filepath" @@ -22,9 +24,6 @@ import ( 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 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 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 } @@ -96,13 +105,15 @@ func NewMachine(opts machine.InitOptions) (machine.VM, error) { // 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 os.IsNotExist(err) { + return nil, errors.Wrap(machine.ErrNoSuchVM, name) + } if err != nil { return nil, err } @@ -159,14 +170,28 @@ func (v *MachineVM) Init(opts machine.InitOptions) error { if err := v.prepare(); err != nil { 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 - 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 func (v *MachineVM) Start(name string, _ machine.StartOptions) error { var ( - err error + conn net.Conn + err error + wait time.Duration = time.Millisecond * 500 ) attr := new(os.ProcAttr) 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) + 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 }