Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement: Added resource for snapshot and revert snapshot #107

Merged
merged 13 commits into from
Sep 8, 2017
14 changes: 8 additions & 6 deletions vsphere/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,14 @@ func Provider() terraform.ResourceProvider {
},

ResourcesMap: map[string]*schema.Resource{
"vsphere_datacenter": resourceVSphereDatacenter(),
"vsphere_file": resourceVSphereFile(),
"vsphere_folder": resourceVSphereFolder(),
"vsphere_virtual_disk": resourceVSphereVirtualDisk(),
"vsphere_virtual_machine": resourceVSphereVirtualMachine(),
"vsphere_license": resourceVSphereLicense(),
"vsphere_datacenter": resourceVSphereDatacenter(),
"vsphere_file": resourceVSphereFile(),
"vsphere_folder": resourceVSphereFolder(),
"vsphere_virtual_disk": resourceVSphereVirtualDisk(),
"vsphere_virtual_machine": resourceVSphereVirtualMachine(),
"vsphere_license": resourceVSphereLicense(),
"vsphere_virtual_machine_snapshot": resourceVSphereSnapshot(),
"vsphere_virtual_machine_snapshot_revert": resourceVSphereRevertSnapshot(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note here to remove the vsphere_virtual_machine_snapshot_revert resource.

},

ConfigureFunc: providerConfigure,
Expand Down
71 changes: 71 additions & 0 deletions vsphere/resource_vsphere_revert_snapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package vsphere

import (
"context"
"fmt"
"log"

"github.com/hashicorp/terraform/helper/schema"
)

func resourceVSphereRevertSnapshot() *schema.Resource {
return &schema.Resource{
Create: resourceVSphereSnapshotRevert,
Delete: resourceVSphereSnapshotDummyDelete,
Read: resourceVSphereSnapshotDummyRead,

Schema: map[string]*schema.Schema{
"datacenter": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"vm_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"snapshot_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"suppress_power_on": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
},
}
}

func resourceVSphereSnapshotRevert(d *schema.ResourceData, meta interface{}) error {
vm, err := findVM(d, meta)
if err != nil {
return fmt.Errorf("Error while getting the VirtualMachine :%s", err)
}
task, err := vm.RevertToSnapshot(context.TODO(), d.Get("snapshot_id").(string), d.Get("suppress_power_on").(bool))
if err != nil {
log.Printf("[ERROR] Error While Creating the Task for Revert Snapshot: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A note on logs: Terraform currently does nothing with INFO and ERROR messages that is anything more meaningful than DEBUG... can all of these messages be changed to DEBUG?

return fmt.Errorf("Error While Creating the Task for Revert Snapshot: %s", err)
}
log.Printf("[INFO] Task created for Revert Snapshot: %v", task)

err = task.Wait(context.TODO())
if err != nil {
log.Printf("[ERROR] Error While waiting for the Task of Revert Snapshot: %v", err)
return fmt.Errorf("Error While waiting for the Task of Revert Snapshot: %s", err)
}
log.Printf("[INFO] Revert Snapshot completed %v", d.Get("snapshot_id").(string))
d.SetId(d.Get("snapshot_id").(string))
return nil

}

func resourceVSphereSnapshotDummyRead(d *schema.ResourceData, meta interface{}) error {
return nil
}

func resourceVSphereSnapshotDummyDelete(d *schema.ResourceData, meta interface{}) error {
return nil
}
104 changes: 104 additions & 0 deletions vsphere/resource_vsphere_revert_snapshot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package vsphere

import (
"context"
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/vim25/mo"
)

func testBasicPreCheckSnapshotRevert(t *testing.T) {
testAccPreCheck(t)

}

func TestAccVmSnapshotRevert_Basic(t *testing.T) {
var vmId, snapshotId, suppressPower string
if v := os.Getenv("VSPHERE_VM_ID"); v != "" {
vmId = v
}
if v := os.Getenv("VSPHERE_VM_SNAPSHOT_ID"); v != "" {
snapshotId = v
}
if v := os.Getenv("VSPHERE_SUPPRESS_POWER_ON"); v != "" {
suppressPower = v
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckVmSnapshotRevertDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckVSphereVMSnapshotRevertConfig_basic(vmId, snapshotId, suppressPower),
Check: resource.ComposeTestCheckFunc(
testAccCheckVmCurrentSnapshot("vsphere_virtual_machine_snapshot_revert.Test_terraform_cases", snapshotId),
),
},
},
})
}

func testAccCheckVmSnapshotRevertDestroy(s *terraform.State) error {

return nil
}

func testAccCheckVmCurrentSnapshot(n, snapshot_name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]

