Skip to content

Commit

Permalink
Merge pull request hashicorp#142 from terraform-providers/f-vmfs-data…
Browse files Browse the repository at this point in the history
…store-resource

New resource: vsphere_vmfs_datastore
  • Loading branch information
vancluever authored Sep 7, 2017
2 parents 1eac679 + 2aab458 commit fb3f4fb
Show file tree
Hide file tree
Showing 13 changed files with 1,664 additions and 1 deletion.
4 changes: 4 additions & 0 deletions tf-vsphere-devrc.mk.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,9 @@ export VSPHERE_HOST_NIC0 ?= vmnic0 # NIC0 for host net tests
export VSPHERE_HOST_NIC1 ?= vmnic1 # NIC1 for host net tests
export VSPHERE_VMFS_EXPECTED ?= scsi-name # Name of expected SCSI disk
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
71 changes: 71 additions & 0 deletions vsphere/datastore_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package vsphere

import (
"context"

"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"
)

// datastoreFromID locates a Datastore by its managed object reference ID.
func datastoreFromID(client *govmomi.Client, id string) (*object.Datastore, error) {
finder := find.NewFinder(client.Client, false)

ref := types.ManagedObjectReference{
Type: "Datastore",
Value: id,
}

ctx, cancel := context.WithTimeout(context.Background(), defaultAPITimeout)
defer cancel()
ds, err := finder.ObjectReference(ctx, ref)
if err != nil {
return nil, err
}
// Should be safe to return here. If our reference returned here and is not a
// datastore, then we have bigger problems and to be honest we should be
// panicking anyway.
return ds.(*object.Datastore), nil
}

// datastoreProperties is a convenience method that wraps fetching the
// Datastore MO from its higher-level object.
func datastoreProperties(ds *object.Datastore) (*mo.Datastore, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultAPITimeout)
defer cancel()
var props mo.Datastore
if err := ds.Properties(ctx, ds.Reference(), nil, &props); err != nil {
return nil, err
}
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)
}
72 changes: 72 additions & 0 deletions vsphere/datastore_summary_structure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package vsphere

import (
"github.com/hashicorp/terraform/helper/schema"
"github.com/vmware/govmomi/vim25/types"
)

// schemaDatastoreSummary returns schema items for resources that
// need to work with a DatastoreSummary.
func schemaDatastoreSummary() map[string]*schema.Schema {
return map[string]*schema.Schema{
// Note that the following fields are not represented in the schema here:
// * Name (more than likely the ID attribute and will be represented in
// resource schema)
// * Type (redundant attribute as the datastore type will be represented by
// the resource)
"accessible": &schema.Schema{
Type: schema.TypeBool,
Description: "The connectivity status of the datastore. If this is false, some other computed attributes may be out of date.",
Computed: true,
},
"capacity": &schema.Schema{
Type: schema.TypeInt,
Description: "Maximum capacity of the datastore, in MB.",
Computed: true,
},
"free_space": &schema.Schema{
Type: schema.TypeInt,
Description: "Available space of this datastore, in MB.",
Computed: true,
},
"maintenance_mode": &schema.Schema{
Type: schema.TypeString,
Description: "The current maintenance mode state of the datastore.",
Computed: true,
},
"multiple_host_access": &schema.Schema{
Type: schema.TypeBool,
Description: "If true, more than one host in the datacenter has been configured with access to the datastore.",
Computed: true,
},
"uncommitted_space": &schema.Schema{
Type: schema.TypeInt,
Description: "Total additional storage space, in MB, potentially used by all virtual machines on this datastore.",
Computed: true,
},
"url": &schema.Schema{
Type: schema.TypeString,
Description: "The unique locator for the datastore.",
Computed: true,
},
}
}

// flattenDatastoreSummary reads various fields from a DatastoreSummary into
// the passed in ResourceData.
func flattenDatastoreSummary(d *schema.ResourceData, obj *types.DatastoreSummary) error {
d.Set("accessible", obj.Accessible)
d.Set("capacity", byteToMB(obj.Capacity))
d.Set("free_space", byteToMB(obj.FreeSpace))
d.Set("maintenance_mode", obj.MaintenanceMode)
d.Set("multiple_host_access", obj.MultipleHostAccess)
d.Set("uncommitted_space", byteToMB(obj.Uncommitted))
d.Set("url", obj.Url)

// Set the name attribute off of the name here - since we do not track this
// here we check for errors
if err := d.Set("name", obj.Name); err != nil {
return err
}
return nil
}
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
Loading

0 comments on commit fb3f4fb

Please sign in to comment.