Skip to content

Commit

Permalink
Merge pull request #158 from terraform-providers/b-customization-net-…
Browse files Browse the repository at this point in the history
…wait-fix

r/virtual_machine: Updated customization and net waiter behaviour
  • Loading branch information
vancluever authored Sep 14, 2017
2 parents 48df967 + cac4cc2 commit b35eeb9
Show file tree
Hide file tree
Showing 6 changed files with 579 additions and 57 deletions.
1 change: 1 addition & 0 deletions tf-vsphere-devrc.mk.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export VSPHERE_ALLOW_UNVERIFIED_SSL ?= false
# The following variables are shared across various tests. To ensure all tests
# succeed, it's probably best to set all of these to valid values.
export VSPHERE_TEMPLATE ?= base-linux # VM template to clone
export VSPHERE_TEMPLATE_WINDOWS ?= base-win # Windows VM template
export VSPHERE_NETWORK_LABEL ?= vm-network # Port group label
export VSPHERE_NETWORK_LABEL_DHCP ?= vm-network # Port group label for DHCP
export VSPHERE_IPV4_ADDRESS ?= 10.0.0.100 # Customization IP address
Expand Down
129 changes: 129 additions & 0 deletions vsphere/event_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package vsphere

import (
"context"
"errors"
"fmt"

"github.com/vmware/govmomi"
"github.com/vmware/govmomi/event"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
)

// A list of known event IDs that we use for querying events.
const (
eventTypeCustomizationSucceeded = "CustomizationSucceeded"
)

// virtualMachineCustomizationWaiter is an object that waits for customization
// of a VirtualMachine to complete, by watching for success or failure events.
//
// The waiter should be created with newWaiter **before** the start of the
// customization task to be 100% certain that completion events are not missed.
type virtualMachineCustomizationWaiter struct {
// This channel will be closed upon completion, and should be blocked on.
done chan struct{}

// Any error received from the waiter - be it the customization failure
// itself, timeouts waiting for the completion events, or other API-related
// errors. This will always be nil until done is closed.
err error
}

// Done returns the done channel. This channel will be closed upon completion,
// and should be blocked on.
func (w *virtualMachineCustomizationWaiter) Done() chan struct{} {
return w.done
}

// Err returns any error received from the waiter. This will always be nil
// until the channel returned by Done is closed.
func (w *virtualMachineCustomizationWaiter) Err() error {
return w.err
}

// newVirtualMachineCustomizationWaiter returns a new
// virtualMachineCustomizationWaiter to use to wait for customization on.
//
// This should be called **before** the start of the customization task to be
// 100% certain that completion events are not missed.
func newVirtualMachineCustomizationWaiter(client *govmomi.Client, vm *object.VirtualMachine) *virtualMachineCustomizationWaiter {
w := &virtualMachineCustomizationWaiter{
done: make(chan struct{}),
}
go func() {
w.err = w.wait(client, vm)
close(w.done)
}()
return w
}

// wait waits for the customization of a supplied VirtualMachine to complete,
// either due to success or error. It does this by watching specifically for
// CustomizationSucceeded and CustomizationFailed events. If the customization
// failed due to some sort of error, the full formatted message is returned as
// an error.
func (w *virtualMachineCustomizationWaiter) wait(client *govmomi.Client, vm *object.VirtualMachine) error {
// Our listener loop callback.
success := make(chan struct{})
cb := func(obj types.ManagedObjectReference, page []types.BaseEvent) error {
for _, be := range page {
switch e := be.(type) {
case *types.CustomizationFailed:
return errors.New(e.GetEvent().FullFormattedMessage)
case *types.CustomizationSucceeded:
close(success)
}
}
return nil
}

mgr := event.NewManager(client.Client)
mgrErr := make(chan error, 1)
// Make a proper background context so that we can gracefully cancel the
// subscriber when we are done with it. This eventually gets passed down to
// the property collector SOAP calls.
pctx, pcancel := context.WithCancel(context.Background())
defer pcancel()
go func() {
mgrErr <- mgr.Events(pctx, []types.ManagedObjectReference{vm.Reference()}, 10, true, false, cb)
}()

// This is our waiter. We want to wait on all of these conditions. We also
// use a different context so that we can give a better error message on
// timeout without interfering with the subscriber's context.
ctx, cancel := context.WithTimeout(context.Background(), defaultAPITimeout)
defer cancel()
select {
case err := <-mgrErr:
return err
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("timeout waiting for customization to complete")
}
case <-success:
// Pass case to break to success
}
return nil
}