if !ok {
return fmt.Errorf("Not found: %s", n)
}

if rs.Primary.ID == "" {
return fmt.Errorf("No Vm Snapshot ID is set")
}
client := testAccProvider.Meta().(*govmomi.Client)

dc, err := getDatacenter(client, "")
if err != nil {
return fmt.Errorf("error %s", err)
}
finder := find.NewFinder(client.Client, true)
finder = finder.SetDatacenter(dc)
vm, err := finder.VirtualMachine(context.TODO(), os.Getenv("VSPHERE_VM_ID"))
if err != nil {
return fmt.Errorf("error %s", err)
}

var vm_object mo.VirtualMachine

err = vm.Properties(context.TODO(), vm.Reference(), []string{"snapshot"}, &vm_object)

if err != nil {
return nil
}
current_snap := vm_object.Snapshot.CurrentSnapshot
snapshot, err := vm.FindSnapshot(context.TODO(), snapshot_name)

if err != nil {
return fmt.Errorf("Error while getting the snapshot %v", snapshot)
}
if fmt.Sprintf("<%s>", snapshot) == fmt.Sprintf("<%s>", current_snap) {
return nil
}

return fmt.Errorf("Test Case failed for revert snapshot. Current snapshot does not match to reverted snapshot")
}
}

func testAccCheckVSphereVMSnapshotRevertConfig_basic(vmId, snapshotId, suppressPowerOn string) string {
return fmt.Sprintf(`
resource "vsphere_virtual_machine_snapshot_revert" "Test_terraform_cases"{
vm_id = "%s"
snapshot_id = "%s"
suppress_power_on = %s
}`, vmId, snapshotId, suppressPowerOn)
}
186 changes: 186 additions & 0 deletions vsphere/resource_vsphere_snapshot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package vsphere
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change the file name here to resource_vsphere_virtual_machine_snapshot.go?


import (
"context"
"fmt"
"log"
"strings"

"github.com/hashicorp/terraform/helper/schema"
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
)

func resourceVSphereSnapshot() *schema.Resource {
return &schema.Resource{
Create: resourceVSphereSnapshotCreate,
Read: resourceVSphereSnapshotRead,
Delete: resourceVSphereSnapshotDelete,

Schema: map[string]*schema.Schema{
"vm_id": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change this to reference the the UUID of the virtual machine versus the VM path name?

vsphere_virtual_machine supplies this via the uuid attribute, so it can be fetched in Terraform. From there, you can get the MoRef for the VM via the FindAllByUuid method.

Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"datacenter": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the VM reference has been changed to UUID, you can get rid of this attribute.

Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"snapshot_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"description": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"memory": {
Type: schema.TypeBool,
Required: true,
ForceNew: true,
},
"quiesce": {
Type: schema.TypeBool,
Required: true,
ForceNew: true,
},
"remove_children": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"consolidate": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
},
}
}

