diff --git a/builtin/providers/vsphere/provider.go b/builtin/providers/vsphere/provider.go index 5c98d31c0..cbe9782ff 100644 --- a/builtin/providers/vsphere/provider.go +++ b/builtin/providers/vsphere/provider.go @@ -46,6 +46,7 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ + "vsphere_file": resourceVSphereFile(), "vsphere_folder": resourceVSphereFolder(), "vsphere_virtual_machine": resourceVSphereVirtualMachine(), }, diff --git a/builtin/providers/vsphere/resource_vsphere_file.go b/builtin/providers/vsphere/resource_vsphere_file.go new file mode 100644 index 000000000..f418d947e --- /dev/null +++ b/builtin/providers/vsphere/resource_vsphere_file.go @@ -0,0 +1,309 @@ +package vsphere + +import ( + "fmt" + "log" + + "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/soap" + "golang.org/x/net/context" +) + +type file struct { + datacenter string + datastore string + sourceFile string + destinationFile string +} + +func resourceVSphereFile() *schema.Resource { + return &schema.Resource{ + Create: resourceVSphereFileCreate, + Read: resourceVSphereFileRead, + Update: resourceVSphereFileUpdate, + Delete: resourceVSphereFileDelete, + + Schema: map[string]*schema.Schema{ + "datacenter": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "datastore": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "source_file": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "destination_file": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func resourceVSphereFileCreate(d *schema.ResourceData, meta interface{}) error { + + log.Printf("[DEBUG] creating file: %#v", d) + client := meta.(*govmomi.Client) + + f := file{} + + if v, ok := d.GetOk("datacenter"); ok { + f.datacenter = v.(string) + } + + if v, ok := d.GetOk("datastore"); ok { + f.datastore = v.(string) + } else { + return fmt.Errorf("datastore argument is required") + } + + if v, ok := d.GetOk("source_file"); ok { + f.sourceFile = v.(string) + } else { + return fmt.Errorf("source_file argument is required") + } + + if v, ok := d.GetOk("destination_file"); ok { + f.destinationFile = v.(string) + } else { + return fmt.Errorf("destination_file argument is required") + } + + err := createFile(client, &f) + if err != nil { + return err + } + + d.SetId(fmt.Sprintf("[%v] %v/%v", f.datastore, f.datacenter, f.destinationFile)) + log.Printf("[INFO] Created file: %s", f.destinationFile) + + return resourceVSphereFileRead(d, meta) +} + +func createFile(client *govmomi.Client, f *file) error { + + finder := find.NewFinder(client.Client, true) + + dc, err := finder.Datacenter(context.TODO(), f.datacenter) + if err != nil { + return fmt.Errorf("error %s", err) + } + finder = finder.SetDatacenter(dc) + + ds, err := getDatastore(finder, f.datastore) + if err != nil { + return fmt.Errorf("error %s", err) + } + + dsurl, err := ds.URL(context.TODO(), dc, f.destinationFile) + if err != nil { + return err + } + + p := soap.DefaultUpload + err = client.Client.UploadFile(f.sourceFile, dsurl, &p) + if err != nil { + return fmt.Errorf("error %s", err) + } + return nil +} + +func resourceVSphereFileRead(d *schema.ResourceData, meta interface{}) error { + + log.Printf("[DEBUG] reading file: %#v", d) + f := file{} + + if v, ok := d.GetOk("datacenter"); ok { + f.datacenter = v.(string) + } + + if v, ok := d.GetOk("datastore"); ok { + f.datastore = v.(string) + } else { + return fmt.Errorf("datastore argument is required") + } + + if v, ok := d.GetOk("source_file"); ok { + f.sourceFile = v.(string) + } else { + return fmt.Errorf("source_file argument is required") + } + + if v, ok := d.GetOk("destination_file"); ok { + f.destinationFile = v.(string) + } else { + return fmt.Errorf("destination_file argument is required") + } + + client := meta.(*govmomi.Client) + finder := find.NewFinder(client.Client, true) + + dc, err := finder.Datacenter(context.TODO(), f.datacenter) + if err != nil { + return fmt.Errorf("error %s", err) + } + finder = finder.SetDatacenter(dc) + + ds, err := getDatastore(finder, f.datastore) + if err != nil { + return fmt.Errorf("error %s", err) + } + + _, err = ds.Stat(context.TODO(), f.destinationFile) + if err != nil { + d.SetId("") + return err + } + + return nil +} + +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) + } else { + return fmt.Errorf("datastore argument is required") + } + + if v, ok := d.GetOk("source_file"); ok { + f.sourceFile = v.(string) + } else { + return fmt.Errorf("source_file argument is required") + } + + if v, ok := d.GetOk("destination_file"); ok { + f.destinationFile = v.(string) + } else { + return fmt.Errorf("destination_file argument is required") + } + + client := meta.(*govmomi.Client) + dc, err := getDatacenter(client, f.datacenter) + if err != nil { + return err + } + + finder := find.NewFinder(client.Client, true) + finder = finder.SetDatacenter(dc) + + ds, err := getDatastore(finder, f.datastore) + if err != nil { + return fmt.Errorf("error %s", err) + } + + fm := object.NewFileManager(client.Client) + task, err := fm.MoveDatastoreFile(context.TODO(), ds.Path(oldDestinationFile.(string)), dc, ds.Path(newDestinationFile.(string)), dc, true) + if err != nil { + return err + } + + _, err = task.WaitForResult(context.TODO(), nil) + if err != nil { + return err + } + + } + + return nil +} + +func resourceVSphereFileDelete(d *schema.ResourceData, meta interface{}) error { + + log.Printf("[DEBUG] deleting file: %#v", d) + f := file{} + + if v, ok := d.GetOk("datacenter"); ok { + f.datacenter = v.(string) + } + + if v, ok := d.GetOk("datastore"); ok { + f.datastore = v.(string) + } else { + return fmt.Errorf("datastore argument is required") + } + + if v, ok := d.GetOk("source_file"); ok { + f.sourceFile = v.(string) + } else { + return fmt.Errorf("source_file argument is required") + } + + if v, ok := d.GetOk("destination_file"); ok { + f.destinationFile = v.(string) + } else { + return fmt.Errorf("destination_file argument is required") + } + + client := meta.(*govmomi.Client) + + err := deleteFile(client, &f) + if err != nil { + return err + } + + d.SetId("") + return nil +} + +func deleteFile(client *govmomi.Client, f *file) error { + + dc, err := getDatacenter(client, f.datacenter) + if err != nil { + return err + } + + finder := find.NewFinder(client.Client, true) + finder = finder.SetDatacenter(dc) + + ds, err := getDatastore(finder, f.datastore) + if err != nil { + return fmt.Errorf("error %s", err) + } + + fm := object.NewFileManager(client.Client) + task, err := fm.DeleteDatastoreFile(context.TODO(), ds.Path(f.destinationFile), dc) + if err != nil { + return err + } + + _, err = task.WaitForResult(context.TODO(), nil) + if err != nil { + return err + } + return nil +} + +// getDatastore gets datastore object +func getDatastore(f *find.Finder, ds string) (*object.Datastore, error) { + + if ds != "" { + dso, err := f.Datastore(context.TODO(), ds) + return dso, err + } else { + dso, err := f.DefaultDatastore(context.TODO()) + return dso, err + } +} diff --git a/builtin/providers/vsphere/resource_vsphere_file_test.go b/builtin/providers/vsphere/resource_vsphere_file_test.go new file mode 100644 index 000000000..81520b0cb --- /dev/null +++ b/builtin/providers/vsphere/resource_vsphere_file_test.go @@ -0,0 +1,203 @@ +package vsphere + +import ( + "fmt" + "io/ioutil" + "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/object" + "golang.org/x/net/context" +) + +// Basic file creation +func TestAccVSphereFile_basic(t *testing.T) { + testVmdkFileData := []byte("# Disk DescriptorFile\n") + testVmdkFile := "/tmp/tf_test.vmdk" + err := ioutil.WriteFile(testVmdkFile, testVmdkFileData, 0644) + if err != nil { + t.Errorf("error %s", err) + return + } + + datacenter := os.Getenv("VSPHERE_DATACENTER") + datastore := os.Getenv("VSPHERE_DATASTORE") + testMethod := "basic" + resourceName := "vsphere_file." + testMethod + destinationFile := "tf_file_test.vmdk" + sourceFile := testVmdkFile + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVSphereFileDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf( + testAccCheckVSphereFileConfig, + testMethod, + datacenter, + datastore, + sourceFile, + destinationFile, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists(resourceName, destinationFile, true), + resource.TestCheckResourceAttr(resourceName, "destination_file", destinationFile), + ), + }, + }, + }) + os.Remove(testVmdkFile) +} + +// file creation followed by a rename of file (update) +func TestAccVSphereFile_renamePostCreation(t *testing.T) { + testVmdkFileData := []byte("# Disk DescriptorFile\n") + testVmdkFile := "/tmp/tf_test.vmdk" + err := ioutil.WriteFile(testVmdkFile, testVmdkFileData, 0644) + if err != nil { + t.Errorf("error %s", err) + return + } + + datacenter := os.Getenv("VSPHERE_DATACENTER") + datastore := os.Getenv("VSPHERE_DATASTORE") + testMethod := "basic" + resourceName := "vsphere_file." + testMethod + destinationFile := "tf_test_file.vmdk" + destinationFileMoved := "tf_test_file_moved.vmdk" + sourceFile := testVmdkFile + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVSphereFolderDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf( + testAccCheckVSphereFileConfig, + testMethod, + datacenter, + datastore, + sourceFile, + destinationFile, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists(resourceName, destinationFile, true), + testAccCheckVSphereFileExists(resourceName, destinationFileMoved, false), + resource.TestCheckResourceAttr(resourceName, "destination_file", destinationFile), + ), + }, + { + Config: fmt.Sprintf( + testAccCheckVSphereFileConfig, + testMethod, + datacenter, + datastore, + sourceFile, + destinationFileMoved, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVSphereFileExists(resourceName, destinationFile, false), + testAccCheckVSphereFileExists(resourceName, destinationFileMoved, true), + resource.TestCheckResourceAttr(resourceName, "destination_file", destinationFileMoved), + ), + }, + }, + }) + os.Remove(testVmdkFile) +} + +func testAccCheckVSphereFileDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*govmomi.Client) + finder := find.NewFinder(client.Client, true) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vsphere_file" { + continue + } + + dc, err := finder.Datacenter(context.TODO(), rs.Primary.Attributes["datacenter"]) + if err != nil { + return fmt.Errorf("error %s", err) + } + + finder = finder.SetDatacenter(dc) + + ds, err := getDatastore(finder, rs.Primary.Attributes["datastore"]) + if err != nil { + return fmt.Errorf("error %s", err) + } + + _, err = ds.Stat(context.TODO(), rs.Primary.Attributes["destination_file"]) + if err != nil { + switch e := err.(type) { + case object.DatastoreNoSuchFileError: + fmt.Printf("Expected error received: %s\n", e.Error()) + return nil + default: + return err + } + } else { + return fmt.Errorf("File %s still exists", rs.Primary.Attributes["destination_file"]) + } + } + + return nil +} + +func testAccCheckVSphereFileExists(n string, df string, exists bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Resource not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + client := testAccProvider.Meta().(*govmomi.Client) + finder := find.NewFinder(client.Client, true) + + dc, err := finder.Datacenter(context.TODO(), rs.Primary.Attributes["datacenter"]) + if err != nil { + return fmt.Errorf("error %s", err) + } + finder = finder.SetDatacenter(dc) + + ds, err := getDatastore(finder, rs.Primary.Attributes["datastore"]) + if err != nil { + return fmt.Errorf("error %s", err) + } + + _, err = ds.Stat(context.TODO(), df) + if err != nil { + switch e := err.(type) { + case object.DatastoreNoSuchFileError: + if exists { + return fmt.Errorf("File does not exist: %s", e.Error()) + } + fmt.Printf("Expected error received: %s\n", e.Error()) + return nil + default: + return err + } + } + return nil + } +} + +const testAccCheckVSphereFileConfig = ` +resource "vsphere_file" "%s" { + datacenter = "%s" + datastore = "%s" + source_file = "%s" + destination_file = "%s" +} +` diff --git a/website/source/docs/providers/vsphere/index.html.markdown b/website/source/docs/providers/vsphere/index.html.markdown index 725a0607a..6557ffa4e 100644 --- a/website/source/docs/providers/vsphere/index.html.markdown +++ b/website/source/docs/providers/vsphere/index.html.markdown @@ -35,6 +35,13 @@ resource "vsphere_folder" "frontend" { path = "frontend" } +# Create a file +resource "vsphere_file" "ubuntu_disk" { + datastore = "local" + source_file = "/home/ubuntu/my_disks/custom_ubuntu.vmdk" + destination_file = "/my_path/disks/custom_ubuntu.vmdk" +} + # Create a virtual machine within the folder resource "vsphere_virtual_machine" "web" { name = "terraform-web" diff --git a/website/source/docs/providers/vsphere/r/file.html.markdown b/website/source/docs/providers/vsphere/r/file.html.markdown new file mode 100644 index 000000000..023c4321e --- /dev/null +++ b/website/source/docs/providers/vsphere/r/file.html.markdown @@ -0,0 +1,30 @@ +--- +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 files (e.g. vmdk disks) from Terraform host machine to remote vSphere. +----------------------------------------------------------------------------------------------------------------------------------------------------- + +# vsphere\_file + +Provides a VMware vSphere virtual machine file resource. This can be used to files (e.g. vmdk disks) from Terraform host machine to remote vSphere. + +## Example Usage + +``` +resource "vsphere_file" "ubuntu_disk" { + datastore = "local" + source_file = "/home/ubuntu/my_disks/custom_ubuntu.vmdk" + destination_file = "/my_path/disks/custom_ubuntu.vmdk" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `source_file` - (Required) The path to the file on 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.