Merge pull request #6927 from hashicorp/rebased_4591

Extend vmware-vmx builder to allow esxi builds. (Rebase of PR #4591)
This commit is contained in:
Megan Marsh 2018-11-06 09:59:26 -08:00 committed by GitHub
commit 8567be43d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 594 additions and 453 deletions

View File

@ -2,68 +2,87 @@ package common
import ( import (
"fmt" "fmt"
"os" "strconv"
"path/filepath"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
) )
// BuilderId for the local artifacts const (
const BuilderId = "mitchellh.vmware" // BuilderId for the local artifacts
BuilderId = "mitchellh.vmware"
BuilderIdESX = "mitchellh.vmware-esx"
ArtifactConfFormat = "artifact.conf.format"
ArtifactConfKeepRegistered = "artifact.conf.keep_registered"
ArtifactConfSkipExport = "artifact.conf.skip_export"
)
// Artifact is the result of running the VMware builder, namely a set // Artifact is the result of running the VMware builder, namely a set
// of files associated with the resulting machine. // of files associated with the resulting machine.
type localArtifact struct { type artifact struct {
builderId string
id string id string
dir string dir OutputDir
f []string f []string
config map[string]string
} }
// NewLocalArtifact returns a VMware artifact containing the files func (a *artifact) BuilderId() string {
// in the given directory. return a.builderId
func NewLocalArtifact(id string, dir string) (packer.Artifact, error) {
files := make([]string, 0, 5)
visit := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
files = append(files, path)
}
return nil
}
if err := filepath.Walk(dir, visit); err != nil {
return nil, err
}
return &localArtifact{
id: id,
dir: dir,
f: files,
}, nil
} }
func (a *localArtifact) BuilderId() string { func (a *artifact) Files() []string {
return BuilderId
}
func (a *localArtifact) Files() []string {
return a.f return a.f
} }
func (a *localArtifact) Id() string { func (a *artifact) Id() string {
return a.id return a.id
} }
func (a *localArtifact) String() string { func (a *artifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir) return fmt.Sprintf("VM files in directory: %s", a.dir)
} }
func (a *localArtifact) State(name string) interface{} { func (a *artifact) State(name string) interface{} {
return nil return a.config[name]
} }
func (a *localArtifact) Destroy() error { func (a *artifact) Destroy() error {
return os.RemoveAll(a.dir) return a.dir.RemoveAll()
}
func NewArtifact(remoteType string, format string, exportOutputPath string, vmName string, skipExport bool, keepRegistered bool, state multistep.StateBag) (packer.Artifact, error) {
var files []string
var dir OutputDir
var err error
if remoteType != "" && !skipExport {
dir = new(LocalOutputDir)
dir.SetOutputDir(exportOutputPath)
files, err = dir.ListFiles()
} else {
files, err = state.Get("dir").(OutputDir).ListFiles()
}
if err != nil {
return nil, err
}
// Set the proper builder ID
builderId := BuilderId
if remoteType != "" {
builderId = BuilderIdESX
}
config := make(map[string]string)
config[ArtifactConfKeepRegistered] = strconv.FormatBool(keepRegistered)
config[ArtifactConfFormat] = format
config[ArtifactConfSkipExport] = strconv.FormatBool(skipExport)
return &artifact{
builderId: builderId,
id: vmName,
dir: dir,
f: files,
config: config,
}, nil
} }

View File