func resourceVSphereSnapshotCreate(d *schema.ResourceData, meta interface{}) error {
vm, err := findVM(d, meta)
if err != nil {
return fmt.Errorf("Error while getting the VirtualMachine :%s", err)
}
task, err := vm.CreateSnapshot(context.TODO(), d.Get("snapshot_name").(string), d.Get("description").(string), d.Get("memory").(bool), d.Get("quiesce").(bool))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to get out of the pattern of the vSphere provider using general blocking contexts for these operations - this could potentially mean the user may have to wait 20 minutes for a failed API call to return.

Can you make sure to replace all calls to context.TODO() with a pattern similar to:

ctx, cancel := context.WithTimeout(context.Background(), defaultAPITimeout) // This is 5 mins
defer cancel()
// your function that needs a context here

This will ensure that we put a timeout on function calls that are more reasonable for the operations that they are trying to perform.

Use a separate context for each call. If you find the code getting a little messy, break the code up into different functions. I'd recommend no more than 2 contexts per function with the latter being for waiting on tasks - like what is below:

ctx, cancel := context.WithTimeout(context.Background(), defaultAPITimeout) // This is 5 mins
defer cancel()
task, err := object.FunctionThatNeedsContext(ctx)
if err != nil {
  return err
}
tctx, tcancel := context.WithTimeout(context.Background(), defaultAPITimeout)
defer tcancel()
info, err := task.WaitForResult(tctx, nil)
// Process info.Result here

taskInfo, err := task.WaitForResult(context.TODO(), nil)
if err != nil {
log.Printf("[ERROR] Error While Creating the Task for Create Snapshot: %v", err)
return fmt.Errorf(" Error While Creating the Task for Create Snapshot: %s", err)
}
log.Printf("[INFO] Task created for Create Snapshot: %v", task)
err = task.Wait(context.TODO())

if err != nil {
log.Printf("[ERROR] Error While waiting for the Task for Create Snapshot: %v", err)
return fmt.Errorf(" Error While waiting for the Task for Create Snapshot: %s", err)
}
log.Printf("[INFO] Create Snapshot completed %v", d.Get("snapshot_name").(string))
log.Println("[INFO] Managed Object Reference: " + taskInfo.Result.(types.ManagedObjectReference).Value)
d.SetId(taskInfo.Result.(types.ManagedObjectReference).Value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for using the MOID as the resource ID!

return nil
}

func resourceVSphereSnapshotDelete(d *schema.ResourceData, meta interface{}) error {
vm, err := findVM(d, meta)
if err != nil {
return fmt.Errorf("Error while getting the VirtualMachine :%s", err)
}
resourceVSphereSnapshotRead(d, meta)
if d.Id() == "" {
log.Printf("[ERROR] Error While finding the Snapshot: %v", err)
return nil
}
log.Printf("[INFO] Deleting snapshot with name: %v", d.Get("snapshot_name").(string))
var consolidate_ptr *bool
var remove_children bool

if v, ok := d.GetOk("consolidate"); ok {
consolidate := v.(bool)
consolidate_ptr = &consolidate
} else {

consolidate := true
consolidate_ptr = &consolidate
}
if v, ok := d.GetOk("remove_children"); ok {
remove_children = v.(bool)
} else {

remove_children = false
}

task, err := vm.RemoveSnapshot(context.TODO(), d.Id(), remove_children, consolidate_ptr)

if err != nil {
log.Printf("[ERROR] Error While Creating the Task for Delete Snapshot: %v", err)
return fmt.Errorf("Error While Creating the Task for Delete Snapshot: %s", err)
}
log.Printf("[INFO] Task created for Delete Snapshot: %v", task)

err = task.Wait(context.TODO())
if err != nil {
log.Printf("[ERROR] Error While waiting for the Task of Delete Snapshot: %v", err)
return fmt.Errorf("Error While waiting for the Task of Delete Snapshot: %s", err)
}
log.Printf("[INFO] Delete Snapshot completed %v", d.Get("snapshot_name").(string))

return nil
}

func resourceVSphereSnapshotRead(d *schema.ResourceData, meta interface{}) error {
vm, err := findVM(d, meta)
if err != nil {
return fmt.Errorf("Error while getting the VirtualMachine :%s", err)
}
snapshot, err := vm.FindSnapshot(context.TODO(), d.Id())

if err != nil {
if strings.Contains(err.Error(), "No snapshots for this VM") || strings.Contains(err.Error(), "snapshot \""+d.Get("snapshot_name").(string)+"\" not found") {
log.Printf("[ERROR] Error While finding the Snapshot: %v", err)
d.SetId("")
return nil
}
log.Printf("[ERROR] Error While finding the Snapshot: %v", err)
return fmt.Errorf("Error while finding the Snapshot :%s", err)
}
log.Printf("[INFO] Snapshot found: %v", snapshot)
return nil
}

func findVM(d *schema.ResourceData, meta interface{}) (*object.VirtualMachine, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once this is changed to find by UUID, can you move this function to a new file called virtual_machine_helper.go? I want to try and start ensuring that all helpers are organized by their inparticular MO.

Also, make sure the function name is self-documenting - in this case, it would be virtualMachineFromUUID.

client := meta.(*govmomi.Client)
var dc *object.Datacenter
var err error
if v, ok := d.GetOk("datacenter"); ok {
dc, err = getDatacenter(client, v.(string))
} else {
dc, err = getDatacenter(client, "")
}
if err != nil {
log.Printf("[ERROR] Error While getting the DC: %v", err)
return nil, fmt.Errorf("Error While getting the DC: %s", err)
}
log.Printf("[INFO] DataCenter is: %v", dc)
log.Println("[INFO] Getting Finder:")
finder := find.NewFinder(client.Client, true)
log.Printf("[INFO] Finder is: %v", finder)
log.Println("[INFO] Setting DataCenter:")
finder = finder.SetDatacenter(dc)
log.Printf("[INFO] DataCenter is Set: %v", finder)
log.Println("[INFO] Getting VM Object: ")
vm, err := finder.VirtualMachine(context.TODO(), d.Get("vm_id").(string))
if err != nil {
log.Printf("[ERROR] Error While getting the Virtual machine object: %v", err)
return nil, err
}
log.Printf("[INFO] Virtual Machine FOUND: %v", vm)
return vm, nil
}
Loading