From 895383ac92036373f151f9ffb022c9aa50035edd Mon Sep 17 00:00:00 2001 From: Davide Agnello Date: Wed, 3 Aug 2016 11:53:59 -0700 Subject: [PATCH] vSphere file resource: extending functionality to copy files in vSphere * Enables copy of files within vSphere * Can copy files between different datacenters and datastores * Update can move uploaded or copied files between datacenters and datastores * Preserves original functionality for backward compatibility --- .../vsphere/resource_vsphere_file.go | 167 ++++++++++++++---- .../vsphere/resource_vsphere_file_test.go | 153 +++++++++++++++- .../providers/vsphere/r/file.html.markdown | 37 +++- 3 files changed, 308 insertions(+), 49 deletions(-) diff --git a/builtin/providers/vsphere/resource_vsphere_file.go b/builtin/providers/vsphere/resource_vsphere_file.go index 55d3d6cbb..c8afe05d9 100644 --- a/builtin/providers/vsphere/resource_vsphere_file.go +++ b/builtin/providers/vsphere/resource_vsphere_file.go @@ -3,6 +3,7 @@ package vsphere import ( "fmt" "log" + "strings" "github.com/hashicorp/terraform/helper/schema" "github.com/vmware/govmomi" @@ -13,10 +14,14 @@ import ( ) type file struct { - datacenter string - datastore string - sourceFile string - destinationFile string + sourceDatacenter string + datacenter string + sourceDatastore string + datastore string + sourceFile string + destinationFile string + createDirectories bool + copyFile bool } func resourceVSphereFile() *schema.Resource { @@ -30,10 +35,20 @@ func resourceVSphereFile() *schema.Resource { "datacenter": { Type: schema.TypeString, Optional: true, + }, + + "source_datacenter": { + Type: schema.TypeString, + Optional: true, ForceNew: true, }, "datastore": { + Type: schema.TypeString, + Required: true, + }, + + "source_datastore": { Type: schema.TypeString, Optional: true, ForceNew: true, @@ -49,6 +64,11 @@ func resourceVSphereFile() *schema.Resource { Type: schema.TypeString, Required: true, }, + + "create_directories": { + Type: schema.TypeBool, + Optional: true, + }, }, } } @@ -60,10 +80,20 @@ func resourceVSphereFileCreate(d *schema.ResourceData, meta interface{}) error { f := file{} + if v, ok := d.GetOk("source_datacenter"); ok { + f.sourceDatacenter = v.(string) + f.copyFile = true + } + if v, ok := d.GetOk("datacenter"); ok { f.datacenter = v.(string) } + if v, ok := d.GetOk("source_datastore"); ok { + f.sourceDatastore = v.(string) + f.copyFile = true + } + if v, ok := d.GetOk("datastore"); ok { f.datastore = v.(string) } else { @@ -82,6 +112,10 @@ func resourceVSphereFileCreate(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("destination_file argument is required") } + if v, ok := d.GetOk("create_directories"); ok { + f.createDirectories = v.(bool) + } + err := createFile(client, &f) if err != nil { return err @@ -108,16 +142,53 @@ func createFile(client *govmomi.Client, f *file) error { return fmt.Errorf("error %s", err) } - dsurl, err := ds.URL(context.TODO(), dc, f.destinationFile) - if err != nil { - return err + if f.copyFile { + // Copying file from withing vSphere + source_dc, err := finder.Datacenter(context.TODO(), f.sourceDatacenter) + if err != nil { + return fmt.Errorf("error %s", err) + } + finder = finder.SetDatacenter(dc) + + source_ds, err := getDatastore(finder, f.sourceDatastore) + if err != nil { + return fmt.Errorf("error %s", err) + } + + fm := object.NewFileManager(client.Client) + if f.createDirectories { + directoryPathIndex := strings.LastIndex(f.destinationFile, "/") + path := f.destinationFile[0:directoryPathIndex] + err = fm.MakeDirectory(context.TODO(), ds.Path(path), dc, true) + if err != nil { + return fmt.Errorf("error %s", err) + } + } + task, err := fm.CopyDatastoreFile(context.TODO(), source_ds.Path(f.sourceFile), source_dc, ds.Path(f.destinationFile), dc, true) + + if err != nil { + return fmt.Errorf("error %s", err) + } + + _, err = task.WaitForResult(context.TODO(), nil) + if err != nil { + return fmt.Errorf("error %s", err) + } + + } else { + // Uploading file to vSphere + dsurl, err := ds.URL(context.TODO(), dc, f.destinationFile) + if err != nil { + return fmt.Errorf("error %s", err) + } + + p := soap.DefaultUpload + err = client.Client.UploadFile(f.sourceFile, dsurl, &p) + if err != nil { + return fmt.Errorf("error %s", err) + } } - p := soap.DefaultUpload - err = client.Client.UploadFile(f.sourceFile, dsurl, &p) - if err != nil { - return fmt.Errorf("error %s", err) - } return nil } @@ -126,10 +197,18 @@ func resourceVSphereFileRead(d *schema.ResourceData, meta interface{}) error { log.Printf("[DEBUG] reading file: %#v", d) f := file{} + if v, ok := d.GetOk("source_datacenter"); ok { + f.sourceDatacenter = v.(string) + } + if v, ok := d.GetOk("datacenter"); ok { f.datacenter = v.(string) } + if v, ok := d.GetOk("source_datastore"); ok { + f.sourceDatastore = v.(string) + } + if v, ok := d.GetOk("datastore"); ok { f.datastore = v.(string) } else { @@ -179,57 +258,69 @@ func resourceVSphereFileRead(d *schema.ResourceData, meta interface{}) error { func resourceVSphereFileUpdate(d *schema.ResourceData, meta interface{}) error { log.Printf("[DEBUG] updating file: %#v", d) - if d.HasChange("destination_file") { - oldDestinationFile, newDestinationFile := d.GetChange("destination_file") - f := file{} - if v, ok := d.GetOk("datacenter"); ok { - f.datacenter = v.(string) - } - - if v, ok := d.GetOk("datastore"); ok { - f.datastore = v.(string) + if d.HasChange("destination_file") || d.HasChange("datacenter") || d.HasChange("datastore") { + // File needs to be moved, get old and new destination changes + var oldDataceneter, newDatacenter, oldDatastore, newDatastore, oldDestinationFile, newDestinationFile string + if d.HasChange("datacenter") { + tmpOldDataceneter, tmpNewDatacenter := d.GetChange("datacenter") + oldDataceneter = tmpOldDataceneter.(string) + newDatacenter = tmpNewDatacenter.(string) } else { - return fmt.Errorf("datastore argument is required") + if v, ok := d.GetOk("datacenter"); ok { + oldDataceneter = v.(string) + newDatacenter = oldDataceneter + } } - - if v, ok := d.GetOk("source_file"); ok { - f.sourceFile = v.(string) + if d.HasChange("datastore") { + tmpOldDatastore, tmpNewDatastore := d.GetChange("datastore") + oldDatastore = tmpOldDatastore.(string) + newDatastore = tmpNewDatastore.(string) } else { - return fmt.Errorf("source_file argument is required") + oldDatastore = d.Get("datastore").(string) + newDatastore = oldDatastore } - - if v, ok := d.GetOk("destination_file"); ok { - f.destinationFile = v.(string) + if d.HasChange("destination_file") { + tmpOldDestinationFile, tmpNewDestinationFile := d.GetChange("destination_file") + oldDestinationFile = tmpOldDestinationFile.(string) + newDestinationFile = tmpNewDestinationFile.(string) } else { - return fmt.Errorf("destination_file argument is required") + oldDestinationFile = d.Get("destination_file").(string) + newDestinationFile = oldDestinationFile } + // Get old and new dataceter and datastore client := meta.(*govmomi.Client) - dc, err := getDatacenter(client, f.datacenter) + dcOld, err := getDatacenter(client, oldDataceneter) + if err != nil { + return err + } + dcNew, err := getDatacenter(client, newDatacenter) if err != nil { return err } - finder := find.NewFinder(client.Client, true) - finder = finder.SetDatacenter(dc) - - ds, err := getDatastore(finder, f.datastore) + finder = finder.SetDatacenter(dcOld) + dsOld, err := getDatastore(finder, oldDatastore) + if err != nil { + return fmt.Errorf("error %s", err) + } + finder = finder.SetDatacenter(dcNew) + dsNew, err := getDatastore(finder, newDatastore) if err != nil { return fmt.Errorf("error %s", err) } + // Move file between old/new dataceter, datastore and path (destination_file) fm := object.NewFileManager(client.Client) - task, err := fm.MoveDatastoreFile(context.TODO(), ds.Path(oldDestinationFile.(string)), dc, ds.Path(newDestinationFile.(string)), dc, true) + task, err := fm.MoveDatastoreFile(context.TODO(), dsOld.Path(oldDestinationFile), dcOld, dsNew.Path(newDestinationFile), dcNew, true) if err != nil { return err } - _, err = task.WaitForResult(context.TODO(), nil) if err != nil { return err } - } return nil diff --git a/builtin/providers/vsphere/resource_vsphere_file_test.go b/builtin/providers/vsphere/resource_vsphere_file_test.go index 81520b0cb..7e5aa44e7 100644 --- a/builtin/providers/vsphere/resource_vsphere_file_test.go +++ b/builtin/providers/vsphere/resource_vsphere_file_test.go @@ -14,7 +14,7 @@ import ( "golang.org/x/net/context" ) -// Basic file creation +// Basic file creation (upload to vSphere) func TestAccVSphereFile_basic(t *testing.T) { testVmdkFileData := []byte("# Disk DescriptorFile\n") testVmdkFile := "/tmp/tf_test.vmdk" @@ -55,6 +55,59 @@ func TestAccVSphereFile_basic(t *testing.T) { os.Remove(testVmdkFile) } +// Basic file copy within vSphere +func TestAccVSphereFile_basicUploadAndCopy(t *testing.T) { + testVmdkFileData := []byte("# Disk DescriptorFile\n") + sourceFile := "/tmp/tf_test.vmdk" + uploadResourceName := "myfileupload" + copyResourceName := "myfilecopy" + sourceDatacenter := os.Getenv("VSPHERE_DATACENTER") + datacenter := sourceDatacenter + sourceDatastore := os.Getenv("VSPHERE_DATASTORE") + datastore := sourceDatastore + destinationFile := "tf_file_test.vmdk" + sourceFileCopy := "${vsphere_file." + uploadResourceName + ".destination_file}" + destinationFileCopy := "tf_file_test_copy.vmdk" + + err := ioutil.WriteFile(sourceFile, testVmdkFileData, 0644) + if err != nil { + t.Errorf("error %s", err) + return + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVSphereFileDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf( + testAccCheckVSphereFileCopyConfig, + uploadResourceName, + datacenter, + datastore, + sourceFile, + destinationFile, + copyResourceName, + datacenter, + datacenter, + datastore, + datastore, + sourceFileCopy, + destinationFileCopy, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists("vsphere_file."+uploadResourceName, destinationFile, true), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileCopy, true), + resource.TestCheckResourceAttr("vsphere_file."+uploadResourceName, "destination_file", destinationFile), + resource.TestCheckResourceAttr("vsphere_file."+copyResourceName, "destination_file", destinationFileCopy), + ), + }, + }, + }) + os.Remove(sourceFile) +} + // file creation followed by a rename of file (update) func TestAccVSphereFile_renamePostCreation(t *testing.T) { testVmdkFileData := []byte("# Disk DescriptorFile\n") @@ -67,7 +120,7 @@ func TestAccVSphereFile_renamePostCreation(t *testing.T) { datacenter := os.Getenv("VSPHERE_DATACENTER") datastore := os.Getenv("VSPHERE_DATASTORE") - testMethod := "basic" + testMethod := "create_upgrade" resourceName := "vsphere_file." + testMethod destinationFile := "tf_test_file.vmdk" destinationFileMoved := "tf_test_file_moved.vmdk" @@ -76,7 +129,7 @@ func TestAccVSphereFile_renamePostCreation(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheckVSphereFolderDestroy, + CheckDestroy: testAccCheckVSphereFileDestroy, Steps: []resource.TestStep{ { Config: fmt.Sprintf( @@ -113,6 +166,84 @@ func TestAccVSphereFile_renamePostCreation(t *testing.T) { os.Remove(testVmdkFile) } +// file upload, then copy, finally the copy is renamed (moved) (update) +func TestAccVSphereFile_uploadAndCopyAndUpdate(t *testing.T) { + testVmdkFileData := []byte("# Disk DescriptorFile\n") + sourceFile := "/tmp/tf_test.vmdk" + uploadResourceName := "myfileupload" + copyResourceName := "myfilecopy" + sourceDatacenter := os.Getenv("VSPHERE_DATACENTER") + datacenter := sourceDatacenter + sourceDatastore := os.Getenv("VSPHERE_DATASTORE") + datastore := sourceDatastore + destinationFile := "tf_file_test.vmdk" + sourceFileCopy := "${vsphere_file." + uploadResourceName + ".destination_file}" + destinationFileCopy := "tf_file_test_copy.vmdk" + destinationFileMoved := "tf_test_file_moved.vmdk" + + err := ioutil.WriteFile(sourceFile, testVmdkFileData, 0644) + if err != nil { + t.Errorf("error %s", err) + return + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVSphereFileDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf( + testAccCheckVSphereFileCopyConfig, + uploadResourceName, + datacenter, + datastore, + sourceFile, + destinationFile, + copyResourceName, + datacenter, + datacenter, + datastore, + datastore, + sourceFileCopy, + destinationFileCopy, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists("vsphere_file."+uploadResourceName, destinationFile, true), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileCopy, true), + resource.TestCheckResourceAttr("vsphere_file."+uploadResourceName, "destination_file", destinationFile), + resource.TestCheckResourceAttr("vsphere_file."+copyResourceName, "destination_file", destinationFileCopy), + ), + }, + { + Config: fmt.Sprintf( + testAccCheckVSphereFileCopyConfig, + uploadResourceName, + datacenter, + datastore, + sourceFile, + destinationFile, + copyResourceName, + datacenter, + datacenter, + datastore, + datastore, + sourceFileCopy, + destinationFileMoved, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists("vsphere_file."+uploadResourceName, destinationFile, true), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileCopy, false), + testAccCheckVSphereFileExists("vsphere_file."+copyResourceName, destinationFileMoved, true), + resource.TestCheckResourceAttr("vsphere_file."+uploadResourceName, "destination_file", destinationFile), + resource.TestCheckResourceAttr("vsphere_file."+copyResourceName, "destination_file", destinationFileMoved), + ), + }, + }, + }) + os.Remove(sourceFile) +} + func testAccCheckVSphereFileDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*govmomi.Client) finder := find.NewFinder(client.Client, true) @@ -201,3 +332,19 @@ resource "vsphere_file" "%s" { destination_file = "%s" } ` +const testAccCheckVSphereFileCopyConfig = ` +resource "vsphere_file" "%s" { + datacenter = "%s" + datastore = "%s" + source_file = "%s" + destination_file = "%s" +} +resource "vsphere_file" "%s" { + source_datacenter = "%s" + datacenter = "%s" + source_datastore = "%s" + datastore = "%s" + source_file = "%s" + destination_file = "%s" +} +` diff --git a/website/source/docs/providers/vsphere/r/file.html.markdown b/website/source/docs/providers/vsphere/r/file.html.markdown index 443aa3046..9bd4c4b17 100644 --- a/website/source/docs/providers/vsphere/r/file.html.markdown +++ b/website/source/docs/providers/vsphere/r/file.html.markdown @@ -3,28 +3,49 @@ layout: "vsphere" page_title: "VMware vSphere: vsphere_file" sidebar_current: "docs-vsphere-resource-file" description: |- - Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere. + Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere or copy fields withing vSphere. --- # vsphere\_file -Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere. +Provides a VMware vSphere virtual machine file resource. This can be used to upload files (e.g. vmdk disks) from the Terraform host machine to a remote vSphere. The file resource can also be used to copy files within vSphere. Files can be copied between Datacenters and/or Datastores. -## Example Usage +Updates to file resources will handle moving a file to a new destination (datacenter and/or datastore and/or destination_file). If any source parameter (e.g. `source_datastore`, `source_datacenter` or `source_file`) are changed, this results in a new resource (new file uploaded or copied and old one being deleted). +## Example Usages + +**Upload file to vSphere:** ``` -resource "vsphere_file" "ubuntu_disk" { +resource "vsphere_file" "ubuntu_disk_upload" { + datacenter = "my_datacenter" datastore = "local" source_file = "/home/ubuntu/my_disks/custom_ubuntu.vmdk" destination_file = "/my_path/disks/custom_ubuntu.vmdk" } ``` +**Copy file within vSphere:** +``` +resource "vsphere_file" "ubuntu_disk_copy" { + source_datacenter = "my_datacenter" + datacenter = "my_datacenter" + source_datastore = "local" + datastore = "local" + source_file = "/my_path/disks/custom_ubuntu.vmdk" + destination_file = "/my_path/custom_ubuntu_id.vmdk" +} +``` + ## Argument Reference +If `source_datacenter` and `source_datastore` are not provided, the file resource will upload the file from Terraform host. If either `source_datacenter` or `source_datastore` are provided, the file resource will copy from within specified locations in vSphere. + The following arguments are supported: -* `source_file` - (Required) The path to the file on the Terraform host that will be uploaded to vSphere. -* `destination_file` - (Required) The path to where the file should be uploaded to on vSphere. -* `datacenter` - (Optional) The name of a Datacenter in which the file will be created/uploaded to. -* `datastore` - (Required) The name of the Datastore in which to create/upload the file to. +* `source_file` - (Required) The path to the file being uploaded from the Terraform host to vSphere or copied within vSphere. +* `destination_file` - (Required) The path to where the file should be uploaded or copied to on vSphere. +* `source_datacenter` - (Optional) The name of a Datacenter in which the file will be copied from. +* `datacenter` - (Optional) The name of a Datacenter in which the file will be uploaded to. +* `source_datastore` - (Optional) The name of the Datastore in which file will be copied from. +* `datastore` - (Required) The name of the Datastore in which to upload the file to. +* `create_directories` - (Optional) Create directories in `destination_file` path parameter if any missing for copy operation. *Note: Directories are not deleted on destroy operation. \ No newline at end of file