Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New resource: vsphere_vmfs_datastore #142

Merged
merged 12 commits into from
Sep 7, 2017
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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the need for only this field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because name is not set in the accompanying schemaDatastoreSummary, as it's an attribute that may be controlled in different ways and go through different workflows depending on the resource. At the same time though, we want to flatten it from the summary data as it's one of the easiest ways to fetch it from the datastore. So rather than just drop the result on the floor if by any chance it's not in the schema, I want to check for errors so that any edge cases get caught (more than likely in testing, so the user will never see it, but it has value nonetheless).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review item was helpful though as now that we use MoRefs for the IDs here on the datastore, we can support renaming of the datastore and also remove a ForceNew case if someone has changed it manually :) I just added that support.

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