summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOpenShift Merge Robot <openshift-merge-robot@users.noreply.github.com>2021-03-28 10:55:45 +0000
committerGitHub <noreply@github.com>2021-03-28 10:55:45 +0000
commitb2e7a3e45c89132e41b0d864dbc4eacacbecf08d (patch)
treef990f0b9b079cecce73f0730ffc2c6a1fe2bec82
parent4831d4134606699615454caa1ee75e7d37ee778b (diff)
parent7a79f708a4521ba7c42da83a204a01ace010ace3 (diff)
downloadpodman-b2e7a3e45c89132e41b0d864dbc4eacacbecf08d.tar.gz
podman-b2e7a3e45c89132e41b0d864dbc4eacacbecf08d.tar.bz2
podman-b2e7a3e45c89132e41b0d864dbc4eacacbecf08d.zip
Merge pull request #9836 from baude/vmcreateresize
Podman machine enhancements
-rw-r--r--cmd/podman/machine/init.go43
-rw-r--r--cmd/podman/machine/rm.go (renamed from cmd/podman/machine/remove.go)28
-rw-r--r--cmd/podman/machine/ssh.go12
-rw-r--r--cmd/podman/machine/start.go12
-rw-r--r--cmd/podman/machine/stop.go12
-rw-r--r--docs/source/machine.rst2
-rw-r--r--docs/source/markdown/podman-machine-init.1.md4
-rw-r--r--docs/source/markdown/podman-machine-rm.1.md (renamed from docs/source/markdown/podman-machine-remove.1.md)10
-rw-r--r--docs/source/markdown/podman-machine-ssh.1.md2
-rw-r--r--docs/source/markdown/podman-machine-start.1.md2
-rw-r--r--docs/source/markdown/podman-machine-stop.1.md2
-rw-r--r--docs/source/markdown/podman-machine.1.md8
-rw-r--r--pkg/machine/config.go14
-rw-r--r--pkg/machine/ignition.go51
-rw-r--r--pkg/machine/qemu/machine.go61
15 files changed, 180 insertions, 83 deletions
diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go
index 900f67e2f..05474fd89 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
index f6ce9e326..cd2cc84f2 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 a7111a195..879122a14 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 44ade2850..80fd77102 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 35fd4ff95..4fcb065a3 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 55df29667..be9ef1e95 100644
--- a/docs/source/machine.rst
+++ b/docs/source/machine.rst
@@ -3,7 +3,7 @@ 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:`start <markdown/podman-machine-start.1>` Start a virtual machine
:doc:`stop <markdown/podman-machine-stop.1>` 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 5ff07de03..be07a7bd5 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
index 07763741d..4da17fdcb 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 bcecd1010..01cec1f57 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 511296b11..7f3a9f592 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 62439cbb1..f4be54511 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 0e3c1ca34..a5d3b78df 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 4933deee8..273deca00 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 ff79d5afb..a68d68ac3 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 b97eb991a..fe155750f 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
}