* the very initial builder: just clones the VM

* removed post-processor

* builder now waits for IP address

* more separated steps

* clean up

* added run and shutdown steps

* added step connecting via ssh

* changed the BuilderId to a proper one

* added shutdown command, only echo works

* added cleanup

* removed wainting for VM to stop, removed error on the non-zero exit status

* removed BuildSuccessFlag

* refactored config structures; fixed interpolation of json template

* the rest of configuration refactoring

* removed duplicated parameters from Config

* removed duplicated parameters drom Config

* changed BuilderId and Artifact

* create a dedicated step for VM hardware configuration

* merged StepSetupCloningEnv into StepCloneVM

* improved cleanup

* added proper handling of 'disconnected' command exit status; added guest os shutdown before halting the machine [in case when no shutdown command is given]

* refactored non-conventional variable and field names

* removed redundant fields from Artifact

* removed the success field at all

* removed ArtifactFiles

* removed unnecessary warnings

* minor change in parameters structure
pull/8480/head
Elizaveta Tretyakova 9 years ago committed by GitHub
parent 8b2a195efd
commit b2c6e44c70

@ -1,21 +1,9 @@
package main
import "fmt"
const BuilderId = "LizaTretyakova.post-processor.vsphere-device"
type ArtifactFile struct {
Name string `json:"name"`
Size int64 `json:"size"`
}
const BuilderId = "LizaTretyakova.vsphere"
type Artifact struct {
BuildName string `json:"name"`
BuilderType string `json:"builder_type"`
BuildTime int64 `json:"build_time"`
ArtifactFiles []ArtifactFile `json:"files"`
ArtifactId string `json:"artifact_id"`
PackerRunUUID string `json:"packer_run_uuid"`
VMName string `json:"vm_name"`
}
func (a *Artifact) BuilderId() string {
@ -23,19 +11,15 @@ func (a *Artifact) BuilderId() string {
}
func (a *Artifact) Files() []string {
var files []string
for _, af := range a.ArtifactFiles {
files = append(files, af.Name)
}
return files
return []string{}
}
func (a *Artifact) Id() string {
return a.ArtifactId
return a.VMName
}
func (a *Artifact) String() string {
return fmt.Sprintf("%s-%s", a.BuildName, a.ArtifactId)
return a.VMName
}
func (a *Artifact) State(name string) interface{} {

@ -0,0 +1,99 @@
package main
import (
"errors"
"log"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/packer"
"github.com/mitchellh/multistep"
"github.com/hashicorp/packer/helper/communicator"
gossh "golang.org/x/crypto/ssh"
"github.com/hashicorp/packer/communicator/ssh"
)
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) {
// Set up the state.
state := new(multistep.BasicStateBag)
state.Put("hook", hook)
state.Put("ui", ui)
// Build the steps.
steps := []multistep.Step{
&StepConfigureHW{
config: b.config,
},
&StepCloneVM{
config: b.config,
},
&StepRun{},
&communicator.StepConnect{
Config: &b.config.Config,
Host: func(state multistep.StateBag) (string, error) {
return state.Get("ip").(string), nil
},
SSHConfig: func(multistep.StateBag) (*gossh.ClientConfig, error) {
return &gossh.ClientConfig{
User: b.config.Config.SSHUsername,
Auth: []gossh.AuthMethod{
gossh.Password(b.config.Config.SSHPassword),
gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(b.config.Config.SSHPassword)),
},
// TODO: add a proper verification
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
}, nil
},
},
&common.StepProvision{},
&StepShutdown{
Command: b.config.ShutdownCommand,
},
}
// Run!
b.runner = common.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.")
}
// No errors, must've worked
artifact := &Artifact{
VMName: b.config.VMName,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}

