Skip to content

Commit

Permalink
r/vmfs_datastore: Add folder support
Browse files Browse the repository at this point in the history
Just as a bonus, added folder support. This commit also ultimately
scaffolds a lot of stuff that we will need for proper inventory support
anyway, including helpers to determine well-known paths for the 4 major
inventory types based off an existing managed object, which will allow
for some saner control around where in inventory a specific item goes.
As an example, with this approach, the vsphere_vmfs_datastore resource
"folder" attribute can only place datastores in folders that are in the
datacenter that the datastore is actually being set up in - so if the
host that the datastore is being set up in is in the "dc1" datacenter,
the folder needs to exist within "/dc1/datastore".
  • Loading branch information
vancluever committed Sep 2, 2017
1 parent 923bab2 commit 418b574
Show file tree
Hide file tree
Showing 8 changed files with 429 additions and 4 deletions.
1 change: 1 addition & 0 deletions tf-vsphere-devrc.mk.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ export VSPHERE_VMFS_REGEXP ?= expr # Regexp for SCSI disk search
export VSPHERE_DS_VMFS_DISK0 ?= scsi-name0 # 1st disk for vmfs_datastore
export VSPHERE_DS_VMFS_DISK1 ?= scsi-name1 # 2nd disk for vmfs_datastore
export VSPHERE_DS_VMFS_DISK2 ?= scsi-name2 # 3rd disk for vmfs_datastore
export VSPHERE_DS_FOLDER ?= ds-folder # Path to a datastore folder

# vi: filetype=make
27 changes: 27 additions & 0 deletions vsphere/datastore_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,30 @@ func datastoreProperties(ds *object.Datastore) (*mo.Datastore, error) {
}
return &props, nil
}

// moveDatastoreToFolder is a complex method that moves a datastore to a given
// relative datastore folder path. "Relative" here means relative to a
// datacenter, which is discovered from the current datastore path.
func moveDatastoreToFolder(client *govmomi.Client, ds *object.Datastore, relative string) error {
folder, err := datastoreFolderFromObject(client, ds, relative)
if err != nil {
return err
}
return moveObjectToFolder(ds.Reference(), folder)
}

// moveDatastoreToFolderRelativeHostSystemID is a complex method that moves a
// datastore to a given datastore path, similar to moveDatastoreToFolder,
// except the path is relative to a HostSystem supplied by ID instead of the
// datastore.
func moveDatastoreToFolderRelativeHostSystemID(client *govmomi.Client, ds *object.Datastore, hsID, relative string) error {
hs, err := hostSystemFromID(client, hsID)
if err != nil {
return err
}
folder, err := datastoreFolderFromObject(client, hs, relative)
if err != nil {
return err
}
return moveObjectToFolder(ds.Reference(), folder)
}
193 changes: 193 additions & 0 deletions vsphere/folder_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package vsphere

import (
"context"
"fmt"
"path"
"reflect"
"strings"

"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
)

// rootPathParticle is the section of a vSphere inventory path that denotes a
// specific kind of inventory item.
type rootPathParticle string

// String implements Stringer for rootPathParticle.
func (p rootPathParticle) String() string {
return string(p)
}

// Delimeter returns the path delimiter for the particle, which is basically
// just a particle with a leading slash.
func (p rootPathParticle) Delimeter() string {
return string("/" + p)
}

// SplitDatacenter is a convenience method that splits out the datacenter path
// from the supplied path for the particle.
func (p rootPathParticle) SplitDatacenter(inventoryPath string) (string, error) {
s := strings.SplitN(inventoryPath, p.Delimeter(), 2)
if len(s) != 2 {
return inventoryPath, fmt.Errorf("could not split path %q on %q", inventoryPath, p.Delimeter())
}
return s[0], nil
}

// SplitRelativeFolder is a convenience method that splits out the relative
// folder from the supplied path for the particle.
func (p rootPathParticle) SplitRelativeFolder(inventoryPath string) (string, error) {
s := strings.SplitN(inventoryPath, p.Delimeter(), 2)
if len(s) != 2 {
return inventoryPath, fmt.Errorf("could not split path %q on %q", inventoryPath, p.Delimeter())
}
return path.Dir(s[1]), nil
}

// NewRootFromPath takes the datacenter path for a specific entity, and then
// appends the new particle supplied.
func (p rootPathParticle) NewRootFromPath(inventoryPath string, newParticle rootPathParticle) (string, error) {
dcPath, err := p.SplitDatacenter(inventoryPath)
if err != nil {
return inventoryPath, err
}
return fmt.Sprintf("%s/%s", dcPath, newParticle), nil
}

// PathFromNewRoot takes the datacenter path for a specific entity, and then
// appends the new particle supplied with the new relative path.
//
// As an example, consider a supplied host path "/dc1/host/cluster1/esxi1", and
// a supplied datastore folder relative path of "/foo/bar". This function will
// split off the datacenter section of the path (/dc1) and combine it with the
// datastore folder with the proper delimiter. The resulting path will be
// "/dc1/datastore/foo/bar".
func (p rootPathParticle) PathFromNewRoot(inventoryPath string, newParticle rootPathParticle, relative string) (string, error) {
rootPath, err := p.NewRootFromPath(inventoryPath, newParticle)
if err != nil {
return inventoryPath, err
}
return path.Clean(fmt.Sprintf("%s/%s", rootPath, relative)), nil
}

