diff --git a/build.sh b/build.sh index efcf7e2b8..e652100c3 100755 --- a/build.sh +++ b/build.sh @@ -11,3 +11,7 @@ rm -f bin/* GOOS=darwin go build -o bin/packer-builder-vsphere-clone.macos ./clone GOOS=linux go build -o bin/packer-builder-vsphere-clone.linux ./clone GOOS=windows go build -o bin/packer-builder-vsphere-clone.exe ./clone + +GOOS=darwin go build -o bin/packer-builder-vsphere-iso.macos ./iso +GOOS=linux go build -o bin/packer-builder-vsphere-iso.linux ./iso +GOOS=windows go build -o bin/packer-builder-vsphere-iso.exe ./iso diff --git a/clone/builder.go b/clone/builder.go index 53f3a1d4b..334484a38 100644 --- a/clone/builder.go +++ b/clone/builder.go @@ -1,8 +1,6 @@ package clone import ( - "errors" - packerCommon "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/helper/communicator" "github.com/hashicorp/packer/packer" @@ -75,18 +73,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe b.runner = packerCommon.NewRunner(steps, b.config.PackerConfig, ui) b.runner.Run(state) - // If there was an error, return that - if rawErr, ok := state.GetOk("error"); ok { - return nil, rawErr.(error) - } - - // If we were interrupted or cancelled, then just exit. - if _, ok := state.GetOk(multistep.StateCancelled); ok { - return nil, errors.New("Build was cancelled.") - } - - if _, ok := state.GetOk(multistep.StateHalted); ok { - return nil, errors.New("Build was halted.") + if err := common.CheckRunStatus(state); err != nil { + return nil, err } artifact := &common.Artifact{ diff --git a/clone/builder_acc_test.go b/clone/builder_acc_test.go index 66e4d2a2d..fbd4d94dc 100644 --- a/clone/builder_acc_test.go +++ b/clone/builder_acc_test.go @@ -1,21 +1,20 @@ package clone import ( - "encoding/json" - "fmt" builderT "github.com/hashicorp/packer/helper/builder/testing" + commonT "github.com/jetbrains-infra/packer-builder-vsphere/common/testing" + "github.com/hashicorp/packer/packer" - "github.com/jetbrains-infra/packer-builder-vsphere/common" "github.com/jetbrains-infra/packer-builder-vsphere/driver" - "math/rand" "testing" + "github.com/jetbrains-infra/packer-builder-vsphere/common" ) func TestBuilderAcc_default(t *testing.T) { config := defaultConfig() builderT.Test(t, builderT.TestCase{ Builder: &Builder{}, - Template: renderConfig(config), + Template: commonT.RenderConfig(config), Check: checkDefault(t, config["vm_name"].(string), config["host"].(string), "datastore1"), }) } @@ -33,7 +32,7 @@ func defaultConfig() map[string]interface{} { "ssh_username": "root", "ssh_password": "jetbrains", } - config["vm_name"] = fmt.Sprintf("test-%v", rand.Intn(1000)) + config["vm_name"] = commonT.NewVMName() return config } @@ -100,7 +99,7 @@ func TestBuilderAcc_artifact(t *testing.T) { config := defaultConfig() builderT.Test(t, builderT.TestCase{ Builder: &Builder{}, - Template: renderConfig(config), + Template: commonT.RenderConfig(config), Check: checkArtifact(t), }) } @@ -133,7 +132,7 @@ func folderConfig() string { config := defaultConfig() config["folder"] = "folder1/folder2" config["linked_clone"] = true // speed up - return renderConfig(config) + return commonT.RenderConfig(config) } func checkFolder(t *testing.T, folder string) builderT.TestCheckFunc { @@ -171,7 +170,7 @@ func resourcePoolConfig() string { config := defaultConfig() config["resource_pool"] = "pool1/pool2" config["linked_clone"] = true // speed up - return renderConfig(config) + return commonT.RenderConfig(config) } func checkResourcePool(t *testing.T, pool string) builderT.TestCheckFunc { @@ -208,7 +207,7 @@ func TestBuilderAcc_datastore(t *testing.T) { func datastoreConfig() string { config := defaultConfig() config["template"] = "alpine-host4" // on esxi-4.vsphere65.test - return renderConfig(config) + return commonT.RenderConfig(config) } func checkDatastore(t *testing.T, name string) builderT.TestCheckFunc { @@ -251,7 +250,7 @@ func TestBuilderAcc_multipleDatastores(t *testing.T) { func multipleDatastoresConfig() string { config := defaultConfig() config["host"] = "esxi-4.vsphere65.test" // host with 2 datastores - return renderConfig(config) + return commonT.RenderConfig(config) } func TestBuilderAcc_linkedClone(t *testing.T) { @@ -265,7 +264,7 @@ func TestBuilderAcc_linkedClone(t *testing.T) { func linkedCloneConfig() string { config := defaultConfig() config["linked_clone"] = true - return renderConfig(config) + return commonT.RenderConfig(config) } func checkLinkedClone(t *testing.T) builderT.TestCheckFunc { @@ -303,7 +302,7 @@ func hardwareConfig() string { config["RAM_reservation"] = 1024 config["linked_clone"] = true // speed up - return renderConfig(config) + return commonT.RenderConfig(config) } func checkHardware(t *testing.T) builderT.TestCheckFunc { @@ -358,7 +357,7 @@ func RAMReservationConfig() string { config["RAM_reserve_all"] = true config["linked_clone"] = true // speed up - return renderConfig(config) + return commonT.RenderConfig(config) } func checkRAMReservation(t *testing.T) builderT.TestCheckFunc { @@ -391,7 +390,7 @@ func sshKeyConfig() string { config["ssh_password"] = "" config["ssh_private_key_file"] = "../test-key.pem" config["linked_clone"] = true // speed up - return renderConfig(config) + return commonT.RenderConfig(config) } func TestBuilderAcc_snapshot(t *testing.T) { @@ -405,7 +404,7 @@ func TestBuilderAcc_snapshot(t *testing.T) { func snapshotConfig() string { config := defaultConfig() config["create_snapshot"] = true - return renderConfig(config) + return commonT.RenderConfig(config) } func checkSnapshot(t *testing.T) builderT.TestCheckFunc { @@ -439,7 +438,7 @@ func templateConfig() string { config := defaultConfig() config["convert_to_template"] = true config["linked_clone"] = true // speed up - return renderConfig(config) + return commonT.RenderConfig(config) } func checkTemplate(t *testing.T) builderT.TestCheckFunc { @@ -460,22 +459,6 @@ func checkTemplate(t *testing.T) builderT.TestCheckFunc { } } -func renderConfig(config map[string]interface{}) string { - t := map[string][]map[string]interface{}{ - "builders": { - map[string]interface{}{ - "type": "test", - }, - }, - } - for k, v := range config { - t["builders"][0][k] = v - } - - j, _ := json.Marshal(t) - return string(j) -} - func testConn(t *testing.T) *driver.Driver { d, err := driver.NewDriver(&driver.ConnectConfig{ VCenterServer: "vcenter.vsphere65.test", diff --git a/clone/config.go b/clone/config.go index 720672eb4..950769215 100644 --- a/clone/config.go +++ b/clone/config.go @@ -3,7 +3,6 @@ package clone import ( packerCommon "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/helper/communicator" - "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template/interpolate" "github.com/jetbrains-infra/packer-builder-vsphere/common" @@ -13,7 +12,7 @@ type Config struct { packerCommon.PackerConfig `mapstructure:",squash"` common.ConnectConfig `mapstructure:",squash"` CloneConfig `mapstructure:",squash"` - HardwareConfig `mapstructure:",squash"` + common.HardwareConfig `mapstructure:",squash"` Comm communicator.Config `mapstructure:",squash"` common.ShutdownConfig `mapstructure:",squash"` CreateSnapshot bool `mapstructure:"create_snapshot"` @@ -24,14 +23,8 @@ type Config struct { func NewConfig(raws ...interface{}) (*Config, []string, error) { c := new(Config) - { - err := config.Decode(c, &config.DecodeOpts{ - Interpolate: true, - InterpolateContext: &c.ctx, - }, raws...) - if err != nil { - return nil, nil, err - } + if err := common.DecodeConfig(c, &c.ctx, raws...); err != nil { + return nil, nil, err } errs := new(packer.MultiError) diff --git a/clone/step_hardware.go b/clone/step_hardware.go index b308ba1ab..94c384405 100644 --- a/clone/step_hardware.go +++ b/clone/step_hardware.go @@ -1,42 +1,21 @@ package clone import ( - "github.com/mitchellh/multistep" "github.com/hashicorp/packer/packer" - "fmt" + "github.com/jetbrains-infra/packer-builder-vsphere/common" "github.com/jetbrains-infra/packer-builder-vsphere/driver" + "github.com/mitchellh/multistep" ) -type HardwareConfig struct { - CPUs int32 `mapstructure:"CPUs"` - CPUReservation int64 `mapstructure:"CPU_reservation"` - CPULimit int64 `mapstructure:"CPU_limit"` - RAM int64 `mapstructure:"RAM"` - RAMReservation int64 `mapstructure:"RAM_reservation"` - RAMReserveAll bool `mapstructure:"RAM_reserve_all"` - DiskSize int64 `mapstructure:"disk_size"` - NestedHV bool `mapstructure:"NestedHV"` -} - -func (c *HardwareConfig) Prepare() []error { - var errs []error - - if c.RAMReservation > 0 && c.RAMReserveAll != false { - errs = append(errs, fmt.Errorf("'RAM_reservation' and 'RAM_reserve_all' cannot be used together")) - } - - return errs -} - type StepConfigureHardware struct { - config *HardwareConfig + config *common.HardwareConfig } func (s *StepConfigureHardware) Run(state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) vm := state.Get("vm").(*driver.VirtualMachine) - if *s.config != (HardwareConfig{}) { + if *s.config != (common.HardwareConfig{}) { ui.Say("Customizing hardware parameters...") err := vm.Configure(&driver.HardwareConfig{ diff --git a/common/check_run_status.go b/common/check_run_status.go new file mode 100644 index 000000000..3d383d9fe --- /dev/null +++ b/common/check_run_status.go @@ -0,0 +1,24 @@ +package common + +import ( + "github.com/mitchellh/multistep" + "errors" +) + +func CheckRunStatus(state *multistep.BasicStateBag) error { + // If there was an error, return that + if rawErr, ok := state.GetOk("error"); ok { + return rawErr.(error) + } + + // If we were interrupted or cancelled, then just exit. + if _, ok := state.GetOk(multistep.StateCancelled); ok { + return errors.New("Build was cancelled.") + } + + if _, ok := state.GetOk(multistep.StateHalted); ok { + return errors.New("Build was halted.") + } + + return nil +} diff --git a/common/decode_config.go b/common/decode_config.go new file mode 100644 index 000000000..575a81cb4 --- /dev/null +++ b/common/decode_config.go @@ -0,0 +1,14 @@ +package common + +import ( + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/template/interpolate" +) + +func DecodeConfig(cfg interface{}, ctx *interpolate.Context, raws ...interface{}) error { + err := config.Decode(cfg, &config.DecodeOpts{ + Interpolate: true, + InterpolateContext: ctx, + }, raws...) + return err +} diff --git a/common/hardware_config.go b/common/hardware_config.go new file mode 100644 index 000000000..90df9a99a --- /dev/null +++ b/common/hardware_config.go @@ -0,0 +1,39 @@ +package common + +import ( + "fmt" + "github.com/jetbrains-infra/packer-builder-vsphere/driver" +) + +type HardwareConfig struct { + CPUs int32 `mapstructure:"CPUs"` + CPUReservation int64 `mapstructure:"CPU_reservation"` + CPULimit int64 `mapstructure:"CPU_limit"` + RAM int64 `mapstructure:"RAM"` + RAMReservation int64 `mapstructure:"RAM_reservation"` + RAMReserveAll bool `mapstructure:"RAM_reserve_all"` + DiskSize int64 `mapstructure:"disk_size"` + NestedHV bool `mapstructure:"NestedHV"` +} + +func (c *HardwareConfig) Prepare() []error { + var errs []error + + if c.RAMReservation > 0 && c.RAMReserveAll != false { + errs = append(errs, fmt.Errorf("'RAM_reservation' and 'RAM_reserve_all' cannot be used together")) + } + + return errs +} + +func (c *HardwareConfig) ToDriverHardwareConfig() driver.HardwareConfig { + return driver.HardwareConfig{ + CPUs: c.CPUs, + CPUReservation: c.CPUReservation, + CPULimit: c.CPULimit, + RAM: c.RAM, + RAMReservation: c.RAMReservation, + RAMReserveAll: c.RAMReserveAll, + DiskSize: c.DiskSize, + } +} diff --git a/common/testing/utility.go b/common/testing/utility.go new file mode 100644 index 000000000..8a26b6d56 --- /dev/null +++ b/common/testing/utility.go @@ -0,0 +1,29 @@ +package testing + +import ( + "fmt" + "math/rand" + "time" + "encoding/json" +) + +func NewVMName() string { + rand.Seed(time.Now().UnixNano()) + return fmt.Sprintf("test-%v", rand.Intn(1000)) +} + +func RenderConfig(config map[string]interface{}) string { + t := map[string][]map[string]interface{}{ + "builders": { + map[string]interface{}{ + "type": "test", + }, + }, + } + for k, v := range config { + t["builders"][0][k] = v + } + + j, _ := json.Marshal(t) + return string(j) +} diff --git a/iso/builder.go b/iso/builder.go new file mode 100644 index 000000000..18bd4e450 --- /dev/null +++ b/iso/builder.go @@ -0,0 +1,70 @@ +package iso + +import ( + packerCommon "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/packer" + "github.com/jetbrains-infra/packer-builder-vsphere/common" + "github.com/jetbrains-infra/packer-builder-vsphere/driver" + "github.com/mitchellh/multistep" +) + +type Builder struct { + config *Config + runner multistep.Runner +} + +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + c, warnings, errs := NewConfig(raws...) + if errs != nil { + return warnings, errs + } + b.config = c + + return warnings, nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + state := new(multistep.BasicStateBag) + state.Put("comm", &b.config.Comm) + state.Put("hook", hook) + state.Put("ui", ui) + + steps := []multistep.Step{} + + steps = append(steps, + &common.StepConnect{ + Config: &b.config.ConnectConfig, + }, + &StepCreateVM{ + config: &b.config.CreateConfig, + }, + ) + + if b.config.CDRomConfig.ISOPath != "" { + steps = append(steps, + &StepAddCDRom{ + config: &b.config.CDRomConfig, + }, + ) + } + + // Run! + b.runner = packerCommon.NewRunner(steps, b.config.PackerConfig, ui) + b.runner.Run(state) + + if err := common.CheckRunStatus(state); err != nil { + return nil, err + } + + artifact := &common.Artifact{ + Name: b.config.VMName, + VM: state.Get("vm").(*driver.VirtualMachine), + } + return artifact, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + b.runner.Cancel() + } +} diff --git a/iso/builder_acc_test.go b/iso/builder_acc_test.go new file mode 100644 index 000000000..8269761d2 --- /dev/null +++ b/iso/builder_acc_test.go @@ -0,0 +1,33 @@ +package iso + +import ( + builderT "github.com/hashicorp/packer/helper/builder/testing" + commonT "github.com/jetbrains-infra/packer-builder-vsphere/common/testing" + "testing" +) + +func TestBuilderAcc_default(t *testing.T) { + config := defaultConfig() + builderT.Test(t, builderT.TestCase{ + Builder: &Builder{}, + Template: commonT.RenderConfig(config), + }) +} + +func defaultConfig() map[string]interface{} { + config := map[string]interface{}{ + "vcenter_server": "vcenter.vsphere65.test", + "username": "root", + "password": "jetbrains", + "insecure_connection": true, + + "host": "esxi-1.vsphere65.test", + + "ssh_username": "root", + "ssh_password": "jetbrains", + + "vm_name": commonT.NewVMName(), + } + + return config +} diff --git a/iso/config.go b/iso/config.go new file mode 100644 index 000000000..aec7c90fb --- /dev/null +++ b/iso/config.go @@ -0,0 +1,43 @@ +package iso + +import ( + packerCommon "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" + "github.com/jetbrains-infra/packer-builder-vsphere/common" +) + +type Config struct { + packerCommon.PackerConfig `mapstructure:",squash"` + common.ConnectConfig `mapstructure:",squash"` + Comm communicator.Config `mapstructure:",squash"` + common.ShutdownConfig `mapstructure:",squash"` + CreateSnapshot bool `mapstructure:"create_snapshot"` + ConvertToTemplate bool `mapstructure:"convert_to_template"` + + CreateConfig `mapstructure:",squash"` + CDRomConfig `mapstructure:",squash"` + + ctx interpolate.Context +} + +func NewConfig(raws ...interface{}) (*Config, []string, error) { + c := new(Config) + if err := common.DecodeConfig(c, &c.ctx, raws...); err != nil { + return nil, nil, err + } + + errs := new(packer.MultiError) + errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...) + errs = packer.MultiErrorAppend(errs, c.ConnectConfig.Prepare()...) + errs = packer.MultiErrorAppend(errs, c.HardwareConfig.Prepare()...) + errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare()...) + errs = packer.MultiErrorAppend(errs, c.CreateConfig.Prepare()...) + + if len(errs.Errors) > 0 { + return nil, nil, errs + } + + return c, nil, nil +} diff --git a/iso/step_add_cdrom.go b/iso/step_add_cdrom.go new file mode 100644 index 000000000..136bc6ab5 --- /dev/null +++ b/iso/step_add_cdrom.go @@ -0,0 +1,40 @@ +package iso + +import ( + "github.com/hashicorp/packer/packer" + "github.com/jetbrains-infra/packer-builder-vsphere/driver" + "github.com/mitchellh/multistep" +) + +type CDRomConfig struct { + ISOPath string `mapstructure:"iso_path"` +} + +func (c *CDRomConfig) Prepare() []error { + var errs []error + + return errs +} + +type StepAddCDRom struct { + config *CDRomConfig +} + +func (s *StepAddCDRom) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + ui.Say("Adding CDRom ...") + + vm := state.Get("vm").(*driver.VirtualMachine) + err := vm.AddCdrom(s.config.ISOPath) + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepAddCDRom) Cleanup(state multistep.StateBag) { + // nothing +} diff --git a/iso/step_create.go b/iso/step_create.go new file mode 100644 index 000000000..c47e9fb8d --- /dev/null +++ b/iso/step_create.go @@ -0,0 +1,110 @@ +package iso + +import ( + "fmt" + "github.com/hashicorp/packer/packer" + "github.com/jetbrains-infra/packer-builder-vsphere/common" + "github.com/jetbrains-infra/packer-builder-vsphere/driver" + "github.com/mitchellh/multistep" +) + +type CreateConfig struct { + common.HardwareConfig `mapstructure:",squash"` + + DiskThinProvisioned bool `mapstructure:"disk_thin_provisioned"` + DiskControllerType string `mapstructure:"disk_controller_type"` + + VMName string `mapstructure:"vm_name"` + Folder string `mapstructure:"folder"` + Host string `mapstructure:"host"` + ResourcePool string `mapstructure:"resource_pool"` + Datastore string `mapstructure:"datastore"` + GuestOSType string `mapstructure:"guest_os_type"` +} + +func (c *CreateConfig) Prepare() []error { + var errs []error + + // needed to avoid changing the original config in case of errors + tmp := *c + + // do recursive calls + errs = append(errs, tmp.HardwareConfig.Prepare()...) + + // check for errors + if tmp.VMName == "" { + errs = append(errs, fmt.Errorf("Target VM name is required")) + } + if tmp.Host == "" { + errs = append(errs, fmt.Errorf("vSphere host is required")) + } + + if len(errs) > 0 { + return errs + } + + // set default values + if tmp.GuestOSType == "" { + tmp.GuestOSType = "otherGuest" + } + + // change the original config + *c = tmp + + return []error{} +} + +type StepCreateVM struct { + config *CreateConfig +} + +func (s *StepCreateVM) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + d := state.Get("driver").(*driver.Driver) + + ui.Say("Creating VM...") + + vm, err := d.CreateVM(&driver.CreateConfig{ + HardwareConfig: s.config.HardwareConfig.ToDriverHardwareConfig(), + + DiskThinProvisioned: s.config.DiskThinProvisioned, + DiskControllerType: s.config.DiskControllerType, + Name: s.config.VMName, + Folder: s.config.Folder, + Host: s.config.Host, + ResourcePool: s.config.ResourcePool, + Datastore: s.config.Datastore, + GuestOS: s.config.GuestOSType, + }) + + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + state.Put("vm", vm) + return multistep.ActionContinue +} + +func (s *StepCreateVM) Cleanup(state multistep.StateBag) { + _, cancelled := state.GetOk(multistep.StateCancelled) + _, halted := state.GetOk(multistep.StateHalted) + if !cancelled && !halted { + return + } + + ui := state.Get("ui").(packer.Ui) + + st := state.Get("vm") + if st == nil { + return + } + vm := st.(*driver.VirtualMachine) + + ui.Say("Destroying VM...") + + err := vm.Destroy() + if err != nil { + ui.Error(err.Error()) + } +} diff --git a/iso/test.sh b/iso/test.sh new file mode 100755 index 000000000..dd91fee3a --- /dev/null +++ b/iso/test.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +export PACKER_ACC=1 +go test -v "$@" diff --git a/test.sh b/test.sh index e69b13b42..34da1fcb7 100755 --- a/test.sh +++ b/test.sh @@ -2,3 +2,4 @@ (cd driver && ./test.sh) (cd clone && ./test.sh) +(cd iso && ./test.sh) \ No newline at end of file