@ -0,0 +1,88 @@
package main
import (
"fmt"
"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"
"strconv"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
communicator.Config `mapstructure:",squash"`
Url string `mapstructure:"url"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Template string `mapstructure:"template"`
VMName string `mapstructure:"vm_name"`
FolderName string `mapstructure:"folder_name"`
DCName string `mapstructure:"dc_name"`
Cpus string `mapstructure:"cpus"`
ShutdownCommand string `mapstructure:"shutdown_command"`
Ram string `mapstructure:"RAM"`
//TODO: add more options
ctx interpolate.Context
}
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
}
// Accumulate any errors
errs := new(packer.MultiError)
// Prepare config(s)
errs = packer.MultiErrorAppend(errs, c.Config.Prepare(&c.ctx)...)
// Check the required params
if c.Url == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("URL required"))
}
if c.Username == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Username required"))
}
if c.Password == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Password required"))
}
if c.Template == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Template VM name required"))
}
if c.VMName == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Target VM name required"))
}
// Verify numeric parameters if present
if c.Cpus != "" {
if _, err = strconv.Atoi(c.Cpus); err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Invalid number of cpu sockets"))
}
}
if c.Ram != "" {
if _, err = strconv.Atoi(c.Ram); err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Invalid number for Ram"))
}
}
// Warnings
var warnings []string
if len(errs.Errors) > 0 {
return nil, warnings, errs
}
return c, warnings, nil
}

@ -7,6 +7,6 @@ func main() {
if err != nil {
panic(err)
}
server.RegisterPostProcessor(new(PostProcessor))
server.RegisterBuilder(new(Builder))
server.Serve()
}

@ -1,271 +0,0 @@
package main
import (
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
"context"
"fmt"
"net/url"
"strconv"
)
type VMOptionalParams struct {
Cpu_sockets int
Ram int
// TODO: add more options
}
type VMRequiredParams struct {
Url string
Username string
Password string
Dc_name string
Folder_name string
Vm_source_name string
Vm_target_name string
}
const DefaultFolder = ""
const Unspecified = -1
var vm_opt_params VMOptionalParams
var vm_req_params VMRequiredParams
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Url string `mapstructure:"url"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Dc_name string `mapstructure:"dc_name"`
Vm_source_name string `mapstructure:"vm_source_name"`
Vm_target_name string `mapstructure:"vm_target_name"`
Cpu_sockets string `mapstructure:"cpus"`
Ram string `mapstructure:"RAM"`
ctx interpolate.Context
}
type PostProcessor struct {
config Config
}
func (p *PostProcessor) Configure(raws ...interface{}) error {
err := config.Decode(&p.config, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &p.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{},
},
}, raws...)
if err != nil {
return err
}
// Accumulate any errors
errs := new(packer.MultiError)
// Check the required params
templates := map[string]*string{
"url": &p.config.Url,
"username": &p.config.Username,
"password": &p.config.Password,
"vm_source_name": &p.config.Vm_source_name,
}
for key, ptr := range templates {
if *ptr == "" {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("%s must be set, %s is present", key, *ptr))
}
}
if len(errs.Errors) > 0 {
return errs
}
// Set optional params
vm_req_params.Folder_name = DefaultFolder
vm_opt_params.Cpu_sockets = Unspecified
if p.config.Cpu_sockets != "" {
vm_opt_params.Cpu_sockets, err = strconv.Atoi(p.config.Cpu_sockets)
if err != nil {
return err
}
}
vm_opt_params.Ram = Unspecified
if p.config.Ram != "" {
vm_opt_params.Ram, err = strconv.Atoi(p.config.Ram)
if err != nil {
return err
}
}
// Set required params
vm_req_params.Url = p.config.Url
vm_req_params.Username = p.config.Username
vm_req_params.Password = p.config.Password
vm_req_params.Dc_name = p.config.Dc_name
vm_req_params.Vm_source_name = p.config.Vm_source_name
vm_req_params.Vm_target_name = vm_req_params.Vm_source_name + "_clone"
if p.config.Vm_target_name != "" {
vm_req_params.Vm_target_name = p.config.Vm_target_name
}
return nil
}
func CloneVM(req_params VMRequiredParams, opt_params VMOptionalParams) error {
// Prepare entities: client (authentification), finder, folder, virtual machine
client, ctx, err := createClient(req_params.Url, req_params.Username, req_params.Password)
if err != nil {
return err
}
finder, ctx, err := createFinder(ctx, client, req_params.Dc_name)
if err != nil {
return err
}
folder, err := finder.FolderOrDefault(ctx, req_params.Folder_name)
if err != nil {
return err
}
vm_src, ctx, err := findVM_by_name(ctx, finder, req_params.Vm_source_name)
if err != nil {
return err
}
// Creating spec's for cloning
var relocateSpec types.VirtualMachineRelocateSpec
var confSpec types.VirtualMachineConfigSpec
// configure HW
if opt_params.Cpu_sockets != Unspecified {
confSpec.NumCPUs = int32(opt_params.Cpu_sockets)
}
if opt_params.Ram != Unspecified {
confSpec.MemoryMB = int64(opt_params.Ram)
}
cloneSpec := types.VirtualMachineCloneSpec{
Location: relocateSpec,
Config: &confSpec,
PowerOn: false,
}
// Cloning itself
task, err := vm_src.Clone(ctx, folder, req_params.Vm_target_name, cloneSpec)
if err != nil {
return err
}
_, err = task.WaitForResult(ctx, nil)
if err != nil {
return err
}
return nil
}
func (p *PostProcessor) PostProcess(ui packer.Ui, source packer.Artifact) (packer.Artifact, bool, error) {
err := CloneVM(vm_req_params, vm_opt_params)
if err != nil {
return nil, false, err
}
// Return:
// source -- the given artifact -- since we didn't change anything;
// false -- don't force packer to keep the source artifact
// nil -- no error occured here
return source, false, nil
}
func createClient(URL, username, password string) (*govmomi.Client, context.Context, error) {
// create context
ctx := context.TODO() // an empty, default context (for those, who is unsure)
// create a client
// (connected to the specified URL,
// logged in with the username-password)
u, err := url.Parse(URL) // create a URL object from string
if err != nil {
return nil, nil, err
}
u.User = url.UserPassword(username, password) // set username and password for automatical authentification
fmt.Println(u.String())
client, err := govmomi.NewClient(ctx, u,true) // creating a client (logs in with given uname&pswd)
if err != nil {
return nil, nil, err
}
return client, ctx, nil
}
func createFinder(ctx context.Context, client *govmomi.Client, dc_name string) (*find.Finder, context.Context, error) {
// Create a finder to search for a vm with the specified name
finder := find.NewFinder(client.Client, false)
// Need to specify the datacenter
if dc_name == "" {
dc, err := finder.DefaultDatacenter(ctx)
if err != nil {
return nil, nil, err
}
var dc_mo mo.Datacenter
err = dc.Properties(ctx, dc.Reference(), []string{"name"}, &dc_mo)
if err != nil {
return nil, nil, err
}
dc_name = dc_mo.Name
finder.SetDatacenter(dc)
} else {
dc, err := finder.Datacenter(ctx, fmt.Sprintf("/%v", dc_name))
if err != nil {
return nil, nil, err
}
finder.SetDatacenter(dc)
}
return finder, ctx, nil
}
func findVM_by_name(ctx context.Context, finder *find.Finder, vm_name string) (*object.VirtualMachine, context.Context, error) {
vm_o, err := finder.VirtualMachine(ctx, vm_name)
if err != nil {
return nil, nil, err
}
return vm_o, ctx, nil
}
func ReconfigureVM(URL, username, password, dc_name, vm_name string, cpus int) error {
client, ctx, err := createClient(URL, username, password)
if err != nil {
return err
}
finder, ctx, err := createFinder(ctx, client, dc_name)
if err != nil {
return err
}
vm_o, ctx, err := findVM_by_name(ctx, finder, vm_name)
if err != nil {
return err
}
// creating new configuration for vm
vmConfigSpec := types.VirtualMachineConfigSpec{}
vmConfigSpec.NumCPUs = int32(cpus)
// finally reconfiguring
task, err := vm_o.Reconfigure(ctx, vmConfigSpec)
if err != nil {
return err
}
_, err = task.WaitForResult(ctx, nil)
if err != nil {
return err
}
return nil
}

