builder/vmware: make things more Go-like

This commit currently breaks the builder though, since the ISo is now
uploaded back into ESX.
This commit is contained in:
Mitchell Hashimoto 2013-11-07 12:00:27 -08:00
parent a828a9a064
commit 483cda18c1
9 changed files with 190 additions and 237 deletions

View File

@ -138,6 +138,18 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName) b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName)
} }
if b.config.RemoteUser == "" {
b.config.RemoteUser = "root"
}
if b.config.RemoteDatastore == "" {
b.config.RemoteDatastore = "datastore1"
}
if b.config.RemotePort == 0 {
b.config.RemotePort = 22
}
if b.config.SSHPort == 0 { if b.config.SSHPort == 0 {
b.config.SSHPort = 22 b.config.SSHPort = 22
} }
@ -165,6 +177,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
"shutdown_timeout": &b.config.RawShutdownTimeout, "shutdown_timeout": &b.config.RawShutdownTimeout,
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout, "ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
"vmx_template_path": &b.config.VMXTemplatePath, "vmx_template_path": &b.config.VMXTemplatePath,
"remote_type": &b.config.RemoteType,
"remote_host": &b.config.RemoteHost, "remote_host": &b.config.RemoteHost,
"remote_datastore": &b.config.RemoteDatastore, "remote_datastore": &b.config.RemoteDatastore,
"remote_user": &b.config.RemoteUser, "remote_user": &b.config.RemoteUser,
@ -338,6 +351,14 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
} }
// Remote configuration validation
if b.config.RemoteType != "" {
if b.config.RemoteHost == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("remote_host must be specified"))
}
}
// Warnings // Warnings
if b.config.ShutdownCommand == "" { if b.config.ShutdownCommand == "" {
warnings = append(warnings, warnings = append(warnings,
@ -353,20 +374,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
} }
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
// Initialize the driver that will handle our interaction with VMware driver, err := NewDriver(&b.config)
var driver Driver
var err error
var sshAddressFunc func(multistep.StateBag) (string, error) = sshAddress
var downloadFunc func(*common.DownloadConfig, multistep.StateBag) (string, error, bool)
if b.config.RemoteType == "" {
driver, err = NewDriver()
} else {
driver, err = NewRemoteDriver(&b.config)
sshAddressFunc = driver.(RemoteDriver).SSHAddress()
downloadFunc = driver.(RemoteDriver).Download()
}
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed creating VMware driver: %s", err) return nil, fmt.Errorf("Failed creating VMware driver: %s", err)
} }
@ -382,7 +390,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Description: "ISO", Description: "ISO",
ResultKey: "iso_path", ResultKey: "iso_path",
Url: b.config.ISOUrls, Url: b.config.ISOUrls,
Download: downloadFunc,
}, },
&stepPrepareOutputDir{}, &stepPrepareOutputDir{},
&common.StepCreateFloppy{ &common.StepCreateFloppy{
@ -395,7 +402,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&stepRun{}, &stepRun{},
&stepTypeBootCommand{}, &stepTypeBootCommand{},
&common.StepConnectSSH{ &common.StepConnectSSH{
SSHAddress: sshAddressFunc, SSHAddress: driver.SSHAddress,
SSHConfig: sshConfig, SSHConfig: sshConfig,
SSHWaitTimeout: b.config.sshWaitTimeout, SSHWaitTimeout: b.config.sshWaitTimeout,
NoPty: b.config.SSHSkipRequestPty, NoPty: b.config.SSHSkipRequestPty,

View File

@ -3,6 +3,7 @@ package vmware
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/mitchellh/multistep"
"log" "log"
"os/exec" "os/exec"
"runtime" "runtime"
@ -20,6 +21,10 @@ type Driver interface {
// Checks if the VMX file at the given path is running. // Checks if the VMX file at the given path is running.
IsRunning(string) (bool, error) IsRunning(string) (bool, error)
// SSHAddress returns the SSH address for the VM that is being
// managed by this driver.
SSHAddress(multistep.StateBag) (string, error)
// Start starts a VM specified by the path to the VMX given. // Start starts a VM specified by the path to the VMX given.
Start(string, bool) error Start(string, bool) error
@ -41,9 +46,20 @@ type Driver interface {
// NewDriver returns a new driver implementation for this operating // NewDriver returns a new driver implementation for this operating
// system, or an error if the driver couldn't be initialized. // system, or an error if the driver couldn't be initialized.
func NewDriver() (Driver, error) { func NewDriver(config *config) (Driver, error) {
drivers := []Driver{} drivers := []Driver{}
if config.RemoteType != "" {
drivers = []Driver{
&ESX5Driver{
Host: config.RemoteHost,
Port: config.RemotePort,
Username: config.RemoteUser,
Password: config.RemotePassword,
Datastore: config.RemoteDatastore,
},
}
} else {
switch runtime.GOOS { switch runtime.GOOS {
case "darwin": case "darwin":
drivers = []Driver{ drivers = []Driver{
@ -63,6 +79,7 @@ func NewDriver() (Driver, error) {
default: default:
return nil, fmt.Errorf("can't find driver for OS: %s", runtime.GOOS) return nil, fmt.Errorf("can't find driver for OS: %s", runtime.GOOS)
} }
}
errs := "" errs := ""
for _, driver := range drivers { for _, driver := range drivers {

View File

@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/communicator/ssh" "github.com/mitchellh/packer/communicator/ssh"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"io" "io"
@ -21,8 +20,13 @@ import (
) )
type ESX5Driver struct { type ESX5Driver struct {
Host string
Port uint
Username string
Password string
Datastore string
comm packer.Communicator comm packer.Communicator
config *config
} }
func (d *ESX5Driver) CompactDisk(diskPathLocal string) error { func (d *ESX5Driver) CompactDisk(diskPathLocal string) error {
@ -76,12 +80,10 @@ func (d *ESX5Driver) Verify() error {
d.connect, d.connect,
d.checkSystemVersion, d.checkSystemVersion,
d.checkGuestIPHackEnabled, d.checkGuestIPHackEnabled,
d.checkOutputFolder,
} }
for _, check := range checks { for _, check := range checks {
err := check() if err := check(); err != nil {
if err != nil {
return err return err
} }
} }
@ -90,7 +92,7 @@ func (d *ESX5Driver) Verify() error {
} }
func (d *ESX5Driver) HostIP() (string, error) { func (d *ESX5Driver) HostIP() (string, error) {
ip := net.ParseIP(d.config.RemoteHost) ip := net.ParseIP(d.Host)
interfaces, err := net.Interfaces() interfaces, err := net.Interfaces()
if err != nil { if err != nil {
return "", err return "", err
@ -117,7 +119,7 @@ func (d *ESX5Driver) VNCAddress(portMin, portMax uint) (string, uint) {
var vncPort uint var vncPort uint
// TODO(dougm) use esxcli network ip connection list // TODO(dougm) use esxcli network ip connection list
for port := portMin; port <= portMax; port++ { for port := portMin; port <= portMax; port++ {
address := fmt.Sprintf("%s:%d", d.config.RemoteHost, port) address := fmt.Sprintf("%s:%d", d.Host, port)
log.Printf("Trying address: %s...", address) log.Printf("Trying address: %s...", address)
l, err := net.DialTimeout("tcp", address, 1*time.Second) l, err := net.DialTimeout("tcp", address, 1*time.Second)
@ -135,23 +137,79 @@ func (d *ESX5Driver) VNCAddress(portMin, portMax uint) (string, uint) {
} }
} }
return d.config.RemoteHost, vncPort return d.Host, vncPort
} }
func (d *ESX5Driver) SSHAddress() func(multistep.StateBag) (string, error) { func (d *ESX5Driver) SSHAddress(state multistep.StateBag) (string, error) {
return d.sshAddress config := state.Get("config").(*config)
if address, ok := state.GetOk("vm_address"); ok {
return address.(string), nil
} }
func (d *ESX5Driver) Download() func(*common.DownloadConfig, multistep.StateBag) (string, error, bool) { r, err := d.esxcli("network", "vm", "list")
return d.download
}
func (d *ESX5Driver) FileExists(path string) bool {
err := d.sh("test", "-e", d.datastorePath(path))
if err != nil { if err != nil {
return false return "", err
} }
return true
record, err := r.find("Name", config.VMName)
if err != nil {
return "", err
}
wid := record["WorldID"]
if wid == "" {
return "", errors.New("VM WorldID not found")
}
r, err = d.esxcli("network", "vm", "port", "list", "-w", wid)
if err != nil {
return "", err
}
record, err = r.read()
if err != nil {
return "", err
}
if record["IPAddress"] == "0.0.0.0" {
return "", errors.New("VM network port found, but no IP address")
}
address := fmt.Sprintf("%s:%d", record["IPAddress"], config.SSHPort)
state.Put("vm_address", address)
return address, nil
}
/*
func (d *ESX5Driver) Download(*common.DownloadConfig, multistep.StateBag) (string, error, bool) {
config := state.Get("config").(*config)
cacheRoot, _ := filepath.Abs(".")
targetFile, err := filepath.Rel(cacheRoot, dconfig.TargetPath)
if err != nil {
return "", err, false
}
if err := d.MkdirAll(filepath.Dir(targetFile)); err != nil {
return "", err, false
}
path := d.datastorePath(targetFile)
if d.verifyChecksum(config.ISOChecksumType, config.ISOChecksum, path) {
log.Println("Initial checksum matched, no download needed.")
return path, nil, true
}
// TODO(dougm) progress and handle interrupt
err = d.sh("wget", dconfig.Url, "-O", path)
return path, err, true
}
*/
func (d *ESX5Driver) DirExists(path string) (bool, error) {
err := d.sh("test", "-e", d.datastorePath(path))
return err == nil, err
} }
func (d *ESX5Driver) MkdirAll(path string) error { func (d *ESX5Driver) MkdirAll(path string) error {
@ -162,32 +220,24 @@ func (d *ESX5Driver) RemoveAll(path string) error {
return d.sh("rm", "-rf", d.datastorePath(path)) return d.sh("rm", "-rf", d.datastorePath(path))
} }
func (d *ESX5Driver) DirType() string {
return "datastore"
}
func (d *ESX5Driver) datastorePath(path string) string { func (d *ESX5Driver) datastorePath(path string) string {
return filepath.Join("/vmfs/volumes", d.config.RemoteDatastore, path) return filepath.Join("/vmfs/volumes", d.Datastore, path)
} }
func (d *ESX5Driver) connect() error { func (d *ESX5Driver) connect() error {
if d.config.RemoteHost == "" { address := fmt.Sprintf("%s:%d", d.Host, d.Port)
return errors.New("A remote_host must be specified.")
}
if d.config.RemotePort == 0 {
d.config.RemotePort = 22
}
address := fmt.Sprintf("%s:%d", d.config.RemoteHost, d.config.RemotePort)
auth := []gossh.ClientAuth{ auth := []gossh.ClientAuth{
gossh.ClientAuthPassword(ssh.Password(d.config.RemotePassword)), gossh.ClientAuthPassword(ssh.Password(d.Password)),
gossh.ClientAuthKeyboardInteractive( gossh.ClientAuthKeyboardInteractive(
ssh.PasswordKeyboardInteractive(d.config.RemotePassword)), ssh.PasswordKeyboardInteractive(d.Password)),
} }
// TODO(dougm) KeyPath support // TODO(dougm) KeyPath support
sshConfig := &ssh.Config{ sshConfig := &ssh.Config{
Connection: ssh.ConnectFunc("tcp", address), Connection: ssh.ConnectFunc("tcp", address),
SSHConfig: &gossh.ClientConfig{ SSHConfig: &gossh.ClientConfig{
User: d.config.RemoteUser, User: d.Username,
Auth: auth, Auth: auth,
}, },
NoPty: true, NoPty: true,
@ -204,7 +254,6 @@ func (d *ESX5Driver) connect() error {
func (d *ESX5Driver) checkSystemVersion() error { func (d *ESX5Driver) checkSystemVersion() error {
r, err := d.esxcli("system", "version", "get") r, err := d.esxcli("system", "version", "get")
if err != nil { if err != nil {
return err return err
} }
@ -214,20 +263,18 @@ func (d *ESX5Driver) checkSystemVersion() error {
return err return err
} }
log.Printf("Connected to %s %s %s", record["Product"], record["Version"], record["Build"]) log.Printf("Connected to %s %s %s", record["Product"],
record["Version"], record["Build"])
return nil return nil
} }
func (d *ESX5Driver) checkGuestIPHackEnabled() error { func (d *ESX5Driver) checkGuestIPHackEnabled() error {
r, err := d.esxcli("system", "settings", "advanced", "list", "-o", "/Net/GuestIPHack") r, err := d.esxcli("system", "settings", "advanced", "list", "-o", "/Net/GuestIPHack")
if err != nil { if err != nil {
return err return err
} }
record, err := r.read() record, err := r.read()
if err != nil { if err != nil {
return err return err
} }
@ -240,42 +287,6 @@ func (d *ESX5Driver) checkGuestIPHackEnabled() error {
return nil return nil
} }
func (d *ESX5Driver) checkOutputFolder() error {
if d.config.RemoteDatastore == "" {
d.config.RemoteDatastore = "datastore1"
}
if !d.config.PackerForce && d.FileExists(d.config.OutputDir) {
return fmt.Errorf("Output folder '%s' already exists. It must not exist.",
d.config.OutputDir)
}
return nil
}
func (d *ESX5Driver) download(config *common.DownloadConfig, state multistep.StateBag) (string, error, bool) {
cacheRoot, _ := filepath.Abs(".")
targetFile, err := filepath.Rel(cacheRoot, config.TargetPath)
if err != nil {
return "", err, false
}
path := d.datastorePath(targetFile)
err = d.MkdirAll(filepath.Dir(targetFile))
if err != nil {
return "", err, false
}
if d.verifyChecksum(d.config.ISOChecksumType, d.config.ISOChecksum, path) {
log.Println("Initial checksum matched, no download needed.")
return path, nil, true
}
// TODO(dougm) progress and handle interrupt
err = d.sh("wget", config.Url, "-O", path)
return path, err, true
}
func (d *ESX5Driver) upload(src, dst string) error { func (d *ESX5Driver) upload(src, dst string) error {
f, err := os.Open(src) f, err := os.Open(src)
if err != nil { if err != nil {
@ -378,41 +389,3 @@ func (r *esxcliReader) find(key, val string) (map[string]string, error) {
} }
} }
} }
func (d *ESX5Driver) sshAddress(state multistep.StateBag) (string, error) {
if address, ok := state.GetOk("vm_address"); ok {
return address.(string), nil
}
r, err := d.esxcli("network", "vm", "list")
if err != nil {
return "", err
}
record, err := r.find("Name", d.config.VMName)
if err != nil {
return "", err
}
wid := record["WorldID"]
if wid == "" {
return "", errors.New("VM WorldID not found")
}
r, err = d.esxcli("network", "vm", "port", "list", "-w", wid)
if err != nil {
return "", err
}
record, err = r.read()
if err != nil {
return "", err
}
if record["IPAddress"] == "0.0.0.0" {
return "", errors.New("VM network port found, but no IP address")
}
address := fmt.Sprintf("%s:%d", record["IPAddress"], d.config.SSHPort)
state.Put("vm_address", address)
return address, nil
}

View File

@ -2,6 +2,7 @@ package vmware
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -58,6 +59,10 @@ func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
return false, nil return false, nil
} }
func (d *Fusion5Driver) SSHAddress(state multistep.StateBag) (string, error) {
return sshAddress(state)
}
func (d *Fusion5Driver) Start(vmxPath string, headless bool) error { func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {
guiArgument := "gui" guiArgument := "gui"
if headless == true { if headless == true {

View File

@ -2,6 +2,7 @@ package vmware
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -86,6 +87,10 @@ func (d *Player5LinuxDriver) IsRunning(vmxPath string) (bool, error) {
return false, nil return false, nil
} }
func (d *Player5LinuxDriver) SSHAddress(state multistep.StateBag) (string, error) {
return sshAddress(state)
}
func (d *Player5LinuxDriver) Start(vmxPath string, headless bool) error { func (d *Player5LinuxDriver) Start(vmxPath string, headless bool) error {
guiArgument := "gui" guiArgument := "gui"
if headless { if headless {

View File

@ -2,6 +2,7 @@ package vmware
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"log" "log"
"os" "os"
"os/exec" "os/exec"
@ -61,6 +62,10 @@ func (d *Workstation9Driver) IsRunning(vmxPath string) (bool, error) {
return false, nil return false, nil
} }
func (d *Workstation9Driver) SSHAddress(state multistep.StateBag) (string, error) {
return sshAddress(state)
}
func (d *Workstation9Driver) Start(vmxPath string, headless bool) error { func (d *Workstation9Driver) Start(vmxPath string, headless bool) error {
guiArgument := "gui" guiArgument := "gui"
if headless { if headless {

View File

@ -1,28 +0,0 @@
package vmware
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
)
type RemoteDriver interface {
Driver
SSHAddress() func(multistep.StateBag) (string, error)
Download() func(*common.DownloadConfig, multistep.StateBag) (string, error, bool)
}
func NewRemoteDriver(config *config) (Driver, error) {
var driver Driver
switch config.RemoteType {
case "esx5":
driver = &ESX5Driver{
config: config,
}
default:
return nil, fmt.Errorf("Unknown product type: '%s'", config.RemoteType)
}
return driver, driver.Verify()
}

View File

@ -1,49 +1,27 @@
package vmware package vmware
import ( import (
"fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log" "log"
"os"
"time" "time"
) )
type OutputDir interface {
FileExists(path string) bool
MkdirAll(path string) error
RemoveAll(path string) error
DirType() string
}
type localOutputDir struct{}
func (localOutputDir) FileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func (localOutputDir) MkdirAll(path string) error {
return os.MkdirAll(path, 0755)
}
func (localOutputDir) RemoveAll(path string) error {
return os.RemoveAll(path)
}
func (localOutputDir) DirType() string {
return "local"
}
type stepPrepareOutputDir struct{} type stepPrepareOutputDir struct{}
func (s *stepPrepareOutputDir) Run(state multistep.StateBag) multistep.StepAction { func (s *stepPrepareOutputDir) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*config) config := state.Get("config").(*config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
for _, dir := range s.outputDirs(state) { dir := s.outputDir(state)
if dir.FileExists(config.OutputDir) && config.PackerForce { exists, err := dir.DirExists(config.OutputDir)
ui.Say(fmt.Sprintf("Deleting previous %s output directory...", dir.DirType())) if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
if exists && config.PackerForce {
ui.Say("Deleting previous output directory...")
dir.RemoveAll(config.OutputDir) dir.RemoveAll(config.OutputDir)
} }
@ -51,7 +29,6 @@ func (s *stepPrepareOutputDir) Run(state multistep.StateBag) multistep.StepActio
state.Put("error", err) state.Put("error", err)
return multistep.ActionHalt return multistep.ActionHalt
} }
}
return multistep.ActionContinue return multistep.ActionContinue
} }
@ -64,8 +41,8 @@ func (s *stepPrepareOutputDir) Cleanup(state multistep.StateBag) {
config := state.Get("config").(*config) config := state.Get("config").(*config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
for _, dir := range s.outputDirs(state) { dir := s.outputDir(state)
ui.Say(fmt.Sprintf("Deleting %s output directory...", dir.DirType())) ui.Say("Deleting output directory...")
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
err := dir.RemoveAll(config.OutputDir) err := dir.RemoveAll(config.OutputDir)
if err == nil { if err == nil {
@ -77,17 +54,16 @@ func (s *stepPrepareOutputDir) Cleanup(state multistep.StateBag) {
} }
} }
} }
}
func (s *stepPrepareOutputDir) outputDirs(state multistep.StateBag) []OutputDir { func (s *stepPrepareOutputDir) outputDir(state multistep.StateBag) (dir OutputDir) {
driver := state.Get("driver").(Driver) driver := state.Get("driver").(Driver)
dirs := []OutputDir{
localOutputDir{}, switch d := driver.(type) {
case OutputDir:
dir = d
default:
dir = new(localOutputDir)
} }
if dir, ok := driver.(OutputDir); ok { return
dirs = append(dirs, dir)
}
return dirs
} }

View File

@ -35,8 +35,6 @@ type StepDownload struct {
// A list of URLs to attempt to download this thing. // A list of URLs to attempt to download this thing.
Url []string Url []string
Download func(*DownloadConfig, multistep.StateBag) (string, error, bool)
} }
func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction { func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction {
@ -55,11 +53,6 @@ func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction {
ui.Say(fmt.Sprintf("Downloading or copying %s", s.Description)) ui.Say(fmt.Sprintf("Downloading or copying %s", s.Description))
downloadFunc := s.Download
if downloadFunc == nil {
downloadFunc = s.download
}
var finalPath string var finalPath string
for _, url := range s.Url { for _, url := range s.Url {
ui.Message(fmt.Sprintf("Downloading or copying: %s", url)) ui.Message(fmt.Sprintf("Downloading or copying: %s", url))
@ -79,7 +72,7 @@ func (s *StepDownload) Run(state multistep.StateBag) multistep.StepAction {
Checksum: checksum, Checksum: checksum,
} }
path, err, retry := downloadFunc(config, state) path, err, retry := s.download(config, state)
if err != nil { if err != nil {
ui.Message(fmt.Sprintf("Error downloading: %s", err)) ui.Message(fmt.Sprintf("Error downloading: %s", err))
} }