@ -1,46 +1,11 @@
package common package common
import ( import (
"io/ioutil"
"os"
"path/filepath"
"testing" "testing"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
) )
func TestLocalArtifact_impl(t *testing.T) { func TestLocalArtifact_impl(t *testing.T) {
var _ packer.Artifact = new(localArtifact) var _ packer.Artifact = new(artifact)
}
func TestNewLocalArtifact(t *testing.T) {
td, err := ioutil.TempDir("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(td)
err = ioutil.WriteFile(filepath.Join(td, "a"), []byte("foo"), 0644)
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Mkdir(filepath.Join(td, "b"), 0755); err != nil {
t.Fatalf("err: %s", err)
}
a, err := NewLocalArtifact("vm1", td)
if err != nil {
t.Fatalf("err: %s", err)
}
if a.BuilderId() != BuilderId {
t.Fatalf("bad: %#v", a.BuilderId())
}
if a.Id() != "vm1" {
t.Fatalf("bad: %#v", a.Id())
}
if len(a.Files()) != 1 {
t.Fatalf("should length 1: %d", len(a.Files()))
}
} }

View File

@ -81,9 +81,26 @@ 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(dconfig *DriverConfig, config *SSHConfig) (Driver, error) { func NewDriver(dconfig *DriverConfig, config *SSHConfig, vmName string) (Driver, error) {
drivers := []Driver{} drivers := []Driver{}
if dconfig.RemoteType != "" {
drivers = []Driver{
&ESX5Driver{
Host: dconfig.RemoteHost,
Port: dconfig.RemotePort,
Username: dconfig.RemoteUser,
Password: dconfig.RemotePassword,
PrivateKeyFile: dconfig.RemotePrivateKey,
Datastore: dconfig.RemoteDatastore,
CacheDatastore: dconfig.RemoteCacheDatastore,
CacheDirectory: dconfig.RemoteCacheDirectory,
VMName: vmName,
CommConfig: config.Comm,
},
}
} else {
switch runtime.GOOS { switch runtime.GOOS {
case "darwin": case "darwin":
drivers = []Driver{ drivers = []Driver{
@ -122,6 +139,7 @@ func NewDriver(dconfig *DriverConfig, config *SSHConfig) (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

@ -8,6 +8,15 @@ import (
type DriverConfig struct { type DriverConfig struct {
FusionAppPath string `mapstructure:"fusion_app_path"` FusionAppPath string `mapstructure:"fusion_app_path"`
RemoteType string `mapstructure:"remote_type"`
RemoteDatastore string `mapstructure:"remote_datastore"`
RemoteCacheDatastore string `mapstructure:"remote_cache_datastore"`
RemoteCacheDirectory string `mapstructure:"remote_cache_directory"`
RemoteHost string `mapstructure:"remote_host"`
RemotePort uint `mapstructure:"remote_port"`
RemoteUser string `mapstructure:"remote_username"`
RemotePassword string `mapstructure:"remote_password"`
RemotePrivateKey string `mapstructure:"remote_private_key_file"`
} }
func (c *DriverConfig) Prepare(ctx *interpolate.Context) []error { func (c *DriverConfig) Prepare(ctx *interpolate.Context) []error {
@ -17,6 +26,21 @@ func (c *DriverConfig) Prepare(ctx *interpolate.Context) []error {
if c.FusionAppPath == "" { if c.FusionAppPath == "" {
c.FusionAppPath = "/Applications/VMware Fusion.app" c.FusionAppPath = "/Applications/VMware Fusion.app"
} }
if c.RemoteUser == "" {
c.RemoteUser = "root"
}
if c.RemoteDatastore == "" {
c.RemoteDatastore = "datastore1"
}
if c.RemoteCacheDatastore == "" {
c.RemoteCacheDatastore = c.RemoteDatastore
}
if c.RemoteCacheDirectory == "" {
c.RemoteCacheDirectory = "packer_cache"
}
if c.RemotePort == 0 {
c.RemotePort = 22
}
return nil return nil
} }

View File

@ -1,4 +1,4 @@
package iso package common
import ( import (
"bufio" "bufio"
@ -10,13 +10,14 @@ import (
"log" "log"
"net" "net"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
"github.com/hashicorp/packer/communicator/ssh" "github.com/hashicorp/packer/communicator/ssh"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/multistep"
helperssh "github.com/hashicorp/packer/helper/ssh" helperssh "github.com/hashicorp/packer/helper/ssh"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
@ -26,7 +27,7 @@ import (
// ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build // ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build
// virtual machines. This driver can only manage one machine at a time. // virtual machines. This driver can only manage one machine at a time.
type ESX5Driver struct { type ESX5Driver struct {
base vmwcommon.VmwareDriver base VmwareDriver
Host string Host string
Port uint Port uint
@ -36,6 +37,8 @@ type ESX5Driver struct {
Datastore string Datastore string
CacheDatastore string CacheDatastore string
CacheDirectory string CacheDirectory string
VMName string
CommConfig communicator.Config
comm packer.Communicator comm packer.Communicator
outputDir string outputDir string
@ -43,7 +46,61 @@ type ESX5Driver struct {
} }
func (d *ESX5Driver) Clone(dst, src string, linked bool) error { func (d *ESX5Driver) Clone(dst, src string, linked bool) error {
return errors.New("Cloning is not supported with the ESX driver.")
linesToArray := func(lines string) []string { return strings.Split(strings.Trim(lines, "\n"), "\n") }
d.SetOutputDir(path.Dir(filepath.ToSlash(dst)))
srcVmx := d.datastorePath(src)
dstVmx := d.datastorePath(dst)
srcDir := path.Dir(srcVmx)
dstDir := path.Dir(dstVmx)
log.Printf("Source: %s\n", srcVmx)
log.Printf("Dest: %s\n", dstVmx)
err := d.MkdirAll()
if err != nil {
return fmt.Errorf("Failed to create the destination directory %s: %s", d.outputDir, err)
}
err = d.sh("cp", strconv.Quote(srcVmx), strconv.Quote(dstVmx))
if err != nil {
return fmt.Errorf("Failed to copy the vmx file %s: %s", srcVmx, err)
}
filesToClone, err := d.run(nil, "find", strconv.Quote(srcDir), "! -name '*.vmdk' ! -name '*.vmx' -type f ! -size 0")
if err != nil {
return fmt.Errorf("Failed to get the file list to copy: %s", err)
}
for _, f := range linesToArray(filesToClone) {
// TODO: linesToArray should really return [] if the string is empty. Instead it returns [""]
if f == "" {
continue
}
err := d.sh("cp", strconv.Quote(f), strconv.Quote(dstDir))
if err != nil {
return fmt.Errorf("Failing to copy %s to %s: %s", f, dstDir, err)
}
}
disksToClone, err := d.run(nil, "sed -ne 's/.*file[Nn]ame = \"\\(.*vmdk\\)\"/\\1/p'", strconv.Quote(srcVmx))
if err != nil {
return fmt.Errorf("Failing to get the vmdk list to clone %s", err)
}
for _, disk := range linesToArray(disksToClone) {
srcDisk := path.Join(srcDir, disk)
if path.IsAbs(disk) {
srcDisk = disk
}
destDisk := path.Join(dstDir, path.Base(disk))
err = d.sh("vmkfstools", "-d thin", "-i", strconv.Quote(srcDisk), strconv.Quote(destDisk))
if err != nil {
return fmt.Errorf("Failing to clone disk %s: %s", srcDisk, err)
}
}
log.Printf("Successfully cloned %s to %s\n", src, dst)
return nil
} }
func (d *ESX5Driver) CompactDisk(diskPathLocal string) error { func (d *ESX5Driver) CompactDisk(diskPathLocal string) error {
@ -65,7 +122,11 @@ func (d *ESX5Driver) IsRunning(string) (bool, error) {
} }
func (d *ESX5Driver) ReloadVM() error { func (d *ESX5Driver) ReloadVM() error {
if d.vmId != "" {
return d.sh("vim-cmd", "vmsvc/reload", d.vmId) return d.sh("vim-cmd", "vmsvc/reload", d.vmId)
} else {
return nil
}
} }
func (d *ESX5Driver) Start(vmxPathLocal string, headless bool) error { func (d *ESX5Driver) Start(vmxPathLocal string, headless bool) error {
@ -122,13 +183,13 @@ func (d *ESX5Driver) IsDestroyed() (bool, error) {
} }
func (d *ESX5Driver) UploadISO(localPath string, checksum string, checksumType string) (string, error) { func (d *ESX5Driver) UploadISO(localPath string, checksum string, checksumType string) (string, error) {
finalPath := d.cachePath(localPath) finalPath := d.CachePath(localPath)
if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil { if err := d.mkdir(filepath.ToSlash(filepath.Dir(finalPath))); err != nil {
return "", err return "", err
} }
log.Printf("Verifying checksum of %s", finalPath) log.Printf("Verifying checksum of %s", finalPath)
if d.verifyChecksum(checksumType, checksum, finalPath) { if d.VerifyChecksum(checksumType, checksum, finalPath) {
log.Println("Initial checksum matched, no upload needed.") log.Println("Initial checksum matched, no upload needed.")
return finalPath, nil return finalPath, nil
} }
@ -141,7 +202,7 @@ func (d *ESX5Driver) UploadISO(localPath string, checksum string, checksumType s
} }
func (d *ESX5Driver) RemoveCache(localPath string) error { func (d *ESX5Driver) RemoveCache(localPath string) error {
finalPath := d.cachePath(localPath) finalPath := d.CachePath(localPath)
log.Printf("Removing remote cache path %s (local %s)", finalPath, localPath) log.Printf("Removing remote cache path %s (local %s)", finalPath, localPath)
return d.sh("rm", "-f", strconv.Quote(finalPath)) return d.sh("rm", "-f", strconv.Quote(finalPath))
} }
@ -375,14 +436,18 @@ func (ESX5Driver) UpdateVMX(_, password string, port uint, data map[string]strin
} }
func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) { func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) {
config := state.Get("config").(*Config) sshc := state.Get("sshConfig").(*SSHConfig).Comm
sshc := config.SSHConfig.Comm
port := sshc.SSHPort port := sshc.SSHPort
if sshc.Type == "winrm" { if sshc.Type == "winrm" {
port = sshc.WinRMPort port = sshc.WinRMPort
} }
if address := config.CommConfig.Host(); address != "" { if address, ok := state.GetOk("vm_address"); ok {
return address.(string), nil
}
if address := d.CommConfig.Host(); address != "" {
state.Put("vm_address", address)
return address, nil return address, nil
} }
@ -396,7 +461,12 @@ func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) {
var displayName string var displayName string
if v, ok := state.GetOk("display_name"); ok { if v, ok := state.GetOk("display_name"); ok {
displayName = v.(string) displayName = v.(string)
} else {
displayName = strings.Replace(d.VMName, " ", "_", -1)
log.Printf("No display_name set; falling back to using VMName %s "+
"to look for SSH IP", displayName)
} }
record, err := r.find("Name", displayName) record, err := r.find("Name", displayName)
if err != nil { if err != nil {
return "", err return "", err
@ -432,14 +502,12 @@ func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) {
if e.Timeout() { if e.Timeout() {
log.Printf("Timeout connecting to %s", record["IPAddress"]) log.Printf("Timeout connecting to %s", record["IPAddress"])
continue continue
} else if strings.Contains(e.Error(), "connection refused") {
log.Printf("Connection refused when connecting to: %s", record["IPAddress"])
continue
} }
} }
} else { } else {
defer conn.Close() defer conn.Close()
address := record["IPAddress"] address := record["IPAddress"]
state.Put("vm_address", address)
return address, nil return address, nil
} }
} }
@ -456,7 +524,7 @@ func (d *ESX5Driver) DirExists() (bool, error) {
} }
func (d *ESX5Driver) ListFiles() ([]string, error) { func (d *ESX5Driver) ListFiles() ([]string, error) {
stdout, err := d.ssh("ls -1p "+d.outputDir, nil) stdout, err := d.ssh("ls -1p "+strconv.Quote(d.outputDir), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -503,7 +571,7 @@ func (d *ESX5Driver) datastorePath(path string) string {
return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, dirPath, filepath.Base(path))) return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.Datastore, dirPath, filepath.Base(path)))
} }
func (d *ESX5Driver) cachePath(path string) string { func (d *ESX5Driver) CachePath(path string) string {
return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.CacheDatastore, d.CacheDirectory, filepath.Base(path))) return filepath.ToSlash(filepath.Join("/vmfs/volumes", d.CacheDatastore, d.CacheDirectory, filepath.Base(path)))
} }
@ -592,7 +660,16 @@ func (d *ESX5Driver) upload(dst, src string) error {
return d.comm.Upload(dst, f, nil) return d.comm.Upload(dst, f, nil)
} }
func (d *ESX5Driver) verifyChecksum(ctype string, hash string, file string) bool { func (d *ESX5Driver) Download(src, dst string) error {
file, err := os.Create(dst)
if err != nil {
return err
}
defer file.Close()
return d.comm.Download(d.datastorePath(src), file)
}
func (d *ESX5Driver) VerifyChecksum(ctype string, hash string, file string) bool {
if ctype == "none" { if ctype == "none" {
if err := d.sh("stat", strconv.Quote(file)); err != nil { if err := d.sh("stat", strconv.Quote(file)); err != nil {
return false return false
@ -661,7 +738,7 @@ func (d *ESX5Driver) esxcli(args ...string) (*esxcliReader, error) {
return &esxcliReader{r, header}, nil return &esxcliReader{r, header}, nil
} }
func (d *ESX5Driver) GetVmwareDriver() vmwcommon.VmwareDriver { func (d *ESX5Driver) GetVmwareDriver() VmwareDriver {
return d.base return d.base
} }

View File

@ -1,16 +1,17 @@
package iso package common
import ( import (
"fmt" "fmt"
"net" "net"
"testing" "testing"
vmwcommon "github.com/hashicorp/packer/builder/vmware/common" "github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/multistep"
) )
func TestESX5Driver_implDriver(t *testing.T) { func TestESX5Driver_implDriver(t *testing.T) {
var _ vmwcommon.Driver = new(ESX5Driver) var _ Driver = new(ESX5Driver)
} }
func TestESX5Driver_UpdateVMX(t *testing.T) { func TestESX5Driver_UpdateVMX(t *testing.T) {
@ -30,11 +31,11 @@ func TestESX5Driver_UpdateVMX(t *testing.T) {
} }
func TestESX5Driver_implOutputDir(t *testing.T) { func TestESX5Driver_implOutputDir(t *testing.T) {
var _ vmwcommon.OutputDir = new(ESX5Driver) var _ OutputDir = new(ESX5Driver)
} }
func TestESX5Driver_implVNCAddressFinder(t *testing.T) { func TestESX5Driver_implVNCAddressFinder(t *testing.T) {
var _ vmwcommon.VNCAddressFinder = new(ESX5Driver) var _ VNCAddressFinder = new(ESX5Driver)
} }
func TestESX5Driver_implRemoteDriver(t *testing.T) { func TestESX5Driver_implRemoteDriver(t *testing.T) {
@ -60,28 +61,19 @@ func TestESX5Driver_HostIP(t *testing.T) {
func TestESX5Driver_CommHost(t *testing.T) { func TestESX5Driver_CommHost(t *testing.T) {
const expected_host = "127.0.0.1" const expected_host = "127.0.0.1"
config := testConfig() conf := make(map[string]interface{})
config["communicator"] = "winrm" conf["communicator"] = "winrm"
config["winrm_username"] = "username" conf["winrm_username"] = "username"
config["winrm_password"] = "password" conf["winrm_password"] = "password"
config["winrm_host"] = expected_host conf["winrm_host"] = expected_host
var b Builder
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if host := b.config.CommConfig.Host(); host != expected_host {
t.Fatalf("setup failed, bad host name: %s", host)
}
var commConfig communicator.Config
err := config.Decode(&commConfig, nil, conf)
state := new(multistep.BasicStateBag) state := new(multistep.BasicStateBag)
state.Put("config", &b.config) sshConfig := SSHConfig{Comm: commConfig}
state.Put("sshConfig", &sshConfig)
driver := ESX5Driver{CommConfig: *(&sshConfig.Comm)}
var driver ESX5Driver
host, err := driver.CommHost(state) host, err := driver.CommHost(state)
if err != nil { if err != nil {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
@ -89,4 +81,11 @@ func TestESX5Driver_CommHost(t *testing.T) {
if host != expected_host { if host != expected_host {
t.Errorf("bad host name: %s", host) t.Errorf("bad host name: %s", host)
} }
address, ok := state.GetOk("vm_address")
if !ok {
t.Error("state not updated with vm_address")
}
if address.(string) != expected_host {
t.Errorf("bad vm_address: %s", address.(string))
}
} }

View File

@ -0,0 +1,26 @@
package common
import (
"fmt"
"github.com/hashicorp/packer/template/interpolate"
)
type ExportConfig struct {
Format string `mapstructure:"format"`
OVFToolOptions []string `mapstructure:"ovftool_options"`
SkipExport bool `mapstructure:"skip_export"`
KeepRegistered bool `mapstructure:"keep_registered"`
SkipCompaction bool `mapstructure:"skip_compaction"`
}
func (c *ExportConfig) Prepare(ctx *interpolate.Context) []error {
var errs []error
if c.Format != "" {
if !(c.Format == "ova" || c.Format == "ovf" || c.Format == "vmx") {
errs = append(
errs, fmt.Errorf("format must be one of ova, ovf, or vmx"))
}
}
return errs
}

View File

@ -4,6 +4,7 @@ package common
// of the output directory for VMware-based products. The abstraction is made // of the output directory for VMware-based products. The abstraction is made
// so that the output directory can be properly made on remote (ESXi) based // so that the output directory can be properly made on remote (ESXi) based
// VMware products as well as local. // VMware products as well as local.
// For remote builds, OutputDir interface is satisfied by the ESX5Driver.
type OutputDir interface { type OutputDir interface {
DirExists() (bool, error) DirExists() (bool, error)
ListFiles() ([]string, error) ListFiles() ([]string, error)

View File

@ -1,11 +1,7 @@
package iso package common
import (
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
)
type RemoteDriver interface { type RemoteDriver interface {
vmwcommon.Driver Driver
// UploadISO uploads a local ISO to the remote side and returns the // UploadISO uploads a local ISO to the remote side and returns the
// new path that should be used in the VMX along with an error if it // new path that should be used in the VMX along with an error if it
@ -30,6 +26,9 @@ type RemoteDriver interface {
// Uploads a local file to remote side. // Uploads a local file to remote side.
upload(dst, src string) error upload(dst, src string) error
// Download a remote file to a local file.
Download(src, dst string) error
// Reload VM on remote side. // Reload VM on remote side.
ReloadVM() error ReloadVM() error
} }

View File

@ -1,11 +1,7 @@
package iso package common
import (
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
)
type RemoteDriverMock struct { type RemoteDriverMock struct {
vmwcommon.DriverMock DriverMock
UploadISOCalled bool UploadISOCalled bool
UploadISOPath string UploadISOPath string
@ -27,7 +23,8 @@ type RemoteDriverMock struct {
IsDestroyedResult bool IsDestroyedResult bool
IsDestroyedErr error IsDestroyedErr error
uploadErr error UploadErr error
DownloadErr error
ReloadVMErr error ReloadVMErr error
} }
@ -61,7 +58,11 @@ func (d *RemoteDriverMock) IsDestroyed() (bool, error) {
} }
func (d *RemoteDriverMock) upload(dst, src string) error { func (d *RemoteDriverMock) upload(dst, src string) error {
return d.uploadErr return d.UploadErr
}
func (d *RemoteDriverMock) Download(src, dst string) error {
return d.DownloadErr
} }
func (d *RemoteDriverMock) RemoveCache(localPath string) error { func (d *RemoteDriverMock) RemoveCache(localPath string) error {

View File

@ -0,0 +1,10 @@
package common
import (
"testing"
)
func TestRemoteDriverMock_impl(t *testing.T) {
var _ Driver = new(RemoteDriverMock)
var _ RemoteDriver = new(RemoteDriverMock)
}

View File

@ -22,12 +22,16 @@ import (
type StepConfigureVMX struct { type StepConfigureVMX struct {
CustomData map[string]string CustomData map[string]string
SkipFloppy bool SkipFloppy bool
VMName string
} }
func (s *StepConfigureVMX) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { func (s *StepConfigureVMX) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) log.Printf("Configuring VMX...\n")
vmxPath := state.Get("vmx_path").(string)
var err error
ui := state.Get("ui").(packer.Ui)
vmxPath := state.Get("vmx_path").(string)
vmxData, err := ReadVMX(vmxPath) vmxData, err := ReadVMX(vmxPath)
if err != nil { if err != nil {
err := fmt.Errorf("Error reading VMX file: %s", err) err := fmt.Errorf("Error reading VMX file: %s", err)
@ -69,7 +73,9 @@ func (s *StepConfigureVMX) Run(_ context.Context, state multistep.StateBag) mult
} }
} }
if err := WriteVMX(vmxPath, vmxData); err != nil { err = WriteVMX(vmxPath, vmxData)
if err != nil {
err := fmt.Errorf("Error writing VMX file: %s", err) err := fmt.Errorf("Error writing VMX file: %s", err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())

View File

@ -1,4 +1,4 @@
package iso package common
import ( import (
"bytes" "bytes"
@ -21,10 +21,12 @@ import (
type StepExport struct { type StepExport struct {
Format string Format string
SkipExport bool SkipExport bool
VMName string
OVFToolOptions []string
OutputDir string OutputDir string
} }
func (s *StepExport) generateArgs(c *Config, displayName string, hidePassword bool) []string { func (s *StepExport) generateArgs(c *DriverConfig, displayName string, hidePassword bool) []string {
password := url.QueryEscape(c.RemotePassword) password := url.QueryEscape(c.RemotePassword)
if hidePassword { if hidePassword {
password = "****" password = "****"
@ -33,18 +35,19 @@ func (s *StepExport) generateArgs(c *Config, displayName string, hidePassword bo
"--noSSLVerify=true", "--noSSLVerify=true",
"--skipManifestCheck", "--skipManifestCheck",
"-tt=" + s.Format, "-tt=" + s.Format,
"vi://" + c.RemoteUser + ":" + password + "@" + c.RemoteHost + "/" + displayName, "vi://" + c.RemoteUser + ":" + password + "@" + c.RemoteHost + "/" + displayName,
s.OutputDir, s.OutputDir,
} }
return append(c.OVFToolOptions, args...) return append(s.OVFToolOptions, args...)
} }
func (s *StepExport) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { func (s *StepExport) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
c := state.Get("config").(*Config) c := state.Get("driverConfig").(*DriverConfig)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
// Skip export if requested // Skip export if requested
if c.SkipExport { if s.SkipExport {
ui.Say("Skipping export of virtual machine...") ui.Say("Skipping export of virtual machine...")
return multistep.ActionContinue return multistep.ActionContinue
} }
@ -60,7 +63,7 @@ func (s *StepExport) Run(_ context.Context, state multistep.StateBag) multistep.
} }
if _, err := exec.LookPath(ovftool); err != nil { if _, err := exec.LookPath(ovftool); err != nil {
err := fmt.Errorf("Error %s not found: %s", ovftool, err) err = fmt.Errorf("Error %s not found: %s", ovftool, err)
state.Put("error", err) state.Put("error", err)
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
@ -68,7 +71,7 @@ func (s *StepExport) Run(_ context.Context, state multistep.StateBag) multistep.
// Export the VM // Export the VM
if s.OutputDir == "" { if s.OutputDir == "" {
s.OutputDir = c.VMName + "." + s.Format s.OutputDir = s.VMName + "." + s.Format
} }
if s.Format == "ova" { if s.Format == "ova" {

View File

@ -1,4 +1,4 @@
package iso package common
import ( import (
"context" "context"
@ -15,9 +15,9 @@ func testStepExport_wrongtype_impl(t *testing.T, remoteType string) {
state := testState(t) state := testState(t)
step := new(StepExport) step := new(StepExport)
var config Config var config DriverConfig
config.RemoteType = "foo" config.RemoteType = "foo"
state.Put("config", &config) state.Put("driverConfig", &config)
if action := step.Run(context.Background(), state); action != multistep.ActionContinue { if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action) t.Fatalf("bad action: %#v", action)

View File

@ -1,11 +1,10 @@
package iso package common
import ( import (
"context" "context"
"fmt" "fmt"
"time" "time"
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
"github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
) )
@ -13,11 +12,14 @@ import (
type StepRegister struct { type StepRegister struct {
registeredPath string registeredPath string
Format string Format string
KeepRegistered bool
SkipExport bool
} }
func (s *StepRegister) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { func (s *StepRegister) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(vmwcommon.Driver) driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
vmxPath := state.Get("vmx_path").(string) vmxPath := state.Get("vmx_path").(string)
if remoteDriver, ok := driver.(RemoteDriver); ok { if remoteDriver, ok := driver.(RemoteDriver); ok {
@ -40,19 +42,18 @@ func (s *StepRegister) Cleanup(state multistep.StateBag) {
return return
} }
driver := state.Get("driver").(vmwcommon.Driver) driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(*Config)
_, cancelled := state.GetOk(multistep.StateCancelled) _, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted) _, halted := state.GetOk(multistep.StateHalted)
if (config.KeepRegistered) && (!cancelled && !halted) { if (s.KeepRegistered) && (!cancelled && !halted) {
ui.Say("Keeping virtual machine registered with ESX host (keep_registered = true)") ui.Say("Keeping virtual machine registered with ESX host (keep_registered = true)")
return return
} }
if remoteDriver, ok := driver.(RemoteDriver); ok { if remoteDriver, ok := driver.(RemoteDriver); ok {
if s.Format == "" || config.SkipExport { if s.SkipExport {
ui.Say("Unregistering virtual machine...") ui.Say("Unregistering virtual machine...")
if err := remoteDriver.Unregister(s.registeredPath); err != nil { if err := remoteDriver.Unregister(s.registeredPath); err != nil {
ui.Error(fmt.Sprintf("Error unregistering VM: %s", err)) ui.Error(fmt.Sprintf("Error unregistering VM: %s", err))
@ -70,7 +71,7 @@ func (s *StepRegister) Cleanup(state multistep.StateBag) {
if destroyed { if destroyed {
break break
} }
time.Sleep(150 * time.Millisecond) time.Sleep(1 * time.Second)
} }
} }
} }

View File

@ -1,4 +1,4 @@
package iso package common
import ( import (
"context" "context"
@ -31,12 +31,12 @@ func TestStepRegister_regularDriver(t *testing.T) {
func TestStepRegister_remoteDriver(t *testing.T) { func TestStepRegister_remoteDriver(t *testing.T) {
state := testState(t) state := testState(t)
step := new(StepRegister) step := &StepRegister{
KeepRegistered: false,
SkipExport: true,
}
driver := new(RemoteDriverMock) driver := new(RemoteDriverMock)
var config Config
config.KeepRegistered = false
state.Put("config", &config)
state.Put("driver", driver) state.Put("driver", driver)
state.Put("vmx_path", "foo") state.Put("vmx_path", "foo")
@ -71,12 +71,9 @@ func TestStepRegister_remoteDriver(t *testing.T) {
} }
func TestStepRegister_WithoutUnregister_remoteDriver(t *testing.T) { func TestStepRegister_WithoutUnregister_remoteDriver(t *testing.T) {
state := testState(t) state := testState(t)
step := new(StepRegister) step := &StepRegister{KeepRegistered: true}
driver := new(RemoteDriverMock) driver := new(RemoteDriverMock)
var config Config
config.KeepRegistered = true
state.Put("config", &config)
state.Put("driver", driver) state.Put("driver", driver)
state.Put("vmx_path", "foo") state.Put("vmx_path", "foo")

View File

@ -70,6 +70,7 @@ func (s *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
// Connect to VNC // Connect to VNC
ui.Say(fmt.Sprintf("Connecting to VM via VNC (%s:%d)", vncIp, vncPort)) ui.Say(fmt.Sprintf("Connecting to VM via VNC (%s:%d)", vncIp, vncPort))
nc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", vncIp, vncPort)) nc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", vncIp, vncPort))
if err != nil { if err != nil {
err := fmt.Errorf("Error connecting to VNC: %s", err) err := fmt.Errorf("Error connecting to VNC: %s", err)

View File

@ -1,11 +1,10 @@
package iso package common
import ( import (
"context" "context"
"fmt" "fmt"
"path/filepath" "path/filepath"
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
"github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
) )
@ -24,7 +23,7 @@ type StepUploadVMX struct {
} }
func (c *StepUploadVMX) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { func (c *StepUploadVMX) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(vmwcommon.Driver) driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
vmxPath := state.Get("vmx_path").(string) vmxPath := state.Get("vmx_path").(string)

View File

@ -1,45 +0,0 @@
package iso
import (
"fmt"
)
const (
ArtifactConfFormat = "artifact.conf.format"
ArtifactConfKeepRegistered = "artifact.conf.keep_registered"
ArtifactConfSkipExport = "artifact.conf.skip_export"
)
// Artifact is the result of running the VMware builder, namely a set
// of files associated with the resulting machine.
type Artifact struct {
builderId string
id string
dir OutputDir
f []string
config map[string]string
}
func (a *Artifact) BuilderId() string {
return a.builderId
}
func (a *Artifact) Files() []string {
return a.f
}
func (a *Artifact) Id() string {
return a.id
}
func (a *Artifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir)
}
func (a *Artifact) State(name string) interface{} {
return a.config[name]
}
func (a *Artifact) Destroy() error {
return a.dir.RemoveAll()
}

View File

@ -1,15 +0,0 @@
package iso
import (
"testing"
"github.com/hashicorp/packer/packer"
)
func TestArtifact_Impl(t *testing.T) {
var raw interface{}
raw = &Artifact{}
if _, ok := raw.(packer.Artifact); !ok {
t.Fatal("Artifact must be a proper artifact")
}
}

View File

@ -6,7 +6,6 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"strconv"
"time" "time"
vmwcommon "github.com/hashicorp/packer/builder/vmware/common" vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
@ -19,8 +18,6 @@ import (
"github.com/hashicorp/packer/template/interpolate" "github.com/hashicorp/packer/template/interpolate"
) )
const BuilderIdESX = "mitchellh.vmware-esx"
type Builder struct { type Builder struct {
config Config config Config
runner multistep.Runner runner multistep.Runner
@ -39,6 +36,7 @@ type Config struct {
vmwcommon.SSHConfig `mapstructure:",squash"` vmwcommon.SSHConfig `mapstructure:",squash"`
vmwcommon.ToolsConfig `mapstructure:",squash"` vmwcommon.ToolsConfig `mapstructure:",squash"`
vmwcommon.VMXConfig `mapstructure:",squash"` vmwcommon.VMXConfig `mapstructure:",squash"`
vmwcommon.ExportConfig `mapstructure:",squash"`
// disk drives // disk drives
AdditionalDiskSize []uint `mapstructure:"disk_additional_size"` AdditionalDiskSize []uint `mapstructure:"disk_additional_size"`
@ -68,27 +66,9 @@ type Config struct {
Serial string `mapstructure:"serial"` Serial string `mapstructure:"serial"`
Parallel string `mapstructure:"parallel"` Parallel string `mapstructure:"parallel"`
// booting a guest
KeepRegistered bool `mapstructure:"keep_registered"`
OVFToolOptions []string `mapstructure:"ovftool_options"`
SkipCompaction bool `mapstructure:"skip_compaction"`
SkipExport bool `mapstructure:"skip_export"`
VMXDiskTemplatePath string `mapstructure:"vmx_disk_template_path"` VMXDiskTemplatePath string `mapstructure:"vmx_disk_template_path"`
VMXTemplatePath string `mapstructure:"vmx_template_path"` VMXTemplatePath string `mapstructure:"vmx_template_path"`
// remote vsphere
RemoteType string `mapstructure:"remote_type"`
RemoteDatastore string `mapstructure:"remote_datastore"`
RemoteCacheDatastore string `mapstructure:"remote_cache_datastore"`
RemoteCacheDirectory string `mapstructure:"remote_cache_directory"`
RemoteHost string `mapstructure:"remote_host"`
RemotePort uint `mapstructure:"remote_port"`
RemoteUser string `mapstructure:"remote_username"`
RemotePassword string `mapstructure:"remote_password"`
RemotePrivateKey string `mapstructure:"remote_private_key_file"`
CommConfig communicator.Config `mapstructure:",squash"`
ctx interpolate.Context ctx interpolate.Context
} }
@ -125,6 +105,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
errs = packer.MultiErrorAppend(errs, b.config.VMXConfig.Prepare(&b.config.ctx)...) errs = packer.MultiErrorAppend(errs, b.config.VMXConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...) errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.VNCConfig.Prepare(&b.config.ctx)...) errs = packer.MultiErrorAppend(errs, b.config.VNCConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.ExportConfig.Prepare(&b.config.ctx)...)
if b.config.DiskName == "" { if b.config.DiskName == "" {
b.config.DiskName = "disk" b.config.DiskName = "disk"
@ -175,26 +156,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.Version = "9" b.config.Version = "9"
} }
if b.config.RemoteUser == "" {
b.config.RemoteUser = "root"
}
if b.config.RemoteDatastore == "" {
b.config.RemoteDatastore = "datastore1"
}
if b.config.RemoteCacheDatastore == "" {
b.config.RemoteCacheDatastore = b.config.RemoteDatastore
}
if b.config.RemoteCacheDirectory == "" {
b.config.RemoteCacheDirectory = "packer_cache"
}
if b.config.RemotePort == 0 {
b.config.RemotePort = 22
}
if b.config.VMXTemplatePath != "" { if b.config.VMXTemplatePath != "" {
if err := b.validateVMXTemplatePath(); err != nil { if err := b.validateVMXTemplatePath(); err != nil {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
@ -221,6 +182,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("remote_host must be specified")) fmt.Errorf("remote_host must be specified"))
} }
if b.config.RemoteType != "esx5" { if b.config.RemoteType != "esx5" {
errs = packer.MultiErrorAppend(errs, errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Only 'esx5' value is accepted for remote_type")) fmt.Errorf("Only 'esx5' value is accepted for remote_type"))
@ -262,20 +224,22 @@ 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) {
driver, err := NewDriver(&b.config) driver, err := vmwcommon.NewDriver(&b.config.DriverConfig, &b.config.SSHConfig, b.config.VMName)
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)
} }
// Determine the output dir implementation // Determine the output dir implementation
var dir OutputDir var dir vmwcommon.OutputDir
switch d := driver.(type) { switch d := driver.(type) {
case OutputDir: case vmwcommon.OutputDir:
dir = d dir = d
default: default:
dir = new(vmwcommon.LocalOutputDir) dir = new(vmwcommon.LocalOutputDir)
} }
// The OutputDir will track remote esxi output; exportOutputPath preserves
// the path to the output on the machine running Packer.
exportOutputPath := b.config.OutputDir exportOutputPath := b.config.OutputDir
if b.config.RemoteType != "" { if b.config.RemoteType != "" {
@ -292,6 +256,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
state.Put("driver", driver) state.Put("driver", driver)
state.Put("hook", hook) state.Put("hook", hook)
state.Put("ui", ui) state.Put("ui", ui)
state.Put("sshConfig", &b.config.SSHConfig)
state.Put("driverConfig", &b.config.DriverConfig)
steps := []multistep.Step{ steps := []multistep.Step{
&vmwcommon.StepPrepareTools{ &vmwcommon.StepPrepareTools{
@ -327,6 +293,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&stepCreateVMX{}, &stepCreateVMX{},
&vmwcommon.StepConfigureVMX{ &vmwcommon.StepConfigureVMX{
CustomData: b.config.VMXData, CustomData: b.config.VMXData,
VMName: b.config.VMName,
}, },
&vmwcommon.StepSuppressMessages{}, &vmwcommon.StepSuppressMessages{},
&common.StepHTTPServer{ &common.StepHTTPServer{
@ -341,8 +308,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VNCPortMax: b.config.VNCPortMax, VNCPortMax: b.config.VNCPortMax,
VNCDisablePassword: b.config.VNCDisablePassword, VNCDisablePassword: b.config.VNCDisablePassword,
}, },
&StepRegister{ &vmwcommon.StepRegister{
Format: b.config.Format, Format: b.config.Format,
KeepRegistered: b.config.KeepRegistered,
SkipExport: b.config.SkipExport,
}, },
&vmwcommon.StepRun{ &vmwcommon.StepRun{
DurationBeforeStop: 5 * time.Second, DurationBeforeStop: 5 * time.Second,
@ -382,17 +351,20 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&vmwcommon.StepConfigureVMX{ &vmwcommon.StepConfigureVMX{
CustomData: b.config.VMXDataPost, CustomData: b.config.VMXDataPost,
SkipFloppy: true, SkipFloppy: true,
VMName: b.config.VMName,
}, },
&vmwcommon.StepCleanVMX{ &vmwcommon.StepCleanVMX{
RemoveEthernetInterfaces: b.config.VMXConfig.VMXRemoveEthernet, RemoveEthernetInterfaces: b.config.VMXConfig.VMXRemoveEthernet,
VNCEnabled: !b.config.DisableVNC, VNCEnabled: !b.config.DisableVNC,
}, },
&StepUploadVMX{ &vmwcommon.StepUploadVMX{
RemoteType: b.config.RemoteType, RemoteType: b.config.RemoteType,
}, },
&StepExport{ &vmwcommon.StepExport{
Format: b.config.Format, Format: b.config.Format,
SkipExport: b.config.SkipExport, SkipExport: b.config.SkipExport,
VMName: b.config.VMName,
OVFToolOptions: b.config.OVFToolOptions,
OutputDir: exportOutputPath, OutputDir: exportOutputPath,
}, },
} }
@ -416,36 +388,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
} }
// Compile the artifact list // Compile the artifact list
var files []string return vmwcommon.NewArtifact(b.config.RemoteType, b.config.Format, exportOutputPath,
if b.config.RemoteType != "" && b.config.Format != "" && !b.config.SkipExport { b.config.VMName, b.config.SkipExport, b.config.KeepRegistered, state)
dir = new(vmwcommon.LocalOutputDir)
dir.SetOutputDir(exportOutputPath)
files, err = dir.ListFiles()
} else {
files, err = state.Get("dir").(OutputDir).ListFiles()
}
if err != nil {
return nil, err
}
// Set the proper builder ID
builderId := vmwcommon.BuilderId
if b.config.RemoteType != "" {
builderId = BuilderIdESX
}
config := make(map[string]string)
config[ArtifactConfKeepRegistered] = strconv.FormatBool(b.config.KeepRegistered)
config[ArtifactConfFormat] = b.config.Format
config[ArtifactConfSkipExport] = strconv.FormatBool(b.config.SkipExport)
return &Artifact{
builderId: builderId,
id: b.config.VMName,
dir: dir,
f: files,
config: config,
}, nil
} }
func (b *Builder) Cancel() { func (b *Builder) Cancel() {

View File

@ -459,13 +459,13 @@ func TestBuilderPrepare_CommConfig(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.CommConfig.WinRMUser != "username" { if b.config.SSHConfig.Comm.WinRMUser != "username" {
t.Errorf("bad winrm_username: %s", b.config.CommConfig.WinRMUser) t.Errorf("bad winrm_username: %s", b.config.SSHConfig.Comm.WinRMUser)
} }
if b.config.CommConfig.WinRMPassword != "password" { if b.config.SSHConfig.Comm.WinRMPassword != "password" {
t.Errorf("bad winrm_password: %s", b.config.CommConfig.WinRMPassword) t.Errorf("bad winrm_password: %s", b.config.SSHConfig.Comm.WinRMPassword)
} }
if host := b.config.CommConfig.Host(); host != "1.2.3.4" { if host := b.config.SSHConfig.Comm.Host(); host != "1.2.3.4" {
t.Errorf("bad host: %s", host) t.Errorf("bad host: %s", host)
} }
} }
@ -487,13 +487,13 @@ func TestBuilderPrepare_CommConfig(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.CommConfig.SSHUsername != "username" { if b.config.SSHConfig.Comm.SSHUsername != "username" {
t.Errorf("bad ssh_username: %s", b.config.CommConfig.SSHUsername) t.Errorf("bad ssh_username: %s", b.config.SSHConfig.Comm.SSHUsername)
} }
if b.config.CommConfig.SSHPassword != "password" { if b.config.SSHConfig.Comm.SSHPassword != "password" {
t.Errorf("bad ssh_password: %s", b.config.CommConfig.SSHPassword) t.Errorf("bad ssh_password: %s", b.config.SSHConfig.Comm.SSHPassword)
} }
if host := b.config.CommConfig.Host(); host != "1.2.3.4" { if host := b.config.SSHConfig.Comm.Host(); host != "1.2.3.4" {
t.Errorf("bad host: %s", host) t.Errorf("bad host: %s", host)
} }
} }

View File

@ -1,44 +0,0 @@
package iso
import (
"fmt"
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
)
// NewDriver returns a new driver implementation for this operating
// system, or an error if the driver couldn't be initialized.
func NewDriver(config *Config) (vmwcommon.Driver, error) {
drivers := []vmwcommon.Driver{}
if config.RemoteType == "" {
return vmwcommon.NewDriver(&config.DriverConfig, &config.SSHConfig)
}
drivers = []vmwcommon.Driver{
&ESX5Driver{
Host: config.RemoteHost,
Port: config.RemotePort,
Username: config.RemoteUser,
Password: config.RemotePassword,
PrivateKeyFile: config.RemotePrivateKey,
Datastore: config.RemoteDatastore,
CacheDatastore: config.RemoteCacheDatastore,
CacheDirectory: config.RemoteCacheDirectory,
},
}
errs := ""
for _, driver := range drivers {
err := driver.Verify()
if err == nil {
return driver, nil
}
errs += "* " + err.Error() + "\n"
}
return nil, fmt.Errorf(
"Unable to initialize any driver for this platform. The errors\n"+
"from each driver are shown below. Please fix at least one driver\n"+
"to continue:\n%s", errs)
}

View File

@ -1,14 +0,0 @@
package iso
// OutputDir is an interface type that abstracts the creation and handling
// of the output directory for VMware-based products. The abstraction is made
// so that the output directory can be properly made on remote (ESXi) based
// VMware products as well as local.
type OutputDir interface {
DirExists() (bool, error)
ListFiles() ([]string, error)
MkdirAll() error
Remove(string) error
RemoveAll() error
SetOutputDir(string)
}

View File

@ -1,12 +0,0 @@
package iso
import (
"testing"
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
)
func TestRemoteDriverMock_impl(t *testing.T) {
var _ vmwcommon.Driver = new(RemoteDriverMock)
var _ RemoteDriver = new(RemoteDriverMock)
}

View File

@ -22,7 +22,7 @@ func (s *stepRemoteUpload) Run(_ context.Context, state multistep.StateBag) mult
driver := state.Get("driver").(vmwcommon.Driver) driver := state.Get("driver").(vmwcommon.Driver)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
remote, ok := driver.(RemoteDriver) remote, ok := driver.(vmwcommon.RemoteDriver)
if !ok { if !ok {
return multistep.ActionContinue return multistep.ActionContinue
} }
@ -36,10 +36,10 @@ func (s *stepRemoteUpload) Run(_ context.Context, state multistep.StateBag) mult
checksum := config.ISOChecksum checksum := config.ISOChecksum
checksumType := config.ISOChecksumType checksumType := config.ISOChecksumType
if esx5, ok := remote.(*ESX5Driver); ok { if esx5, ok := remote.(*vmwcommon.ESX5Driver); ok {
remotePath := esx5.cachePath(path) remotePath := esx5.CachePath(path)
if esx5.verifyChecksum(checksumType, checksum, remotePath) { if esx5.VerifyChecksum(checksumType, checksum, remotePath) {
ui.Say("Remote cache was verified skipping remote upload...") ui.Say("Remote cache was verified skipping remote upload...")
state.Put(s.Key, remotePath) state.Put(s.Key, remotePath)
return multistep.ActionContinue return multistep.ActionContinue
@ -68,7 +68,7 @@ func (s *stepRemoteUpload) Cleanup(state multistep.StateBag) {
driver := state.Get("driver").(vmwcommon.Driver) driver := state.Get("driver").(vmwcommon.Driver)
remote, ok := driver.(RemoteDriver) remote, ok := driver.(vmwcommon.RemoteDriver)
if !ok { if !ok {
return return
} }

View File

@ -34,13 +34,27 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
// Run executes a Packer build and returns a packer.Artifact representing // Run executes a Packer build and returns a packer.Artifact representing
// a VMware image. // a VMware image.
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) {
driver, err := vmwcommon.NewDriver(&b.config.DriverConfig, &b.config.SSHConfig) driver, err := vmwcommon.NewDriver(&b.config.DriverConfig, &b.config.SSHConfig, b.config.VMName)
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)
} }
// Setup the directory // Determine the output dir implementation
dir := new(vmwcommon.LocalOutputDir) var dir vmwcommon.OutputDir
switch d := driver.(type) {
case vmwcommon.OutputDir:
dir = d
default:
dir = new(vmwcommon.LocalOutputDir)
}
// The OutputDir will track remote esxi output; exportOutputPath preserves
// the path to the output on the machine running Packer.
exportOutputPath := b.config.OutputDir
if b.config.RemoteType != "" {
b.config.OutputDir = b.config.VMName
}
dir.SetOutputDir(b.config.OutputDir) dir.SetOutputDir(b.config.OutputDir)
// Set up the state. // Set up the state.
@ -51,6 +65,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
state.Put("driver", driver) state.Put("driver", driver)
state.Put("hook", hook) state.Put("hook", hook)
state.Put("ui", ui) state.Put("ui", ui)
state.Put("sshConfig", &b.config.SSHConfig)
state.Put("driverConfig", &b.config.DriverConfig)
// Build the steps. // Build the steps.
steps := []multistep.Step{ steps := []multistep.Step{
@ -73,6 +89,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
&vmwcommon.StepConfigureVMX{ &vmwcommon.StepConfigureVMX{
CustomData: b.config.VMXData, CustomData: b.config.VMXData,
VMName: b.config.VMName,
}, },
&vmwcommon.StepSuppressMessages{}, &vmwcommon.StepSuppressMessages{},
&common.StepHTTPServer{ &common.StepHTTPServer{
@ -80,6 +97,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
HTTPPortMin: b.config.HTTPPortMin, HTTPPortMin: b.config.HTTPPortMin,
HTTPPortMax: b.config.HTTPPortMax, HTTPPortMax: b.config.HTTPPortMax,
}, },
&vmwcommon.StepUploadVMX{
RemoteType: b.config.RemoteType,
},
&vmwcommon.StepConfigureVNC{ &vmwcommon.StepConfigureVNC{
Enabled: !b.config.DisableVNC, Enabled: !b.config.DisableVNC,
VNCBindAddress: b.config.VNCBindAddress, VNCBindAddress: b.config.VNCBindAddress,
@ -87,6 +107,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VNCPortMax: b.config.VNCPortMax, VNCPortMax: b.config.VNCPortMax,
VNCDisablePassword: b.config.VNCDisablePassword, VNCDisablePassword: b.config.VNCDisablePassword,
}, },
&vmwcommon.StepRegister{
Format: b.config.Format,
KeepRegistered: b.config.KeepRegistered,
SkipExport: b.config.SkipExport,
},
&vmwcommon.StepRun{ &vmwcommon.StepRun{
DurationBeforeStop: 5 * time.Second, DurationBeforeStop: 5 * time.Second,
Headless: b.config.Headless, Headless: b.config.Headless,
@ -125,11 +150,22 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&vmwcommon.StepConfigureVMX{ &vmwcommon.StepConfigureVMX{
CustomData: b.config.VMXDataPost, CustomData: b.config.VMXDataPost,
SkipFloppy: true, SkipFloppy: true,
VMName: b.config.VMName,
}, },
&vmwcommon.StepCleanVMX{ &vmwcommon.StepCleanVMX{
RemoveEthernetInterfaces: b.config.VMXConfig.VMXRemoveEthernet, RemoveEthernetInterfaces: b.config.VMXConfig.VMXRemoveEthernet,
VNCEnabled: !b.config.DisableVNC, VNCEnabled: !b.config.DisableVNC,
}, },
&vmwcommon.StepUploadVMX{
RemoteType: b.config.RemoteType,
},
&vmwcommon.StepExport{
Format: b.config.Format,
SkipExport: b.config.SkipExport,
VMName: b.config.VMName,
OVFToolOptions: b.config.OVFToolOptions,
OutputDir: exportOutputPath,
},
} }
// Run the steps. // Run the steps.
@ -150,7 +186,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
return nil, errors.New("Build was halted.") return nil, errors.New("Build was halted.")
} }
return vmwcommon.NewLocalArtifact(b.config.VMName, b.config.OutputDir) // Artifact
log.Printf("Generating artifact...")
return vmwcommon.NewArtifact(b.config.RemoteType, b.config.Format, exportOutputPath,
b.config.VMName, b.config.SkipExport, b.config.KeepRegistered, state)
} }
// Cancel. // Cancel.

View File

@ -25,10 +25,10 @@ type Config struct {
vmwcommon.SSHConfig `mapstructure:",squash"` vmwcommon.SSHConfig `mapstructure:",squash"`
vmwcommon.ToolsConfig `mapstructure:",squash"` vmwcommon.ToolsConfig `mapstructure:",squash"`
vmwcommon.VMXConfig `mapstructure:",squash"` vmwcommon.VMXConfig `mapstructure:",squash"`
vmwcommon.ExportConfig `mapstructure:",squash"`
Linked bool `mapstructure:"linked"` Linked bool `mapstructure:"linked"`
RemoteType string `mapstructure:"remote_type"` RemoteType string `mapstructure:"remote_type"`
SkipCompaction bool `mapstructure:"skip_compaction"`
SourcePath string `mapstructure:"source_path"` SourcePath string `mapstructure:"source_path"`
VMName string `mapstructure:"vm_name"` VMName string `mapstructure:"vm_name"`
@ -69,7 +69,9 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
errs = packer.MultiErrorAppend(errs, c.VMXConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.VMXConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...)
if c.RemoteType == "" {
if c.SourcePath == "" { if c.SourcePath == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is blank, but is required")) errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is blank, but is required"))
} else { } else {
@ -78,6 +80,27 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
fmt.Errorf("source_path is invalid: %s", err)) fmt.Errorf("source_path is invalid: %s", err))
} }
} }
} else {
// Remote configuration validation
if c.RemoteHost == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("remote_host must be specified"))
}
if c.RemoteType != "esx5" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Only 'esx5' value is accepted for remote_type"))
}
}
if c.Format == "" {
c.Format = "ovf"
}
if !(c.Format == "ova" || c.Format == "ovf" || c.Format == "vmx") {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("format must be one of ova, ovf, or vmx"))
}
// Warnings // Warnings
var warnings []string var warnings []string

View File

@ -3,7 +3,9 @@ package vmx
import ( import (
"context" "context"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -18,9 +20,15 @@ type StepCloneVMX struct {
Path string Path string
VMName string VMName string
Linked bool Linked bool
tempDir string
} }
func (s *StepCloneVMX) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { func (s *StepCloneVMX) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
halt := func(err error) multistep.StepAction {
state.Put("error", err)
return multistep.ActionHalt
}
driver := state.Get("driver").(vmwcommon.Driver) driver := state.Get("driver").(vmwcommon.Driver)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
@ -29,9 +37,11 @@ func (s *StepCloneVMX) Run(_ context.Context, state multistep.StateBag) multiste
ui.Say("Cloning source VM...") ui.Say("Cloning source VM...")
log.Printf("Cloning from: %s", s.Path) log.Printf("Cloning from: %s", s.Path)
log.Printf("Cloning to: %s", vmxPath) log.Printf("Cloning to: %s", vmxPath)
if err := driver.Clone(vmxPath, s.Path, s.Linked); err != nil { if err := driver.Clone(vmxPath, s.Path, s.Linked); err != nil {
state.Put("error", err) state.Put("error", err)
return multistep.ActionHalt return halt(err)
} }
// Read in the machine configuration from the cloned VMX file // Read in the machine configuration from the cloned VMX file
@ -40,10 +50,22 @@ func (s *StepCloneVMX) Run(_ context.Context, state multistep.StateBag) multiste
// network type so that it can work out things like IP's and MAC // network type so that it can work out things like IP's and MAC
// addresses // addresses
// * The disk compaction step needs the paths to all attached disks // * The disk compaction step needs the paths to all attached disks
if remoteDriver, ok := driver.(vmwcommon.RemoteDriver); ok {
remoteVmxPath := vmxPath
tempDir, err := ioutil.TempDir("", "packer-vmx")
if err != nil {
return halt(err)
}
s.tempDir = tempDir
vmxPath = filepath.Join(tempDir, s.VMName+".vmx")
if err = remoteDriver.Download(remoteVmxPath, vmxPath); err != nil {
return halt(err)
}
}
vmxData, err := vmwcommon.ReadVMX(vmxPath) vmxData, err := vmwcommon.ReadVMX(vmxPath)
if err != nil { if err != nil {
state.Put("error", err) return halt(err)
return multistep.ActionHalt
} }
var diskFilenames []string var diskFilenames []string
@ -104,4 +126,7 @@ func (s *StepCloneVMX) Run(_ context.Context, state multistep.StateBag) multiste
} }
func (s *StepCloneVMX) Cleanup(state multistep.StateBag) { func (s *StepCloneVMX) Cleanup(state multistep.StateBag) {
if s.tempDir != "" {
os.RemoveAll(s.tempDir)
}
} }

View File

@ -8,7 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/hashicorp/packer/builder/vmware/iso" vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
"github.com/hashicorp/packer/common" "github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/multistep"
@ -20,7 +20,7 @@ import (
var builtins = map[string]string{ var builtins = map[string]string{
vsphere.BuilderId: "vmware", vsphere.BuilderId: "vmware",
iso.BuilderIdESX: "vmware", vmwcommon.BuilderIdESX: "vmware",
} }
type Config struct { type Config struct {
@ -96,9 +96,9 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac
"Artifact type %s does not fit this requirement", artifact.BuilderId()) "Artifact type %s does not fit this requirement", artifact.BuilderId())
} }
f := artifact.State(iso.ArtifactConfFormat) f := artifact.State(vmwcommon.ArtifactConfFormat)
k := artifact.State(iso.ArtifactConfKeepRegistered) k := artifact.State(vmwcommon.ArtifactConfKeepRegistered)
s := artifact.State(iso.ArtifactConfSkipExport) s := artifact.State(vmwcommon.ArtifactConfSkipExport)
if f != "" && k != "true" && s == "false" { if f != "" && k != "true" && s == "false" {
return nil, false, errors.New("To use this post-processor with exporting behavior you need set keep_registered as true") return nil, false, errors.New("To use this post-processor with exporting behavior you need set keep_registered as true")

View File

@ -56,7 +56,8 @@ builder.
### Required: ### Required:
- `source_path` (string) - Path to the source VMX file to clone. - `source_path` (string) - Path to the source VMX file to clone. If
`remote_type` is enabled then this specifies a path on the `remote_host`.
### Optional: ### Optional:
@ -124,6 +125,40 @@ builder.
the builder. By default this is `output-BUILDNAME` where "BUILDNAME" is the the builder. By default this is `output-BUILDNAME` where "BUILDNAME" is the
name of the build. name of the build.
- `remote_cache_datastore` (string) - The path to the datastore where
supporting files will be stored during the build on the remote machine. By
default this is the same as the `remote_datastore` option. This only has an
effect if `remote_type` is enabled.
- `remote_cache_directory` (string) - The path where the ISO and/or floppy
files will be stored during the build on the remote machine. The path is
relative to the `remote_cache_datastore` on the remote machine. By default
this is "packer\_cache". This only has an effect if `remote_type`
is enabled.
- `remote_datastore` (string) - The path to the datastore where the resulting
VM will be stored when it is built on the remote machine. By default this
is "datastore1". This only has an effect if `remote_type` is enabled.
- `remote_host` (string) - The host of the remote machine used for access.
This is only required if `remote_type` is enabled.
- `remote_password` (string) - The SSH password for the user used to access
the remote machine. By default this is empty. This only has an effect if
`remote_type` is enabled.
- `remote_private_key_file` (string) - The path to the PEM encoded private key
file for the user used to access the remote machine. By default this is empty.
This only has an effect if `remote_type` is enabled.
- `remote_type` (string) - The type of remote machine that will be used to
build this VM rather than a local desktop product. The only value accepted
for this currently is "esx5". If this is not set, a desktop product will
be used. By default, this is not set.
- `remote_username` (string) - The username for the SSH user that will access
the remote machine. This is required if `remote_type` is enabled.
- `shutdown_command` (string) - The command to use to gracefully shut down the - `shutdown_command` (string) - The command to use to gracefully shut down the
machine once all the provisioning is done. By default this is an empty machine once all the provisioning is done. By default this is an empty
string, which tells Packer to just forcefully shut down the machine unless a string, which tells Packer to just forcefully shut down the machine unless a
@ -157,6 +192,25 @@ builder.
slightly larger. If you find this to be the case, you can disable compaction slightly larger. If you find this to be the case, you can disable compaction
using this configuration value. Defaults to `false`. using this configuration value. Defaults to `false`.
- `skip_export` (boolean) - Defaults to `false`. When enabled, Packer will
not export the VM. Useful if the build output is not the resultant image,
but created inside the VM.
- `keep_registered` (boolean) - Set this to `true` if you would like to keep
the VM registered with the remote ESXi server. This is convenient if you
use packer to provision VMs on ESXi and don't want to use ovftool to
deploy the resulting artifact (VMX or OVA or whatever you used as `format`).
Defaults to `false`.
- `ovftool_options` (array of strings) - Extra options to pass to ovftool
during export. Each item in the array is a new argument. The options
`--noSSLVerify`, `--skipManifestCheck`, and `--targetType` are reserved,
and should not be passed to this argument.
- `format` (string) - Either "ovf", "ova" or "vmx", this specifies the output
format of the exported virtual machine. This defaults to "ovf".
Before using this option, you need to install `ovftool`.
- `tools_upload_flavor` (string) - The flavor of the VMware Tools ISO to - `tools_upload_flavor` (string) - The flavor of the VMware Tools ISO to
upload into the VM. Valid values are `darwin`, `linux`, and `windows`. By upload into the VM. Valid values are `darwin`, `linux`, and `windows`. By
default, this is empty, which means VMware tools won't be uploaded. default, this is empty, which means VMware tools won't be uploaded.
@ -230,6 +284,46 @@ contention. You can tune this delay on a per-builder basis by specifying
} }
``` ```
- `<f1>` - `<f12>` - Simulates pressing a function key.
- `<up>` `<down>` `<left>` `<right>` - Simulates pressing an arrow key.
- `<spacebar>` - Simulates pressing the spacebar.
- `<insert>` - Simulates pressing the insert key.
- `<home>` `<end>` - Simulates pressing the home and end keys.
- `<pageUp>` `<pageDown>` - Simulates pressing the page up and page down keys.
- `<leftAlt>` `<rightAlt>` - Simulates pressing the alt key.
- `<leftCtrl>` `<rightCtrl>` - Simulates pressing the ctrl key.
- `<leftShift>` `<rightShift>` - Simulates pressing the shift key.
- `<leftAltOn>` `<rightAltOn>` - Simulates pressing and holding the alt key.
- `<leftCtrlOn>` `<rightCtrlOn>` - Simulates pressing and holding the ctrl
key.
- `<leftShiftOn>` `<rightShiftOn>` - Simulates pressing and holding the
shift key.
- `<leftAltOff>` `<rightAltOff>` - Simulates releasing a held alt key.
- `<leftCtrlOff>` `<rightCtrlOff>` - Simulates releasing a held ctrl key.
- `<leftShiftOff>` `<rightShiftOff>` - Simulates releasing a held shift key.
- `<wait>` `<wait5>` `<wait10>` - Adds a 1, 5 or 10 second pause before
sending any additional keys. This is useful if you have to generally wait
for the UI to update before typing more.
In addition to the special keys, each command to type is treated as a
[configuration template](/docs/templates/configuration-templates.html). The
available variables are:
<%= partial "partials/builders/boot-command" %> <%= partial "partials/builders/boot-command" %>
Example boot command. This is actually a working boot command used to start an Example boot command. This is actually a working boot command used to start an