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
This commit is contained in:
parent
fa64ac7815
commit
895383ac92
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue