diff --git a/builder/cloudstack/step_create_instance.go b/builder/cloudstack/step_create_instance.go index 11303f46e..6cc97ae0a 100644 --- a/builder/cloudstack/step_create_instance.go +++ b/builder/cloudstack/step_create_instance.go @@ -18,7 +18,7 @@ import ( // userDataTemplateData represents variables for user_data interpolation type userDataTemplateData struct { HTTPIP string - HTTPPort uint + HTTPPort int } // stepCreateInstance represents a Packer build step that creates CloudStack instances. @@ -86,7 +86,7 @@ func (s *stepCreateInstance) Run(_ context.Context, state multistep.StateBag) mu } if config.UserData != "" { - httpPort := state.Get("http_port").(uint) + httpPort := state.Get("http_port").(int) httpIP, err := hostIP() if err != nil { err := fmt.Errorf("Failed to determine host IP: %s", err) diff --git a/builder/hyperv/common/step_type_boot_command.go b/builder/hyperv/common/step_type_boot_command.go index dc4b27c85..0d296ce8c 100644 --- a/builder/hyperv/common/step_type_boot_command.go +++ b/builder/hyperv/common/step_type_boot_command.go @@ -15,7 +15,7 @@ import ( type bootCommandTemplateData struct { HTTPIP string - HTTPPort uint + HTTPPort int Name string } @@ -29,7 +29,7 @@ type StepTypeBootCommand struct { } func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { - httpPort := state.Get("http_port").(uint) + httpPort := state.Get("http_port").(int) ui := state.Get("ui").(packer.Ui) driver := state.Get("driver").(Driver) vmName := state.Get("vmName").(string) diff --git a/builder/hyperv/iso/builder_test.go b/builder/hyperv/iso/builder_test.go index a79ae4d87..a07ab38bd 100644 --- a/builder/hyperv/iso/builder_test.go +++ b/builder/hyperv/iso/builder_test.go @@ -610,7 +610,7 @@ func TestUserVariablesInBootCommand(t *testing.T) { state.Put("config", &b.config) state.Put("driver", driver) state.Put("hook", hook) - state.Put("http_port", uint(0)) + state.Put("http_port", 0) state.Put("ui", ui) state.Put("vmName", "packer-foo") diff --git a/builder/hyperv/vmcx/builder_test.go b/builder/hyperv/vmcx/builder_test.go index 60c36735b..735d1be8a 100644 --- a/builder/hyperv/vmcx/builder_test.go +++ b/builder/hyperv/vmcx/builder_test.go @@ -511,7 +511,7 @@ func TestUserVariablesInBootCommand(t *testing.T) { state.Put("config", &b.config) state.Put("driver", driver) state.Put("hook", hook) - state.Put("http_port", uint(0)) + state.Put("http_port", 0) state.Put("ui", ui) state.Put("vmName", "packer-foo") diff --git a/builder/parallels/common/step_type_boot_command.go b/builder/parallels/common/step_type_boot_command.go index 4e1e47b20..83abd28c2 100644 --- a/builder/parallels/common/step_type_boot_command.go +++ b/builder/parallels/common/step_type_boot_command.go @@ -14,7 +14,7 @@ import ( type bootCommandTemplateData struct { HTTPIP string - HTTPPort uint + HTTPPort int Name string } @@ -32,7 +32,7 @@ type StepTypeBootCommand struct { // Run types the boot command by sending key scancodes into the VM. func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { debug := state.Get("debug").(bool) - httpPort := state.Get("http_port").(uint) + httpPort := state.Get("http_port").(int) ui := state.Get("ui").(packer.Ui) driver := state.Get("driver").(Driver) diff --git a/builder/parallels/pvm/builder.go b/builder/parallels/pvm/builder.go index 72af59f5c..001801849 100644 --- a/builder/parallels/pvm/builder.go +++ b/builder/parallels/pvm/builder.go @@ -46,7 +46,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { state.Put("driver", driver) state.Put("hook", hook) state.Put("ui", ui) - state.Put("http_port", uint(0)) + state.Put("http_port", 0) // Build the steps. steps := []multistep.Step{ diff --git a/builder/qemu/builder.go b/builder/qemu/builder.go index c30f22f3e..5ca3f555b 100644 --- a/builder/qemu/builder.go +++ b/builder/qemu/builder.go @@ -116,12 +116,12 @@ type Config struct { QemuArgs [][]string `mapstructure:"qemuargs"` QemuBinary string `mapstructure:"qemu_binary"` ShutdownCommand string `mapstructure:"shutdown_command"` - SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` - SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` + SSHHostPortMin int `mapstructure:"ssh_host_port_min"` + SSHHostPortMax int `mapstructure:"ssh_host_port_max"` UseDefaultDisplay bool `mapstructure:"use_default_display"` VNCBindAddress string `mapstructure:"vnc_bind_address"` - VNCPortMin uint `mapstructure:"vnc_port_min"` - VNCPortMax uint `mapstructure:"vnc_port_max"` + VNCPortMin int `mapstructure:"vnc_port_min"` + VNCPortMax int `mapstructure:"vnc_port_max"` VMName string `mapstructure:"vm_name"` // These are deprecated, but we keep them around for BC @@ -337,6 +337,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { errs = packer.MultiErrorAppend( errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max")) } + if b.config.SSHHostPortMin < 0 { + errs = packer.MultiErrorAppend( + errs, errors.New("ssh_host_port_min must be positive")) + } if b.config.VNCPortMin > b.config.VNCPortMax { errs = packer.MultiErrorAppend( diff --git a/builder/qemu/ssh.go b/builder/qemu/ssh.go index ec030df2e..1fe4537bd 100644 --- a/builder/qemu/ssh.go +++ b/builder/qemu/ssh.go @@ -9,6 +9,6 @@ func commHost(state multistep.StateBag) (string, error) { } func commPort(state multistep.StateBag) (int, error) { - sshHostPort := state.Get("sshHostPort").(uint) + sshHostPort := state.Get("sshHostPort").(int) return int(sshHostPort), nil } diff --git a/builder/qemu/step_configure_vnc.go b/builder/qemu/step_configure_vnc.go index 1f1cd088d..465288e7f 100644 --- a/builder/qemu/step_configure_vnc.go +++ b/builder/qemu/step_configure_vnc.go @@ -4,9 +4,8 @@ import ( "context" "fmt" "log" - "math/rand" - "net" + "github.com/hashicorp/packer/common/net" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" ) @@ -18,10 +17,12 @@ import ( // ui packer.Ui // // Produces: -// vnc_port uint - The port that VNC is configured to listen on. -type stepConfigureVNC struct{} +// vnc_port int - The port that VNC is configured to listen on. +type stepConfigureVNC struct { + l *net.Listener +} -func (stepConfigureVNC) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { +func (s *stepConfigureVNC) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) ui := state.Get("ui").(packer.Ui) @@ -31,22 +32,22 @@ func (stepConfigureVNC) Run(_ context.Context, state multistep.StateBag) multist msg := fmt.Sprintf("Looking for available port between %d and %d on %s", config.VNCPortMin, config.VNCPortMax, config.VNCBindAddress) ui.Say(msg) log.Print(msg) - var vncPort uint - portRange := int(config.VNCPortMax - config.VNCPortMin) - for { - if portRange > 0 { - vncPort = uint(rand.Intn(portRange)) + config.VNCPortMin - } else { - vncPort = config.VNCPortMin - } - log.Printf("Trying port: %d", vncPort) - l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", config.VNCBindAddress, vncPort)) - if err == nil { - defer l.Close() - break - } + var err error + s.l, err = net.ListenRangeConfig{ + Addr: config.VNCBindAddress, + Min: config.VNCPortMin, + Max: config.VNCPortMax, + Network: "tcp", + }.Listen(ctx) + if err != nil { + err := fmt.Errorf("Error finding port: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt } + s.l.Listener.Close() // free port, but don't unlock lock file + vncPort := s.l.Port log.Printf("Found available VNC port: %d on IP: %s", vncPort, config.VNCBindAddress) state.Put("vnc_port", vncPort) @@ -55,4 +56,11 @@ func (stepConfigureVNC) Run(_ context.Context, state multistep.StateBag) multist return multistep.ActionContinue } -func (stepConfigureVNC) Cleanup(multistep.StateBag) {} +func (s *stepConfigureVNC) Cleanup(multistep.StateBag) { + if s.l != nil { + err := s.l.Close() + if err != nil { + log.Printf("failed to unlock port lockfile: %v", err) + } + } +} diff --git a/builder/qemu/step_forward_ssh.go b/builder/qemu/step_forward_ssh.go index 6e9efaca1..b525998de 100644 --- a/builder/qemu/step_forward_ssh.go +++ b/builder/qemu/step_forward_ssh.go @@ -4,9 +4,8 @@ import ( "context" "fmt" "log" - "math/rand" - "net" + "github.com/hashicorp/packer/common/net" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" ) @@ -17,31 +16,30 @@ import ( // Uses: // // Produces: -type stepForwardSSH struct{} +type stepForwardSSH struct { + l *net.Listener +} -func (s *stepForwardSSH) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { +func (s *stepForwardSSH) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) ui := state.Get("ui").(packer.Ui) log.Printf("Looking for available communicator (SSH, WinRM, etc) port between %d and %d", config.SSHHostPortMin, config.SSHHostPortMax) - var sshHostPort uint - - portRange := config.SSHHostPortMax - config.SSHHostPortMin + 1 - offset := uint(rand.Intn(int(portRange))) - - for { - sshHostPort = offset + config.SSHHostPortMin - log.Printf("Trying port: %d", sshHostPort) - l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort)) - if err == nil { - defer l.Close() - break - } - offset++ - if offset == portRange { - offset = 0 - } + var err error + s.l, err = net.ListenRangeConfig{ + Addr: config.VNCBindAddress, + Min: config.VNCPortMin, + Max: config.VNCPortMax, + Network: "tcp", + }.Listen(ctx) + if err != nil { + err := fmt.Errorf("Error finding port: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt } + s.l.Listener.Close() // free port, but don't unlock lock file + sshHostPort := s.l.Port ui.Say(fmt.Sprintf("Found port for communicator (SSH, WinRM, etc): %d.", sshHostPort)) // Save the port we're using so that future steps can use it @@ -50,4 +48,11 @@ func (s *stepForwardSSH) Run(_ context.Context, state multistep.StateBag) multis return multistep.ActionContinue } -func (s *stepForwardSSH) Cleanup(state multistep.StateBag) {} +func (s *stepForwardSSH) Cleanup(state multistep.StateBag) { + if s.l != nil { + err := s.l.Close() + if err != nil { + log.Printf("failed to unlock port lockfile: %v", err) + } + } +} diff --git a/builder/qemu/step_run.go b/builder/qemu/step_run.go index c806e10dd..aea087795 100644 --- a/builder/qemu/step_run.go +++ b/builder/qemu/step_run.go @@ -21,11 +21,11 @@ type stepRun struct { type qemuArgsTemplateData struct { HTTPIP string - HTTPPort uint + HTTPPort int HTTPDir string OutputDir string Name string - SSHHostPort uint + SSHHostPort int } func (s *stepRun) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { @@ -63,7 +63,7 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error config := state.Get("config").(*Config) isoPath := state.Get("iso_path").(string) vncIP := state.Get("vnc_ip").(string) - vncPort := state.Get("vnc_port").(uint) + vncPort := state.Get("vnc_port").(int) ui := state.Get("ui").(packer.Ui) driver := state.Get("driver").(Driver) @@ -74,12 +74,12 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error defaultArgs := make(map[string]interface{}) var deviceArgs []string var driveArgs []string - var sshHostPort uint + var sshHostPort int defaultArgs["-name"] = vmName defaultArgs["-machine"] = fmt.Sprintf("type=%s", config.MachineType) if config.Comm.Type != "none" { - sshHostPort = state.Get("sshHostPort").(uint) + sshHostPort = state.Get("sshHostPort").(int) defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0,hostfwd=tcp::%v-:%d", sshHostPort, config.Comm.Port()) } else { defaultArgs["-netdev"] = fmt.Sprintf("user,id=user.0") @@ -121,7 +121,7 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error if vncIpOk && vncPortOk { vncIp := vncIpRaw.(string) - vncPort := vncPortRaw.(uint) + vncPort := vncPortRaw.(int) ui.Message(fmt.Sprintf( "The VM will be run headless, without a GUI. If you want to\n"+ @@ -175,7 +175,7 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error if len(config.QemuArgs) > 0 { ui.Say("Overriding defaults Qemu arguments with QemuArgs...") - httpPort := state.Get("http_port").(uint) + httpPort := state.Get("http_port").(int) ctx := config.ctx if config.Comm.Type != "none" { ctx.Data = qemuArgsTemplateData{ diff --git a/builder/qemu/step_type_boot_command.go b/builder/qemu/step_type_boot_command.go index 86e838d25..b524967cb 100644 --- a/builder/qemu/step_type_boot_command.go +++ b/builder/qemu/step_type_boot_command.go @@ -19,7 +19,7 @@ const KeyLeftShift uint32 = 0xFFE1 type bootCommandTemplateData struct { HTTPIP string - HTTPPort uint + HTTPPort int Name string } @@ -29,7 +29,7 @@ type bootCommandTemplateData struct { // config *config // http_port int // ui packer.Ui -// vnc_port uint +// vnc_port int // // Produces: // @@ -38,9 +38,9 @@ type stepTypeBootCommand struct{} func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) debug := state.Get("debug").(bool) - httpPort := state.Get("http_port").(uint) + httpPort := state.Get("http_port").(int) ui := state.Get("ui").(packer.Ui) - vncPort := state.Get("vnc_port").(uint) + vncPort := state.Get("vnc_port").(int) vncIP := state.Get("vnc_ip").(string) if config.VNCConfig.DisableVNC { diff --git a/builder/virtualbox/common/run_config.go b/builder/virtualbox/common/run_config.go index cf94c306b..e5cae02a6 100644 --- a/builder/virtualbox/common/run_config.go +++ b/builder/virtualbox/common/run_config.go @@ -10,8 +10,8 @@ type RunConfig struct { Headless bool `mapstructure:"headless"` VRDPBindAddress string `mapstructure:"vrdp_bind_address"` - VRDPPortMin uint `mapstructure:"vrdp_port_min"` - VRDPPortMax uint `mapstructure:"vrdp_port_max"` + VRDPPortMin int `mapstructure:"vrdp_port_min"` + VRDPPortMax int `mapstructure:"vrdp_port_max"` } func (c *RunConfig) Prepare(ctx *interpolate.Context) (errs []error) { diff --git a/builder/virtualbox/common/ssh_config.go b/builder/virtualbox/common/ssh_config.go index 3136bca98..10159cc86 100644 --- a/builder/virtualbox/common/ssh_config.go +++ b/builder/virtualbox/common/ssh_config.go @@ -11,8 +11,8 @@ import ( type SSHConfig struct { Comm communicator.Config `mapstructure:",squash"` - SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` - SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` + SSHHostPortMin int `mapstructure:"ssh_host_port_min"` + SSHHostPortMax int `mapstructure:"ssh_host_port_max"` SSHSkipNatMapping bool `mapstructure:"ssh_skip_nat_mapping"` // These are deprecated, but we keep them around for BC diff --git a/builder/virtualbox/common/step_configure_vrdp.go b/builder/virtualbox/common/step_configure_vrdp.go index 6cc960ad3..54bde727b 100644 --- a/builder/virtualbox/common/step_configure_vrdp.go +++ b/builder/virtualbox/common/step_configure_vrdp.go @@ -4,9 +4,8 @@ import ( "context" "fmt" "log" - "math/rand" - "net" + "github.com/hashicorp/packer/common/net" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" ) @@ -23,33 +22,33 @@ import ( // vrdp_port unit - The port that VRDP is configured to listen on. type StepConfigureVRDP struct { VRDPBindAddress string - VRDPPortMin uint - VRDPPortMax uint + VRDPPortMin int + VRDPPortMax int + + l *net.Listener } -func (s *StepConfigureVRDP) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { +func (s *StepConfigureVRDP) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { driver := state.Get("driver").(Driver) ui := state.Get("ui").(packer.Ui) vmName := state.Get("vmName").(string) log.Printf("Looking for available port between %d and %d on %s", s.VRDPPortMin, s.VRDPPortMax, s.VRDPBindAddress) - var vrdpPort uint - portRange := int(s.VRDPPortMax - s.VRDPPortMin) - - for { - if portRange > 0 { - vrdpPort = uint(rand.Intn(portRange)) + s.VRDPPortMin - } else { - vrdpPort = s.VRDPPortMin - } - - log.Printf("Trying port: %d", vrdpPort) - l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.VRDPBindAddress, vrdpPort)) - if err == nil { - defer l.Close() - break - } + var err error + s.l, err = net.ListenRangeConfig{ + Addr: s.VRDPBindAddress, + Min: s.VRDPPortMin, + Max: s.VRDPPortMax, + Network: "tcp", + }.Listen(ctx) + if err != nil { + err := fmt.Errorf("Error finding port: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt } + s.l.Listener.Close() // free port, but don't unlock lock file + vrdpPort := s.l.Port command := []string{ "modifyvm", vmName, @@ -72,4 +71,11 @@ func (s *StepConfigureVRDP) Run(_ context.Context, state multistep.StateBag) mul return multistep.ActionContinue } -func (s *StepConfigureVRDP) Cleanup(state multistep.StateBag) {} +func (s *StepConfigureVRDP) Cleanup(state multistep.StateBag) { + if s.l != nil { + err := s.l.Close() + if err != nil { + log.Printf("failed to unlock port lockfile: %v", err) + } + } +} diff --git a/builder/virtualbox/common/step_forward_ssh.go b/builder/virtualbox/common/step_forward_ssh.go index b4e3b2f25..345b9c55f 100644 --- a/builder/virtualbox/common/step_forward_ssh.go +++ b/builder/virtualbox/common/step_forward_ssh.go @@ -4,9 +4,8 @@ import ( "context" "fmt" "log" - "math/rand" - "net" + "github.com/hashicorp/packer/common/net" "github.com/hashicorp/packer/helper/communicator" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" @@ -23,12 +22,14 @@ import ( // Produces: type StepForwardSSH struct { CommConfig *communicator.Config - HostPortMin uint - HostPortMax uint + HostPortMin int + HostPortMax int SkipNatMapping bool + + l *net.Listener } -func (s *StepForwardSSH) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { +func (s *StepForwardSSH) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { driver := state.Get("driver").(Driver) ui := state.Get("ui").(packer.Ui) vmName := state.Get("vmName").(string) @@ -45,22 +46,21 @@ func (s *StepForwardSSH) Run(_ context.Context, state multistep.StateBag) multis log.Printf("Looking for available communicator (SSH, WinRM, etc) port between %d and %d", s.HostPortMin, s.HostPortMax) - portRange := int(s.HostPortMax - s.HostPortMin + 1) - offset := rand.Intn(portRange) - - for { - sshHostPort = offset + int(s.HostPortMin) - log.Printf("Trying port: %d", sshHostPort) - l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", sshHostPort)) - if err == nil { - defer l.Close() - break - } - offset++ - if offset == portRange { - offset = 0 - } + var err error + s.l, err = net.ListenRangeConfig{ + Addr: "127.0.0.1", + Min: s.HostPortMin, + Max: s.HostPortMax, + Network: "tcp", + }.Listen(ctx) + if err != nil { + err := fmt.Errorf("Error creating port forwarding rule: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt } + s.l.Listener.Close() // free port, but don't unlock lock file + sshHostPort = s.l.Port // Create a forwarded port mapping to the VM ui.Say(fmt.Sprintf("Creating forwarded port mapping for communicator (SSH, WinRM, etc) (host port %d)", sshHostPort)) @@ -83,4 +83,11 @@ func (s *StepForwardSSH) Run(_ context.Context, state multistep.StateBag) multis return multistep.ActionContinue } -func (s *StepForwardSSH) Cleanup(state multistep.StateBag) {} +func (s *StepForwardSSH) Cleanup(state multistep.StateBag) { + if s.l != nil { + err := s.l.Close() + if err != nil { + log.Printf("failed to unlock port lockfile: %v", err) + } + } +} diff --git a/builder/virtualbox/common/step_run.go b/builder/virtualbox/common/step_run.go index 689b58602..d3a3fb66b 100644 --- a/builder/virtualbox/common/step_run.go +++ b/builder/virtualbox/common/step_run.go @@ -35,7 +35,7 @@ func (s *StepRun) Run(_ context.Context, state multistep.StateBag) multistep.Ste if vrdpIpOk && vrdpPortOk { vrdpIp := vrdpIpRaw.(string) - vrdpPort := vrdpPortRaw.(uint) + vrdpPort := vrdpPortRaw.(int) ui.Message(fmt.Sprintf( "The VM will be run headless, without a GUI. If you want to\n"+ diff --git a/builder/virtualbox/common/step_type_boot_command.go b/builder/virtualbox/common/step_type_boot_command.go index fb596cc82..88d92f321 100644 --- a/builder/virtualbox/common/step_type_boot_command.go +++ b/builder/virtualbox/common/step_type_boot_command.go @@ -22,7 +22,7 @@ type bootCommandTemplateData struct { HTTPIP string // HTTPPort is the HTTP server port. - HTTPPort uint + HTTPPort int // Name is the VM's name. Name string @@ -43,7 +43,7 @@ type StepTypeBootCommand struct { func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { debug := state.Get("debug").(bool) driver := state.Get("driver").(Driver) - httpPort := state.Get("http_port").(uint) + httpPort := state.Get("http_port").(int) ui := state.Get("ui").(packer.Ui) vmName := state.Get("vmName").(string) diff --git a/builder/vmware/common/driver_config.go b/builder/vmware/common/driver_config.go index b600ae237..1add0bb10 100644 --- a/builder/vmware/common/driver_config.go +++ b/builder/vmware/common/driver_config.go @@ -20,7 +20,7 @@ type DriverConfig struct { RemoteCacheDatastore string `mapstructure:"remote_cache_datastore"` RemoteCacheDirectory string `mapstructure:"remote_cache_directory"` RemoteHost string `mapstructure:"remote_host"` - RemotePort uint `mapstructure:"remote_port"` + RemotePort int `mapstructure:"remote_port"` RemoteUser string `mapstructure:"remote_username"` RemotePassword string `mapstructure:"remote_password"` RemotePrivateKey string `mapstructure:"remote_private_key_file"` diff --git a/builder/vmware/common/driver_esx5.go b/builder/vmware/common/driver_esx5.go index b82aac7b2..5d8ef49cd 100644 --- a/builder/vmware/common/driver_esx5.go +++ b/builder/vmware/common/driver_esx5.go @@ -3,6 +3,7 @@ package common import ( "bufio" "bytes" + "context" "encoding/csv" "errors" "fmt" @@ -30,7 +31,7 @@ type ESX5Driver struct { base VmwareDriver Host string - Port uint + Port int Username string Password string PrivateKeyFile string @@ -359,8 +360,8 @@ func (d *ESX5Driver) GuestAddress(multistep.StateBag) (string, error) { return result, nil } -func (d *ESX5Driver) VNCAddress(_ string, portMin, portMax uint) (string, uint, error) { - var vncPort uint +func (d *ESX5Driver) VNCAddress(ctx context.Context, _ string, portMin, portMax int) (string, int, error) { + var vncPort int //Process ports ESXi is listening on to determine which are available //This process does best effort to detect ports that are unavailable, @@ -426,7 +427,7 @@ func (d *ESX5Driver) VNCAddress(_ string, portMin, portMax uint) (string, uint, } // UpdateVMX, adds the VNC port to the VMX data. -func (ESX5Driver) UpdateVMX(_, password string, port uint, data map[string]string) { +func (ESX5Driver) UpdateVMX(_, password string, port int, data map[string]string) { // Do not set remotedisplay.vnc.ip - this breaks ESXi. data["remotedisplay.vnc.enabled"] = "TRUE" data["remotedisplay.vnc.port"] = fmt.Sprintf("%d", port) diff --git a/builder/vmware/common/driver_esx5_test.go b/builder/vmware/common/driver_esx5_test.go index 38232bbe7..67b18ee8a 100644 --- a/builder/vmware/common/driver_esx5_test.go +++ b/builder/vmware/common/driver_esx5_test.go @@ -50,7 +50,7 @@ func TestESX5Driver_HostIP(t *testing.T) { port := listen.Addr().(*net.TCPAddr).Port defer listen.Close() - driver := ESX5Driver{Host: "localhost", Port: uint(port)} + driver := ESX5Driver{Host: "localhost", Port: port} state := new(multistep.BasicStateBag) if host, _ := driver.HostIP(state); host != expected_host { diff --git a/builder/vmware/common/run_config.go b/builder/vmware/common/run_config.go index c4bfc3413..da9581427 100644 --- a/builder/vmware/common/run_config.go +++ b/builder/vmware/common/run_config.go @@ -10,8 +10,8 @@ type RunConfig struct { Headless bool `mapstructure:"headless"` VNCBindAddress string `mapstructure:"vnc_bind_address"` - VNCPortMin uint `mapstructure:"vnc_port_min"` - VNCPortMax uint `mapstructure:"vnc_port_max"` + VNCPortMin int `mapstructure:"vnc_port_min"` + VNCPortMax int `mapstructure:"vnc_port_max"` VNCDisablePassword bool `mapstructure:"vnc_disable_password"` } @@ -31,6 +31,9 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) (errs []error) { if c.VNCPortMin > c.VNCPortMax { errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) } + if c.VNCPortMin < 0 { + errs = append(errs, fmt.Errorf("vnc_port_min must be positive")) + } return } diff --git a/builder/vmware/common/step_configure_vnc.go b/builder/vmware/common/step_configure_vnc.go index 54f5ac182..1b426ea63 100644 --- a/builder/vmware/common/step_configure_vnc.go +++ b/builder/vmware/common/step_configure_vnc.go @@ -5,8 +5,8 @@ import ( "fmt" "log" "math/rand" - "net" + "github.com/hashicorp/packer/common/net" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" ) @@ -18,43 +18,38 @@ import ( // vmx_path string // // Produces: -// vnc_port uint - The port that VNC is configured to listen on. +// vnc_port int - The port that VNC is configured to listen on. type StepConfigureVNC struct { Enabled bool VNCBindAddress string - VNCPortMin uint - VNCPortMax uint + VNCPortMin int + VNCPortMax int VNCDisablePassword bool + + l *net.Listener } type VNCAddressFinder interface { - VNCAddress(string, uint, uint) (string, uint, error) + VNCAddress(context.Context, string, int, int) (string, int, error) // UpdateVMX, sets driver specific VNC values to VMX data. - UpdateVMX(vncAddress, vncPassword string, vncPort uint, vmxData map[string]string) + UpdateVMX(vncAddress, vncPassword string, vncPort int, vmxData map[string]string) } -func (StepConfigureVNC) VNCAddress(vncBindAddress string, portMin, portMax uint) (string, uint, error) { - // Find an open VNC port. Note that this can still fail later on - // because we have to release the port at some point. But this does its - // best. - var vncPort uint - portRange := int(portMax - portMin) - for { - if portRange > 0 { - vncPort = uint(rand.Intn(portRange)) + portMin - } else { - vncPort = portMin - } - - log.Printf("Trying port: %d", vncPort) - l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", vncBindAddress, vncPort)) - if err == nil { - defer l.Close() - break - } +func (s *StepConfigureVNC) VNCAddress(ctx context.Context, vncBindAddress string, portMin, portMax int) (string, int, error) { + var err error + s.l, err = net.ListenRangeConfig{ + Addr: s.VNCBindAddress, + Min: s.VNCPortMin, + Max: s.VNCPortMax, + Network: "tcp", + }.Listen(ctx) + if err != nil { + return "", 0, err } - return vncBindAddress, vncPort, nil + + s.l.Listener.Close() // free port, but don't unlock lock file + return s.l.Address, s.l.Port, nil } func VNCPassword(skipPassword bool) string { @@ -75,7 +70,7 @@ func VNCPassword(skipPassword bool) string { return string(password) } -func (s *StepConfigureVNC) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { +func (s *StepConfigureVNC) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { if !s.Enabled { log.Println("Skipping VNC configuration step...") return multistep.ActionContinue @@ -99,8 +94,10 @@ func (s *StepConfigureVNC) Run(_ context.Context, state multistep.StateBag) mult } else { vncFinder = s } + log.Printf("Looking for available port between %d and %d", s.VNCPortMin, s.VNCPortMax) - vncBindAddress, vncPort, err := vncFinder.VNCAddress(s.VNCBindAddress, s.VNCPortMin, s.VNCPortMax) + vncBindAddress, vncPort, err := vncFinder.VNCAddress(ctx, s.VNCBindAddress, s.VNCPortMin, s.VNCPortMax) + if err != nil { state.Put("error", err) ui.Error(err.Error()) @@ -109,7 +106,7 @@ func (s *StepConfigureVNC) Run(_ context.Context, state multistep.StateBag) mult vncPassword := VNCPassword(s.VNCDisablePassword) - log.Printf("Found available VNC port: %d", vncPort) + log.Printf("Found available VNC port: %v", s.l) vncFinder.UpdateVMX(vncBindAddress, vncPassword, vncPort, vmxData) @@ -120,14 +117,14 @@ func (s *StepConfigureVNC) Run(_ context.Context, state multistep.StateBag) mult return multistep.ActionHalt } - state.Put("vnc_port", vncPort) - state.Put("vnc_ip", vncBindAddress) + state.Put("vnc_port", s.l.Port) + state.Put("vnc_ip", s.l.Address) state.Put("vnc_password", vncPassword) return multistep.ActionContinue } -func (StepConfigureVNC) UpdateVMX(address, password string, port uint, data map[string]string) { +func (*StepConfigureVNC) UpdateVMX(address, password string, port int, data map[string]string) { data["remotedisplay.vnc.enabled"] = "TRUE" data["remotedisplay.vnc.port"] = fmt.Sprintf("%d", port) data["remotedisplay.vnc.ip"] = address @@ -136,5 +133,10 @@ func (StepConfigureVNC) UpdateVMX(address, password string, port uint, data map[ } } -func (StepConfigureVNC) Cleanup(multistep.StateBag) { +func (s *StepConfigureVNC) Cleanup(multistep.StateBag) { + if s.l != nil { + if err := s.l.Close(); err != nil { + log.Printf("failed to unlock port lockfile: %v", err) + } + } } diff --git a/builder/vmware/common/step_run.go b/builder/vmware/common/step_run.go index 2dad6faed..953d8ea5a 100644 --- a/builder/vmware/common/step_run.go +++ b/builder/vmware/common/step_run.go @@ -43,7 +43,7 @@ func (s *StepRun) Run(_ context.Context, state multistep.StateBag) multistep.Ste if vncIpOk && vncPortOk && vncPasswordOk { vncIp := vncIpRaw.(string) - vncPort := vncPortRaw.(uint) + vncPort := vncPortRaw.(int) vncPassword := vncPasswordRaw.(string) ui.Message(fmt.Sprintf( diff --git a/builder/vmware/common/step_type_boot_command.go b/builder/vmware/common/step_type_boot_command.go index b241d57ba..ad615f6a4 100644 --- a/builder/vmware/common/step_type_boot_command.go +++ b/builder/vmware/common/step_type_boot_command.go @@ -20,7 +20,7 @@ import ( // Uses: // http_port int // ui packer.Ui -// vnc_port uint +// vnc_port int // // Produces: // @@ -34,7 +34,7 @@ type StepTypeBootCommand struct { } type bootCommandTemplateData struct { HTTPIP string - HTTPPort uint + HTTPPort int Name string } @@ -46,10 +46,10 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) debug := state.Get("debug").(bool) driver := state.Get("driver").(Driver) - httpPort := state.Get("http_port").(uint) + httpPort := state.Get("http_port").(int) ui := state.Get("ui").(packer.Ui) vncIp := state.Get("vnc_ip").(string) - vncPort := state.Get("vnc_port").(uint) + vncPort := state.Get("vnc_port").(int) vncPassword := state.Get("vnc_password") // Wait the for the vm to boot. diff --git a/common/http_config.go b/common/http_config.go index bb9b46b26..8a6809ac4 100644 --- a/common/http_config.go +++ b/common/http_config.go @@ -9,8 +9,8 @@ import ( // HTTPConfig contains configuration for the local HTTP Server type HTTPConfig struct { HTTPDir string `mapstructure:"http_directory"` - HTTPPortMin uint `mapstructure:"http_port_min"` - HTTPPortMax uint `mapstructure:"http_port_max"` + HTTPPortMin int `mapstructure:"http_port_min"` + HTTPPortMax int `mapstructure:"http_port_max"` } func (c *HTTPConfig) Prepare(ctx *interpolate.Context) []error { diff --git a/common/http_config_test.go b/common/http_config_test.go index eb42148b8..02b800845 100644 --- a/common/http_config_test.go +++ b/common/http_config_test.go @@ -24,11 +24,11 @@ func TestHTTPConfigPrepare_Bounds(t *testing.T) { if err != nil { t.Fatalf("should not have error: %s", err) } - portMin := uint(8000) + portMin := 8000 if h.HTTPPortMin != portMin { t.Fatalf("HTTPPortMin: expected %d got %d", portMin, h.HTTPPortMin) } - portMax := uint(9000) + portMax := 9000 if h.HTTPPortMax != portMax { t.Fatalf("HTTPPortMax: expected %d got %d", portMax, h.HTTPPortMax) } diff --git a/common/net/configure_port.go b/common/net/configure_port.go new file mode 100644 index 000000000..dbce7ee8f --- /dev/null +++ b/common/net/configure_port.go @@ -0,0 +1,99 @@ +package net + +import ( + "context" + "fmt" + "log" + "math/rand" + "net" + "strconv" + + "github.com/gofrs/flock" + + "github.com/hashicorp/packer/packer" +) + +var _ net.Listener = &Listener{} + +// Listener wraps a net.Lister with some magic packer capabilies. For example +// until you call Listener.Close, any call to ListenRangeConfig.Listen cannot +// bind to Port. Packer tries tells moving parts which port they can use, but +// often the port has to be released before a 3rd party is started, like a VNC +// server. +type Listener struct { + // Listener can be closed but Port will be file locked by packer until + // Close is called. + net.Listener + Port int + Address string + lock *flock.Flock +} + +func (l *Listener) Close() error { + err := l.lock.Unlock() + if err != nil { + log.Printf("cannot unlock lockfile %#v: %v", l, err) + } + return l.Listener.Close() +} + +// ListenRangeConfig contains options for listening to a free address [Min,Max) +// range. ListenRangeConfig wraps a net.ListenConfig. +type ListenRangeConfig struct { + // tcp", "udp" + Network string + Addr string + Min, Max int + net.ListenConfig +} + +// Listen tries to Listen to a random open TCP port in the [min, max) range +// until ctx is cancelled. +// Listen uses net.ListenConfig.Listen internally. +func (lc ListenRangeConfig) Listen(ctx context.Context) (*Listener, error) { + portRange := lc.Max - lc.Min + for { + if err := ctx.Err(); err != nil { + return nil, err + } + + port := lc.Min + if portRange > 0 { + port += rand.Intn(portRange) + } + + log.Printf("Trying port: %d", port) + + lockFilePath, err := packer.CachePath("port", strconv.Itoa(port)) + if err != nil { + return nil, err + } + + lock := flock.New(lockFilePath) + locked, err := lock.TryLock() + if err != nil { + return nil, err + } + if !locked { + continue // this port seems to be locked by another packer goroutine + } + + l, err := lc.ListenConfig.Listen(ctx, lc.Network, fmt.Sprintf("%s:%d", lc.Addr, port)) + if err != nil { + if err := lock.Unlock(); err != nil { + log.Printf("Could not unlock file lock for port %d: %v", port, err) + } + + continue // this port is most likely already open + } + + log.Printf("Found available port: %d on IP: %s", port, lc.Addr) + return &Listener{ + Address: lc.Addr, + Port: port, + Listener: l, + lock: lock, + }, err + + } +} diff --git a/common/step_http_server.go b/common/step_http_server.go index 8e9e58983..b78abc657 100644 --- a/common/step_http_server.go +++ b/common/step_http_server.go @@ -3,11 +3,10 @@ package common import ( "context" "fmt" - "log" - "math/rand" - "net" + "net/http" + "github.com/hashicorp/packer/common/net" "github.com/hashicorp/packer/helper/common" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" @@ -24,44 +23,38 @@ import ( // http_port int - The port the HTTP server started on. type StepHTTPServer struct { HTTPDir string - HTTPPortMin uint - HTTPPortMax uint + HTTPPortMin int + HTTPPortMax int - l net.Listener + l *net.Listener } -func (s *StepHTTPServer) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { +func (s *StepHTTPServer) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) - var httpPort uint = 0 if s.HTTPDir == "" { - state.Put("http_port", httpPort) + state.Put("http_port", 0) return multistep.ActionContinue } // Find an available TCP port for our HTTP server var httpAddr string - portRange := int(s.HTTPPortMax - s.HTTPPortMin) - for { - var err error - var offset uint = 0 + var err error + s.l, err = net.ListenRangeConfig{ + Min: s.HTTPPortMin, + Max: s.HTTPPortMax, + Addr: "0.0.0.0", + Network: "tcp", + }.Listen(ctx) - if portRange > 0 { - // Intn will panic if portRange == 0, so we do a check. - // Intn is from [0, n), so add 1 to make from [0, n] - offset = uint(rand.Intn(portRange + 1)) - } - - httpPort = offset + s.HTTPPortMin - httpAddr = fmt.Sprintf("0.0.0.0:%d", httpPort) - log.Printf("Trying port: %d", httpPort) - s.l, err = net.Listen("tcp", httpAddr) - if err == nil { - break - } + if err != nil { + err := fmt.Errorf("Error finding port: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt } - ui.Say(fmt.Sprintf("Starting HTTP server on port %d", httpPort)) + ui.Say(fmt.Sprintf("Starting HTTP server on port %d", s.l.Port)) // Start the HTTP server and run it in the background fileServer := http.FileServer(http.Dir(s.HTTPDir)) @@ -69,8 +62,8 @@ func (s *StepHTTPServer) Run(_ context.Context, state multistep.StateBag) multis go server.Serve(s.l) // Save the address into the state so it can be accessed in the future - state.Put("http_port", httpPort) - SetHTTPPort(fmt.Sprintf("%d", httpPort)) + state.Put("http_port", s.l.Port) + SetHTTPPort(fmt.Sprintf("%d", s.l.Port)) return multistep.ActionContinue } diff --git a/config.go b/config.go index 409b8f12d..1b7946b24 100644 --- a/config.go +++ b/config.go @@ -24,8 +24,8 @@ const PACKERSPACE = "-PACKERSPACE-" type config struct { DisableCheckpoint bool `json:"disable_checkpoint"` DisableCheckpointSignature bool `json:"disable_checkpoint_signature"` - PluginMinPort uint - PluginMaxPort uint + PluginMinPort int + PluginMaxPort int Builders map[string]string PostProcessors map[string]string `json:"post-processors"` diff --git a/helper/communicator/step_connect_ssh.go b/helper/communicator/step_connect_ssh.go index 37db32eae..2eb975b99 100644 --- a/helper/communicator/step_connect_ssh.go +++ b/helper/communicator/step_connect_ssh.go @@ -186,7 +186,8 @@ func (s *StepConnectSSH) waitForSSH(state multistep.StateBag, cancel <-chan stru Timeout: s.Config.SSHReadWriteTimeout, } - log.Println("[INFO] Attempting SSH connection...") + log.Printf("[INFO] Attempting SSH connection to %s...", address) + log.Printf("[DEBUG] Config to %#v...", config) comm, err = ssh.New(address, config) if err != nil { log.Printf("[DEBUG] SSH handshake err: %s", err) diff --git a/packer/cache.go b/packer/cache.go index f878ab565..958a2367b 100644 --- a/packer/cache.go +++ b/packer/cache.go @@ -11,6 +11,7 @@ var DefaultCacheDir = "packer_cache" // // When the directory is not absolute, CachePath will try to get // current working directory to be able to return a full path. +// CachePath tries to create the resulting path if it doesn't exist. // // CachePath can error in case it cannot find the cwd. // @@ -19,7 +20,11 @@ var DefaultCacheDir = "packer_cache" // PACKER_CACHE_DIR="" CacheDir("foo") => "./packer_cache/foo // PACKER_CACHE_DIR="bar" CacheDir("foo") => "./bar/foo // PACKER_CACHE_DIR="/home/there" CacheDir("foo", "bar") => "/home/there/foo/bar -func CachePath(paths ...string) (string, error) { +func CachePath(paths ...string) (path string, err error) { + defer func() { + // create the dir based on return path it it doesn't exist + os.MkdirAll(filepath.Base(path), os.ModePerm) + }() cacheDir := DefaultCacheDir if cd := os.Getenv("PACKER_CACHE_DIR"); cd != "" { cacheDir = cd diff --git a/packer/plugin/client.go b/packer/plugin/client.go index edcab1d7a..7cfcb8e21 100644 --- a/packer/plugin/client.go +++ b/packer/plugin/client.go @@ -56,7 +56,7 @@ type ClientConfig struct { // The minimum and maximum port to use for communicating with // the subprocess. If not set, this defaults to 10,000 and 25,000 // respectively. - MinPort, MaxPort uint + MinPort, MaxPort int // StartTimeout is the timeout to wait for the plugin to say it // has started successfully. diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go index e836eb894..9b28c2bbb 100644 --- a/provisioner/ansible/provisioner.go +++ b/provisioner/ansible/provisioner.go @@ -52,7 +52,7 @@ type Config struct { EmptyGroups []string `mapstructure:"empty_groups"` HostAlias string `mapstructure:"host_alias"` User string `mapstructure:"user"` - LocalPort uint `mapstructure:"local_port"` + LocalPort int `mapstructure:"local_port"` SSHHostKeyFile string `mapstructure:"ssh_host_key_file"` SSHAuthorizedKeyFile string `mapstructure:"ssh_authorized_key_file"` SFTPCmd string `mapstructure:"sftp_command"` @@ -271,12 +271,11 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { ui.Say(err.Error()) continue } - portUint64, err := strconv.ParseUint(portStr, 10, 0) + p.config.LocalPort, err = strconv.Atoi(portStr) if err != nil { ui.Say(err.Error()) continue } - p.config.LocalPort = uint(portUint64) return l, nil } return nil, errors.New("Error setting up SSH proxy connection") diff --git a/provisioner/ansible/provisioner_test.go b/provisioner/ansible/provisioner_test.go index 099f72bcc..7905f5600 100644 --- a/provisioner/ansible/provisioner_test.go +++ b/provisioner/ansible/provisioner_test.go @@ -245,13 +245,13 @@ func TestProvisionerPrepare_LocalPort(t *testing.T) { config["ssh_authorized_key_file"] = publickey_file.Name() config["playbook_file"] = playbook_file.Name() - config["local_port"] = uint(65537) + config["local_port"] = 65537 err = p.Prepare(config) if err == nil { t.Fatal("should have error") } - config["local_port"] = uint(22222) + config["local_port"] = 22222 err = p.Prepare(config) if err != nil { t.Fatalf("err: %s", err) diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go index fab92ee49..ae8048f05 100644 --- a/provisioner/inspec/provisioner.go +++ b/provisioner/inspec/provisioner.go @@ -52,7 +52,7 @@ type Config struct { Backend string `mapstructure:"backend"` User string `mapstructure:"user"` Host string `mapstructure:"host"` - LocalPort uint `mapstructure:"local_port"` + LocalPort int `mapstructure:"local_port"` SSHHostKeyFile string `mapstructure:"ssh_host_key_file"` SSHAuthorizedKeyFile string `mapstructure:"ssh_authorized_key_file"` } @@ -264,12 +264,11 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { ui.Say(err.Error()) continue } - portUint64, err := strconv.ParseUint(portStr, 10, 0) + p.config.LocalPort, err = strconv.Atoi(portStr) if err != nil { ui.Say(err.Error()) continue } - p.config.LocalPort = uint(portUint64) return l, nil } return nil, errors.New("Error setting up SSH proxy connection") @@ -338,7 +337,7 @@ func (p *Provisioner) executeInspec(ui packer.Ui, comm packer.Communicator, priv args = append(args, "--key-files", privKeyFile) } args = append(args, "--user", p.config.User) - args = append(args, "--port", strconv.FormatUint(uint64(p.config.LocalPort), 10)) + args = append(args, "--port", strconv.Itoa(p.config.LocalPort)) } args = append(args, "--attrs") diff --git a/provisioner/inspec/provisioner_test.go b/provisioner/inspec/provisioner_test.go index 6857f7004..f814768a9 100644 --- a/provisioner/inspec/provisioner_test.go +++ b/provisioner/inspec/provisioner_test.go @@ -254,13 +254,13 @@ func TestProvisionerPrepare_LocalPort(t *testing.T) { config["ssh_authorized_key_file"] = publickey_file.Name() config["profile"] = profile_file.Name() - config["local_port"] = uint(65537) + config["local_port"] = 65537 err = p.Prepare(config) if err == nil { t.Fatal("should have error") } - config["local_port"] = uint(22222) + config["local_port"] = 22222 err = p.Prepare(config) if err != nil { t.Fatalf("err: %s", err)