@ -0,0 +1,172 @@
package main
import (
"github.com/vmware/govmomi"
"context"
"github.com/mitchellh/multistep"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/govmomi/object"
"github.com/hashicorp/packer/packer"
"github.com/vmware/govmomi/find"
"fmt"
"net/url"
)
type CloneParameters struct {
client *govmomi.Client
folder *object.Folder
vmSrc *object.VirtualMachine
ctx context.Context
vmName string
confSpec *types.VirtualMachineConfigSpec
}
type StepCloneVM struct{
config *Config
success bool
}
func (s *StepCloneVM) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
ui.Say("start cloning...")
confSpec := state.Get("confSpec").(types.VirtualMachineConfigSpec)
// Prepare entities: client (authentification), finder, folder, virtual machine
client, ctx, err := createClient(s.config.Url, s.config.Username, s.config.Password)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
finder, ctx, err := createFinder(ctx, client, s.config.DCName)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
folder, err := finder.FolderOrDefault(ctx, s.config.FolderName)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
vmSrc, err := finder.VirtualMachine(ctx, s.config.Template)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
cloneParameters := CloneParameters{
client: client,
folder: folder,
vmSrc: vmSrc,
ctx: ctx,
vmName: s.config.VMName,
confSpec: &confSpec,
}
vm, err := cloneVM(&cloneParameters)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
state.Put("vm", vm)
state.Put("ctx", ctx)
s.success = true
return multistep.ActionContinue
}
func (s *StepCloneVM) Cleanup(state multistep.StateBag) {
if !s.success {
return
}
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
if cancelled || halted {
vm := state.Get("vm").(*object.VirtualMachine)
ctx := state.Get("ctx").(context.Context)
ui := state.Get("ui").(packer.Ui)
ui.Say("destroying VM...")
task, err := vm.Destroy(ctx)
if err != nil {
ui.Error(err.Error())
return
}
_, err = task.WaitForResult(ctx, nil)
if err != nil {
ui.Error(err.Error())
return
}
}
}
func cloneVM(params *CloneParameters) (vm *object.VirtualMachine, err error) {
vm = nil
err = nil
// Creating specs for cloning
var relocateSpec types.VirtualMachineRelocateSpec
cloneSpec := types.VirtualMachineCloneSpec{
Location: relocateSpec,
Config: params.confSpec,
PowerOn: false,
}
// Cloning itself
task, err := params.vmSrc.Clone(params.ctx, params.folder, params.vmName, cloneSpec)
if err != nil {
return
}
info, err := task.WaitForResult(params.ctx, nil)
if err != nil {
return
}
vm = object.NewVirtualMachine(params.client.Client, info.Result.(types.ManagedObjectReference))
return vm, nil
}
func createClient(URL, username, password string) (*govmomi.Client, context.Context, error) {
// create context
ctx := context.TODO() // an empty, default context (for those, who is unsure)
// create a client
// (connected to the specified URL,
// logged in with the username-password)
u, err := url.Parse(URL) // create a URL object from string
if err != nil {
return nil, nil, err
}
u.User = url.UserPassword(username, password) // set username and password for automatical authentification
fmt.Println(u.String())
client, err := govmomi.NewClient(ctx, u,true) // creating a client (logs in with given uname&pswd)
if err != nil {
return nil, nil, err
}
return client, ctx, nil
}
func createFinder(ctx context.Context, client *govmomi.Client, dcName string) (*find.Finder, context.Context, error) {
// Create a finder to search for a vm with the specified name
finder := find.NewFinder(client.Client, false)
// Need to specify the datacenter
if dcName == "" {
dc, err := finder.DefaultDatacenter(ctx)
if err != nil {
return nil, nil, err
}
finder.SetDatacenter(dc)
} else {
dc, err := finder.Datacenter(ctx, dcName)
if err != nil {
return nil, nil, err
}
finder.SetDatacenter(dc)
}
return finder, ctx, nil
}