const (
rootPathParticleVM = rootPathParticle("vm")
rootPathParticleNetwork = rootPathParticle("network")
rootPathParticleHost = rootPathParticle("host")
rootPathParticleDatastore = rootPathParticle("datastore")
)

// datacenterPathFromHostSystemID returns the datacenter section of a
// HostSystem's inventory path.
func datacenterPathFromHostSystemID(client *govmomi.Client, hsID string) (string, error) {
hs, err := hostSystemFromID(client, hsID)
if err != nil {
return "", err
}
return rootPathParticleHost.SplitDatacenter(hs.InventoryPath)
}

// datastoreRootPathFromHostSystemID returns the root datastore folder path
// for a specific host system ID.
func datastoreRootPathFromHostSystemID(client *govmomi.Client, hsID string) (string, error) {
hs, err := hostSystemFromID(client, hsID)
if err != nil {
return "", err
}
return rootPathParticleHost.NewRootFromPath(hs.InventoryPath, rootPathParticleDatastore)
}

// folderFromObject returns an *object.Folder from a given object of specific
// types, and relative path of a type defined in folderType. If no such folder
// is found, an appropriate error will be returned.
//
// The list of supported object types will grow as the provider supports more
// resources.
func folderFromObject(client *govmomi.Client, obj interface{}, folderType rootPathParticle, relative string) (*object.Folder, error) {
if err := validateVirtualCenter(client); err != nil {
return nil, err
}
var p string
var err error
switch o := obj.(type) {
case (*object.Datastore):
p, err = rootPathParticleDatastore.PathFromNewRoot(o.InventoryPath, folderType, relative)
case (*object.HostSystem):
p, err = rootPathParticleHost.PathFromNewRoot(o.InventoryPath, folderType, relative)
default:
return nil, fmt.Errorf("unsupported object type %T", o)
}
if err != nil {
return nil, err
}
// Set up a finder. Don't set datacenter here as we are looking for full
// path, should not be necessary.
finder := find.NewFinder(client.Client, false)
ctx, cancel := context.WithTimeout(context.Background(), defaultAPITimeout)
defer cancel()
folder, err := finder.Folder(ctx, p)
if err != nil {
return nil, err
}
return folder, nil
}

// datastoreFolderFromObject returns an *object.Folder from a given object,
// and relative datastore folder path. If no such folder is found, of if it is
// not a datastore folder, an appropriate error will be returned.
func datastoreFolderFromObject(client *govmomi.Client, obj interface{}, relative string) (*object.Folder, error) {
folder, err := folderFromObject(client, obj, rootPathParticleDatastore, relative)
if err != nil {
return nil, err
}

return validateDatastoreFolder(folder)
}

// validateDatastoreFolder checks to make sure the folder is a datastore
// folder, and returns it if it is not, or an error if it isn't.
func validateDatastoreFolder(folder *object.Folder) (*object.Folder, error) {
var props mo.Folder
ctx, cancel := context.WithTimeout(context.Background(), defaultAPITimeout)
defer cancel()
if err := folder.Properties(ctx, folder.Reference(), nil, &props); err != nil {
return nil, err
}
if !reflect.DeepEqual(props.ChildType, []string{"Folder", "Datastore", "StoragePod"}) {
return nil, fmt.Errorf("%q is not a datastore folder", folder.InventoryPath)
}
return folder, nil
}

// pathIsEmpty checks a folder path to see if it's "empty" (ie: would resolve
// to the root inventory path for a given type in a datacenter - "" or "/").
func pathIsEmpty(path string) bool {
return path == "" || path == "/"
}

// normalizeFolderPath is a SchemaStateFunc that normalizes a folder path.
func normalizeFolderPath(v interface{}) string {
p := v.(string)
if pathIsEmpty(p) {
return ""
}
return strings.TrimPrefix(path.Clean(p), "/")
}

// moveObjectToFolder moves a object by reference into a folder.
func moveObjectToFolder(ref types.ManagedObjectReference, folder *object.Folder) error {
ctx, cancel := context.WithTimeout(context.Background(), defaultAPITimeout)
defer cancel()
task, err := folder.MoveInto(ctx, []types.ManagedObjectReference{ref})
if err != nil {
return err
}
tctx, tcancel := context.WithTimeout(context.Background(), defaultAPITimeout)
defer tcancel()
return task.Wait(tctx)
}
25 changes: 24 additions & 1 deletion vsphere/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package vsphere
import (
"fmt"
"os"
"regexp"
"testing"
"time"

Expand Down Expand Up @@ -44,13 +45,35 @@ func testClientVariablesForResource(s *terraform.State, addr string) (testCheckV
}, nil
}

// testAccESXiFlagSet returns true if VSPHERE_TEST_ESXI is set.
func testAccESXiFlagSet() bool {
return os.Getenv("VSPHERE_TEST_ESXI") != ""
}