// selectEventsForReference allows you to query events for a specific
// ManagedObjectReference.
//
// Event types can be supplied to this function via the eventTypes parameter.
// This is highly recommended when you expect the list of events to be large,
// as there is no limit on returned events.
func selectEventsForReference(client *govmomi.Client, ref types.ManagedObjectReference, eventTypes []string) ([]types.BaseEvent, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultAPITimeout)
defer cancel()
filter := types.EventFilterSpec{
Entity: &types.EventFilterSpecByEntity{
Entity: ref,
Recursion: types.EventFilterSpecRecursionOptionAll,
},
EventTypeId: eventTypes,
}
mgr := event.NewManager(client.Client)
return mgr.QueryEvents(ctx, filter)
}
67 changes: 49 additions & 18 deletions vsphere/resource_vsphere_virtual_machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@ func resourceVSphereVirtualMachine() *schema.Resource {
Default: false,
},

"wait_for_guest_net": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},

"enable_disk_uuid": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Expand Down Expand Up @@ -695,6 +701,16 @@ func resourceVSphereVirtualMachineUpdate(d *schema.ResourceData, meta interface{
log.Printf("[ERROR] %s", err)
return err
}

// Wait for VM guest networking before returning, so that Read can get
// accurate networking info for the state.
if d.Get("wait_for_guest_net").(bool) {
log.Printf("[DEBUG] Waiting for routeable guest network access")
if err := waitForGuestVMNet(client, vm); err != nil {
return err
}
log.Printf("[DEBUG] Guest has routeable network access.")
}
}

return resourceVSphereVirtualMachineRead(d, meta)
Expand Down Expand Up @@ -961,6 +977,24 @@ func resourceVSphereVirtualMachineCreate(d *schema.ResourceData, meta interface{
d.SetId(vm.Path())
log.Printf("[INFO] Created virtual machine: %s", d.Id())

newVM, err := virtualMachineFromManagedObjectID(client, vm.moid)
if err != nil {
return err
}
newProps, err := virtualMachineProperties(newVM)
if err != nil {
return err
}

if newProps.Runtime.PowerState == types.VirtualMachinePowerStatePoweredOn && d.Get("wait_for_guest_net").(bool) {
// We also need to wait for the guest networking to ensure an accurate set
// of information can be read into state and reported to the provisioners.
log.Printf("[DEBUG] Waiting for routeable guest network access")
if err := waitForGuestVMNet(client, newVM); err != nil {
return err
}
log.Printf("[DEBUG] Guest has routeable network access.")
}
return resourceVSphereVirtualMachineRead(d, meta)
}

Expand All @@ -987,23 +1021,6 @@ func resourceVSphereVirtualMachineRead(d *schema.ResourceData, meta interface{})
log.Printf("[DEBUG] Set the moid: %#v", vm.Reference().Value)
}

state, err := vm.PowerState(context.TODO())
if err != nil {
return err
}

if state == types.VirtualMachinePowerStatePoweredOn {
// wait for interfaces to appear
log.Printf("[DEBUG] Waiting for interfaces to appear")

_, err = vm.WaitForNetIP(context.TODO(), false)
if err != nil {
return err
}

log.Printf("[DEBUG] Successfully waited for interfaces to appear")
}

var mvm mo.VirtualMachine
collector := property.DefaultCollector(client.Client)
if err := collector.RetrieveOne(context.TODO(), vm.Reference(), []string{"guest", "summary", "datastore", "config", "runtime"}, &mvm); err != nil {
Expand Down Expand Up @@ -1775,6 +1792,7 @@ func createCdroms(client *govmomi.Client, vm *object.VirtualMachine, datacenter
}

func (vm *virtualMachine) setupVirtualMachine(c *govmomi.Client) error {
var cw *virtualMachineCustomizationWaiter
dc, err := getDatacenter(c, vm.datacenter)

if err != nil {
Expand Down Expand Up @@ -2204,6 +2222,7 @@ func (vm *virtualMachine) setupVirtualMachine(c *govmomi.Client) error {
log.Printf("[DEBUG] custom spec: %v", customSpec)

log.Printf("[DEBUG] VM customization starting")
cw = newVirtualMachineCustomizationWaiter(c, newVM)
taskb, err := newVM.Customize(context.TODO(), customSpec)
if err != nil {
return err
Expand All @@ -2212,7 +2231,6 @@ func (vm *virtualMachine) setupVirtualMachine(c *govmomi.Client) error {
if err != nil {
return err
}
log.Printf("[DEBUG] VM customization finished")
}

if vm.hasBootableVmdk || vm.template != "" {
Expand All @@ -2228,9 +2246,22 @@ func (vm *virtualMachine) setupVirtualMachine(c *govmomi.Client) error {
if err != nil {
return err
}
if cw != nil {
// Customization is not yet done here 100%. We need to wait for the
// customization completion events to confirm, so start listening for those
// now.
<-cw.Done()
if cw.Err() != nil {
return cw.Err()
}
log.Printf("[DEBUG] VM customization finished")
}
}

vm.moid = newVM.Reference().Value
return nil
}

func getNetworkName(c *govmomi.Client, vm *object.VirtualMachine, nic types.BaseVirtualEthernetCard) (string, error) {
backingInfo := nic.GetVirtualEthernetCard().Backing
var deviceName string
Expand Down
Loading

0 comments on commit b35eeb9

Please sign in to comment.