@ -0,0 +1,44 @@
package main
import (
"github.com/mitchellh/multistep"
"github.com/hashicorp/packer/packer"
"strconv"
"github.com/vmware/govmomi/vim25/types"
)
type StepConfigureHW struct{
config *Config
}
func (s *StepConfigureHW) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
ui.Say("configuring virtual hardware...")
var confSpec types.VirtualMachineConfigSpec
// configure HW
if s.config.Cpus != "" {
cpus, err := strconv.Atoi(s.config.Cpus)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
confSpec.NumCPUs = int32(cpus)
}
if s.config.Ram != "" {
ram, err := strconv.Atoi(s.config.Ram)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
confSpec.MemoryMB = int64(ram)
}
state.Put("confSpec", confSpec)
return multistep.ActionContinue
}
func (s *StepConfigureHW) Cleanup(multistep.StateBag) {}

@ -0,0 +1,72 @@
package main
import (
"github.com/mitchellh/multistep"
"github.com/hashicorp/packer/packer"
"github.com/vmware/govmomi/object"
"context"
"fmt"
"github.com/vmware/govmomi/vim25/types"
)
type StepRun struct{
// TODO: add boot time to provide a proper timeout during cleanup
}
func (s *StepRun) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
vm := state.Get("vm").(*object.VirtualMachine)
ctx := state.Get("ctx").(context.Context)
ui.Say("VM power on...")
task, err := vm.PowerOn(ctx)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
_, err = task.WaitForResult(ctx, nil)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
ui.Say("VM waiting for IP...")
ip, err := vm.WaitForIP(ctx)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
state.Put("ip", ip)
ui.Say(fmt.Sprintf("VM ip %v", ip))
return multistep.ActionContinue
}
func (s *StepRun) Cleanup(state multistep.StateBag) {
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
if cancelled || halted {
vm := state.Get("vm").(*object.VirtualMachine)
ctx := state.Get("ctx").(context.Context)
ui := state.Get("ui").(packer.Ui)
if state, err := vm.PowerState(ctx); state != types.VirtualMachinePowerStatePoweredOff && err == nil {
ui.Say("shutting down VM...")
task, err := vm.PowerOff(ctx)
if err != nil {
ui.Error(err.Error())
return
}
_, err = task.WaitForResult(ctx, nil)
if err != nil {
ui.Error(err.Error())
return
}
} else if err != nil {
ui.Error(err.Error())
return
}
}
}