// testAccSkipIfNotEsxi skips a test if VSPHERE_TEST_ESXI is not set.
func testAccSkipIfNotEsxi(t *testing.T) {
if os.Getenv("VSPHERE_TEST_ESXI") == "" {
if !testAccESXiFlagSet() {
t.Skip("set VSPHERE_TEST_ESXI to run ESXi-specific acceptance tests")
}
}

// testAccSkipIfEsxi skips a test if VSPHERE_TEST_ESXI is set.
func testAccSkipIfEsxi(t *testing.T) {
if testAccESXiFlagSet() {
t.Skip("test skipped as VSPHERE_TEST_ESXI is set")
}
}

// expectErrorIfNotVirtualCenter returns the error message that
// validateVirtualCenter returns if VSPHERE_TEST_ESXI is set, to allow for test
// cases that will still run on ESXi, but will expect validation failure.
func expectErrorIfNotVirtualCenter() *regexp.Regexp {
if testAccESXiFlagSet() {
return regexp.MustCompile(errVirtualCenterOnly)
}
return nil
}

// testGetPortGroup is a convenience method to fetch a static port group
// resource for testing.
func testGetPortGroup(s *terraform.State, resourceName string) (*types.HostPortGroup, error) {
Expand Down
51 changes: 49 additions & 2 deletions vsphere/resource_vsphere_vmfs_datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,19 @@ const (
waitForDeleteError = "waitForDeleteError"
)

// formatCreateRollbackError defines the verbose error for extending a disk on
// creation where rollback is not possible.
// formatCreateRollbackErrorFolder defines the verbose error for moving a
// datastore to a folder on creation where rollback was not possible.
const formatCreateRollbackErrorFolder = `
WARNING: Dangling resource!
There was an error moving your datastore to the desired folder %q:
%s
Additionally, there was an error removing the created datastore:
%s
You will need to remove this datastore manually before trying again.
`

// formatCreateRollbackErrorUpdate defines the verbose error for extending a
// disk on creation where rollback is not possible.
const formatCreateRollbackErrorUpdate = `
WARNING: Dangling resource!
There was an error extending your datastore with disk: %q:
Expand Down Expand Up @@ -69,6 +80,12 @@ func resourceVSphereVmfsDatastore() *schema.Resource {
Description: "The managed object ID of the host to set up the datastore on.",
Required: true,
},
"folder": &schema.Schema{
Type: schema.TypeString,
Description: "The path to the datastore folder to put the datastore in.",
Optional: true,
StateFunc: normalizeFolderPath,
},
"disks": &schema.Schema{
Type: schema.TypeList,
Description: "The disks to add to the datastore.",
Expand Down Expand Up @@ -115,6 +132,20 @@ func resourceVSphereVmfsDatastoreCreate(d *schema.ResourceData, meta interface{}
return fmt.Errorf("error creating datastore with disk %s: %s", disk, err)
}

// Move the datastore to the correct folder first, if specified.
folder := d.Get("folder").(string)
if !pathIsEmpty(folder) {
if err := moveDatastoreToFolderRelativeHostSystemID(client, ds, hsID, folder); err != nil {
if remErr := removeDatastore(dss, ds); remErr != nil {
// We could not destroy the created datastore and there is now a dangling
// resource. We need to instruct the user to remove the datastore
// manually.
return fmt.Errorf(formatCreateRollbackErrorFolder, folder, err, remErr)
}
return fmt.Errorf("could not move datastore to folder %q: %s", folder, err)
}
}

// Now add any remaining disks.
for _, disk := range disks[1:] {
spec, err := diskSpecForExtend(dss, ds, disk.(string))
Expand Down Expand Up @@ -162,6 +193,13 @@ func resourceVSphereVmfsDatastoreRead(d *schema.ResourceData, meta interface{})
return err
}

// Set the folder
folder, err := rootPathParticleDatastore.SplitRelativeFolder(ds.InventoryPath)
if err != nil {
return fmt.Errorf("error parsing datastore path %q: %s", ds.InventoryPath, err)
}
d.Set("folder", normalizeFolderPath(folder))

// We also need to update the disk list from the summary.
var disks []string
for _, disk := range props.Info.(*types.VmfsDatastoreInfo).Vmfs.Extent {
Expand All @@ -170,6 +208,7 @@ func resourceVSphereVmfsDatastoreRead(d *schema.ResourceData, meta interface{})
if err := d.Set("disks", disks); err != nil {
return err
}

return nil
}

Expand All @@ -194,6 +233,14 @@ func resourceVSphereVmfsDatastoreUpdate(d *schema.ResourceData, meta interface{}
}
}

// Update folder if necessary
if d.HasChange("folder") {
folder := d.Get("folder").(string)
if err := moveDatastoreToFolder(client, ds, folder); err != nil {
return fmt.Errorf("Could not move datastore to folder %q: %s", folder, err)
}
}

// Veto this update if it means a disk was removed. Shrinking
// datastores/removing extents is not supported.
old, new := d.GetChange("disks")
Expand Down
Loading

0 comments on commit 418b574

Please sign in to comment.