@ -0,0 +1,83 @@
package main
import (
"github.com/mitchellh/multistep"
"github.com/hashicorp/packer/packer"
"github.com/vmware/govmomi/object"
"context"
"fmt"
"log"
"time"
"bytes"
)
type StepShutdown struct{
Command string
}
func (s *StepShutdown) Run(state multistep.StateBag) multistep.StepAction {
// is set during the communicator.StepConnect
comm := state.Get("communicator").(packer.Communicator)
ui := state.Get("ui").(packer.Ui)
vm := state.Get("vm").(*object.VirtualMachine)
ctx := state.Get("ctx").(context.Context)
ui.Say("VM shutdown...")
if s.Command != "" {
ui.Say("Gracefully halting virtual machine...")
log.Printf("Executing shutdown command: %s", s.Command)
var stdout, stderr bytes.Buffer
cmd := &packer.RemoteCmd{
Command: s.Command,
Stdout: &stdout,
Stderr: &stderr,
}
if err := comm.Start(cmd); err != nil {
err := fmt.Errorf("Failed to send shutdown command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// TODO: add timeout
for !cmd.Exited {
ui.Say("Waiting for remote cmd to finish...")
time.Sleep(150 * time.Millisecond)
}
if cmd.ExitStatus != 0 && cmd.ExitStatus != packer.CmdDisconnect {
err := fmt.Errorf("Cmd exit status %v, not 0", cmd.ExitStatus)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
} else if cmd.ExitStatus == packer.CmdDisconnect {
ui.Say("VM disconnected")
}
} else {
ui.Say("Forcibly halting virtual machine...")
err := vm.ShutdownGuest(ctx)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
task, err := vm.PowerOff(ctx)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
_, err = task.WaitForResult(ctx, nil)
if err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
}
ui.Say("VM stopped")
return multistep.ActionContinue
}
func (s *StepShutdown) Cleanup(state multistep.StateBag) {}
Loading…
Cancel
Save