VMware vSphere Integrated Containers provider (#206)

* Add Virtual Kubelet provider for VIC

Initial virtual kubelet provider for VMware VIC.  This provider currently
handles creating and starting of a pod VM via the VIC portlayer and persona
server.  Image store handling via the VIC persona server.  This provider
currently requires the feature/wolfpack branch of VIC.

* Added pod stop and delete.  Also added node capacity.

Added the ability to stop and delete pod VMs via VIC.  Also retrieve
node capacity information from the VCH.

* Cleanup and readme file

Some file clean up and added a Readme.md markdown file for the VIC
provider.

* Cleaned up errors, added function comments, moved operation code

1. Cleaned up error handling.  Set standard for creating errors.
2. Added method prototype comments for all interface functions.
3. Moved PodCreator, PodStarter, PodStopper, and PodDeleter to a new folder.

* Add mocking code and unit tests for podcache, podcreator, and podstarter

Used the unit test framework used in VIC to handle assertions in the provider's
unit test.  Mocking code generated using OSS project mockery, which is compatible
with the testify assertion framework.

* Vendored packages for the VIC provider

Requires feature/wolfpack branch of VIC and a few specific commit sha of
projects used within VIC.

* Implementation of POD Stopper and Deleter unit tests (#4)

* Updated files for initial PR
This commit is contained in:
Loc Nguyen
2018-06-04 15:41:32 -07:00
committed by Ria Bhatia
parent 98a111e8b7
commit 513cebe7b7
6296 changed files with 1123685 additions and 8 deletions

142
vendor/github.com/vmware/vic/pkg/vsphere/compute/rp.go generated vendored Normal file
View File

@@ -0,0 +1,142 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package compute
import (
"context"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/vm"
)
// ResourcePool struct defines the ResourcePool which provides additional
// VIC specific methods over object.ResourcePool as well as keeps some state
type ResourcePool struct {
*object.ResourcePool
*session.Session
}
// NewResourcePool returns a New ResourcePool object
func NewResourcePool(ctx context.Context, session *session.Session, moref types.ManagedObjectReference) *ResourcePool {
return &ResourcePool{
ResourcePool: object.NewResourcePool(
session.Vim25(),
moref,
),
Session: session,
}
}
func (rp *ResourcePool) GetChildrenVMs(ctx context.Context, s *session.Session) ([]*vm.VirtualMachine, error) {
op := trace.FromContext(ctx, "GetChildrenVMs")
var err error
var mrp mo.ResourcePool
var vms []*vm.VirtualMachine
if err = rp.Properties(op, rp.Reference(), []string{"vm"}, &mrp); err != nil {
op.Errorf("Unable to get children vm of resource pool %s: %s", rp.Name(), err)
return vms, err
}
for _, o := range mrp.Vm {
v := vm.NewVirtualMachine(op, s, o)
vms = append(vms, v)
}
return vms, nil
}
func (rp *ResourcePool) GetChildVM(ctx context.Context, s *session.Session, name string) (*vm.VirtualMachine, error) {
op := trace.FromContext(ctx, "GetChildVM")
searchIndex := object.NewSearchIndex(s.Client.Client)
child, err := searchIndex.FindChild(op, rp.Reference(), name)
if err != nil {
return nil, errors.Errorf("Unable to find VM(%s): %s", name, err.Error())
}
if child == nil {
return nil, nil
}
// instantiate the vm object
return vm.NewVirtualMachine(op, s, child.Reference()), nil
}
func (rp *ResourcePool) GetCluster(ctx context.Context) (*object.ComputeResource, error) {
op := trace.FromContext(ctx, "GetCluster")
var err error
var mrp mo.ResourcePool
if err = rp.Properties(op, rp.Reference(), []string{"owner"}, &mrp); err != nil {
op.Errorf("Unable to get cluster of resource pool %s: %s", rp.Name(), err)
return nil, err
}
return object.NewComputeResource(rp.Client.Client, mrp.Owner), nil
}
func (rp *ResourcePool) GetDatacenter(ctx context.Context) (*object.Datacenter, error) {
op := trace.FromContext(ctx, "GetDatacenter")
dcRef, err := rp.getLowestAncestor(op, "Datacenter")
if err != nil || dcRef == nil {
op.Errorf("Unable to get datacenter ancestor of rp %s: %s", rp.Name(), err)
return nil, errors.Errorf("Unable to get datacenter ancestor of rp %s: %s", rp.Name(), err)
}
return object.NewDatacenter(rp.Client.Client, *dcRef), nil
}
func (rp *ResourcePool) getAncestors(op trace.Operation, inType string) ([]types.ManagedObjectReference, error) {
client := rp.Session.Vim25()
ancestors, err := mo.Ancestors(op, client, client.ServiceContent.PropertyCollector, rp.Reference())
if err != nil {
op.Errorf("Unable to get ancestors of rp %s: %s", rp.Name(), err)
return nil, err
}
outAncestors := make([]types.ManagedObjectReference, 0, len(ancestors))
for _, ancestor := range ancestors {
if ancestor.Self.Type == inType {
a := ancestor.Self
outAncestors = append(outAncestors, a)
}
}
return outAncestors, nil
}
func (rp *ResourcePool) getLowestAncestor(op trace.Operation, inType string) (*types.ManagedObjectReference, error) {
ancestors, err := rp.getAncestors(op, inType)
if err != nil {
op.Errorf("Unable to get ancestors of rp %s: %s", rp.Name(), err)
return nil, err
}
if len(ancestors) == 0 {
return nil, nil
}
index := len(ancestors) - 1
return &ancestors[index], nil
}

View File

@@ -0,0 +1,103 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package compute
import (
"context"
"net/url"
"testing"
"github.com/vmware/govmomi/simulator"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/test"
)
func TestRp(t *testing.T) {
ctx := context.Background()
for i, model := range []*simulator.Model{simulator.ESX(), simulator.VPX()} {
t.Logf("%d", i)
defer model.Remove()
err := model.Create()
if err != nil {
t.Fatal(err)
}
s := model.Service.NewServer()
defer s.Close()
s.URL.User = url.UserPassword("user", "pass")
t.Logf("server URL: %s", s.URL)
var sess *session.Session
if i == 0 {
sess, err = test.SessionWithESX(ctx, s.URL.String())
} else {
sess, err = test.SessionWithVPX(ctx, s.URL.String())
}
if err != nil {
t.Fatal(err)
}
defer sess.Logout(ctx)
testGetChildrenVMs(ctx, sess, t)
testGetChildVM(ctx, sess, t)
testGetCluster(ctx, sess, t)
testGetDatacenter(ctx, sess, t)
}
}
func testGetChildrenVMs(ctx context.Context, sess *session.Session, t *testing.T) {
rp := NewResourcePool(ctx, sess, sess.Pool.Reference())
vms, err := rp.GetChildrenVMs(ctx, sess)
if err != nil {
t.Errorf("Failed to get children vm of resource pool %s, %s", rp.Name(), err)
}
// if vms == nil || len(vms) == 0 {
// t.Error("Didn't get children VM")
// }
for _, vm := range vms {
t.Logf("vm: %s", vm)
}
}
func testGetChildVM(ctx context.Context, sess *session.Session, t *testing.T) {
rp := NewResourcePool(ctx, sess, sess.Pool.Reference())
vm, err := rp.GetChildVM(ctx, sess, "random")
if err == nil && vm != nil {
t.Logf("vm: %s", vm.Reference())
t.Errorf("Should not find VM random")
}
}
func testGetCluster(ctx context.Context, sess *session.Session, t *testing.T) {
rp := NewResourcePool(ctx, sess, sess.Pool.Reference())
cluster, err := rp.GetCluster(ctx)
if err != nil {
t.Logf("Failed to owner cluster: %s", err)
t.Errorf("Should get owner")
}
t.Logf("Cluster: %s", cluster)
}
func testGetDatacenter(ctx context.Context, sess *session.Session, t *testing.T) {
rp := NewResourcePool(ctx, sess, sess.Pool.Reference())
datacenter, err := rp.GetDatacenter(ctx)
if err != nil {
t.Logf("Failed to find parent Datacenter: %s", err)
t.Errorf("Should get Datacenter")
}
t.Logf("Datacenter: %s", datacenter)
}

View File

@@ -0,0 +1,43 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package compute
import (
"context"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/vsphere/session"
)
// VirtualApp struct defines the VirtualApp which provides additional
// VIC specific methods over object.VirtualApp as well as keeps some state
type VirtualApp struct {
*object.VirtualApp
*session.Session
}
// NewResourcePool returns a New ResourcePool object
func NewVirtualApp(ctx context.Context, session *session.Session, moref types.ManagedObjectReference) *VirtualApp {
return &VirtualApp{
VirtualApp: object.NewVirtualApp(
session.Vim25(),
moref,
),
Session: session,
}
}

View File

@@ -0,0 +1,395 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package datastore
import (
"context"
"errors"
"fmt"
"io"
"net/url"
"os"
"path"
"regexp"
"strings"
"github.com/google/uuid"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/tasks"
)
// Helper gives access to the datastore regardless of type (esx, esx + vc,
// or esx + vc + vsan). Also wraps paths to a given root directory
type Helper struct {
// The Datastore API likes everything in "path/to/thing" format.
ds *object.Datastore
s *session.Session
// The FileManager API likes everything in "[dsname] path/to/thing" format.
fm *object.FileManager
// The datastore url (including root) in "[dsname] /path" format.
RootURL object.DatastorePath
}
// NewDatastore returns a Datastore.
// ctx is a context,
// s is an authenticated session
// ds is the vsphere datastore
// rootdir is the top level directory to root all data. If root does not exist,
// it will be created. If it already exists, NOOP. This cannot be empty.
func NewHelper(ctx context.Context, s *session.Session, ds *object.Datastore, rootdir string) (*Helper, error) {
op := trace.FromContext(ctx, "NewHelper")
d := &Helper{
ds: ds,
s: s,
fm: object.NewFileManager(s.Vim25()),
}
if path.IsAbs(rootdir) {
rootdir = rootdir[1:]
}
if err := d.mkRootDir(op, rootdir); err != nil {
op.Infof("error creating root directory %s: %s", rootdir, err)
return nil, err
}
if d.RootURL.Path == "" {
return nil, fmt.Errorf("failed to create root directory")
}
op.Infof("Datastore path is %s", d.RootURL.String())
return d, nil
}
func NewHelperFromURL(ctx context.Context, s *session.Session, u *url.URL) (*Helper, error) {
fm := object.NewFileManager(s.Vim25())
vsDs, err := s.Finder.DatastoreOrDefault(ctx, u.Host)
if err != nil {
return nil, err
}
d := &Helper{
ds: vsDs,
s: s,
fm: fm,
}
d.RootURL.FromString(u.Path)
return d, nil
}
func NewHelperFromSession(ctx context.Context, s *session.Session) *Helper {
return &Helper{
ds: s.Datastore,
s: s,
fm: object.NewFileManager(s.Vim25()),
}
}
// GetDatastores returns a map of datastores given a map of names and urls
func GetDatastores(ctx context.Context, s *session.Session, dsURLs map[string]*url.URL) (map[string]*Helper, error) {
stores := make(map[string]*Helper)
for name, dsURL := range dsURLs {
d, err := NewHelperFromURL(ctx, s, dsURL)
if err != nil {
return nil, err
}
stores[name] = d
}
return stores, nil
}
func (d *Helper) Summary(ctx context.Context) (*types.DatastoreSummary, error) {
var mds mo.Datastore
if err := d.ds.Properties(ctx, d.ds.Reference(), []string{"info", "summary"}, &mds); err != nil {
return nil, err
}
return &mds.Summary, nil
}
func mkdir(op trace.Operation, sess *session.Session, fm *object.FileManager, createParentDirectories bool, path string) (string, error) {
op.Infof("Creating directory %s", path)
if err := fm.MakeDirectory(op, path, sess.Datacenter, createParentDirectories); err != nil {
if soap.IsSoapFault(err) {
soapFault := soap.ToSoapFault(err)
if _, ok := soapFault.VimFault().(types.FileAlreadyExists); ok {
op.Debugf("File already exists: %s", path)
return "", os.ErrExist
}
}
op.Debugf("Creating %s error: %s", path, err)
return "", err
}
return path, nil
}
// Mkdir creates directories.
func (d *Helper) Mkdir(ctx context.Context, createParentDirectories bool, dirs ...string) (string, error) {
op := trace.FromContext(ctx, "Mkdir")
return mkdir(op, d.s, d.fm, createParentDirectories, path.Join(d.RootURL.String(), path.Join(dirs...)))
}
// Ls returns a list of dirents at the given path (relative to root)
//
// A note aboutpaths and the datastore browser.
// None of these work paths work
// r, err := ds.Ls(ctx, "ds:///vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
// r, err := ds.Ls(ctx, "[vsanDatastore] ds:///vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/")
// r, err := ds.Ls(ctx, "[vsanDatastore] //vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/")
// r, err := ds.Ls(ctx, "[] ds:///vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
// r, err := ds.Ls(ctx, "[] /vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
// r, err := ds.Ls(ctx, "[] ../vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
// r, err := ds.Ls(ctx, "[] ./vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
// r, err := ds.Ls(ctx, "[52a67632ac3497a3-411916fd50bedc27] /0ea65357-0494-d42d-2ede-000c292dc5b5")
// r, err := ds.Ls(ctx, "[vsan:52a67632ac3497a3-411916fd50bedc27] /0ea65357-0494-d42d-2ede-000c292dc5b5")
// r, err := ds.Ls(ctx, "[vsan:52a67632ac3497a3-411916fd50bedc27] 0ea65357-0494-d42d-2ede-000c292dc5b5")
// r, err := ds.Ls(ctx, "[vsanDatastore] /vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
// The only URI that works on VC + VSAN.
// r, err := ds.Ls(ctx, "[vsanDatastore] /0ea65357-0494-d42d-2ede-000c292dc5b5")
//
func (d *Helper) Ls(ctx context.Context, p string) (*types.HostDatastoreBrowserSearchResults, error) {
spec := types.HostDatastoreBrowserSearchSpec{
MatchPattern: []string{"*"},
Details: &types.FileQueryFlags{
FileType: true,
FileOwner: types.NewBool(true),
},
}
b, err := d.ds.Browser(ctx)
if err != nil {
return nil, err
}
task, err := b.SearchDatastore(ctx, path.Join(d.RootURL.String(), p), &spec)
if err != nil {
return nil, err
}
info, err := task.WaitForResult(ctx, nil)
if err != nil {
return nil, err
}
res := info.Result.(types.HostDatastoreBrowserSearchResults)
return &res, nil
}
// LsDirs returns a list of dirents at the given path (relative to root)
func (d *Helper) LsDirs(ctx context.Context, p string) (*types.ArrayOfHostDatastoreBrowserSearchResults, error) {
spec := &types.HostDatastoreBrowserSearchSpec{
MatchPattern: []string{"*"},
Details: &types.FileQueryFlags{
FileType: true,
FileOwner: types.NewBool(true),
},
}
b, err := d.ds.Browser(ctx)
if err != nil {
return nil, err
}
task, err := b.SearchDatastoreSubFolders(ctx, path.Join(d.RootURL.String(), p), spec)
if err != nil {
return nil, err
}
info, err := task.WaitForResult(ctx, nil)
if err != nil {
return nil, err
}
res := info.Result.(types.ArrayOfHostDatastoreBrowserSearchResults)
return &res, nil
}
func (d *Helper) Upload(ctx context.Context, r io.Reader, pth string) error {
return d.ds.Upload(ctx, r, path.Join(d.RootURL.Path, pth), &soap.DefaultUpload)
}
func (d *Helper) Download(ctx context.Context, pth string) (io.ReadCloser, error) {
rc, _, err := d.ds.Download(ctx, path.Join(d.RootURL.Path, pth), &soap.DefaultDownload)
return rc, err
}
func (d *Helper) Stat(ctx context.Context, pth string) (types.BaseFileInfo, error) {
i, err := d.ds.Stat(ctx, path.Join(d.RootURL.Path, pth))
if err != nil {
switch err.(type) {
case object.DatastoreNoSuchDirectoryError:
return nil, os.ErrNotExist
default:
return nil, err
}
}
return i, nil
}
func (d *Helper) Mv(ctx context.Context, fromPath, toPath string) error {
op := trace.FromContext(ctx, "Mv")
from := path.Join(d.RootURL.String(), fromPath)
to := path.Join(d.RootURL.String(), toPath)
op.Infof("Moving %s to %s", from, to)
err := tasks.Wait(ctx, func(context.Context) (tasks.Task, error) {
return d.fm.MoveDatastoreFile(ctx, from, d.s.Datacenter, to, d.s.Datacenter, true)
})
return err
}
func (d *Helper) Rm(ctx context.Context, pth string) error {
op := trace.FromContext(ctx, "Rm")
f := path.Join(d.RootURL.String(), pth)
op.Infof("Removing %s", pth)
return d.ds.NewFileManager(d.s.Datacenter, true).Delete(ctx, f) // TODO: NewHelper should create the DatastoreFileManager
}
func (d *Helper) IsVSAN(ctx context.Context) bool {
// #nosec: Errors unhandled.
dsType, _ := d.ds.Type(ctx)
return dsType == types.HostFileSystemVolumeFileSystemTypeVsan
}
// This creates the root directory in the datastore and sets the rooturl and
// rootdir in the datastore struct so we can reuse it for other routines. This
// handles vsan + vc, vsan + esx, and esx. The URI conventions are not the
// same for each and this tries to create the directory and stash the relevant
// result so the URI doesn't need to be recomputed for every datastore
// operation.
func (d *Helper) mkRootDir(op trace.Operation, rootdir string) error {
if rootdir == "" {
return fmt.Errorf("root directory is empty")
}
if path.IsAbs(rootdir) {
return fmt.Errorf("root directory (%s) must not be an absolute path", rootdir)
}
// Handle vsan
// Vsan will complain if the root dir exists. Just call it directly and
// swallow the error if it's already there.
if d.IsVSAN(op) {
comps := strings.Split(rootdir, "/")
nm := object.NewDatastoreNamespaceManager(d.s.Vim25())
// This returns the vmfs path (including the datastore and directory
// UUIDs). Use the directory UUID in future operations because it is
// the stable path which we can use regardless of vsan state.
uuid, err := nm.CreateDirectory(op, d.ds, comps[0], "")
if err != nil {
if !soap.IsSoapFault(err) {
return err
}
soapFault := soap.ToSoapFault(err)
if _, ok := soapFault.VimFault().(types.FileAlreadyExists); !ok {
return err
}
// XXX UGLY HACK until we move this into the installer. Use the
// display name if the dir exists since we can't get the UUID after the
// directory is created.
uuid = comps[0]
err = nil
}
rootdir = path.Join(path.Base(uuid), path.Join(comps[1:]...))
}
rooturl := d.ds.Path(rootdir)
// create the rest of the root dir in case of vSAN, otherwise
// create the full path
if _, err := mkdir(op, d.s, d.fm, true, rooturl); err != nil {
if !os.IsExist(err) {
return err
}
op.Infof("datastore root %s already exists", rooturl)
}
d.RootURL.FromString(rooturl)
return nil
}
func PathFromString(dsp string) (*object.DatastorePath, error) {
var p object.DatastorePath
if !p.FromString(dsp) {
return nil, errors.New(dsp + " not a datastore path")
}
return &p, nil
}
// Parse the datastore format ([datastore1] /path/to/thing) to groups.
var datastoreFormat = regexp.MustCompile(`^\[([\w\d\(\)-_\.\s]+)\]`)
var pathFormat = regexp.MustCompile(`\s([\/\w-_\.]*$)`)
// Converts `[datastore] /path` to ds:// URL
func ToURL(ds string) (*url.URL, error) {
u := new(url.URL)
var matches []string
if matches = datastoreFormat.FindStringSubmatch(ds); len(matches) != 2 {
return nil, fmt.Errorf("Ambiguous datastore hostname format encountered from input: %s.", ds)
}
u.Host = matches[1]
if matches = pathFormat.FindStringSubmatch(ds); len(matches) != 2 {
return nil, fmt.Errorf("Ambiguous datastore path format encountered from input: %s.", ds)
}
u.Path = path.Clean(matches[1])
u.Scheme = "ds"
return u, nil
}
// Converts ds:// URL for datastores to datastore format ([datastore1] /path/to/thing)
func URLtoDatastore(u *url.URL) (string, error) {
scheme := "ds"
if u.Scheme != scheme {
return "", fmt.Errorf("url (%s) is not a datastore", u.String())
}
return fmt.Sprintf("[%s] %s", u.Host, u.Path), nil
}
// TestName builds a unique datastore name
func TestName(suffix string) string {
return uuid.New().String()[0:16] + "-" + suffix
}

View File

@@ -0,0 +1,219 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package datastore
import (
"math/rand"
"net/url"
"os"
"path"
"testing"
"time"
"context"
"github.com/stretchr/testify/assert"
)
// test if we can get a Datastore via the rooturl
func TestDatastoreGetDatastores(t *testing.T) {
ctx, ds, cleanupfunc := DSsetup(t)
if t.Failed() {
return
}
defer cleanupfunc()
firstSummary, err := ds.Summary(ctx)
if !assert.NoError(t, err) {
return
}
t.Logf("Name:\t%s\n", firstSummary.Name)
t.Logf(" Path:\t%s\n", ds.ds.InventoryPath)
t.Logf(" Type:\t%s\n", firstSummary.Type)
t.Logf(" URL:\t%s\n", firstSummary.Url)
t.Logf(" Capacity:\t%.1f GB\n", float64(firstSummary.Capacity)/(1<<30))
t.Logf(" Free:\t%.1f GB\n", float64(firstSummary.FreeSpace)/(1<<30))
inMap := make(map[string]*url.URL)
p, err := url.Parse(ds.RootURL.String())
if !assert.NoError(t, err) {
return
}
inMap["foo"] = p
dstores, err := GetDatastores(context.TODO(), ds.s, inMap)
if !assert.NoError(t, err) || !assert.NotNil(t, dstores) {
return
}
secondSummary, err := ds.Summary(ctx)
if !assert.NoError(t, err) {
return
}
if !assert.Equal(t, firstSummary, secondSummary) {
return
}
}
func TestDatastoreRestart(t *testing.T) {
// creates a root in the datastore
ctx, ds, cleanupfunc := DSsetup(t)
if t.Failed() {
return
}
defer cleanupfunc()
// Create a nested dir in the root and use that as the datastore
nestedRoot := path.Join(ds.RootURL.Path, "foo")
ds, err := NewHelper(ctx, ds.s, ds.s.Datastore, nestedRoot)
if !assert.NoError(t, err) {
return
}
// test we can ls the root
_, err = ds.Ls(ctx, "")
if !assert.NoError(t, err) {
return
}
// create a dir
_, err = ds.Mkdir(ctx, true, "baz")
if !assert.NoError(t, err) {
return
}
// create a new datastore object with the same path as the nested one
ds, err = NewHelper(ctx, ds.s, ds.s.Datastore, nestedRoot)
if !assert.NoError(t, err) {
return
}
// try to create the same baz dir, assert it exists
_, err = ds.Mkdir(ctx, true, "baz")
if !assert.Error(t, err) || !assert.True(t, os.IsExist(err)) {
return
}
assert.NotEmpty(t, ds.RootURL)
}
func TestDatastoreCreateDir(t *testing.T) {
ctx, ds, cleanupfunc := DSsetup(t)
if t.Failed() {
return
}
defer cleanupfunc()
_, err := ds.Ls(ctx, "")
if !assert.NoError(t, err) {
return
}
// assert create dir of a dir that exists is os.ErrExists
_, err = ds.Mkdir(ctx, true, "foo")
if !assert.NoError(t, err) {
return
}
_, err = ds.Mkdir(ctx, true, "foo")
if !assert.Error(t, err) || !assert.True(t, os.IsExist(err)) {
return
}
}
func TestDatastoreMkdirAndLs(t *testing.T) {
ctx, ds, cleanupfunc := DSsetup(t)
if t.Failed() {
return
}
defer cleanupfunc()
dirs := []string{"dir1", "dir1/child1"}
// create the dir then test it exists by calling ls
for _, dir := range dirs {
_, err := ds.Mkdir(ctx, true, dir)
if !assert.NoError(t, err) {
return
}
_, err = ds.Ls(ctx, dir)
if !assert.NoError(t, err) {
return
}
}
}
func TestDatastoreToURLParsing(t *testing.T) {
expectedURL := "ds://datastore1/path/to/thing"
input := [][]string{
{"[datastore1] /path/to/thing", expectedURL},
{"[datastore1] path/to/thing", expectedURL},
{"[datastore1] ///path////to/thing", expectedURL},
{"[Datastore (1)] /path/to/thing", "ds://Datastore%20(1)/path/to/thing"},
{"[datastore1] path", "ds://datastore1/path"},
{"[datastore1] pa-th", "ds://datastore1/pa-th"},
{"[datastore1] pa_th", "ds://datastore1/pa_th"},
{"[data_store1] pa_th", "ds://data_store1/pa_th"},
}
dsoutputs := []string{
"[datastore1] /path/to/thing",
"[datastore1] path/to/thing",
"[datastore1] /path/to/thing",
"[Datastore (1)] /path/to/thing",
"[datastore1] path",
"[datastore1] pa-th",
"[datastore1] pa_th",
"[data_store1] pa_th",
}
for i, in := range input {
u, err := ToURL(in[0])
if !assert.NoError(t, err) || !assert.NotNil(t, u) {
return
}
if !assert.Equal(t, in[1], u.String()) {
return
}
out, err := URLtoDatastore(u)
if !assert.NoError(t, err) || !assert.True(t, len(out) > 0) {
return
}
if !assert.Equal(t, dsoutputs[i], out) {
return
}
}
}
// From https://siongui.github.io/2015/04/13/go-generate-random-string/
func RandomString(strlen int) string {
rand.Seed(time.Now().UTC().UnixNano())
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
result := make([]byte, strlen)
for i := 0; i < strlen; i++ {
result[i] = chars[rand.Intn(len(chars))]
}
return string(result)
}

View File

@@ -0,0 +1,83 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package datastore
import (
"context"
"testing"
"time"
log "github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/tasks"
"github.com/vmware/vic/pkg/vsphere/test/env"
)
// Used in testing
func Session(ctx context.Context, t *testing.T) *session.Session {
config := &session.Config{
Service: env.URL(t),
/// XXX Why does this insist on having this field populated?
DatastorePath: env.DS(t),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
}
s := session.NewSession(config)
_, err := s.Connect(ctx)
if err != nil {
s.Client.Logout(ctx)
t.Log(err.Error())
t.SkipNow()
}
_, err = s.Populate(ctx)
if err != nil {
t.Log(err.Error())
t.SkipNow()
}
return s
}
func DSsetup(t *testing.T) (context.Context, *Helper, func()) {
log.SetLevel(log.DebugLevel)
ctx := context.Background()
sess := Session(ctx, t)
ds, err := NewHelper(ctx, sess, sess.Datastore, TestName("dstests"))
if !assert.NoError(t, err) {
return ctx, nil, nil
}
f := func() {
log.Infof("Removing test root %s", ds.RootURL.String())
err := tasks.Wait(ctx, func(context.Context) (tasks.Task, error) {
return ds.fm.DeleteDatastoreFile(ctx, ds.RootURL.String(), sess.Datacenter)
})
if err != nil {
log.Errorf(err.Error())
return
}
}
return ctx, ds, f
}

152
vendor/github.com/vmware/vic/pkg/vsphere/diag/diag.go generated vendored Normal file
View File

@@ -0,0 +1,152 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package diag
import (
"bytes"
"context"
"crypto/tls"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/vmware/vic/pkg/trace"
)
// StatusCodeFatalThreshold defines a threshold after which all codes can be treated as fatal.
const StatusCodeFatalThreshold = 64
const (
// VCStatusOK vSphere API is available.
VCStatusOK = 0
// VCStatusInvalidURL Provided vSphere API URL is wrong.
VCStatusInvalidURL = 64
// VCStatusErrorQuery Error happened trying to query vSphere API
VCStatusErrorQuery = 65
// VCStatusErrorResponse Received response doesn't contain expected data.
VCStatusErrorResponse = 66
// VCStatusIncorrectResponse Received in case if returned data from server is different from expected.
VCStatusIncorrectResponse = 67
// VCStatusNotXML Received response is not XML
VCStatusNotXML = 68
// VCStatusUnknownHost is returned in case if DNS failed to resolve name.
VCStatusUnknownHost = 69
// VCStatusHostIsNotReachable
VCStatusHostIsNotReachable = 70
)
// UserReadableVCAPITestDescription convert API test code into user readable text
func UserReadableVCAPITestDescription(code int) string {
switch code {
case VCStatusOK:
return "vSphere API target responds as expected"
case VCStatusInvalidURL:
return "vSphere API target url is invalid"
case VCStatusErrorQuery:
return "vSphere API target failed to respond to the query"
case VCStatusIncorrectResponse:
return "vSphere API target returns unexpected response"
case VCStatusErrorResponse:
return "vSphere API target returns error"
case VCStatusNotXML:
return "vSphere API target returns non XML response"
case VCStatusUnknownHost:
return "vSphere API target can not be resolved from VCH"
case VCStatusHostIsNotReachable:
return "vSphere API target is out of reach. Wrong routing table?"
default:
return "vSphere API target test returned unknown code"
}
}
// CheckAPIAvailability accesses vSphere API to ensure it is a correct end point that is up and running.
func CheckAPIAvailability(targetURL string) int {
op := trace.NewOperation(context.Background(), "api test")
errorCode := VCStatusErrorQuery
u, err := url.Parse(targetURL)
if err != nil {
return VCStatusInvalidURL
}
u.Path = "/sdk/vimService.wsdl"
apiURL := u.String()
op.Debugf("Checking access to: %s", apiURL)
for attempts := 5; errorCode != VCStatusOK && attempts > 0; attempts-- {
// #nosec: TLS InsecureSkipVerify set true
c := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
// Is 20 seconds enough to receive any response from vSphere target server?
Timeout: time.Second * 20,
}
errorCode = queryAPI(op, c.Get, apiURL)
}
return errorCode
}
func queryAPI(op trace.Operation, getter func(string) (*http.Response, error), apiURL string) int {
resp, err := getter(apiURL)
if err != nil {
errTxt := err.Error()
op.Errorf("Query error: %s", err)
if strings.Contains(errTxt, "no such host") {
return VCStatusUnknownHost
}
if strings.Contains(errTxt, "no route to host") {
return VCStatusHostIsNotReachable
}
if strings.Contains(errTxt, "host is down") {
return VCStatusHostIsNotReachable
}
return VCStatusErrorQuery
}
data := make([]byte, 65636)
n, err := io.ReadFull(resp.Body, data)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
op.Errorf("Query error: %s", err)
return VCStatusErrorResponse
}
if n >= len(data) {
// #nosec: Errors unhandled.
io.Copy(ioutil.Discard, resp.Body)
}
// #nosec: Errors unhandled.
resp.Body.Close()
contentType := strings.ToLower(resp.Header.Get("Content-Type"))
if !strings.Contains(contentType, "text/xml") {
op.Errorf("Unexpected content type %s, should be text/xml", contentType)
op.Errorf("Response from the server: %s", string(data))
return VCStatusNotXML
}
// we just want to make sure that response contains something familiar that we could
// use as vSphere API marker.
if !bytes.Contains(data, []byte("urn:vim25Service")) {
op.Errorf("Server response doesn't contain 'urn:vim25Service': %s", string(data))
return VCStatusIncorrectResponse
}
return VCStatusOK
}

View File

@@ -0,0 +1,117 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package diag
import (
"bytes"
"context"
"errors"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vic/pkg/trace"
)
func TestCheckAPIAvailability(t *testing.T) {
assert.Equal(t, VCStatusErrorQuery, CheckAPIAvailability("http://127.0.0.1:65535"))
assert.Equal(t, VCStatusErrorQuery, CheckAPIAvailability("http://127.0.0.1:65536"))
}
func TestCheckAPIAvailabilityQueryWithGetterError(t *testing.T) {
op := trace.NewOperation(context.Background(), "test")
f := func(s string) (*http.Response, error) { return nil, errors.New("wrong query") }
code := queryAPI(op, f, "testurl")
assert.Equal(t, VCStatusErrorQuery, code)
}
type readerWithError struct {
err error
data *bytes.Reader
}
func (r *readerWithError) Read(b []byte) (int, error) {
if r.err != nil {
return 0, r.err
}
return r.data.Read(b)
}
func (r *readerWithError) Close() error {
return r.err
}
func TestCheckAPIAvailabilityQueryReadError(t *testing.T) {
op := trace.NewOperation(context.Background(), "test")
f := func(s string) (*http.Response, error) {
hr := &http.Response{
Body: &readerWithError{
err: errors.New("read error happened"),
},
}
return hr, nil
}
code := queryAPI(op, f, "testurl")
assert.Equal(t, VCStatusErrorResponse, code)
}
func TestCheckAPIAvailabilityQueryIncorrectDataType(t *testing.T) {
op := trace.NewOperation(context.Background(), "test")
f := func(s string) (*http.Response, error) {
hr := &http.Response{
Body: &readerWithError{
data: bytes.NewReader([]byte("some data")),
},
}
return hr, nil
}
code := queryAPI(op, f, "testurl")
assert.Equal(t, VCStatusNotXML, code)
}
func TestCheckAPIAvailabilityQueryIncorrectData(t *testing.T) {
op := trace.NewOperation(context.Background(), "test")
f := func(s string) (*http.Response, error) {
hr := &http.Response{
Body: &readerWithError{
data: bytes.NewReader([]byte("some data")),
},
Header: http.Header{"Content-Type": []string{"text/xml"}},
}
return hr, nil
}
code := queryAPI(op, f, "testurl")
assert.Equal(t, VCStatusIncorrectResponse, code)
}
func TestCheckAPIAvailabilityQueryCorrectData(t *testing.T) {
op := trace.NewOperation(context.Background(), "test")
f := func(s string) (*http.Response, error) {
hr := &http.Response{
Body: &readerWithError{
data: bytes.NewReader([]byte("some urn:vim25Service data")),
},
Header: http.Header{"Content-Type": []string{"text/xml"}},
}
return hr, nil
}
code := queryAPI(op, f, "testurl")
assert.Equal(t, VCStatusOK, code)
}
func TestCheckAPIAvailabilityIncorrectDNSName(t *testing.T) {
assert.Equal(t, VCStatusUnknownHost, CheckAPIAvailability("https://example.notexisting.domain"))
}

View File

@@ -0,0 +1,39 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package diagnostic
import (
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/pkg/vsphere/session"
)
// Manager struct defines the Manager which provides additional
// VIC specific methods over object.DiagnosticManager
type Manager struct {
*object.DiagnosticManager
*session.Session
}
// NewDiagnosticManager returns a new DiagnosticManager object
func NewDiagnosticManager(session *session.Session) *Manager {
return &Manager{
DiagnosticManager: object.NewDiagnosticManager(
session.Vim25(),
),
Session: session,
}
}

102
vendor/github.com/vmware/vic/pkg/vsphere/disk/config.go generated vendored Normal file
View File

@@ -0,0 +1,102 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package disk
import (
"fmt"
"hash/fnv"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/fs"
)
type VirtualDiskConfig struct {
// The URI in the datastore this disk can be found with
DatastoreURI *object.DatastorePath
// The URI in the datastore to the parent of this disk
ParentDatastoreURI *object.DatastorePath
// The size of the disk
CapacityInKB int64
// Underlying filesystem
Filesystem Filesystem
// Base disk UUID
UUID string
DiskMode types.VirtualDiskMode
}
func NewPersistentDisk(URI *object.DatastorePath) *VirtualDiskConfig {
return &VirtualDiskConfig{
DatastoreURI: URI,
DiskMode: types.VirtualDiskModeIndependent_persistent,
Filesystem: fs.NewExt4(),
}
}
func NewNonPersistentDisk(URI *object.DatastorePath) *VirtualDiskConfig {
return &VirtualDiskConfig{
DatastoreURI: URI,
DiskMode: types.VirtualDiskModeIndependent_nonpersistent,
Filesystem: fs.NewExt4(),
}
}
func (d *VirtualDiskConfig) WithParent(parent *object.DatastorePath) *VirtualDiskConfig {
d.ParentDatastoreURI = parent
return d
}
func (d *VirtualDiskConfig) WithFilesystem(ftype FilesystemType) *VirtualDiskConfig {
switch ftype {
case Xfs:
d.Filesystem = fs.NewXFS()
default:
d.Filesystem = fs.NewExt4()
}
return d
}
func (d *VirtualDiskConfig) WithCapacity(capacity int64) *VirtualDiskConfig {
d.CapacityInKB = capacity
return d
}
// WithUUID can only be set on the base disk layer due to disklib bug
// TODO: add an error mechanism for validating conditional settings like this
func (d *VirtualDiskConfig) WithUUID(uuid string) *VirtualDiskConfig {
d.UUID = uuid
return d
}
func (d *VirtualDiskConfig) Hash() uint64 {
key := fmt.Sprintf("%s-%t", d.DatastoreURI, d.IsPersistent())
hash := fnv.New64a()
hash.Write([]byte(key))
return hash.Sum64()
}
func (d *VirtualDiskConfig) IsPersistent() bool {
return d.DiskMode == types.VirtualDiskModeIndependent_persistent || d.DiskMode == types.VirtualDiskModePersistent
}

473
vendor/github.com/vmware/vic/pkg/vsphere/disk/disk.go generated vendored Normal file
View File

@@ -0,0 +1,473 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package disk
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"strings"
"sync"
"sync/atomic"
"github.com/vmware/vic/pkg/trace"
)
// FilesystemType represents the filesystem in use by a virtual disk
type FilesystemType uint8
const (
// Ext4 represents the ext4 file system
Ext4 FilesystemType = iota + 1
// Xfs represents the XFS file system
Xfs
// Ntfs represents the NTFS file system
Ntfs
// directory in which to perform the direct mount of disk for bind mount
// to actual target
diskBindBase = "/.filesystem-by-label/"
// used to isolate applications from the lost+found in the root of ext4
VolumeDataDir = "/.vic.vol.data"
)
// Filesystem defines the interface for handling an attached virtual disk
type Filesystem interface {
Mkfs(op trace.Operation, devPath, label string) error
SetLabel(op trace.Operation, devPath, labelName string) error
Mount(op trace.Operation, devPath, targetPath string, options []string) error
Unmount(op trace.Operation, path string) error
}
// Semaphore represents the number of references to a disk
type Semaphore struct {
resource string
refname string
count uint64
}
// NewSemaphore creates and returns a Semaphore initialized to 0
func NewSemaphore(r, n string) *Semaphore {
return &Semaphore{
resource: r,
refname: n,
count: 0,
}
}
// Increment increases the reference count by one
func (r *Semaphore) Increment() uint64 {
return atomic.AddUint64(&r.count, 1)
}
// Decrement decreases the reference count by one
func (r *Semaphore) Decrement() uint64 {
return atomic.AddUint64(&r.count, ^uint64(0))
}
// Count returns the current reference count
func (r *Semaphore) Count() uint64 {
return atomic.LoadUint64(&r.count)
}
// InUseError is returned when a detach is attempted on a disk that is
// still in use
type InUseError struct {
error
}
// VirtualDisk represents a VMDK in the datastore, the device node it may be
// attached at (if it's attached), the mountpoint it is mounted at (if
// mounted), and other configuration.
type VirtualDisk struct {
*VirtualDiskConfig
// The device node the disk is attached to
DevicePath string
// The path on the filesystem this device is attached to.
mountPath string
// The options that the disk is currently mounted with.
mountOpts string
// To avoid attach/detach races, this lock serializes operations to the disk.
l sync.Mutex
mountedRefs *Semaphore
attachedRefs *Semaphore
}
// NewVirtualDisk creates and returns a new VirtualDisk object associated with the
// given datastore formatted with the specified FilesystemType
func NewVirtualDisk(op trace.Operation, config *VirtualDiskConfig, disks map[uint64]*VirtualDisk) (*VirtualDisk, error) {
if !strings.HasSuffix(config.DatastoreURI.String(), ".vmdk") {
return nil, fmt.Errorf("%s doesn't have a vmdk suffix", config.DatastoreURI.String())
}
if d, ok := disks[config.Hash()]; ok {
return d, nil
}
op.Debugf("Didn't find the disk %s in the DiskManager cache, creating it", config.DatastoreURI)
uri := config.DatastoreURI.String()
d := &VirtualDisk{
VirtualDiskConfig: config,
mountedRefs: NewSemaphore(uri, "mount"),
attachedRefs: NewSemaphore(uri, "attach"),
}
disks[config.Hash()] = d
return d, nil
}
func (d *VirtualDisk) setAttached(op trace.Operation, devicePath string) (err error) {
if d.DevicePath == "" {
// Question: what happens if this is called a second time with a different devicePath?
d.DevicePath = devicePath
}
count := d.attachedRefs.Increment()
op.Debugf("incremented attach count for %s: %d", d.DatastoreURI, count)
return nil
}
func (d *VirtualDisk) canBeDetached() error {
if !d.attached() {
return fmt.Errorf("%s is already detached", d.DatastoreURI)
}
if d.mounted() {
return fmt.Errorf("%s is mounted (%s)", d.DatastoreURI, d.mountPath)
}
if d.inUseByOther() {
return fmt.Errorf("Detach skipped - %s is still in use", d.DatastoreURI)
}
return nil
}
func (d *VirtualDisk) setDetached(op trace.Operation, disks map[uint64]*VirtualDisk) {
// we only call this when it's been detached, so always make the updates
op.Debugf("Dropping %s from the DiskManager cache", d.DatastoreURI)
d.DevicePath = ""
delete(disks, d.Hash())
}
// Mkfs formats the disk with Filesystem and sets the disk label
func (d *VirtualDisk) Mkfs(op trace.Operation, labelName string) error {
d.l.Lock()
defer d.l.Unlock()
if !d.attached() {
return fmt.Errorf("%s isn't attached", d.DatastoreURI)
}
if d.mounted() {
return fmt.Errorf("%s is still mounted (%s)", d.DatastoreURI, d.mountPath)
}
return d.Filesystem.Mkfs(op, d.DevicePath, labelName)
}
// SetLabel sets this disk's label
func (d *VirtualDisk) SetLabel(op trace.Operation, labelName string) error {
d.l.Lock()
defer d.l.Unlock()
if !d.attached() {
return fmt.Errorf("%s isn't attached", d.DatastoreURI)
}
return d.Filesystem.SetLabel(op, d.DevicePath, labelName)
}
func (d *VirtualDisk) attached() bool {
return d.DevicePath != ""
}
// Attached returns true if this disk is attached, false otherwise
func (d *VirtualDisk) Attached() bool {
d.l.Lock()
defer d.l.Unlock()
return d.attached()
}
func (d *VirtualDisk) attachedByOther() bool {
return d.attachedRefs.Count() > 1
}
// AttachedByOther returns true if the attached references are > 1
func (d *VirtualDisk) AttachedByOther() bool {
d.l.Lock()
defer d.l.Unlock()
return d.attachedByOther()
}
func (d *VirtualDisk) mountedByOther() bool {
return d.mountedRefs.Count() > 1
}
// MountedByOther returns true if the mounted references are > 1
func (d *VirtualDisk) MountedByOther() bool {
d.l.Lock()
defer d.l.Unlock()
return d.mountedByOther()
}
func (d *VirtualDisk) inUseByOther() bool {
return d.mountedByOther() || d.attachedByOther()
}
// InUseByOther returns true if the disk is currently attached or
// mounted by someone else
func (d *VirtualDisk) InUseByOther() bool {
d.l.Lock()
defer d.l.Unlock()
return d.inUseByOther()
}
// Mount attempts to mount this disk. A NOP occurs if the disk is already mounted
// It returns the path at which the disk is mounted
// Enhancement: allow provision of mount path and refcount for:
// specific mount point and options
func (d *VirtualDisk) Mount(op trace.Operation, options []string) (string, error) {
d.l.Lock()
defer d.l.Unlock()
op.Debugf("Mounting %s", d.DatastoreURI)
if !d.attached() {
err := fmt.Errorf("%s isn't attached", d.DatastoreURI)
op.Error(err)
return "", err
}
opts := strings.Join(options, ";")
if !d.mounted() {
mntpath, err := ioutil.TempDir("", "mnt")
if err != nil {
err := fmt.Errorf("unable to create mountpint: %s", err)
op.Error(err)
return "", err
}
// get mount source, disk is already mounted if this func returns without error
mntsrc, err := d.getMountSource(op, options)
if err != nil {
op.Error(err)
return "", err
}
// then mount it at the correct source
if strings.HasSuffix(mntsrc, VolumeDataDir) {
// append bind mount options if we are masking lost+found
options = append(options, "bind")
}
if err = d.Filesystem.Mount(op, mntsrc, mntpath, options); err != nil {
op.Errorf("Failed to mount disk: %s", err)
return "", err
}
d.mountPath = mntpath
d.mountOpts = opts
} else {
// basic santiy check for matching options - we don't want to share a r/o mount
// if the request was for r/w. Ideally we'd just mount this at a different location with the
// requested options but that requires separate ref counting.
// TODO: support differing mount opts
if d.mountOpts != opts {
op.Errorf("Unable to use mounted disk due to differing options: %s != %s", d.mountOpts, opts)
return "", fmt.Errorf("incompatible mount options for disk reuse")
}
}
count := d.mountedRefs.Increment()
op.Debugf("incremented mount count for %s: %d", d.mountPath, count)
return d.mountPath, nil
}
// getMountSource mounts the disk rootfs, checks if it has volumeDataDir, if so it returns volumeDataDir
// as the mount source to mask the lost+found folder, otherwise it returns the device path
// NOTE: this mount should not be counted in the ref counts, bindTarget will be unmounted when disk detaches.
// TODO: if we support different mount opts, we can't use the same bindTarget anymore.
// need to assign each opt a different name, we can add a field in VirtualDisk that tracks bindTarget
func (d *VirtualDisk) getMountSource(op trace.Operation, options []string) (string, error) {
// need to first mount the disk under the diskBindBase
bindTarget := path.Join(diskBindBase, d.DevicePath)
// sanity check to make sure previous bindTarget is cleaned up properly
var e1, e2 error
_, e1 = os.Stat(bindTarget)
if e1 == nil {
// bindTarget exists, check whether or not bindTarget is a mount point
e2 = os.Remove(bindTarget)
}
// we don't want to remount under the same mountpoint, so we only mounts under the following cases
// first case: bindTarget exists but not a mountpoint
// second case: bindTarget doesn't exist
if (e1 == nil && e2 == nil) || os.IsNotExist(e1) {
// #nosec
if err := os.MkdirAll(bindTarget, 0744); err != nil {
err = fmt.Errorf("unable to create mount point %s: %s", bindTarget, err)
op.Error(err)
return "", err
}
if err := d.Filesystem.Mount(op, d.DevicePath, bindTarget, options); err != nil {
op.Errorf("Failed to mount disk: %s", err)
return "", err
}
}
mntsrc := path.Join(bindTarget, VolumeDataDir)
// if the volume contains a volumeDataDir directory then mount that instead of the root of the filesystem
// if we cannot read it we go with the root of the filesystem
_, err := os.Stat(mntsrc)
if err != nil {
if os.IsNotExist(err) {
// if there's no such directory then revert to using the device directly
op.Infof("No " + VolumeDataDir + " data directory in volume, mounting filesystem directly")
mntsrc = d.DevicePath
} else {
return "", fmt.Errorf("unable to determine whether lost+found masking is required: %s", err)
}
}
return mntsrc, nil
}
// Unmount attempts to unmount a virtual disk
func (d *VirtualDisk) Unmount(op trace.Operation) error {
d.l.Lock()
defer d.l.Unlock()
if !d.mounted() {
return fmt.Errorf("%s already unmounted", d.DatastoreURI)
}
count := d.mountedRefs.Decrement()
op.Debugf("decremented mount count for %s: %d", d.mountPath, count)
if count > 0 {
return nil
}
// no more mount references to this disk, so actually unmount
if err := d.Filesystem.Unmount(op, d.mountPath); err != nil {
err := fmt.Errorf("failed to unmount disk: %s", err)
op.Error(err)
return err
}
// only remove the mount directory - if we've succeeded in the unmount there won't be anything in it
// if we somehow get here and there is content we do NOT want to delete it
if err := os.Remove(d.mountPath); err != nil {
err := fmt.Errorf("failed to clean up mount point: %s", err)
op.Error(err)
return err
}
d.mountPath = ""
// mountpath is cleaned, we need to clean up the bindTarget as well
bindTarget := path.Join(diskBindBase, d.DevicePath)
if err := d.Filesystem.Unmount(op, bindTarget); err != nil {
return fmt.Errorf("failed to clean up actual mount point on device: %s", err)
}
// only remove the mount directory - if we've succeeded in the unmount there won't be anything in it
// if we somehow get here and there is content we do NOT want to delete it
if err := os.Remove(bindTarget); err != nil {
err := fmt.Errorf("failed to clean up actual mount point: %s", err)
return err
}
return nil
}
func (d *VirtualDisk) mountPathFn() (string, error) {
if !d.mounted() {
return "", fmt.Errorf("%s isn't mounted", d.DatastoreURI)
}
return d.mountPath, nil
}
// MountPath returns the path on which the virtual disk is mounted,
// or an error if the disk is not mounted
func (d *VirtualDisk) MountPath() (string, error) {
d.l.Lock()
defer d.l.Unlock()
return d.mountPathFn()
}
// DiskPath returns a URL referencing the path of the virtual disk
// on the datastore
func (d *VirtualDisk) DiskPath() url.URL {
d.l.Lock()
defer d.l.Unlock()
return url.URL{
Scheme: "ds",
Path: d.DatastoreURI.String(),
}
}
func (d *VirtualDisk) mounted() bool {
return d.mountPath != ""
}
// Mounted returns true if the virtual disk is mounted, false otherwise
func (d *VirtualDisk) Mounted() bool {
d.l.Lock()
defer d.l.Unlock()
return d.mounted()
}
func (d *VirtualDisk) canBeUnmounted() error {
if !d.attached() {
return fmt.Errorf("%s is detached", d.DatastoreURI)
}
if !d.mounted() {
return fmt.Errorf("%s is unmounted", d.DatastoreURI)
}
return nil
}
func (d *VirtualDisk) setUmounted() error {
if !d.mounted() {
return fmt.Errorf("%s already unmounted", d.DatastoreURI)
}
d.mountPath = ""
return nil
}

View File

@@ -0,0 +1,292 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package disk
import (
"path"
"testing"
"context"
log "github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/mount"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/lib/guest"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/datastore"
)
// Create a disk, make an ext filesystem on it, set the label, mount it,
// unmount it, then clean up.
func TestCreateFS(t *testing.T) {
log.SetLevel(log.InfoLevel)
if testing.Verbose() {
log.SetLevel(log.DebugLevel)
}
session := Session(context.Background(), t)
if session == nil {
return
}
op := trace.NewOperation(context.TODO(), t.Name())
vchvm, err := guest.GetSelf(op, session)
if err != nil {
t.Skip("Not in a vm")
}
view := ContainerView(op, session, vchvm)
if view == nil {
t.Skip("Can't create a view")
}
imagestore := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: datastore.TestName(t.Name()),
}
// file manager
fm := object.NewFileManager(session.Vim25())
// create a directory in the datastore
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
if !assert.NoError(t, err) {
return
}
// Nuke the image store
defer func() {
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
if !assert.NoError(t, err) {
return
}
_, err = task.WaitForResult(context.TODO(), nil)
if !assert.NoError(t, err) {
return
}
}()
// create a diskmanager
vdm, err := NewDiskManager(op, session, view)
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
return
}
diskSize := int64(1 << 10)
scratch := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: path.Join(imagestore.Path, "scratch.vmdk"),
}
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
d, err := vdm.CreateAndAttach(op, config)
if !assert.NoError(t, err) {
return
}
// make the filesysetem
if err = d.Mkfs(op, "foo"); !assert.NoError(t, err) {
return
}
// set the label
if err = d.SetLabel(op, "foo"); !assert.NoError(t, err) {
return
}
// do the mount
dir, err := d.Mount(op, nil)
if !assert.NoError(t, err) {
return
}
// boom
if mounted, err := mount.Mounted(dir); !assert.NoError(t, err) || !assert.True(t, mounted) {
return
}
// clean up
err = d.Unmount(op)
if !assert.NoError(t, err) {
return
}
err = vdm.Detach(op, config)
if !assert.NoError(t, err) {
return
}
}
func TestAttachFS(t *testing.T) {
log.SetLevel(log.InfoLevel)
if testing.Verbose() {
log.SetLevel(log.DebugLevel)
}
session := Session(context.Background(), t)
if session == nil {
return
}
op := trace.NewOperation(context.TODO(), t.Name())
vchvm, err := guest.GetSelf(op, session)
if err != nil {
t.Skip("Not in a vm")
}
view := ContainerView(op, session, vchvm)
if view == nil {
t.Skip("Can't create a view")
}
imagestore := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: datastore.TestName(t.Name()),
}
// file manager
fm := object.NewFileManager(session.Vim25())
// create a directory in the datastore
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
if !assert.NoError(t, err) {
return
}
// Nuke the image store
defer func() {
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
if !assert.NoError(t, err) {
return
}
_, err = task.WaitForResult(context.TODO(), nil)
if !assert.NoError(t, err) {
return
}
}()
// create a diskmanager
vdm, err := NewDiskManager(op, session, view)
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
return
}
diskSize := int64(1 << 10)
scratch := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: path.Join(imagestore.Path, "scratch.vmdk"),
}
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
d, err := vdm.CreateAndAttach(op, config)
if !assert.NoError(t, err) {
return
}
// make the filesysetem
if err = d.Mkfs(op, "foo"); !assert.NoError(t, err) {
return
}
// set the label
if err = d.SetLabel(op, "foo"); !assert.NoError(t, err) {
return
}
// do the mount
dir, err := d.Mount(op, nil)
if !assert.NoError(t, err) {
return
}
// boom
if mounted, err := mount.Mounted(dir); !assert.NoError(t, err) || !assert.True(t, mounted) {
return
}
// clean up
err = d.Unmount(op)
if !assert.NoError(t, err) {
return
}
err = vdm.Detach(op, config)
if !assert.NoError(t, err) {
return
}
child := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: path.Join(imagestore.Path, "child.vmdk"),
}
config = NewPersistentDisk(child).WithParent(scratch)
d, err = vdm.CreateAndAttach(op, config)
if !assert.NoError(t, err) {
return
}
// do the mount
dir, err = d.Mount(op, nil)
if !assert.NoError(t, err) {
return
}
// boom
if mounted, err := mount.Mounted(dir); !assert.NoError(t, err) || !assert.True(t, mounted) {
return
}
// clean up
err = d.Unmount(op)
if !assert.NoError(t, err) {
return
}
err = vdm.Detach(op, config)
if !assert.NoError(t, err) {
return
}
for i := 0; i < 5; i++ {
config = NewPersistentDisk(child)
d, err = vdm.CreateAndAttach(op, config)
if !assert.NoError(t, err) {
return
}
// do the mount
dir, err = d.Mount(op, nil)
if !assert.NoError(t, err) {
return
}
// boom
if mounted, err := mount.Mounted(dir); !assert.NoError(t, err) || !assert.True(t, mounted) {
return
}
// clean up
err = d.Unmount(op)
if !assert.NoError(t, err) {
return
}
err = vdm.Detach(op, config)
if !assert.NoError(t, err) {
return
}
}
}

View File

@@ -0,0 +1,668 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package disk
import (
"context"
"fmt"
"net/url"
"os"
"sync"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/view"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/datastore"
"github.com/vmware/vic/pkg/vsphere/guest"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/tasks"
"github.com/vmware/vic/pkg/vsphere/vm"
)
const (
// You can assign the device to (1:z ), where 1 is SCSI controller 1 and z is a virtual device node from 0 to 15.
// https://pubs.vmware.com/vsphere-65/index.jsp#com.vmware.vsphere.vm_admin.doc/GUID-5872D173-A076-42FE-8D0B-9DB0EB0E7362.html
MaxAttachedDisks = 16
)
// Manager manages disks for the vm it runs on. The expectation is this is run
// from a VM on a vsphere instance. This VM creates disks on ESX, attaches
// them to itself, writes to them, then detaches them.
type Manager struct {
// We can't have more than this number of disks attached.
maxAttached chan bool
// reference to the vm this is running on.
vm *vm.VirtualMachine
// VirtualDiskManager that is used to create vmdks directly on datastore
// from https://pubs.vmware.com/vsphere-65/index.jsp?topic=%2Fcom.vmware.vspsdk.apiref.doc%2Fvim.VirtualDiskManager.html
// Most VirtualDiskManager APIs will be DEPRECATED as of vSphere 6.5. Please use VStorageObjectManager APIs to manage Virtual disks.
vdMngr *object.VirtualDiskManager
// ContainerView - https://pubs.vmware.com/vsphere-6-0/index.jsp#com.vmware.wssdk.apiref.doc/vim.view.ContainerView.html
view *view.ContainerView
// The controller on this vm.
controller *types.ParaVirtualSCSIController
// The PCI + SCSI device /dev node string format the disks can be attached with
byPathFormat string
// serialize reconfigure operations
mu sync.Mutex
// map of URIs to VirtualDisk structs so that we can return the same instance to the caller, required for ref counting
Disks map[uint64]*VirtualDisk
// used for locking the disk cache
disksLock sync.Mutex
}
// NewDiskManager creates a new Manager instance associated with the caller VM
func NewDiskManager(op trace.Operation, session *session.Session, v *view.ContainerView) (*Manager, error) {
defer trace.End(trace.Begin(""))
vm, err := guest.GetSelf(op, session)
if err != nil {
return nil, errors.Trace(err)
}
// create handle to the docker daemon VM as we need to mount disks on it
controller, byPathFormat, err := verifyParavirtualScsiController(op, vm)
if err != nil {
op.Errorf("scsi controller verification failed: %s", err.Error())
return nil, err
}
return &Manager{
maxAttached: make(chan bool, MaxAttachedDisks),
vm: vm,
vdMngr: object.NewVirtualDiskManager(vm.Vim25()),
view: v,
controller: controller,
byPathFormat: byPathFormat,
Disks: make(map[uint64]*VirtualDisk),
}, nil
}
// toSpec converts the given config to VirtualDisk spec
func (m *Manager) toSpec(config *VirtualDiskConfig) *types.VirtualDisk {
backing := &types.VirtualDiskFlatVer2BackingInfo{
DiskMode: string(config.DiskMode),
ThinProvisioned: types.NewBool(true),
VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
FileName: config.DatastoreURI.String(),
},
}
if config.UUID != "" {
backing.Uuid = config.UUID
}
disk := &types.VirtualDisk{
VirtualDevice: types.VirtualDevice{
Key: -1,
ControllerKey: m.controller.Key,
UnitNumber: new(int32),
Backing: backing,
},
// As of vSphere API 5.5 capacityInKB is deprecated. Documentation suggest using capacityInBytes but we can't unset CapacityInKB and its default value 0 causes problems
// ... Exception thrown during reconfigure: (vim.vm.ConfigSpec) {
// ...
// --> unitNumber = -1,
// --> capacityInKB = 0,
// --> capacityInBytes = 8192000000,
// --> shares = (vim.SharesInfo) null,
// ...
CapacityInBytes: config.CapacityInKB * 1024,
CapacityInKB: config.CapacityInKB,
}
if config.ParentDatastoreURI != nil {
backing.Parent = &types.VirtualDiskFlatVer2BackingInfo{
VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
FileName: config.ParentDatastoreURI.String(),
},
}
// Capacity needs to be 0 as we inherit it from the parent
disk.CapacityInBytes = 0
disk.CapacityInKB = 0
}
// It's possible the VCH has a disk already attached.
*disk.VirtualDevice.UnitNumber = -1
return disk
}
// CreateAndAttach creates a new VMDK, attaches it and ensures that the device becomes visible to the caller.
// Returns a VirtualDisk corresponding to the created and attached disk.
func (m *Manager) CreateAndAttach(op trace.Operation, config *VirtualDiskConfig) (*VirtualDisk, error) {
defer trace.End(trace.Begin(config.DatastoreURI.String()))
// Get or create entry in disk cache
m.disksLock.Lock()
d, err := NewVirtualDisk(op, config, m.Disks)
if err != nil {
m.disksLock.Unlock()
op.Errorf("Unable to create disk entry: %s", err)
return nil, err
}
// take disk lock before we release the cache lock - this prevents the disk being removed from the cache
// before we get a chance to adjust refcounts
d.l.Lock()
defer d.l.Unlock()
m.disksLock.Unlock()
// check if the disk is attached from the perspective of the cache entry
if d.DevicePath != "" {
// this is a horrificaly misnamed call - it's incrementing the reference count
d.setAttached(op, "")
return d, nil
}
op.Infof("Create/attach vmdk %s from parent %s", config.DatastoreURI, config.ParentDatastoreURI)
// we use findDiskByFilename to check if the disk is already attached
// if it is then it's indicative of an error because it wasn't found in the cache, but this lets us recover
_, ferr := findDiskByFilename(op, m.vm, d.DatastoreURI.String(), d.IsPersistent())
if os.IsNotExist(ferr) {
if err := m.attach(op, config); err != nil {
return nil, errors.Trace(err)
}
} else {
op.Errorf("Failed to determine if disk is already attached: %s", err)
// this will be tidied up if/when the waitForDevice fails
}
op.Debugf("Mapping vmdk to pci device %s", config.DatastoreURI)
devicePath, err := m.devicePathByURI(op, config.DatastoreURI, d.IsPersistent())
if err != nil {
return nil, errors.Trace(err)
}
blockDev, err := waitForDevice(op, devicePath)
if err != nil {
op.Errorf("waitForDevice failed for %s with %s", d.DatastoreURI, errors.ErrorStack(err))
// ensure that the disk is detached if it's the publish that's failed
disk, findErr := findDiskByFilename(op, m.vm, d.DatastoreURI.String(), d.IsPersistent())
if findErr != nil {
op.Debugf("findDiskByFilename(%s) failed with %s", d.DatastoreURI, errors.ErrorStack(findErr))
}
if detachErr := m.detach(op, disk); detachErr != nil {
op.Debugf("detach(%s) failed with %s", d.DatastoreURI, errors.ErrorStack(detachErr))
}
return nil, errors.Trace(err)
}
err = d.setAttached(op, blockDev)
return d, err
}
// Create creates a disk without a parent (and doesn't attach it).
func (m *Manager) Create(op trace.Operation, config *VirtualDiskConfig) (*VirtualDisk, error) {
defer trace.End(trace.Begin(config.DatastoreURI.String()))
var err error
d, err := NewVirtualDisk(op, config, m.Disks)
if err != nil {
return nil, errors.Trace(err)
}
d.l.Lock()
defer d.l.Unlock()
spec := &types.FileBackedVirtualDiskSpec{
VirtualDiskSpec: types.VirtualDiskSpec{
DiskType: string(types.VirtualDiskTypeThin),
AdapterType: string(types.VirtualDiskAdapterTypeLsiLogic),
},
CapacityKb: config.CapacityInKB,
}
op.Infof("Creating vmdk for layer or volume %s", d.DatastoreURI)
err = tasks.Wait(op, func(ctx context.Context) (tasks.Task, error) {
return m.vdMngr.CreateVirtualDisk(ctx, d.DatastoreURI.String(), nil, spec)
})
if err != nil {
return nil, errors.Trace(err)
}
return d, nil
}
// Gets a disk given a datastore path URI to the vmdk
func (m *Manager) Get(op trace.Operation, config *VirtualDiskConfig) (*VirtualDisk, error) {
defer trace.End(trace.Begin(config.DatastoreURI.String()))
d, err := NewVirtualDisk(op, config, m.Disks)
if err != nil {
return nil, errors.Trace(err)
}
d.l.Lock()
defer d.l.Unlock()
d.ParentDatastoreURI, err = m.DiskParent(op, config)
return d, err
}
// DiskParent returns the parent for an existing disk, based on the disk datastore URI in the config,
// and ignoring any parent specified in the config.
// datastore path will be nil if the disk has no parent
func (m *Manager) DiskParent(op trace.Operation, config *VirtualDiskConfig) (*object.DatastorePath, error) {
defer trace.End(trace.Begin(config.DatastoreURI.String()))
info, err := m.vdMngr.QueryVirtualDiskInfo(op, config.DatastoreURI.String(), m.vm.Datacenter, true)
if err != nil {
op.Errorf("Error querying parents (%s): %s", config.DatastoreURI, err.Error())
return nil, err
}
// the last elem in the info list is the disk we just looked up.
p := info[len(info)-1]
if p.Parent != "" {
ppth, err := datastore.PathFromString(p.Parent)
if err != nil {
op.Errorf("Error converting parent to datastore URI (%s): %s", p.Parent, err)
return nil, err
}
return ppth, nil
}
// no parent
return nil, nil
}
// TODO(FA) this doesn't work since delta disks get set with `deletable =
// false` when they become parents. This needs some thought and will require
// some answers from a larger context.
//func (m *DiskManager) Delete(ctx context.Context, d *VirtualDisk) error {
// defer trace.End(trace.Begin(d.DatastoreURI))
//
// log.Infof("Deleting %s", d.DatastoreURI)
//
// d.l.Lock()
// defer d.l.Unlock()
//
// if d.isAttached() {
// return fmt.Errorf("cannot delete %s, still attached (%s)", d.DatastoreURI, d.devicePath)
// }
//
// // TODO(FA) Check if disk is a parent.
//
// vdm := object.NewVirtualDiskManager(m.vm.Client())
// task, err := vdm.DeleteVirtualDisk(ctx, d.DatastoreURI, nil)
// if err != nil {
// return err
// }
//
// err = task.Wait(ctx)
// if err != nil {
// return errors.Trace(err)
// }
//
// return nil
// }
// Attach attempts to attach a virtual disk
func (m *Manager) attach(op trace.Operation, config *VirtualDiskConfig) error {
defer trace.End(trace.Begin(""))
disk := m.toSpec(config)
deviceList := object.VirtualDeviceList{}
deviceList = append(deviceList, disk)
changeSpec, err := deviceList.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd)
if err != nil {
return err
}
machineSpec := types.VirtualMachineConfigSpec{}
machineSpec.DeviceChange = append(machineSpec.DeviceChange, changeSpec...)
// ensure we abide by max attached disks limits
m.maxAttached <- true
m.mu.Lock()
defer m.mu.Unlock()
// make sure the op is still valid as the above line could block for a long time
select {
case <-op.Done():
return op.Err()
default:
}
_, err = m.vm.WaitForResult(op, func(ctx context.Context) (tasks.Task, error) {
t, er := m.vm.Reconfigure(ctx, machineSpec)
if t != nil {
op.Debugf("Attach reconfigure task=%s", t.Reference())
}
return t, er
})
if err != nil {
select {
case <-m.maxAttached:
default:
}
op.Errorf("vmdk storage driver failed to attach disk: %s", errors.ErrorStack(err))
return errors.Trace(err)
}
return nil
}
// Detach attempts to detach a virtual disk
func (m *Manager) Detach(op trace.Operation, config *VirtualDiskConfig) error {
defer trace.End(trace.Begin(""))
// we have to hold the cache lock until we're done deleting the cache entry
// or until we know we're not going to delete the entry
m.disksLock.Lock()
defer m.disksLock.Unlock()
d, err := NewVirtualDisk(op, config, m.Disks)
if err != nil {
return errors.Trace(err)
}
d.l.Lock()
defer d.l.Unlock()
count := d.attachedRefs.Decrement()
op.Debugf("decremented attach count for %s: %d", d.DatastoreURI, count)
if count > 0 {
return nil
}
if err := d.canBeDetached(); err != nil {
op.Errorf("disk needs to be detached but is still in use: %s", err)
return errors.Trace(err)
}
op.Infof("Detaching disk %s", d.DevicePath)
disk, err := findDiskByFilename(op, m.vm, d.DatastoreURI.String(), d.IsPersistent())
if err != nil {
return errors.Trace(err)
}
if err = m.detach(op, disk); err != nil {
op.Errorf("detach for %s failed with %s", d.DevicePath, errors.ErrorStack(err))
return errors.Trace(err)
}
// this deletes the disk from the disk cache
d.setDetached(op, m.Disks)
return nil
}
func (m *Manager) DetachAll(op trace.Operation) error {
defer trace.End(trace.Begin(""))
disks, err := findAllDisks(op, m.vm)
if err != nil {
return err
}
for _, disk := range disks {
if err2 := m.detach(op, disk); err != nil {
op.Errorf("error detaching disk: %s", err2.Error())
// return the last error on the return of this function
err = err2
// if we failed here that means we have a disk attached, ensure we abide by max attached disks limits
m.maxAttached <- true
}
}
return err
}
func (m *Manager) detach(op trace.Operation, disk *types.VirtualDisk) error {
config := []types.BaseVirtualDeviceConfigSpec{
&types.VirtualDeviceConfigSpec{
Device: disk,
Operation: types.VirtualDeviceConfigSpecOperationRemove,
},
}
spec := types.VirtualMachineConfigSpec{}
spec.DeviceChange = config
m.mu.Lock()
defer m.mu.Unlock()
_, err := m.vm.WaitForResult(op, func(ctx context.Context) (tasks.Task, error) {
t, er := m.vm.Reconfigure(ctx, spec)
if t != nil {
op.Debugf("Detach reconfigure task=%s", t.Reference())
}
return t, er
})
if err == nil {
select {
case <-m.maxAttached:
default:
}
}
return err
}
func (m *Manager) devicePathByURI(op trace.Operation, datastoreURI *object.DatastorePath, persistent bool) (string, error) {
disk, err := findDiskByFilename(op, m.vm, datastoreURI.String(), persistent)
if err != nil {
op.Errorf("findDisk failed for %s with %s", datastoreURI.String(), errors.ErrorStack(err))
return "", errors.Trace(err)
}
sysPath := fmt.Sprintf(m.byPathFormat, *disk.UnitNumber)
return sysPath, nil
}
// AttachAndMount creates and attaches a vmdk as a non-persistent disk, mounts it, and returns the mount path.
func (m *Manager) AttachAndMount(op trace.Operation, datastoreURI *object.DatastorePath, persistent bool) (string, error) {
var config *VirtualDiskConfig
op.Infof("Attach/Mount %s", datastoreURI.String())
if !persistent {
config = NewNonPersistentDisk(datastoreURI)
} else {
config = NewPersistentDisk(datastoreURI)
}
d, err := m.CreateAndAttach(op, config)
if err != nil {
return "", err
}
// don't update access time - that would cause the diff operation to mutate the filesystem
opts := []string{"noatime"}
if !persistent {
opts = append(opts, "ro")
}
return d.Mount(op, opts)
}
// UnmountAndDetach unmounts and detaches a disk, subsequently cleaning the mount path
func (m *Manager) UnmountAndDetach(op trace.Operation, datastoreURI *object.DatastorePath, persistent bool) error {
var config *VirtualDiskConfig
if !persistent {
config = NewNonPersistentDisk(datastoreURI)
} else {
config = NewPersistentDisk(datastoreURI)
}
d, err := NewVirtualDisk(op, config, m.Disks)
if err != nil {
return err
}
op.Infof("Unmount and Detach %s:%s", d.mountPath, d.DatastoreURI)
err = d.Unmount(op)
derr := m.Detach(op, config)
if err != nil || derr != nil {
op.Errorf("Error during unmount or detach, unmount: %s, detach: %s", err, derr)
// prioritize first error
if err == nil {
err = derr
}
}
return err
}
func (m *Manager) InUse(op trace.Operation, config *VirtualDiskConfig, filter func(vm *mo.VirtualMachine) bool) ([]*vm.VirtualMachine, error) {
defer trace.End(trace.Begin(""))
mngr := view.NewManager(m.vm.Vim25())
// Create view of VirtualMachine objects under the VCH's resource pool
view2, err := mngr.CreateContainerView(op, m.vm.Session.Pool.Reference(), []string{"VirtualMachine"}, true)
if err != nil {
op.Errorf("failed to create view: %s", err)
return nil, err
}
defer view2.Destroy(op)
var mos []mo.VirtualMachine
// Retrieve needed properties of all machines under this view
err = view2.Retrieve(op, []string{"VirtualMachine"}, []string{"name", "config.hardware", "runtime.powerState"}, &mos)
if err != nil {
return nil, err
}
var vms []*vm.VirtualMachine
// iterate over them to see whether they have the disk we want
for i := range mos {
mo := mos[i]
op.Debugf("Working on vm %q", mo.Name)
if !filter(&mo) {
op.Debugf("Filtering out vm %q", mo.Name)
continue
}
for _, device := range mo.Config.Hardware.Device {
label := device.GetVirtualDevice().DeviceInfo.GetDescription().Label
db := device.GetVirtualDevice().Backing
if db == nil {
continue
}
switch t := db.(type) {
case types.BaseVirtualDeviceFileBackingInfo:
if config.DatastoreURI.String() == t.GetVirtualDeviceFileBackingInfo().FileName {
op.Infof("Found active user of target disk %s: %q", label, mo.Name)
vms = append(vms, vm.NewVirtualMachine(context.Background(), m.vm.Session, mo.Reference()))
}
default:
}
}
}
return vms, nil
}
func (m *Manager) DiskFinder(op trace.Operation, filter func(p string) bool) (string, error) {
defer trace.End(trace.Begin(""))
mngr := view.NewManager(m.vm.Vim25())
// Create view of VirtualMachine objects under the VCH's resource pool
view2, err := mngr.CreateContainerView(op, m.vm.Session.Pool.Reference(), []string{"VirtualMachine"}, true)
if err != nil {
op.Errorf("failed to create view: %s", err)
return "", err
}
defer view2.Destroy(op)
var mos []mo.VirtualMachine
// Retrieve needed properties of all machines under this view
err = view2.Retrieve(op, []string{"VirtualMachine"}, []string{"name", "config.hardware", "runtime.powerState"}, &mos)
if err != nil {
return "", err
}
// iterate over them to see whether they have the disk we want
for i := range mos {
mo := mos[i]
op.Debugf("Working on vm %q", mo.Name)
// observed empty fields here when copying to all 14 volumes on a cVM so being paranoid
if mo.Config == nil || mo.Config.Hardware.Device == nil {
op.Warnf("Skipping disk presence check for %q: failed to retrieve vm config", mo.Name)
continue
}
for _, device := range mo.Config.Hardware.Device {
label := device.GetVirtualDevice().DeviceInfo.GetDescription().Label
db := device.GetVirtualDevice().Backing
if db == nil {
continue
}
switch t := db.(type) {
case types.BaseVirtualDeviceFileBackingInfo:
diskPath := t.GetVirtualDeviceFileBackingInfo().FileName
if filter(diskPath) {
op.Infof("Found disk matching filter: (label: %s), %q", label, diskPath)
return diskPath, nil
}
default:
}
}
}
return "", errors.New("Not found")
}
func (m *Manager) Owners(op trace.Operation, url *url.URL, filter func(vm *mo.VirtualMachine) bool) ([]*vm.VirtualMachine, error) {
dsPath, err := datastore.PathFromString(url.Path)
if err != nil {
return nil, err
}
return m.InUse(op, NewPersistentDisk(dsPath), filter)
}

View File

@@ -0,0 +1,739 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package disk
import (
"context"
"fmt"
"io/ioutil"
"math/rand"
"os"
"path"
"sync"
"testing"
"time"
log "github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/view"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/guest"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/datastore"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/tasks"
"github.com/vmware/vic/pkg/vsphere/test/env"
)
func Session(ctx context.Context, t *testing.T) *session.Session {
config := &session.Config{
Service: env.URL(t),
DatastorePath: env.DS(t),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
}
s := session.NewSession(config)
_, err := s.Connect(ctx)
if err != nil {
t.Skip(err.Error())
return nil
}
// we're treating this as an atomic behaviour, so log out if we failed
defer func() {
if err != nil {
t.Skip(err.Error())
s.Client.Logout(ctx)
}
}()
_, err = s.Populate(ctx)
if err != nil {
t.Skip(err.Error())
return nil
}
// Vsan has special UUID / URI handling of top level directories which
// we've implemented at another level. We can't import them here or it'd
// be a circular dependency. Also, we already have tests that test these
// cases but from a higher level.
if err != nil || s.IsVSAN(ctx) {
t.Logf("error: %s", err.Error())
t.SkipNow()
}
return s
}
func ContainerView(ctx context.Context, session *session.Session, vm *object.VirtualMachine) *view.ContainerView {
mngr := view.NewManager(session.Vim25())
pool, err := vm.ResourcePool(ctx)
if err != nil {
return nil
}
// Create view of VirtualMachine objects under the VCH's resource pool
v, err := mngr.CreateContainerView(ctx, pool.Reference(), []string{"VirtualMachine"}, true)
if err != nil {
return nil
}
return v
}
// Create a lineage of disks inheriting from eachother, write portion of a
// string to each, the confirm the result is the whole string
func TestCreateAndDetach(t *testing.T) {
log.SetLevel(log.InfoLevel)
if testing.Verbose() {
log.SetLevel(log.DebugLevel)
}
session := Session(context.Background(), t)
if session == nil {
return
}
op := trace.NewOperation(context.TODO(), t.Name())
vchvm, err := guest.GetSelf(op, session)
if err != nil {
t.Skip("Not in a vm")
}
view := ContainerView(op, session, vchvm)
if view == nil {
t.Skip("Can't create a view")
}
imagestore := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: datastore.TestName(t.Name()),
}
// file manager
fm := object.NewFileManager(session.Vim25())
// create a directory in the datastore
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
if !assert.NoError(t, err) {
return
}
// Nuke the image store
defer func() {
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
if !assert.NoError(t, err) {
return
}
_, err = task.WaitForResult(context.TODO(), nil)
if !assert.NoError(t, err) {
return
}
}()
// create a diskmanager
vdm, err := NewDiskManager(op, session, view)
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
return
}
diskSize := int64(1 << 10)
scratch := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: path.Join(imagestore.Path, "scratch.vmdk"),
}
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
parent, err := vdm.Create(op, config)
if !assert.NoError(t, err) {
return
}
numChildren := 3
children := make([]*VirtualDisk, numChildren)
testString := "Ground control to Major Tom"
writeSize := len(testString) / numChildren
// Create children which inherit from each other
for i := 0; i < numChildren; i++ {
p := &object.DatastorePath{
Datastore: imagestore.Datastore,
Path: path.Join(imagestore.Path, fmt.Sprintf("child%d.vmdk", i)),
}
config := NewPersistentDisk(p).WithParent(parent.DatastoreURI)
child, cerr := vdm.CreateAndAttach(op, config)
if !assert.NoError(t, cerr) {
return
}
refs := child.attachedRefs.Count()
assert.EqualValues(t, 1, refs, "Expected %d attach references, found %d", refs)
// attempt to recreate and attach the already attached disk
config = NewPersistentDisk(p).WithParent(parent.DatastoreURI)
child, cerr = vdm.CreateAndAttach(op, config)
if !assert.NoError(t, cerr) {
return
}
refs = child.attachedRefs.Count()
assert.EqualValues(t, 2, refs, "Expected %d attach references, found %d", refs)
// attempt detach
cerr = vdm.Detach(op, config)
if !assert.NoError(t, cerr) {
return
}
// should not actually detach, and should still have 1 reference
refs = child.attachedRefs.Count()
assert.EqualValues(t, 1, refs, "Expected %d attach references, found %d", refs)
children[i] = child
// Write directly to the disk
f, cerr := os.OpenFile(child.DevicePath, os.O_RDWR, os.FileMode(0777))
if !assert.NoError(t, cerr) {
return
}
start := i * writeSize
end := start + writeSize
if i == numChildren-1 {
// last chunk, write to the end.
_, cerr = f.WriteAt([]byte(testString[start:]), int64(start))
if !assert.NoError(t, cerr) || !assert.NoError(t, f.Sync()) {
return
}
// Try to read the whole string
b := make([]byte, len(testString))
f.Seek(0, 0)
_, cerr = f.Read(b)
if !assert.NoError(t, cerr) {
return
}
//check against the test string
if !assert.Equal(t, testString, string(b)) {
return
}
} else {
_, cerr = f.WriteAt([]byte(testString[start:end]), int64(start))
if !assert.NoError(t, cerr) || !assert.NoError(t, f.Sync()) {
return
}
}
f.Close()
cerr = vdm.Detach(op, config)
if !assert.NoError(t, cerr) {
return
}
// use this image as the next parent
parent = child
}
}
func TestRefCounting(t *testing.T) {
log.SetLevel(log.InfoLevel)
if testing.Verbose() {
log.SetLevel(log.DebugLevel)
}
session := Session(context.Background(), t)
if session == nil {
return
}
op := trace.NewOperation(context.TODO(), t.Name())
vchvm, err := guest.GetSelf(op, session)
if err != nil {
t.Skip("Not in a vm")
}
view := ContainerView(op, session, vchvm)
if view == nil {
t.Skip("Can't create a view")
}
imagestore := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: datastore.TestName(t.Name()),
}
// file manager
fm := object.NewFileManager(session.Vim25())
// create a directory in the datastore
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
if !assert.NoError(t, err) {
return
}
// Nuke the image store
defer func() {
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
if !assert.NoError(t, err) {
return
}
_, err = task.WaitForResult(context.TODO(), nil)
if !assert.NoError(t, err) {
return
}
}()
// create a diskmanager
vdm, err := NewDiskManager(op, session, view)
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
return
}
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
return
}
diskSize := int64(1 << 10)
scratch := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: path.Join(imagestore.Path, "scratch.vmdk"),
}
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
p, err := vdm.Create(op, config)
if !assert.NoError(t, err) {
return
}
assert.False(t, p.Attached(), "%s is attached but should not be", p.DatastoreURI)
child := &object.DatastorePath{
Datastore: imagestore.Datastore,
Path: path.Join(imagestore.Path, "testDisk.vmdk"),
}
config = NewPersistentDisk(child).WithParent(scratch)
// attempt attach
assert.NoError(t, vdm.attach(op, config), "Error attempting to attach %s", config)
devicePath, err := vdm.devicePathByURI(op, child, config.IsPersistent())
if !assert.NoError(t, err) {
return
}
d, err := NewVirtualDisk(op, config, vdm.Disks)
if !assert.NoError(t, err) {
return
}
blockDev, err := waitForDevice(op, devicePath)
if !assert.NoError(t, err) {
return
}
assert.False(t, d.Attached(), "%s is attached but should not be", d.DatastoreURI)
// Attach the disk
assert.NoError(t, d.setAttached(op, blockDev), "Error attempting to mark %s as attached", d.DatastoreURI)
assert.True(t, d.Attached(), "%s is not attached but should be", d.DatastoreURI)
assert.NoError(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
assert.Equal(t, 1, d.attachedRefs.Count(), "%s has %d attach references but should have 1", d.DatastoreURI, d.attachedRefs.Count())
// attempt another attach at disk level to increase reference count
// TODO(jzt): This should probably eventually use the attach code coming in
// https://github.com/vmware/vic/issues/5422
assert.NoError(t, d.setAttached(op, blockDev), "Error attempting to mark %s as attached", d.DatastoreURI)
assert.True(t, d.Attached(), "%s is not attached but should be", d.DatastoreURI)
assert.Error(t, d.canBeDetached(), "%s should not be detachable but is", d.DatastoreURI)
assert.True(t, d.InUseByOther(), "%s is not in use but should be", d.DatastoreURI)
assert.Equal(t, 2, d.attachedRefs.Count(), "%s has %d attach references but should have 2", d.DatastoreURI, d.attachedRefs.Count())
// reduce reference count by calling detach
d.setDetached(op, vdm.Disks)
assert.True(t, d.Attached(), "%s is not attached but should be", d.DatastoreURI)
assert.NoError(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
assert.Equal(t, 1, d.attachedRefs.Count(), "%s has %d attach references but should have 1", d.DatastoreURI, d.attachedRefs.Count())
// test mount reference counting
assert.NoError(t, d.Mkfs(op, "testDisk"), "Error attempting to format %s", d.DatastoreURI)
// create temp mount path
dir, err := ioutil.TempDir("", "mnt")
if !assert.NoError(t, err) {
return
}
// cleanup
defer func() {
assert.NoError(t, os.RemoveAll(dir), "Error cleaning up mount path %s", dir)
}()
// initial mount
dir, err = d.Mount(op, nil)
assert.NoError(t, err, "Error attempting to mount %s at %s", d.DatastoreURI, dir)
mountPath, err := d.MountPath()
if !assert.NoError(t, err) {
return
}
assert.True(t, d.Mounted(), "%s is not mounted but should be", d.DatastoreURI)
assert.Error(t, d.canBeDetached(), "%s should not be detachable but is", d.DatastoreURI)
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
assert.Equal(t, 1, d.mountedRefs.Count(), "%s has %d mount references but should have 1", d.DatastoreURI, d.mountedRefs.Count())
assert.Equal(t, dir, mountPath, "%s is mounted at %s but should be mounted at %s", d.DatastoreURI, mountPath, dir)
// attempt another mount
dir, err = d.Mount(op, nil)
assert.NoError(t, err, "Error attempting to mount %s at %s", d.DatastoreURI, dir)
assert.True(t, d.Mounted(), "%s is not mounted but should be", d.DatastoreURI)
assert.Error(t, d.canBeDetached(), "%s should not be detachable but is", d.DatastoreURI)
assert.True(t, d.InUseByOther(), "%s is not in use but should be", d.DatastoreURI)
assert.Equal(t, 2, d.mountedRefs.Count(), "%s has %d mount references but should have 2", d.DatastoreURI, d.mountedRefs.Count())
// attempt unmount
assert.NoError(t, d.Unmount(op), "Error attempting to unmount %s", d.DatastoreURI)
assert.True(t, d.Mounted(), "%s is not mounted but should be", d.DatastoreURI)
assert.Error(t, d.canBeDetached(), "%s should not be detachable but is", d.DatastoreURI)
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
assert.Equal(t, 1, d.mountedRefs.Count(), "%s has %d mount references but should have 1", d.DatastoreURI, d.mountedRefs.Count())
// actually unmount
assert.NoError(t, d.Unmount(op), "Error attempting to unmount %s", d.DatastoreURI)
assert.False(t, d.Mounted(), "%s is mounted but should not be", d.DatastoreURI)
assert.NoError(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
assert.Equal(t, 0, d.mountedRefs.Count(), "%s has %d mount references but should have 0", d.DatastoreURI, d.mountedRefs.Count())
// detach
assert.NoError(t, vdm.Detach(op, config), "Error attempting to detach %s", d.DatastoreURI)
assert.False(t, d.Attached(), "%s is attached but should not be", d.DatastoreURI)
assert.False(t, d.Mounted(), "%s is mounted but should not be", d.DatastoreURI)
assert.Error(t, d.canBeDetached(), "%s should not be detachable but is", d.DatastoreURI)
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
assert.Equal(t, 0, d.attachedRefs.Count(), "%s has %d attach references but should have 0", d.DatastoreURI, d.attachedRefs.Count())
assert.Equal(t, 0, d.mountedRefs.Count(), "%s has %d mount references but should have 0", d.DatastoreURI, d.mountedRefs.Count())
if !assert.NoError(t, err) {
return
}
}
func TestRefCountingParallel(t *testing.T) {
log.SetLevel(log.InfoLevel)
if testing.Verbose() {
log.SetLevel(log.DebugLevel)
}
session := Session(context.Background(), t)
if session == nil {
return
}
op := trace.NewOperation(context.TODO(), t.Name())
vchvm, err := guest.GetSelf(op, session)
if err != nil {
t.Skip("Not in a vm")
}
view := ContainerView(op, session, vchvm)
if view == nil {
t.Skip("Can't create a view")
}
imagestore := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: datastore.TestName(t.Name()),
}
// file manager
fm := object.NewFileManager(session.Vim25())
// create a directory in the datastore
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
if !assert.NoError(t, err) {
return
}
// Nuke the image store
defer func() {
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
if !assert.NoError(t, err) {
return
}
_, err = task.WaitForResult(context.TODO(), nil)
if !assert.NoError(t, err) {
return
}
}()
// create a diskmanager
vdm, err := NewDiskManager(op, session, view)
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
return
}
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
return
}
diskSize := int64(1 << 10)
uri := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: path.Join(imagestore.Path, "test.vmdk"),
}
config := NewPersistentDisk(uri).WithCapacity(diskSize)
d, err := vdm.CreateAndAttach(op, config)
if !assert.NoError(t, err) {
return
}
assert.True(t, d.Attached(), "%s is not attached but should be", d.DatastoreURI)
assert.NoError(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
assert.EqualValues(t, 1, d.attachedRefs.Count(), "%s has %d attach references but should have 1", d.DatastoreURI, d.attachedRefs.Count())
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
var err error
defer wg.Done()
for j := 0; j < 5; j++ {
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
d, err = vdm.CreateAndAttach(op, config)
if !assert.NoError(t, err) {
return
}
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
err = vdm.Detach(op, config)
if !assert.NoError(t, err) {
return
}
}
}()
}
wg.Wait()
assert.True(t, d.Attached(), "%s is not attached but should be", d.DatastoreURI)
assert.NoError(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
assert.EqualValues(t, 1, d.attachedRefs.Count(), "%s has %d attach references but should have 1", d.DatastoreURI, d.attachedRefs.Count())
err = vdm.Detach(op, config)
if !assert.NoError(t, err) {
log.Error("Error detaching disk: %s", err.Error())
return
}
assert.False(t, d.Attached(), "%s is attached but should not be", d.DatastoreURI)
assert.Error(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
assert.EqualValues(t, 0, d.attachedRefs.Count(), "%s has %d attach references but should have 0", d.DatastoreURI, d.attachedRefs.Count())
}
func TestInUse(t *testing.T) {
log.SetLevel(log.InfoLevel)
if testing.Verbose() {
log.SetLevel(log.DebugLevel)
}
session := Session(context.Background(), t)
if session == nil {
return
}
op := trace.NewOperation(context.TODO(), t.Name())
vchvm, err := guest.GetSelf(op, session)
if err != nil {
t.Skip("Not in a vm")
}
view := ContainerView(op, session, vchvm)
if view == nil {
t.Skip("Can't create a view")
}
imagestore := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: datastore.TestName(t.Name()),
}
// file manager
fm := object.NewFileManager(session.Vim25())
// create a directory in the datastore
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
if !assert.NoError(t, err) {
return
}
// Nuke the image store
defer func() {
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
if !assert.NoError(t, err) {
return
}
_, err = task.WaitForResult(context.TODO(), nil)
if !assert.NoError(t, err) {
return
}
}()
// create a diskmanager
vdm, err := NewDiskManager(op, session, view)
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
return
}
// helper fn
reconfigure := func(changes []types.BaseVirtualDeviceConfigSpec) error {
t.Logf("Calling reconfigure")
machineSpec := types.VirtualMachineConfigSpec{}
machineSpec.DeviceChange = changes
_, err := vdm.vm.WaitForResult(op, func(ctx context.Context) (tasks.Task, error) {
t, er := vdm.vm.Reconfigure(ctx, machineSpec)
if t != nil {
op.Debugf("reconfigure task=%s", t.Reference())
}
return t, er
})
return err
}
// 1MB
diskSize := int64(1 << 10)
scratch := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: path.Join(imagestore.Path, "scratch.vmdk"),
}
// config
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
// attach + create spec (scratch)
spec := []types.BaseVirtualDeviceConfigSpec{
&types.VirtualDeviceConfigSpec{
Device: vdm.toSpec(config),
Operation: types.VirtualDeviceConfigSpecOperationAdd,
FileOperation: types.VirtualDeviceConfigSpecFileOperationCreate,
},
}
// filter powered off vms
filter := func(vm *mo.VirtualMachine) bool {
return vm.Runtime.PowerState != types.VirtualMachinePowerStatePoweredOn
}
vms, err := vdm.InUse(op, config, filter)
if !assert.NoError(t, err) && !assert.Len(t, vms, 0) {
return
}
// reconfigure
err = reconfigure(spec)
if !assert.NoError(t, err) {
return
}
t.Logf("scratch created and attached")
vms, err = vdm.InUse(op, config, filter)
if !assert.NoError(t, err) && !assert.Len(t, vms, 1) {
return
}
t.Logf("InUse by %s", vms)
// ref to scratch (needed for detach as initial spec's Key and UnitNumber was unset)
disk, err := findDiskByFilename(op, vdm.vm, scratch.Path, true)
if !assert.NoError(t, err) {
return
}
// DO NOT DETACH AND START WORKING ON THE CHILD
// child
child := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: path.Join(imagestore.Path, "child.vmdk"),
}
// config
config = NewPersistentDisk(child).WithParent(scratch)
// detach (scratch) AND attach + create (child) spec
spec = []types.BaseVirtualDeviceConfigSpec{
&types.VirtualDeviceConfigSpec{
Device: disk,
Operation: types.VirtualDeviceConfigSpecOperationRemove,
},
&types.VirtualDeviceConfigSpec{
Device: vdm.toSpec(config),
Operation: types.VirtualDeviceConfigSpecOperationAdd,
FileOperation: types.VirtualDeviceConfigSpecFileOperationCreate,
},
}
// reconfigure
err = reconfigure(spec)
if !assert.NoError(t, err) {
return
}
t.Logf("scratch detached, child created and attached")
vms, err = vdm.InUse(op, config, filter)
if !assert.NoError(t, err) && !assert.Len(t, vms, 1) {
return
}
t.Logf("InUse by %s", vms)
// ref to child (needed for detach as initial spec's Key and UnitNumber was unset)
disk, err = findDiskByFilename(op, vdm.vm, child.Path, true)
if !assert.NoError(t, err) {
return
}
// detach spec (child)
spec = []types.BaseVirtualDeviceConfigSpec{
&types.VirtualDeviceConfigSpec{
Device: disk,
Operation: types.VirtualDeviceConfigSpecOperationRemove,
},
}
// reconfigure
err = reconfigure(spec)
if !assert.NoError(t, err) {
return
}
t.Logf("child detached")
}

View File

@@ -0,0 +1,238 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package disk
import (
"context"
"fmt"
"os"
"path"
"testing"
log "github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/guest"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/datastore"
"github.com/vmware/vic/pkg/vsphere/tasks"
)
// TestLazyDetach tests lazy detach functionality to make sure that every ESXi version shows this behaviour
// https://github.com/vmware/vic/issues/5565
func TestLazyDetach(t *testing.T) {
log.SetLevel(log.InfoLevel)
if testing.Verbose() {
log.SetLevel(log.DebugLevel)
}
session := Session(context.Background(), t)
if session == nil {
return
}
op := trace.NewOperation(context.TODO(), t.Name())
vchvm, err := guest.GetSelf(op, session)
if err != nil {
t.Skip("Not in a vm")
}
view := ContainerView(op, session, vchvm)
if view == nil {
t.Skip("Can't create a view")
}
imagestore := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: datastore.TestName(t.Name()),
}
// file manager
fm := object.NewFileManager(session.Vim25())
// create a directory in the datastore
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
if !assert.NoError(t, err) {
return
}
// Nuke the image store
defer func() {
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
if !assert.NoError(t, err) {
return
}
_, err = task.WaitForResult(context.TODO(), nil)
if !assert.NoError(t, err) {
return
}
}()
// create a diskmanager
vdm, err := NewDiskManager(op, session, view)
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
return
}
// helper fn
reconfigure := func(changes []types.BaseVirtualDeviceConfigSpec) error {
t.Logf("Calling reconfigure")
machineSpec := types.VirtualMachineConfigSpec{}
machineSpec.DeviceChange = changes
_, err := vdm.vm.WaitForResult(op, func(ctx context.Context) (tasks.Task, error) {
t, er := vdm.vm.Reconfigure(ctx, machineSpec)
if t != nil {
op.Debugf("reconfigure task=%s", t.Reference())
}
return t, er
})
return err
}
oddity := "Ground control to Major Tom"
operation := func(path *object.DatastorePath, read bool) error {
// this is fundamentally checking persistent disks
devicePath, err := vdm.devicePathByURI(op, path, true)
if err != nil {
return err
}
blockDev, err := waitForDevice(op, devicePath)
if err != nil {
return err
}
f, err := os.OpenFile(blockDev, os.O_RDWR, os.FileMode(0777))
if err != nil {
return err
}
defer f.Close()
if read {
// Try to read the whole string
b := make([]byte, len(oddity))
_, err = f.Read(b)
if err != nil {
return err
}
// Check against the test string
if oddity != string(b) {
return fmt.Errorf("Read string is not the same one we wrote")
}
} else {
// Write directly to the disk
_, err = f.Write([]byte(oddity))
if err != nil {
return err
}
}
return f.Sync()
}
// 1MB
diskSize := int64(1 << 10)
scratch := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: path.Join(imagestore.Path, "scratch.vmdk"),
}
// config
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
// attach + create spec (scratch)
spec := []types.BaseVirtualDeviceConfigSpec{
&types.VirtualDeviceConfigSpec{
Device: vdm.toSpec(config),
Operation: types.VirtualDeviceConfigSpecOperationAdd,
FileOperation: types.VirtualDeviceConfigSpecFileOperationCreate,
},
}
// reconfigure
err = reconfigure(spec)
if !assert.NoError(t, err) {
return
}
t.Logf("scratch created and attached")
// ref to scratch (needed for detach as initial spec's Key and UnitNumber was unset)
disk, err := findDiskByFilename(op, vdm.vm, scratch.Path, true)
if !assert.NoError(t, err) {
return
}
err = operation(scratch, false)
if !assert.NoError(t, err) {
return
}
// DO NOT DETACH AND START WORKING ON THE CHILD
// child
child := &object.DatastorePath{
Datastore: session.Datastore.Name(),
Path: path.Join(imagestore.Path, "child.vmdk"),
}
// config
config = NewPersistentDisk(child).WithParent(scratch)
// detach (scratch) AND attach + create (child) spec
spec = []types.BaseVirtualDeviceConfigSpec{
&types.VirtualDeviceConfigSpec{
Device: disk,
Operation: types.VirtualDeviceConfigSpecOperationRemove,
},
&types.VirtualDeviceConfigSpec{
Device: vdm.toSpec(config),
Operation: types.VirtualDeviceConfigSpecOperationAdd,
FileOperation: types.VirtualDeviceConfigSpecFileOperationCreate,
},
}
// reconfigure
err = reconfigure(spec)
if !assert.NoError(t, err) {
return
}
t.Logf("scratch detached, child created and attached")
err = operation(child, true)
if !assert.NoError(t, err) {
return
}
// ref to child (needed for detach as initial spec's Key and UnitNumber was unset)
disk, err = findDiskByFilename(op, vdm.vm, child.Path, true)
if !assert.NoError(t, err) {
return
}
// detach spec (child)
spec = []types.BaseVirtualDeviceConfigSpec{
&types.VirtualDeviceConfigSpec{
Device: disk,
Operation: types.VirtualDeviceConfigSpecOperationRemove,
},
}
// reconfigure
err = reconfigure(spec)
if !assert.NoError(t, err) {
return
}
t.Logf("child detached")
}

339
vendor/github.com/vmware/vic/pkg/vsphere/disk/util.go generated vendored Normal file
View File

@@ -0,0 +1,339 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package disk
import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/vm"
)
const (
// The duration waitForPath will tolerate before timing out.
// TODO FIXME see GH issues 2340 and 2385
// TODO We need to add a vSphere cancellation step to cancel calls that are taking too long
// TODO Remove these TODOs after 2385 is completed
pathTimeout = 60 * time.Second
)
// scsiScan tells the kernel to rescan the scsi bus.
func scsiScan() error {
root := "/sys/class/scsi_host"
dirs, err := ioutil.ReadDir(root)
if err != nil {
return err
}
for _, dir := range dirs {
file := path.Join(root, dir.Name(), "scan")
// Channel, SCSI target ID, and LUN: "-" == rescan all
err = ioutil.WriteFile(file, []byte("- - -"), 0)
if err != nil {
return err
}
}
return nil
}
// Waits for a device to appear in the given directory and returns the
// resultant dev path (e.g /dev/sda). For instance, if sysPath is
// /sys/bus/pci/devices/0000:03:00.0/host0/subsystem/devices/0:0:0:0/block, the
// directory to appear in block will be mapped to /dev/<device>. waitForDevice
// will wait for the entry to appear as a scsi target AND the blockdev to exist
// in /dev/, returning the path to /dev/<blockdev>.
func waitForDevice(op trace.Operation, sysPath string) (string, error) {
defer trace.End(trace.Begin(sysPath))
var err error
op, _ = trace.WithTimeout(&op, time.Duration(pathTimeout), "waitForDevice(%s)", sysPath)
errCh := make(chan error)
var blockDev string
go func() {
t := time.NewTicker(200 * time.Microsecond)
defer t.Stop()
defer close(errCh)
for range t.C {
// We've timed out.
if op.Err() != nil {
return
}
// Syspath includes the scsi target itself. Wait for it and try
// again before trying to identify the device node it maps to.
dirents, err := ioutil.ReadDir(sysPath)
if err != nil {
// try again
if os.IsNotExist(err) {
op.Debugf("Expected %s to appear. Trying again.", sysPath)
continue
}
errCh <- err
return
}
if len(dirents) > 1 {
errCh <- fmt.Errorf("too many devices returned: %#v", dirents)
return
}
if len(dirents) == 1 {
blockDev = "/dev/" + dirents[0].Name()
// check it exists
if _, err := os.Stat(blockDev); err != nil {
// try again
if os.IsNotExist(err) {
continue
}
errCh <- err
return
}
// happy path
return
}
// run a manual scan of the scsi bus
if serr := scsiScan(); serr != nil {
op.Warnf("scsi scan: %s", serr)
}
}
}()
op.Debugf("Waiting for attached disk to appear in %s, or timeout", sysPath)
select {
case err = <-errCh:
if err != nil {
return "", err
}
op.Infof("Attached disk present at %s", blockDev)
case <-op.Done():
if op.Err() != nil {
return "", errors.Errorf("timeout waiting for layer to present in %s", sysPath)
}
}
return blockDev, nil
}
// Ensures that a paravirtual scsi controller is present and determines the
// base path of disks attached to it returns a handle to the controller and a
// format string, with a single decimal for the disk unit number which will
// result in the /sys/bus/pci/devices/{pci id like
// 0000:03:00.0}/host{N provided by kernel}/subsystem/devices/N:0:{disk id like 0}:0/block/sd{Y provided by kernel} path.
// The directory inside block isn't a devnode, but it's name can be mapped to
// its /dev/ path.
func verifyParavirtualScsiController(op trace.Operation, vm *vm.VirtualMachine) (*types.ParaVirtualSCSIController, string, error) {
devices, err := vm.Device(op)
if err != nil {
op.Errorf("vmware driver failed to retrieve device list for VM %s: %s", vm, errors.ErrorStack(err))
return nil, "", errors.Trace(err)
}
controller, ok := devices.PickController((*types.ParaVirtualSCSIController)(nil)).(*types.ParaVirtualSCSIController)
if controller == nil || !ok {
err = errors.Errorf("vmware driver failed to find a paravirtual SCSI controller - ensure setup ran correctly")
op.Errorf(err.Error())
return nil, "", errors.Trace(err)
}
// build the base path
// first we determine which label we're looking for (requires VMW hardware version >=10)
controllerLabel := fmt.Sprintf("SCSI%d", controller.BusNumber)
op.Debugf("Looking for scsi controller with label %s", controllerLabel)
pciDevicesDir := "/sys/bus/pci/devices"
pciBus, err := os.Open(pciDevicesDir)
if err != nil {
op.Errorf("Failed to open %s for reading: %s", pciDevicesDir, errors.ErrorStack(err))
return controller, "", errors.Trace(err)
}
defer pciBus.Close()
pciDevices, err := pciBus.Readdirnames(0)
if err != nil {
op.Errorf("Failed to read contents of %s: %s", pciDevicesDir, errors.ErrorStack(err))
return controller, "", errors.Trace(err)
}
var controllerName string
for _, pciDev := range pciDevices {
labelPath := path.Join(pciDevicesDir, pciDev, "label")
flabel, err := os.Open(labelPath)
if err != nil {
if !os.IsNotExist(err) {
op.Errorf("Unable to read label from %s: %s", labelPath, errors.ErrorStack(err))
}
continue
}
defer flabel.Close()
buf := make([]byte, len(controllerLabel))
_, err = flabel.Read(buf)
if err != nil {
op.Errorf("Unable to read label from %s: %s", labelPath, errors.ErrorStack(err))
continue
}
if controllerLabel == string(buf) {
// we've found our controller
controllerName = pciDev
op.Debugf("Found pvscsi controller directory: %s", controllerName)
break
}
}
if controllerName == "" {
err := errors.Errorf("Failed to locate pvscsi controller directory")
return controller, "", errors.Trace(err)
}
// Use the block subsystem directly.
// /sys/bus/pci/devices/0000:03:00.0/host0/subsystem/devices/0:0:0:0/block
// hostN (host0 in this case) is provided to us by the kernel.
// N:0:0:X where N is from above X is provided by vsphere
// Glob for the scsi host
matches, err := filepath.Glob(path.Join(pciDevicesDir, controllerName, "host*"))
if err != nil {
return controller, "", fmt.Errorf("scsi host glob failed: %s", err.Error())
}
if len(matches) != 1 {
return controller, "", fmt.Errorf("too many scsi hosts")
}
// Get the number on the end
hostStr := matches[0]
hostidx := string(hostStr[len(hostStr)-1])
// First param in the X:X:X:X path is the host id. So `host2` will mean all devices will start with 2.
formatString := path.Join(hostStr, fmt.Sprintf("subsystem/devices/%s:0:%%d:0/block/", hostidx))
op.Debugf("Disk location format: %s", formatString)
return controller, formatString, nil
}
func findDisk(op trace.Operation, vm *vm.VirtualMachine, filter func(diskName string, mode string) bool) ([]*types.VirtualDisk, error) {
defer trace.End(trace.Begin(vm.String()))
devices, err := vm.Device(op)
if err != nil {
return nil, fmt.Errorf("Failed to refresh devices for vm: %s", errors.ErrorStack(err))
}
candidates := devices.Select(func(device types.BaseVirtualDevice) bool {
db := device.GetVirtualDevice().Backing
if db == nil {
return false
}
backing, ok := device.GetVirtualDevice().Backing.(*types.VirtualDiskFlatVer2BackingInfo)
if !ok {
return false
}
backingFileName := backing.VirtualDeviceFileBackingInfo.FileName
mode := backing.DiskMode
op.Debugf("backing file name %s, mode: %s", backingFileName, mode)
return filter(backingFileName, mode)
})
if len(candidates) == 0 {
return nil, nil
}
disks := make([]*types.VirtualDisk, len(candidates))
for idx, disk := range candidates {
disks[idx] = disk.(*types.VirtualDisk)
}
return disks, nil
}
// Find the disk by name attached to the given vm.
func findDiskByFilename(op trace.Operation, vm *vm.VirtualMachine, name string, persistent bool) (*types.VirtualDisk, error) {
defer trace.End(trace.Begin(vm.String()))
op.Debugf("Looking for attached disk matching filename %s", name)
candidates, err := findDisk(op, vm, func(diskName string, mode string) bool {
if persistent != (mode == string(types.VirtualDiskModePersistent) || mode == string(types.VirtualDiskModeIndependent_persistent)) {
return false
}
match := strings.HasSuffix(diskName, name)
if match {
op.Debugf("Found candidate disk for %s at %s", name, diskName)
}
return match
})
if err != nil {
op.Errorf("error finding disk: %s", err.Error())
return nil, err
}
if len(candidates) == 0 {
op.Infof("No disks match name and persistence: %s, %t", name, persistent)
return nil, os.ErrNotExist
}
if len(candidates) > 1 {
op.Errorf("Multiple disks match name: %s", name)
// returning the first allows doing something with it
return candidates[0], errors.Errorf("multiple disks match name: %s", name)
}
return candidates[0], nil
}
func findAllDisks(op trace.Operation, vm *vm.VirtualMachine) ([]*types.VirtualDisk, error) {
defer trace.End(trace.Begin(vm.String()))
op.Debugf("Looking for all attached disks")
disks, err := findDisk(op, vm, func(diskName string, mode string) bool {
return true
})
if err != nil {
op.Errorf("error finding disk: %s", err.Error())
return nil, err
}
return disks, nil
}

View File

@@ -0,0 +1,33 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package disk
import (
"os"
"runtime"
"testing"
)
func TestScsiScan(t *testing.T) {
if runtime.GOOS != "linux" {
t.SkipNow()
}
err := scsiScan()
if err != nil && os.IsNotExist(err) {
// ignoring "permission denied" or "read-only file system" errors
t.Error(err)
}
}

125
vendor/github.com/vmware/vic/pkg/vsphere/disk/vmdk.go generated vendored Normal file
View File

@@ -0,0 +1,125 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package disk
import (
"net/url"
"github.com/vmware/govmomi/task"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/datastore"
"github.com/vmware/vic/pkg/vsphere/session"
)
// Vmdk is intended to be embedded by stores that manage VMDK-based data resources
type Vmdk struct {
*Manager
*datastore.Helper
*session.Session
}
const (
DiskBackendKey = "msg.disk.noBackEnd"
LockedFileKey = "msg.fileio.lock"
)
// Mount mounts the disk, returning the mount path and the function used to unmount/detaches
// when no longer in use
func (v *Vmdk) Mount(op trace.Operation, uri *url.URL, persistent bool) (string, func(), error) {
if uri.Scheme != "ds" {
return "", nil, errors.New("vmdk path must be a datastore url with \"ds\" scheme")
}
dsPath, err := datastore.PathFromString(uri.Path)
if err != nil {
return "", nil, err
}
cleanFunc := func() {
if err := v.UnmountAndDetach(op, dsPath, persistent); err != nil {
op.Errorf("Error cleaning up disk: %s", err.Error())
}
}
mountPath, err := v.AttachAndMount(op, dsPath, persistent)
return mountPath, cleanFunc, err
}
func LockedVMDKFilter(vm *mo.VirtualMachine) bool {
if vm == nil {
return false
}
return vm.Runtime.PowerState == types.VirtualMachinePowerStatePoweredOn
}
// IsLockedError will determine if the error received is:
// a. related to a vmdk
// b. due to the vmdk being locked
// It will return false in absence of confirmation, meaning incomplete vim errors
// will return false
func IsLockedError(err error) bool {
disks := LockedDisks(err)
//if device is locked, disks will not be nil
return len(disks) > 0
}
// LockedDisks returns locked devices path in the error if it's device lock error
func LockedDisks(err error) []string {
var faultMessage []types.LocalizableMessage
if soap.IsSoapFault(err) {
switch f := soap.ToSoapFault(err).VimFault().(type) {
case *types.GenericVmConfigFault:
faultMessage = f.FaultMessage
}
} else if soap.IsVimFault(err) {
faultMessage = soap.ToVimFault(err).GetMethodFault().FaultMessage
} else {
switch err := err.(type) {
case task.Error:
faultMessage = err.Fault().GetMethodFault().FaultMessage
}
}
if faultMessage == nil {
return nil
}
lockedFile := false
var devices []string
for _, message := range faultMessage {
switch message.Key {
case LockedFileKey:
lockedFile = true
case DiskBackendKey:
for _, arg := range message.Arg {
if device, ok := arg.Value.(string); ok {
devices = append(devices, device)
continue
}
}
}
}
if lockedFile {
// make sure locked devices are returned only when both keys appear in the error
return devices
}
return nil
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,454 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"encoding/base64"
"net"
"net/url"
"os/exec"
"testing"
"time"
"github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
// [BEGIN] SLIMMED DOWNED and MODIFIED VERSION of github.com/vmware/vic/lib/metadata
type Common struct {
ExecutionEnvironment string `vic:"0.1" recurse:"depth=0"`
ID string `vic:"0.1" scope:"read-only" key:"id"`
Name string `vic:"0.1" scope:"read-only" key:"name"`
Notes string `vic:"0.1" scope:"read-only" key:"notes"`
}
type ContainerVM struct {
Common `vic:"0.1" scope:"read-only" key:"common"`
Version string `vic:"0.1" scope:"hidden" key:"version"`
Aliases map[string]string `vic:"0.1" recurse:"depth=0"`
Interaction url.URL `vic:"0.1" recurse:"depth=0"`
AgentKey []byte `vic:"0.1" recurse:"depth=0"`
}
type ExecutorConfig struct {
Common `vic:"0.1" scope:"read-only" key:"common"`
Sessions map[string]SessionConfig `vic:"0.1" scope:"hidden" key:"sessions"`
Key string `json:"string"`
}
type ExecutorConfigPointers struct {
Common `vic:"0.1" scope:"read-only" key:"common"`
Sessions map[string]*SessionConfig `vic:"0.1" scope:"hidden" key:"sessions"`
Key string `json:"string"` // will inherit parent vic attributes
}
type Cmd struct {
Path string `vic:"0.1" scope:"hidden" key:"path"`
Args []string `vic:"0.1" scope:"hidden" key:"args"`
Env []string `vic:"0.1" scope:"hidden" key:"env"`
Dir string `vic:"0.1" scope:"hidden" key:"dir"`
Cmd *exec.Cmd `vic:"0.1" scope:"hidden" key:"cmd" recurse:"depth=0"`
}
type SessionConfig struct {
Common `vic:"0.1" scope:"hidden" key:"common" json:"page"`
Cmd Cmd `vic:"0.1" scope:"hidden" key:"cmd"`
Tty bool `vic:"0.1" scope:"hidden" key:"tty"`
}
type ExecutorConfigPointersVisible struct {
Sessions map[string]*VisibleSessionConfig `vic:"0.1" scope:"read-only" key:"sessions"`
}
type VisibleSessionConfig struct {
Cmd Cmd `vic:"0.1" scope:"read-only" key:"cmd"`
Tty bool `vic:"0.1" scope:"read-only" key:"tty"`
}
// [END] SLIMMED VERSION of github.com/vmware/vic/lib/metadata
// make it verbose during testing
func init() {
logger.Level = logrus.DebugLevel
}
func TestBasic(t *testing.T) {
type Type struct {
Int int `vic:"0.1" scope:"read-write" key:"int"`
Bool bool `vic:"0.1" scope:"read-write" key:"bool"`
Float float64 `vic:"0.1" scope:"read-write" key:"float"`
String string `vic:"0.1" scope:"read-write" key:"string"`
}
Struct := Type{
42,
true,
3.14,
"Grrr",
}
encoded := map[string]string{}
Encode(MapSink(encoded), Struct)
expected := map[string]string{
visibleRW("int"): "42",
visibleRW("bool"): "true",
visibleRW("float"): "3.14E+00",
visibleRW("string"): "Grrr",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
}
func TestBasicMap(t *testing.T) {
type Type struct {
IntMap map[string]int `vic:"0.1" scope:"read-only" key:"intmap"`
}
// key is not present
var decoded Type
Decode(MapSource(nil), &decoded)
assert.NotNil(t, decoded.IntMap)
assert.Empty(t, decoded.IntMap)
IntMap := Type{
map[string]int{
"1st": 12345,
"2nd": 67890,
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), IntMap)
expected := map[string]string{
visibleRO("intmap" + Separator + "1st"): "12345",
visibleRO("intmap" + Separator + "2nd"): "67890",
visibleRO("intmap"): "1st" + Separator + "2nd",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
// Decode to new variable
decoded = Type{}
Decode(MapSource(encoded), &decoded)
assert.Equal(t, IntMap, decoded, "Encoded and decoded does not match")
// Decode to already existing variable
IntMapOptimusPrime := Type{
map[string]int{
"first": 1,
"second": 2,
"1st": 0,
},
}
Decode(MapSource(encoded), &IntMapOptimusPrime)
// We expect a merge and over-write
expectedOptimusPrime := Type{
map[string]int{
"1st": 12345,
"2nd": 67890,
"first": 1,
"second": 2,
},
}
assert.Equal(t, IntMapOptimusPrime, expectedOptimusPrime, "Decoded and expected does not match")
}
func TestBasicSlice(t *testing.T) {
type Type struct {
IntSlice []int `vic:"0.1" scope:"read-only" key:"intslice"`
}
IntSlice := Type{
[]int{1, 2, 3, 4, 5},
}
encoded := map[string]string{}
Encode(MapSink(encoded), IntSlice)
expected := map[string]string{
visibleRO("intslice~"): "1" + Separator + "2" + Separator + "3" + Separator + "4" + Separator + "5",
visibleRO("intslice"): "4",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
decoded.IntSlice = make([]int, 1)
Decode(MapSource(encoded), &decoded)
assert.Equal(t, IntSlice, decoded, "Encoded and decoded does not match")
}
func TestStruct(t *testing.T) {
type Type struct {
Common Common `vic:"0.1" scope:"read-only" key:"common"`
}
Struct := Type{
Common: Common{
ID: "0xDEADBEEF",
Name: "Struct",
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), Struct)
expected := map[string]string{
visibleRO("common/id"): "0xDEADBEEF",
visibleRO("common/name"): "Struct",
visibleRO("common/notes"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
}
func TestTime(t *testing.T) {
type Type struct {
Time time.Time `vic:"0.1" scope:"read-only" key:"time"`
}
Time := Type{
Time: time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC),
}
encoded := map[string]string{}
Encode(MapSink(encoded), Time)
expected := map[string]string{
visibleRO("time"): "2009-11-10 23:00:00 +0000 UTC",
}
assert.Equal(t, encoded, expected, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Time, decoded, "Encoded and decoded does not match")
}
func TestNet(t *testing.T) {
type Type struct {
Net net.IPNet `vic:"0.1" scope:"read-only" key:"net"`
}
// 127.0.0.1/8
n := net.IPNet{IP: net.IP{0x7f, 0x0, 0x0, 0x1}, Mask: net.IPMask{0xff, 0x0, 0x0, 0x0}}
Net := Type{
Net: n,
}
encoded := map[string]string{}
Encode(MapSink(encoded), Net)
expected := map[string]string{
visibleRO("net/IP"): base64.StdEncoding.EncodeToString(n.IP),
visibleRO("net/Mask"): base64.StdEncoding.EncodeToString(n.Mask),
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Net, decoded, "Encoded and decoded does not match")
}
func TestNilNetPointer(t *testing.T) {
type Type struct {
Net *net.IPNet `vic:"0.1" scope:"read-only" key:"net"`
}
Net := Type{
Net: nil,
}
// Net should be nil - pointers are supposed to be nil if the referenced tree is zero valued
encoded := map[string]string{}
Encode(MapSink(encoded), Net)
expected := map[string]string{}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Net, decoded, "Encoded and decoded does not match")
}
func TestPointer(t *testing.T) {
type Type struct {
Pointer *ContainerVM `vic:"0.1" scope:"hidden" key:"pointer"`
PointerOmitnested *ContainerVM `vic:"0.1" scope:"non-persistent" key:"pointeromitnested" recurse:"depth=0"`
}
Pointer := Type{
Pointer: &ContainerVM{Version: "0.1"},
}
encoded := map[string]string{}
Encode(MapSink(encoded), Pointer)
expected := map[string]string{
visibleRO("pointer/common/id"): "",
visibleRO("pointer/common/name"): "",
visibleRO("pointer/common/notes"): "",
"pointer/version": "0.1",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Pointer, decoded, "Encoded and decoded does not match")
}
func TestInheritenceOfNonPersistence(t *testing.T) {
type CommonPersistence struct {
ExecutionEnvironment string `vic:"0.1" recurse:"depth=0"`
ID string `vic:"0.1" scope:"read-only" key:"id"`
Name string `vic:"0.1" scope:"read-only" key:"name"`
Notes string `vic:"0.1" scope:"hidden" key:"notes"`
}
type Type struct {
Common CommonPersistence `vic:"0.1" scope:"read-write,non-persistent" key:"common"`
}
Struct := Type{
Common: CommonPersistence{
ID: "0xDEADBEEF",
Name: "Struct",
},
}
encoded := map[string]string{}
filterSink := ScopeFilterSink(NonPersistent|Hidden, MapSink(encoded))
Encode(filterSink, Struct)
expected := map[string]string{
visibleRONonpersistent("common/id"): "0xDEADBEEF",
visibleRONonpersistent("common/name"): "Struct",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
}
func TestInheritenceOfNonPersistenceWithPointer(t *testing.T) {
type Persistence struct {
ExecutorConfigPointersVisible `vic:"0.1" scope:"read-only,non-persistent" key:"pointers"`
}
Struct := Persistence{
ExecutorConfigPointersVisible: ExecutorConfigPointersVisible{
Sessions: map[string]*VisibleSessionConfig{
"primary": {
Tty: true,
},
},
},
}
encoded := map[string]string{}
filterSink := ScopeFilterSink(NonPersistent|Hidden, MapSink(encoded))
Encode(filterSink, Struct)
expected := map[string]string{
visibleRONonpersistent("pointers/sessions"): "primary",
visibleRONonpersistent("pointers/sessions" + Separator + "primary/tty"): "true",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Persistence
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
}
func TestFilterSink(t *testing.T) {
type CommonPersistence struct {
ExecutionEnvironment string `vic:"0.1" recurse:"depth=0"`
ID string `vic:"0.1" scope:"read-only" key:"id"`
Name string `vic:"0.1" scope:"read-only,non-persistent" key:"name"`
Notes string `vic:"0.1" scope:"read-only" key:"notes"`
}
type Type struct {
Common CommonPersistence `vic:"0.1" scope:"read-write" key:"common"`
}
Struct := Type{
Common: CommonPersistence{
ID: "0xDEADBEEF",
Name: "Struct",
},
}
encoded := map[string]string{}
filterSink := ScopeFilterSink(NonPersistent|Hidden, MapSink(encoded))
Encode(filterSink, Struct)
expected := map[string]string{
visibleRONonpersistent("common/name"): "Struct",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
// strip ID as that would be filtered out
Struct.Common.ID = ""
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
}

View File

@@ -0,0 +1,537 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"encoding/base64"
"fmt"
"net"
"testing"
"time"
"github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
// make it verbose during testing
func init() {
logger.Level = logrus.DebugLevel
}
func TestEmbedded(t *testing.T) {
type Type struct {
Common `vic:"0.1" scope:"read-only" key:"common"`
}
Embedded := Type{
Common: Common{
ID: "0xDEADBEEF",
Name: "Embedded",
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), Embedded)
expected := map[string]string{
visibleRO("common/id"): "0xDEADBEEF",
visibleRO("common/name"): "Embedded",
visibleRO("common/notes"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Embedded, decoded, "Encoded and decoded does not match")
}
func TestNetPointer(t *testing.T) {
type Type struct {
Net *net.IPNet `vic:"0.1" scope:"read-only" key:"net"`
}
// 127.0.0.1/8
n := net.IPNet{IP: net.IP{0x7f, 0x0, 0x0, 0x1}, Mask: net.IPMask{0xff, 0x0, 0x0, 0x0}}
Net := Type{
Net: &n,
}
encoded := map[string]string{}
Encode(MapSink(encoded), Net)
expected := map[string]string{
visibleRO("net/IP"): base64.StdEncoding.EncodeToString(n.IP),
visibleRO("net/Mask"): base64.StdEncoding.EncodeToString(n.Mask),
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Net, decoded, "Encoded and decoded does not match")
}
func TestTimePointer(t *testing.T) {
d := time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC)
type Type struct {
Time *time.Time `vic:"0.1" scope:"read-only" key:"time"`
}
Time := Type{
Time: &d,
}
encoded := map[string]string{}
Encode(MapSink(encoded), Time)
expected := map[string]string{
visibleRO("time"): "2009-11-10 23:00:00 +0000 UTC",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, Time, decoded, "Encoded and decoded does not match")
}
func TestStructMap(t *testing.T) {
type Type struct {
StructMap map[string]Common `vic:"0.1" scope:"read-only" key:"map"`
}
StructMap := Type{
map[string]Common{
"Key1": {
ID: "0xDEADBEEF",
Name: "beef",
},
"Key2": {
ID: "0x8BADF00D",
Name: "food",
},
"Key3": {
ID: "0xDEADF00D",
Name: "dead",
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), StructMap)
expected := map[string]string{
visibleRO("map" + Separator + "Key1/id"): "0xDEADBEEF",
visibleRO("map" + Separator + "Key1/name"): "beef",
visibleRO("map" + Separator + "Key1/notes"): "",
visibleRO("map" + Separator + "Key2/id"): "0x8BADF00D",
visibleRO("map" + Separator + "Key2/name"): "food",
visibleRO("map" + Separator + "Key2/notes"): "",
visibleRO("map" + Separator + "Key3/id"): "0xDEADF00D",
visibleRO("map" + Separator + "Key3/name"): "dead",
visibleRO("map" + Separator + "Key3/notes"): "",
visibleRO("map"): "Key1" + Separator + "Key2" + Separator + "Key3",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, StructMap, decoded, "Encoded and decoded does not match")
}
func TestIntStructMap(t *testing.T) {
type Type struct {
StructMap map[int]Common `vic:"0.1" scope:"read-only" key:"map"`
}
StructMap := Type{
map[int]Common{
1: {
ID: "0xDEADBEEF",
Name: "beef",
},
2: {
ID: "0x8BADF00D",
Name: "food",
},
3: {
ID: "0xDEADF00D",
Name: "dead",
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), StructMap)
expected := map[string]string{
visibleRO("map" + Separator + "1/id"): "0xDEADBEEF",
visibleRO("map" + Separator + "1/name"): "beef",
visibleRO("map" + Separator + "1/notes"): "",
visibleRO("map" + Separator + "2/id"): "0x8BADF00D",
visibleRO("map" + Separator + "2/name"): "food",
visibleRO("map" + Separator + "2/notes"): "",
visibleRO("map" + Separator + "3/id"): "0xDEADF00D",
visibleRO("map" + Separator + "3/name"): "dead",
visibleRO("map" + Separator + "3/notes"): "",
visibleRO("map"): "1" + Separator + "2" + Separator + "3",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, StructMap, decoded, "Encoded and decoded does not match")
}
func TestStructSlice(t *testing.T) {
type Type struct {
StructSlice []Common `vic:"0.1" scope:"read-only" key:"slice"`
}
StructSlice := Type{
[]Common{
{
ID: "0xDEADFEED",
Name: "feed",
},
{
ID: "0xFACEFEED",
Name: "face",
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), StructSlice)
expected := map[string]string{
visibleRO("slice"): "1",
visibleRO("slice" + Separator + "0/id"): "0xDEADFEED",
visibleRO("slice" + Separator + "0/name"): "feed",
visibleRO("slice" + Separator + "0/notes"): "",
visibleRO("slice" + Separator + "1/id"): "0xFACEFEED",
visibleRO("slice" + Separator + "1/name"): "face",
visibleRO("slice" + Separator + "1/notes"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, StructSlice, decoded, "Encoded and decoded does not match")
}
func TestMultipleScope(t *testing.T) {
MultipleScope := struct {
MultipleScope string `vic:"0.1" scope:"read-only,hidden,non-persistent" key:"multiscope"`
}{
"MultipleScope",
}
encoded := map[string]string{}
Encode(MapSink(encoded), MultipleScope)
expected := map[string]string{}
assert.Equal(t, expected, encoded, "Not equal")
}
func TestUnknownScope(t *testing.T) {
UnknownScope := struct {
UnknownScope int `vic:"0.1" scope:"unknownscope" key:"unknownscope"`
}{
42,
}
encoded := map[string]string{}
Encode(MapSink(encoded), UnknownScope)
expected := map[string]string{}
assert.Equal(t, encoded, expected, "Not equal")
}
func TestUnknownProperty(t *testing.T) {
UnknownProperty := struct {
UnknownProperty int `vic:"0.1" scope:"hidden" key:"unknownproperty" recurse:"unknownproperty"`
}{
42,
}
encoded := map[string]string{}
Encode(MapSink(encoded), UnknownProperty)
expected := map[string]string{
hidden("unknownproperty"): "42",
}
assert.Equal(t, expected, encoded, "Not equal")
}
func TestOmitNested(t *testing.T) {
OmitNested := struct {
Time time.Time `vic:"0.1" scope:"volatile" key:"time" recurse:"depth=0"`
CurrentTime time.Time `vic:"0.1" scope:"volatile" key:"time"`
}{
Time: time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC),
CurrentTime: time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC),
}
encoded := map[string]string{}
Encode(MapSink(encoded), OmitNested)
expected := map[string]string{
visibleRO("time"): "2009-11-10 23:00:00 +0000 UTC",
}
assert.Equal(t, expected, encoded, "Encoded and decoded does not match")
}
func TestComplex(t *testing.T) {
type Type struct {
ExecutorConfig ExecutorConfig `vic:"0.1" scope:"hidden" key:"executorconfig"`
}
ExecutorConfig := Type{
ExecutorConfig{
Sessions: map[string]SessionConfig{
"Session1": {
Common: Common{
ID: "SessionID",
Name: "SessionName",
},
Tty: true,
Cmd: Cmd{
Path: "/vmware",
Args: []string{"/bin/imagec", "-standalone"},
Env: []string{"PATH=/bin", "USER=imagec"},
Dir: "/",
},
},
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), ExecutorConfig)
expected := map[string]string{
visibleRO("executorconfig/common/id"): "",
visibleRO("executorconfig/common/name"): "",
visibleRO("executorconfig/common/notes"): "",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/id"): "SessionID",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/name"): "SessionName",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/notes"): "",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/path"): "/vmware",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args~"): "/bin/imagec" + Separator + "-standalone",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args"): "1",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env~"): "PATH=/bin" + Separator + "USER=imagec",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env"): "1",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/dir"): "/",
hidden("executorconfig/sessions" + Separator + "Session1/tty"): "true",
hidden("executorconfig/sessions"): "Session1",
hidden("executorconfig/Key"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, ExecutorConfig, decoded, "Encoded and decoded does not match")
}
func TestComplexPointer(t *testing.T) {
type Type struct {
ExecutorConfig *ExecutorConfig `vic:"0.1" scope:"hidden" key:"executorconfig"`
}
ExecutorConfig := Type{
&ExecutorConfig{
Sessions: map[string]SessionConfig{
"Session1": {
Common: Common{
ID: "SessionID",
Name: "SessionName",
},
Tty: true,
Cmd: Cmd{
Path: "/vmware",
Args: []string{"/bin/imagec", "-standalone"},
Env: []string{"PATH=/bin", "USER=imagec"},
Dir: "/",
},
},
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), ExecutorConfig)
expected := map[string]string{
visibleRO("executorconfig/common/id"): "",
visibleRO("executorconfig/common/name"): "",
visibleRO("executorconfig/common/notes"): "",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/id"): "SessionID",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/name"): "SessionName",
visibleRO("executorconfig/sessions" + Separator + "Session1/common/notes"): "",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/path"): "/vmware",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args~"): "/bin/imagec" + Separator + "-standalone",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args"): "1",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env~"): "PATH=/bin" + Separator + "USER=imagec",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env"): "1",
hidden("executorconfig/sessions" + Separator + "Session1/cmd/dir"): "/",
hidden("executorconfig/sessions" + Separator + "Session1/tty"): "true",
hidden("executorconfig/sessions"): "Session1",
hidden("executorconfig/Key"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Type
Decode(MapSource(encoded), &decoded)
assert.Equal(t, ExecutorConfig, decoded, "Encoded and decoded does not match")
}
// TestPointerDecode tests the translation from a type where the sessions are direct values to
// one where they are pointers
func TestPointerDecode(t *testing.T) {
reference := ExecutorConfig{
Sessions: map[string]SessionConfig{
"Session1": {
Common: Common{
ID: "SessionID",
Name: "SessionName",
},
Tty: true,
Cmd: Cmd{
Path: "/vmware",
Args: []string{"/bin/imagec", "-standalone"},
Env: []string{"PATH=/bin", "USER=imagec"},
Dir: "/",
},
},
},
}
encoded := map[string]string{}
Encode(MapSink(encoded), reference)
expected := map[string]string{
visibleRO("common/id"): "",
visibleRO("common/name"): "",
visibleRO("common/notes"): "",
visibleRO("sessions" + Separator + "Session1/common/id"): "SessionID",
visibleRO("sessions" + Separator + "Session1/common/name"): "SessionName",
visibleRO("sessions" + Separator + "Session1/common/notes"): "",
hidden("sessions" + Separator + "Session1/cmd/path"): "/vmware",
hidden("sessions" + Separator + "Session1/cmd/args~"): "/bin/imagec" + Separator + "-standalone",
hidden("sessions" + Separator + "Session1/cmd/args"): "1",
hidden("sessions" + Separator + "Session1/cmd/env~"): "PATH=/bin" + Separator + "USER=imagec",
hidden("sessions" + Separator + "Session1/cmd/env"): "1",
hidden("sessions" + Separator + "Session1/cmd/dir"): "/",
hidden("sessions" + Separator + "Session1/tty"): "true",
hidden("sessions"): "Session1",
hidden("Key"): "",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded ExecutorConfigPointers
Decode(MapSource(encoded), &decoded)
// cannot assert equality at a high level because of the different structure types, but we can test the
// common structure fragments
assert.Equal(t, reference.Sessions["Session1"], *decoded.Sessions["Session1"], "Encoded and decoded sessions do not match")
}
func TestInsideOutside(t *testing.T) {
type Inside struct {
ID string `vic:"0.1" scope:"read-write" key:"id"`
Name string `vic:"0.1" scope:"read-write" key:"name"`
}
type Outside struct {
Inside Inside `vic:"0.1" scope:"read-only" key:"inside"`
ID string `vic:"0.1" scope:"read-write" key:"id"`
Name string `vic:"0.1" scope:"read-write" key:"name"`
}
outside := Outside{
Inside: Inside{
ID: "inside",
Name: "Inside",
},
ID: "outside",
Name: "Outside",
}
encoded := map[string]string{}
Encode(MapSink(encoded), outside)
expected := map[string]string{
visibleRW("inside.id"): "inside",
visibleRW("inside.name"): "Inside",
visibleRW("id"): "outside",
visibleRW("name"): "Outside",
}
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
var decoded Outside
Decode(MapSource(encoded), &decoded)
assert.Equal(t, outside, decoded, "Encoded and decoded does not match")
}
func TestIPSlice(t *testing.T) {
type Slice struct {
Slice []net.IP `vic:"0.1" scope:"read-only" key:"slice"`
}
ips := []net.IP{
net.ParseIP("10.10.10.10"),
net.ParseIP("10.10.10.1"),
}
encodedIPs := make([]string, len(ips))
for i := range ips {
Encode(func(key, value string) error {
encodedIPs[i] = value
return nil
}, ips[i])
}
s := Slice{
Slice: ips,
}
encoded := make(map[string]string)
Encode(MapSink(encoded), s)
expected := map[string]string{
visibleRO("slice"): fmt.Sprintf("%d", len(ips)-1),
}
for i := range encodedIPs {
expected[visibleRO(fmt.Sprintf("slice"+Separator+"%d", i))] = encodedIPs[i]
}
assert.Equal(t, expected, encoded, "Encoded and expected do not match")
var decoded Slice
Decode(MapSource(encoded), &decoded)
assert.Equal(t, s, decoded, "Encoded and decoded do not match")
}

View File

@@ -0,0 +1,477 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"encoding/base64"
"fmt"
"reflect"
"strconv"
"strings"
"time"
)
var (
nilValue = reflect.ValueOf(nil)
)
type decoder func(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error)
var (
kindDecoders map[reflect.Kind]decoder
intfDecoders map[reflect.Type]decoder
)
func init() {
kindDecoders = map[reflect.Kind]decoder{
reflect.String: decodeString,
reflect.Struct: decodeStruct,
reflect.Slice: decodeSlice,
reflect.Array: decodeSlice,
reflect.Map: decodeMap,
reflect.Ptr: decodePtr,
reflect.Int: decodePrimitive,
reflect.Int8: decodePrimitive,
reflect.Int16: decodePrimitive,
reflect.Int32: decodePrimitive,
reflect.Int64: decodePrimitive,
reflect.Bool: decodePrimitive,
reflect.Float32: decodePrimitive,
reflect.Float64: decodePrimitive,
}
intfDecoders = map[reflect.Type]decoder{
reflect.TypeOf(time.Time{}): decodeTime,
}
}
// decode is the generic switcher that decides which decoder to use for a field
func decode(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
// if depth has reached zero, we skip decoding entirely
if depth.depth == 0 {
return dest, nil
}
depth.depth--
// obtain the handler from the map, checking for the more specific interfaces first
dec, ok := intfDecoders[dest.Type()]
if ok {
return dec(src, dest, prefix, depth)
}
dec, ok = kindDecoders[dest.Kind()]
if ok {
return dec(src, dest, prefix, depth)
}
logger.Debugf("Skipping unsupported field, interface: %T, kind %s", dest, dest.Kind())
return dest, nil
}
// decodeString is the degenerative case where what we get is what we need
func decodeString(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
v, err := src(prefix)
if err != nil {
logger.Debugf("No value found in data source for string at key %q", prefix)
return nilValue, err
}
return reflect.ValueOf(v), nil
}
// decodePrimitive wraps the fromString primitive decoding in a manner that can be called via decode
func decodePrimitive(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
var this reflect.Value
if !dest.CanAddr() {
logger.Debugf("Making new primitive for %s", prefix)
ptr := reflect.New(dest.Type())
this = ptr.Elem()
} else {
logger.Debugf("Reusing existing struct for %s", prefix)
this = dest
}
// see if there's a value to decode
v, err := src(prefix)
if err != nil {
logger.Debugf("No value available for key to primitive %s", prefix)
return nilValue, err
}
t := this.Type()
this.Set(fromString(reflect.Zero(t), v))
return this, nil
}
func decodePtr(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
// if we're not following pointers, then return immediately
if !depth.follow {
return dest, nil
}
// value representing the run-time data
logger.Debugf("Decoding pointer into object: %#v", dest)
// if the pointer is nil we need to create the destination type
target := dest
if dest.IsNil() {
target = reflect.New(dest.Type().Elem())
}
// check to see if the resulting object is not nil
// If it is nil, then there was nothing to decode and the pointer remains nil
result, err := decode(src, target.Elem(), prefix, depth)
logger.Debugf("target is now %#v, %+q ", target, target.Type())
if !result.IsValid() || err == ErrKeyNotFound {
// leave the pointer as nil if the result is zero type or invalid
return dest, nil
}
// neither pointer, nor zero
// NOTE: if the returned result is not addressable this can panic - that generally
// indicates an incorrect implementation of a decodeX method... those should always
// return addressable Values. See decodeByteSlice as an example - this uses make([]byte)
// rather than built in string(bytes) conversion specifically to get an addressable return
if dest.IsNil() {
dest = target
}
dest.Elem().Set(result)
return dest, nil
}
var typeType = reflect.TypeOf((*reflect.Type)(nil)).Elem()
func decodeStruct(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
// value representing the run-time data
logger.Debugf("Decoding struct into object: %#v, type: %s", dest, dest.Type().Name())
var this reflect.Value
if !dest.CanAddr() {
logger.Debugf("Making new struct for %s", prefix)
ptr := reflect.New(dest.Type())
this = ptr.Elem()
} else {
logger.Debugf("Reusing existing struct for %s", prefix)
this = dest
}
// do we have any data for this struct at all
var valid bool
var err error
noKeysFound := true
// iterate through every field in the struct
for i := 0; i < this.NumField(); i++ {
field := this.Field(i)
key, fdepth := calculateKeyFromField(this.Type().Field(i), prefix, depth)
if key == "" {
// this is either a malformed key or explicitly skipped
continue
}
// Dump what we have so far
logger.Debugf("Key: %s, Kind: %s Value: %s", key, field.Kind(), field.String())
// check to see if the resulting object is not nil
// If it is nil, then there was nothing to decode
var result reflect.Value
result, err = decode(src, field, key, fdepth)
if result.IsValid() {
logger.Debugf("Setting field %s to %#v", this.Type().Field(i).Name, result)
field.Set(result)
valid = true
if err != ErrKeyNotFound {
noKeysFound = false
}
} else {
logger.Debugf("Invalid result for field %s", this.Type().Field(i).Name)
}
}
if !valid || noKeysFound {
logger.Debugf("No valid result, returning nil value")
return nilValue, err
}
logger.Debugf("Return decoded structure for %s: %#v", prefix, this)
return this, nil
}
func decodeByteSlice(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
logger.Debugf("Converting string to []byte")
base, err := src(prefix)
if err != nil {
logger.Debugf("No value found in data source for []byte %q", prefix)
return nilValue, err
}
bytes, err := base64.StdEncoding.DecodeString(base)
if err != nil {
logger.Debugf("Expected base64 encoded string for []byte %q: %s", prefix, err)
return nilValue, err
}
length := len(bytes)
// we don't even try to merge byte arrays - no idea how to get append behaviour
// correct with reflection
// use make([]byte) rather than built in string(bytes) conversion to get an addressable return value
logger.Debugf("Making new slice for %s", prefix)
this := make([]byte, length, length)
copy(this, bytes)
return reflect.ValueOf(this), nil
}
func decodeSlice(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
// value representing the run-time data
logger.Debugf("Decoding struct into object: %#v", dest)
kind := dest.Type().Elem().Kind()
if kind == reflect.Uint8 {
return decodeByteSlice(src, dest, prefix, depth)
}
// do we have any data for this struct at all
length := 0
curLen := 0
// get the length of the array
len, err := src(prefix)
if err != nil || len == "" {
logger.Debugf("No value available for key %s - will create empty array if needed", prefix)
} else {
// if there's any data at all then we can assume we need to be extant
lengthValue := fromString(reflect.ValueOf(0), len)
length = int(lengthValue.Int()) + 1
}
var this reflect.Value
if !dest.IsValid() || dest.IsNil() || length > dest.Cap() {
logger.Debugf("Making new slice for %s", prefix)
this = reflect.MakeSlice(dest.Type(), length, length)
} else {
this = dest
this.SetLen(length)
curLen = this.Len()
}
// determine the key given the array type
if kind == reflect.Struct || isEncodableSliceElemType(dest.Type().Elem()) {
for i := 0; i < length; i++ {
// convert key to name|index format
key := appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
// if there's already a struct in the array at this index then we pass that as the current
// value
var cur reflect.Value
if i < curLen {
cur = this.Index(i)
} else {
cur = reflect.Zero(dest.Type().Elem())
}
var result reflect.Value
// #nosec: Errors unhandled.
result, _ = decode(src, cur, key, depth)
if result.IsValid() {
this.Index(i).Set(result)
}
continue
}
return this, nil
}
// convert key to name|index format
key := appendToPrefix(prefix, "", "~")
kval, err := src(key)
if err != nil {
logger.Debugf("No value found in data source for key %q", key)
return this, err
}
// lookup the key and split it
values := strings.Split(kval, Separator)
for i := 0; i < length; i++ {
v := values[i]
t := this.Type().Elem()
k := fromString(reflect.Zero(t), v)
// set the i'th slice item
this.Index(i).Set(k)
}
return this, nil
}
func decodeMap(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
// value representing the run-time data
logger.Debugf("Decoding struct into object: %#v", dest)
// if the value is the zero type, we have to create ourselves
var this reflect.Value
if !dest.IsValid() || dest.IsNil() {
logger.Debugf("Making new maps for %s", prefix)
this = reflect.MakeMap(dest.Type())
} else {
this = dest
}
mapkeys, err := src(prefix)
if mapkeys == "" || err != nil {
logger.Debugf("No value found in data source for maps keys %q", prefix)
return this, err
}
keytype := this.Type().Key()
valtype := this.Type().Elem()
// split the list of map keys and iterate
for _, value := range strings.Split(mapkeys, Separator) {
k := fromString(reflect.Zero(keytype), value)
target := this.MapIndex(k)
if !target.IsValid() {
target = reflect.Zero(valtype)
}
key := appendToPrefix(prefix, Separator, value)
// check to see if the resulting object is not nil
// If it is nil, then there was nothing to decode and the pointer remains nil
// #nosec: Errors unhandled.
result, _ := decode(src, target, key, depth)
if result.IsValid() {
this.SetMapIndex(k, result)
}
}
return this, nil
}
func decodeTime(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
v, err := src(prefix)
if err != nil {
logger.Debugf("No value found in data source for time %q", prefix)
return nilValue, err
}
t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", v)
if err != nil {
logger.Debugf("Failed to convert value %q to time", v)
}
return reflect.ValueOf(t), nil
}
// fromString converts string representation of a basic type to basic type
func fromString(field reflect.Value, value string) reflect.Value {
// handle the zero value
// TODO: can probably handle this more efficiently with a nil pointer return
// as whatever we're populating with primitives will already have their zero
// value.
if value == "" {
return reflect.Zero(field.Type())
}
switch field.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
s, err := strconv.ParseInt(value, 10, 64)
if err != nil {
logger.Errorf("Failed to convert value %#v (%s) to int: %s", value, field.Kind(), err.Error())
return field
}
return reflect.ValueOf(s).Convert(field.Type())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
s, err := strconv.ParseUint(value, 10, 64)
if err != nil {
logger.Errorf("Failed to convert value %#v (%s) to uint: %s", value, field.Kind(), err.Error())
return field
}
return reflect.ValueOf(s).Convert(field.Type())
case reflect.Bool:
s, err := strconv.ParseBool(value)
if err != nil {
logger.Errorf("Failed to convert value %#v (%s) to bool: %s", value, field.Kind(), err.Error())
return field
}
return reflect.ValueOf(s)
case reflect.String:
return reflect.ValueOf(value)
case reflect.Float32, reflect.Float64:
s, err := strconv.ParseFloat(value, 64)
if err != nil {
logger.Errorf("Failed to convert value %#v (%s) to float: %s", value, field.Kind(), err.Error())
return field
}
return reflect.ValueOf(s)
}
logger.Debugf("Invalid Kind: %s (%#v)", field.Kind(), value)
return field
}
// DataSource provides a function that, give a key will return a value
// this is to be used during extraConfig decode to obtain values. Should
// return ErrKeyNotFound if the key does not exist in the data source.
type DataSource func(string) (string, error)
// Decode populates a destination with data from the supplied data source
func Decode(src DataSource, dest interface{}) interface{} {
if src == nil {
logger.Warnf("Decode source is nil - unable to continue")
return dest
}
// #nosec: Errors unhandled.
value, _ := decode(src, reflect.ValueOf(dest), DefaultPrefix, Unbounded)
return value.Interface()
}
// DecodeWithPrefix populates a destination with data from the supplied data source, using
// the specified prefix - this allows for decode into substructres.
func DecodeWithPrefix(src DataSource, dest interface{}, prefix string) interface{} {
if src == nil {
logger.Warnf("Decode source is nil - unable to continue")
return dest
}
// #nosec: Errors unhandled.
value, _ := decode(src, reflect.ValueOf(dest), prefix, Unbounded)
return value.Interface()
}
// MapSource takes a key/value map and uses that as the datasource for decoding into
// target structures
func MapSource(src map[string]string) DataSource {
return func(key string) (string, error) {
val, ok := src[key]
if !ok {
return "", ErrKeyNotFound
}
return val, nil
}
}

View File

@@ -0,0 +1,30 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import "errors"
// GuestInfoSource uses the rpcvmx mechanism to access the guestinfo key/value map as
// the datasource for decoding into target structures
func GuestInfoSource() (DataSource, error) {
return GuestInfoSourceWithPrefix("")
}
// GuestInfoSourceWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSourceWithPrefix(prefix string) (DataSource, error) {
return nil, errors.New("Not implemented on OSX")
}

View File

@@ -0,0 +1,63 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"fmt"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vmw-guestinfo/rpcvmx"
"github.com/vmware/vmw-guestinfo/vmcheck"
)
// GuestInfoSource uses the rpcvmx mechanism to access the guestinfo key/value map as
// the datasource for decoding into target structures
func GuestInfoSource() (DataSource, error) {
return GuestInfoSourceWithPrefix("")
}
// GuestInfoSourceWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSourceWithPrefix(prefix string) (DataSource, error) {
// Check we're using a vcpu (which doesn't assume this is UID 0).
if !vmcheck.IsVirtualCPU() {
return nil, fmt.Errorf("not in a virtual world")
}
guestinfo := rpcvmx.NewConfig()
source := func(key string) (string, error) {
if key != GuestInfoSecretKey {
key = addPrefixToKey(DefaultGuestInfoPrefix, prefix, key)
}
value, err := guestinfo.String(key, "")
if value == "" {
err = ErrKeyNotFound
} else if value == "<nil>" {
value = ""
}
if key != GuestInfoSecretKey { // don't log the secret key
log.Debugf("GuestInfoSource: key: %s, value: %#v, error: %s", key, value, err)
}
return value, err
}
return new(SecretKey).Source(source), nil
}

View File

@@ -0,0 +1,32 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"errors"
)
// GuestInfoSource uses the rpcvmx mechanism to access the guestinfo key/value map as
// the datasource for decoding into target structures
func GuestInfoSource() (DataSource, error) {
return GuestInfoSourceWithPrefix("")
}
// GuestInfoSourceWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSourceWithPrefix(prefix string) (DataSource, error) {
return nil, errors.New("Not implemented on Windows")
}

View File

@@ -0,0 +1,47 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
/*
Package extraconfig provides Encode/Decode methods to convert data between Go structs and VMware Extraconfig values.
The implementation understands the following set of annotations and map the fields to appropriate extraConfig keys - in the case where the key describes a boolean state, omitting the annotation implies the opposite:
hidden - hidden from GuestOS
read-only - value can only be modified via vSphere APIs
read-write - value can be modified
non-persistent - value will be lost on VM reboot
volatile - field is not exported directly, but via a function that freshens the value each time)
The struct fields are required to be annotated with the "vic" tag, otherwise extraconfig package simply skips them. Scope and key tags are also required.
Scope tag can contain multiple values (comma separated)
Key tag can contain extra properties (comma separated) but the first element has to the name of the key.
type Example struct {
// skipped - does not contain any tag
Note string
// skipped - does not contain scope and key
ID string `vic:"0.1"`
// valid - extraconfig will encode this using a read-only key (as instructed by scope)
Name string `vic:"0.1" scope:"read-only" key:"name"`
// valid - but extraconfig won't nest into the struct (so it's value will be type's zero value)
Time time.Time `vic:"0.1" scope:"volatile" key:"time,omitnested"`
}
*/

View File

@@ -0,0 +1,300 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"encoding/base64"
"errors"
"fmt"
"net"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"time"
)
var (
ErrKeyNotFound = errors.New("key not found")
)
type encoder func(sink DataSink, src reflect.Value, prefix string, depth recursion)
var kindEncoders map[reflect.Kind]encoder
var intfEncoders map[reflect.Type]encoder
func init() {
kindEncoders = map[reflect.Kind]encoder{
reflect.String: encodeString,
reflect.Struct: encodeStruct,
reflect.Slice: encodeSlice,
reflect.Array: encodeSlice,
reflect.Map: encodeMap,
reflect.Ptr: encodePtr,
reflect.Int: encodePrimitive,
reflect.Int8: encodePrimitive,
reflect.Int16: encodePrimitive,
reflect.Int32: encodePrimitive,
reflect.Int64: encodePrimitive,
reflect.Bool: encodePrimitive,
reflect.Float32: encodePrimitive,
reflect.Float64: encodePrimitive,
}
intfEncoders = map[reflect.Type]encoder{
reflect.TypeOf(time.Time{}): encodeTime,
}
}
// decode is the generic switcher that decides which decoder to use for a field
func encode(sink DataSink, src reflect.Value, prefix string, depth recursion) {
// if depth has reached zero, we skip encoding entirely
if depth.depth == 0 {
return
}
depth.depth--
// obtain the handler from the map, checking for the more specific interfaces first
enc, ok := intfEncoders[src.Type()]
if ok {
enc(sink, src, prefix, depth)
return
}
enc, ok = kindEncoders[src.Kind()]
if ok {
enc(sink, src, prefix, depth)
return
}
logger.Debugf("Skipping unsupported field, interface: %T, kind %s", src, src.Kind())
}
// encodeString is the degenerative case where what we get is what we need
func encodeString(sink DataSink, src reflect.Value, prefix string, depth recursion) {
err := sink(prefix, src.String())
if err != nil {
logger.Errorf("Failed to encode string for key %s: %s", prefix, err)
}
}
// encodePrimitive wraps the toString primitive encoding in a manner that can be called via encode
func encodePrimitive(sink DataSink, src reflect.Value, prefix string, depth recursion) {
err := sink(prefix, toString(src))
if err != nil {
logger.Errorf("Failed to encode primitive for key %s: %s", prefix, err)
}
}
func encodePtr(sink DataSink, src reflect.Value, prefix string, depth recursion) {
// if we're not following pointers, return immediately
if !depth.follow {
return
}
logger.Debugf("Encoding object: %#v", src)
if src.IsNil() {
// no need to attempt anything
return
}
encode(sink, src.Elem(), prefix, depth)
}
func encodeStruct(sink DataSink, src reflect.Value, prefix string, depth recursion) {
logger.Debugf("Encoding object: %#v", src)
// iterate through every field in the struct
for i := 0; i < src.NumField(); i++ {
field := src.Field(i)
key, fdepth := calculateKeyFromField(src.Type().Field(i), prefix, depth)
if key == "" {
logger.Debugf("Skipping field %s with empty computed key", src.Type().Field(i).Name)
continue
}
// Dump what we have so far
logger.Debugf("Key: %s, Kind: %s Value: %s", key, field.Kind(), field.String())
encode(sink, field, key, fdepth)
}
}
func isEncodableSliceElemType(t reflect.Type) bool {
switch t {
case reflect.TypeOf((net.IP)(nil)):
return true
}
return false
}
func encodeSlice(sink DataSink, src reflect.Value, prefix string, depth recursion) {
logger.Debugf("Encoding object: %#v", src)
length := src.Len()
if length == 0 {
logger.Debug("Skipping empty slice")
return
}
// determine the key given the array type
kind := src.Type().Elem().Kind()
if kind == reflect.Uint8 {
// special []byte array handling
logger.Debugf("Converting []byte to base64 string")
str := base64.StdEncoding.EncodeToString(src.Bytes())
encode(sink, reflect.ValueOf(str), prefix, depth)
return
} else if kind == reflect.Struct || isEncodableSliceElemType(src.Type().Elem()) {
for i := 0; i < length; i++ {
// convert key to name|index format
key := appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
encode(sink, src.Index(i), key, depth)
}
} else {
// else assume it's primitive - we'll panic/recover and continue it not
defer func() {
if err := recover(); err != nil {
logger.Errorf("unable to encode %s (slice) for %s: %s", src.Type(), prefix, err)
}
}()
values := make([]string, length)
for i := 0; i < length; i++ {
values[i] = toString(src.Index(i))
}
// convert key to name|index format
key := appendToPrefix(prefix, "", "~")
err := sink(key, strings.Join(values, Separator))
if err != nil {
logger.Errorf("Failed to encode slice data for key %s: %s", key, err)
}
}
// prefix contains the length of the array
// seems insane calling toString(ValueOf(..)) but it means we're using the same path for everything
err := sink(prefix, toString(reflect.ValueOf(length-1)))
if err != nil {
logger.Errorf("Failed to encode slice length for key %s: %s", prefix, err)
}
}
func encodeMap(sink DataSink, src reflect.Value, prefix string, depth recursion) {
logger.Debugf("Encoding object: %#v", src)
// iterate over keys and recurse
mkeys := src.MapKeys()
length := len(mkeys)
if length == 0 {
logger.Debug("Skipping empty map")
return
}
logger.Debugf("Encoding map entries based off prefix: %s", prefix)
keys := make([]string, length)
for i, v := range mkeys {
keys[i] = toString(v)
key := appendToPrefix(prefix, Separator, keys[i])
encode(sink, src.MapIndex(v), key, depth)
}
// sort the keys before joining - purely to make testing viable
sort.Strings(keys)
err := sink(prefix, strings.Join(keys, Separator))
if err != nil {
logger.Errorf("Failed to encode map keys for key %s: %s", prefix, err)
}
}
func encodeTime(sink DataSink, src reflect.Value, prefix string, depth recursion) {
err := sink(prefix, src.Interface().(time.Time).String())
if err != nil {
logger.Errorf("Failed to encode time for key %s: %s", prefix, err)
}
}
// toString converts a basic type to its string representation
func toString(field reflect.Value) string {
switch field.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(field.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(field.Uint(), 10)
case reflect.Bool:
return strconv.FormatBool(field.Bool())
case reflect.String:
return field.String()
case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(field.Float(), 'E', -1, 64)
default:
panic(field.Type().String() + " is an unhandled type")
}
}
// DataSink provides a function that, given a key/value will persist that
// in some manner suited for later retrieval
type DataSink func(string, string) error
// Encode serializes the given type to the supplied data sink
func Encode(sink DataSink, src interface{}) {
encode(sink, reflect.ValueOf(src), DefaultPrefix, Unbounded)
}
// EncodeWithPrefix serializes the given type to the supplied data sink, using
// the supplied prefix - this allows for serialization of subsections of a
// struct
func EncodeWithPrefix(sink DataSink, src interface{}, prefix string) {
encode(sink, reflect.ValueOf(src), prefix, Unbounded)
}
// MapSink takes a map and populates it with key/value pairs from the encode
func MapSink(sink map[string]string) DataSink {
// this is a very basic mechanism of allowing serialized updates to a sink
// a more involved approach is necessary if wanting to do concurrent read/write
mutex := sync.Mutex{}
return func(key, value string) error {
mutex.Lock()
defer mutex.Unlock()
sink[key] = value
return nil
}
}
// ScopeFilterSink will create a DataSink that only stores entries where the key scope
// matches one or more scopes in the filter.
// The filter is a bitwise composion of scope flags
func ScopeFilterSink(filter uint, sink DataSink) DataSink {
return func(key, value string) error {
logger.Debugf("Filtering encode of %s with scopes: %v", key, calculateScopeFromKey(key))
scope := calculateScope(calculateScopeFromKey(key))
if scope&filter != 0 {
sink(key, value)
} else {
logger.Debugf("Skipping encode of %s with scopes that do not match filter: %v", key, calculateScopeFromKey(key))
}
return nil
}
}

View File

@@ -0,0 +1,30 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import "errors"
// GuestInfoSink uses the rpcvmx mechanism to update the guestinfo key/value map as
// the datasink for encoding target structures
func GuestInfoSink() (DataSink, error) {
return GuestInfoSinkWithPrefix("")
}
// GuestInfoSinkWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSinkWithPrefix(prefix string) (DataSink, error) {
return nil, errors.New("Not implemented on OSX")
}

View File

@@ -0,0 +1,63 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"fmt"
"strings"
log "github.com/Sirupsen/logrus"
"github.com/vmware/vmw-guestinfo/rpcvmx"
"github.com/vmware/vmw-guestinfo/vmcheck"
)
// GuestInfoSink uses the rpcvmx mechanism to update the guestinfo key/value map as
// the datasink for encoding target structures
func GuestInfoSink() (DataSink, error) {
return GuestInfoSinkWithPrefix("")
}
// GuestInfoSinkWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSinkWithPrefix(prefix string) (DataSink, error) {
// Check we're using a vcpu (which doesn't assume this is UID 0).
if !vmcheck.IsVirtualCPU() {
return nil, fmt.Errorf("not in a virtual world")
}
guestinfo := rpcvmx.NewConfig()
return func(key, value string) error {
if strings.Contains(key, "/") {
// quietly skip if it's a read-only key
return nil
}
key = addPrefixToKey(DefaultGuestInfoPrefix, prefix, key)
if value == "" {
value = "<nil>"
}
log.Debugf("GuestInfoSink: setting key: %s, value: %#v", key, value)
err := guestinfo.SetString(key, value)
if err != nil {
log.Errorf("GuestInfoSink: error: %#v", err)
}
return err
}, nil
}

View File

@@ -0,0 +1,30 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import "errors"
// GuestInfoSink uses the rpcvmx mechanism to update the guestinfo key/value map as
// the datasink for encoding target structures
func GuestInfoSink() (DataSink, error) {
return GuestInfoSinkWithPrefix("")
}
// GuestInfoSinkWithPrefix adds a prefix to all keys accessed. The key must not have leading
// or trailing separator characters, but may have separators in other positions. The separator
// (either . or /) will be replaced with the appropriate value for the key in question.
func GuestInfoSinkWithPrefix(prefix string) (DataSink, error) {
return nil, errors.New("Not implemented on Windows")
}

View File

@@ -0,0 +1,21 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import "testing"
func TestExistingBasic(t *testing.T) {
t.Skip("decode into existing target tests not yet implemented")
}

View File

@@ -0,0 +1,510 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"fmt"
"os"
"reflect"
"strconv"
"strings"
"github.com/Sirupsen/logrus"
"github.com/vmware/vic/pkg/log"
)
const (
// DefaultTagName value
DefaultTagName = "vic"
// DefaultPrefix value
DefaultPrefix = ""
// DefaultGuestInfoPrefix value
DefaultGuestInfoPrefix = "guestinfo.vice."
//Separator for slice values and map keys
Separator = "|"
// suffix separator character
suffixSeparator = "@"
// secret suffix
secretSuffix = "secret"
// non-persistent suffix
nonpersistentSuffix = "non-persistent"
)
const (
// Invalid value
Invalid = 1 << iota
// Hidden value
Hidden
// ReadOnly value
ReadOnly
// ReadWrite value
ReadWrite
// NonPersistent value
NonPersistent
// Volatile value
Volatile
// Secret value
Secret
)
type recursion struct {
// depth is a recursion depth, 0 equating to skip field
depth int
// follow controls whether we follow pointers
follow bool
}
// Unbounded is the value used for unbounded recursion
var Unbounded = recursion{depth: -1, follow: true}
var logger = &logrus.Logger{
Out: os.Stderr,
// We're using our own text formatter to skip the \n and \t escaping logrus
// was doing on non TTY Out (we redirect to a file) descriptors.
Formatter: log.NewTextFormatter(),
Hooks: make(logrus.LevelHooks),
Level: logrus.InfoLevel,
}
// SetLogLevel for the extraconfig package
func SetLogLevel(level logrus.Level) {
logger.Level = level
}
// calculateScope returns the uint representation of scope tag
func calculateScope(scopes []string) uint {
var scope uint
empty := true
for i := range scopes {
if scopes[i] != "" {
empty = false
break
}
}
if empty {
return Hidden | ReadOnly
}
for _, v := range scopes {
switch v {
case "hidden":
scope |= Hidden
case "read-only":
scope |= ReadOnly
case "read-write":
scope |= ReadWrite
case nonpersistentSuffix:
scope |= NonPersistent
case "volatile":
scope |= Volatile
case secretSuffix:
scope |= Secret | ReadOnly
default:
return Invalid
}
}
return scope
}
func isSecret(key string) bool {
suffix := strings.Split(key, suffixSeparator)
if len(suffix) < 2 {
// no @ separator
return false
}
for i := range suffix[1:] {
if suffix[i+1] == secretSuffix {
return true
}
}
return false
}
func isNonPersistent(key string) bool {
suffix := strings.Split(key, suffixSeparator)
if len(suffix) < 2 {
// no @ separator
return false
}
for i := range suffix[1:] {
if suffix[i+1] == nonpersistentSuffix {
return true
}
}
return false
}
func calculateScopeFromKey(key string) []string {
scopes := []string{}
if !strings.HasPrefix(key, DefaultGuestInfoPrefix) {
scopes = append(scopes, "hidden")
}
if strings.Contains(key, "/") {
scopes = append(scopes, "read-only")
} else {
scopes = append(scopes, "read-write")
}
if isSecret(key) {
scopes = append(scopes, secretSuffix)
}
if isNonPersistent(key) {
scopes = append(scopes, nonpersistentSuffix)
}
return scopes
}
func calculateKeyFromField(field reflect.StructField, prefix string, depth recursion) (string, recursion) {
skip := recursion{}
//skip unexported fields
if field.PkgPath != "" {
logger.Debugf("Skipping %s (not exported)", field.Name)
return "", skip
}
// get the annotations
tags := field.Tag
logger.Debugf("Tags: %#v", tags)
var key string
var scopes []string
var scope uint
fdepth := depth
prefixScopes := calculateScopeFromKey(prefix)
prefixScope := calculateScope(prefixScopes)
// do we have DefaultTagName?
if tags.Get(DefaultTagName) != "" {
// get the scopes
scopes = strings.Split(tags.Get("scope"), ",")
logger.Debugf("Scopes: %#v", scopes)
// get the keys and split properties from it
key = tags.Get("key")
logger.Debugf("Key specified: %s", key)
// get the keys and split properties from it
recurse := tags.Get("recurse")
if recurse != "" {
props := strings.Split(recurse, ",")
// process properties
for _, prop := range props {
// determine recursion depth
if strings.HasPrefix(prop, "depth") {
parts := strings.Split(prop, "=")
if len(parts) != 2 {
logger.Warnf("Skipping field with incorrect recurse property: %s", prop)
return "", skip
}
val, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
logger.Warnf("Skipping field with incorrect recurse value: %s", parts[1])
return "", skip
}
fdepth.depth = int(val)
} else if prop == "nofollow" {
fdepth.follow = false
} else if prop == "follow" {
fdepth.follow = true
} else {
logger.Warnf("Ignoring unknown recurse property %s (%s)", key, prop)
continue
}
}
}
} else {
logger.Debugf("%s not tagged - inheriting parent scope", field.Name)
scopes = prefixScopes
}
if key == "" {
logger.Debugf("%s does not specify key - defaulting to fieldname", field.Name)
key = field.Name
}
scope = calculateScope(scopes)
// non-persistent is inherited, even if other scopes are specified
if prefixScope&NonPersistent != 0 {
scope |= NonPersistent
}
// re-calculate the key based on the scope and prefix
if key = calculateKey(scope, prefix, key); key == "" {
logger.Debugf("Skipping %s (unknown scope %s)", field.Name, scopes)
return "", skip
}
return key, fdepth
}
// calculateKey calculates the key based on the scope and current prefix
func calculateKey(scope uint, prefix string, key string) string {
if scope&Invalid != 0 {
logger.Debugf("invalid scope")
return ""
}
newSep := "/"
oldSep := "."
key = strings.TrimSpace(key)
hide := scope&Hidden != 0
write := scope&ReadWrite != 0
visible := strings.HasPrefix(prefix, DefaultGuestInfoPrefix)
if !hide && write {
oldSep = "/"
newSep = "."
}
// strip any existing suffix from the prefix - it'll be re-added if still applicable
suffix := strings.Index(prefix, suffixSeparator)
if suffix != -1 {
prefix = prefix[:suffix]
}
// assemble the actual keypath with appropriate separators
out := key
if prefix != "" {
out = strings.Join([]string{prefix, key}, newSep)
}
if scope&Secret != 0 {
out += suffixSeparator + secretSuffix
}
if scope&NonPersistent != 0 {
if hide {
logger.Debugf("Unable to combine non-persistent and hidden scopes")
return ""
}
out += suffixSeparator + nonpersistentSuffix
}
// we don't care about existing separators when hiden
if hide {
if !visible {
return out
}
// strip the prefix and the leading r/w signifier
return out[len(DefaultGuestInfoPrefix)+1:]
}
// ensure that separators are correct
out = strings.Replace(out, oldSep, newSep, -1)
// Assemble the base that controls key publishing in guest
if !visible {
return DefaultGuestInfoPrefix + newSep + out
}
// prefix will have been mangled by strings.Replace
return DefaultGuestInfoPrefix + out[len(DefaultGuestInfoPrefix):]
}
// utility function to allow adding of arbitrary prefix into key
// header is a leading segment that is preserved, prefix is injected after that
func addPrefixToKey(header, prefix, key string) string {
if prefix == "" {
return key
}
base := strings.TrimPrefix(key, header)
separator := base[0]
var modifiedPrefix string
if separator == '.' {
modifiedPrefix = strings.Replace(prefix, "/", ".", -1)
} else {
modifiedPrefix = strings.Replace(prefix, ".", "/", -1)
}
// we assume (given usage comment for WithPrefix) that there's no leading or trailing separator
// on the prefix. base has a leading separator
// guestinfoPrefix is const so adding it to the format string directly
return fmt.Sprintf(header+"%c%s%s", separator, modifiedPrefix, base)
}
// appendToPrefix will join the value to the prefix with the separator (if any) while ensuring that
// any suffixes are moved to the end of the key
func appendToPrefix(prefix, separator, value string) string {
// strip any existing suffix from the prefix - it'll be re-added if still applicable
index := strings.Index(prefix, suffixSeparator)
suffix := ""
if index != -1 {
suffix = prefix[index:]
prefix = prefix[:index]
}
// suffix wil still include the suffix separator if present
key := fmt.Sprintf("%s%s%s%s", prefix, separator, value, suffix)
return key
}
func calculateKeys(v reflect.Value, field string, prefix string) []string {
logger.Debugf("v=%#v, field=%#v, prefix=%#v", v, field, prefix)
if v.Kind() == reflect.Ptr {
return calculateKeys(v.Elem(), field, prefix)
}
if field == "" {
return []string{prefix}
}
s := strings.SplitN(field, ".", 2)
field = ""
iterate := false
if s[0] == "*" {
iterate = true
}
if len(s) > 1 {
field = s[1]
}
if !iterate {
switch v.Kind() {
case reflect.Map:
found := false
for _, k := range v.MapKeys() {
sk := k.Convert(reflect.TypeOf(""))
if sk.String() == s[0] {
v = v.MapIndex(k)
found = true
break
}
}
if !found {
panic(fmt.Sprintf("could not find map key %s", s[0]))
}
prefix = appendToPrefix(prefix, Separator, s[0])
case reflect.Array, reflect.Slice:
i, err := strconv.Atoi(s[0])
if err != nil {
panic(fmt.Sprintf("bad array index %s: %s", s[0], err))
}
switch v.Type().Elem().Kind() {
case reflect.Struct:
prefix = appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
case reflect.Uint8:
return []string{prefix}
default:
prefix = appendToPrefix(prefix, "", "~")
}
v = v.Index(i)
case reflect.Struct:
f, found := v.Type().FieldByName(s[0])
if !found {
panic(fmt.Sprintf("could not find field %s", s[0]))
}
prefix, _ = calculateKeyFromField(f, prefix, recursion{})
v = v.FieldByIndex(f.Index)
default:
panic(fmt.Sprintf("cannot get field from type %s", v.Type()))
}
return calculateKeys(v, field, prefix)
}
var out []string
switch v.Kind() {
case reflect.Map:
for _, k := range v.MapKeys() {
sk := k.Convert(reflect.TypeOf(""))
prefix := appendToPrefix(prefix, Separator, sk.String())
out = append(out, calculateKeys(v.MapIndex(k), field, prefix)...)
}
case reflect.Array, reflect.Slice:
switch v.Type().Elem().Kind() {
case reflect.Struct:
for i := 0; i < v.Len(); i++ {
prefix := appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
out = append(out, calculateKeys(v.Index(i), field, prefix)...)
}
case reflect.Uint8:
return []string{prefix}
default:
return []string{appendToPrefix(prefix, "", "~")}
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
prefix, _ := calculateKeyFromField(v.Type().Field(i), prefix, recursion{})
out = append(out, calculateKeys(v.Field(i), field, prefix)...)
}
default:
panic(fmt.Sprintf("can't iterate type %s", v.Type().String()))
}
return out
}
// CalculateKeys gets the keys in extraconfig corresponding to the field
// specification passed in for obj. Examples:
//
// type struct A {
// I int `vic:"0.1" scope:"read-only" key:"i"`
// Str string `vic:"0.1" scope:"read-only" key:"str"`
// }
//
// type struct B {
// A A `vic:"0.1" scope:"read-only" key:"a"`
// Array []A `vic:"0.1" scope:"read-only" key:"array"`
// Map map[string]string `vic:"0.1" scope:"read-only" key:"map"`
// }
//
// b := B{}
// b.Array = []A{A{}}
// b.Map = map[string]string{"foo": "", "bar": ""}
// // returns []string{"a/str"}
// CalculateKeys(b, "A.Str", "")
//
// // returns []string{"array|0"}
// CalculateKeys(b, "Array.0", "")
//
// // returns []string{"array|0"}
// CalculateKeys(b, "Array.*", "")
//
// // returns []string{"map|foo", "map|bar"}
// CalculateKeys(b, "Map.*", "")
//
// // returns []string{"map|foo"}
// CalculateKeys(b, "Map.foo", "")
//
// // returns []string{"map|foo/str"}
// CalculateKeys(b, "Map.foo.str", "")
//
func CalculateKeys(obj interface{}, field string, prefix string) []string {
return calculateKeys(reflect.ValueOf(obj), field, prefix)
}

View File

@@ -0,0 +1,269 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"testing"
"strings"
"github.com/stretchr/testify/assert"
)
func visibleRO(key string) string {
return calculateKey(calculateScope([]string{"read-only"}), "", key)
}
func visibleRONonpersistent(key string) string {
return calculateKey(calculateScope([]string{"read-only", "non-persistent"}), "", key)
}
func visibleRW(key string) string {
return calculateKey(calculateScope([]string{"read-write"}), "", key)
}
func hidden(key string) string {
return calculateKey(calculateScope([]string{"hidden"}), "", key)
}
func TestHidden(t *testing.T) {
scopes := []string{"hidden"}
key := calculateKey(calculateScope(scopes), "a/b", "c")
assert.Equal(t, "a/b/c", key, "Key should remain hidden")
}
func TestHide(t *testing.T) {
scopes := []string{"hidden"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+"/a/b", "c")
assert.Equal(t, "a/b/c", key, "Key should be hidden")
}
func TestReveal(t *testing.T) {
scopes := []string{"read-only"}
key := calculateKey(calculateScope(scopes), "a/b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+"/a/b/c", key, "Key should be exposed")
}
func TestVisibleReadOnly(t *testing.T) {
scopes := []string{"read-only"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+"/a/b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+"/a/b/c", key, "Key should be remain visible and read-only")
}
func TestVisibleReadWrite(t *testing.T) {
scopes := []string{"read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c", key, "Key should be remain visible and read-write")
}
func TestTopLevelReadOnly(t *testing.T) {
scopes := []string{"read-only"}
key := calculateKey(calculateScope(scopes), "", "a")
assert.Equal(t, DefaultGuestInfoPrefix+"/a", key, "Key should be visible and read-only")
}
func TestReadOnlyToReadWrite(t *testing.T) {
scopes := []string{"read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+"/a/b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c", key, "Key should be visible and change to read-write")
}
func TestReadWriteToReadOnly(t *testing.T) {
scopes := []string{"read-only"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+"/a/b/c", key, "Key should be visible and change to read-only")
}
func TestCompoundKey(t *testing.T) {
scopes := []string{"read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a", "b/c")
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c", key, "Key should be visible and read-write")
}
func TestNoScopes(t *testing.T) {
scopes := []string{}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a/b", "c")
assert.Equal(t, "a/b/c", key, "Key should be completely proscriptive")
key = calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.Equal(t, "a.b/c", key, "Key should be hidden")
key = calculateKey(calculateScope(scopes), "a.b", "c")
assert.Equal(t, "a.b/c", key, "Key should remain hidden")
}
func TestSecret(t *testing.T) {
scopes := []string{"secret", "read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c"+suffixSeparator+secretSuffix, key, "Key should have secret suffix")
}
func TestNonpersistent(t *testing.T) {
scopes := []string{"non-persistent", "read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c"+suffixSeparator+nonpersistentSuffix, key, "Key should have non-persistent suffix")
}
func TestMultipleSuffixes(t *testing.T) {
scopes := []string{"non-persistent", "secret", "read-write"}
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
assert.True(t, strings.Contains(key, suffixSeparator+secretSuffix) && strings.Contains(key, suffixSeparator+nonpersistentSuffix), "Key should contain both secret and non-persistent suffix")
}
func TestCalculateKeys(t *testing.T) {
type AStruct struct {
I int
}
type Type struct {
ExecutorConfig ExecutorConfig `vic:"0.1" scope:"hidden" key:"executorconfig"`
Array []AStruct `vic:"0.1" scope:"read-write" key:"array"`
Ptr *AStruct `vic:"0.1" scope:"read-only" key:"ptr"`
Str string `vic:"0.1" scope:"read-only" key:"str"`
Bytes []uint8 `vic:"0.1" scope:"read-write" key:"bytes"`
}
ec := Type{
ExecutorConfig: ExecutorConfig{
Sessions: map[string]SessionConfig{
"Session1": {
Common: Common{
ID: "SessionID",
Name: "SessionName",
},
Tty: true,
Cmd: Cmd{
Path: "/vmware",
Args: []string{"/bin/imagec", "-standalone"},
Env: []string{"PATH=/bin", "USER=imagec"},
Dir: "/",
},
},
},
},
Array: []AStruct{
{I: 0},
},
Ptr: &AStruct{
I: 1,
},
Str: "foo",
Bytes: []byte{0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf},
}
var tests = []struct {
in string
out []string
}{
{
"ExecutorConfig.*",
[]string{
visibleRO("executorconfig/common"),
hidden("executorconfig/sessions"),
"executorconfig/Key",
},
},
{
"ExecutorConfig.Sessions.*",
[]string{"executorconfig/sessions" + Separator + "Session1"},
},
{
"ExecutorConfig.Sessions.Session1.Cmd.Args",
[]string{"executorconfig/sessions" + Separator + "Session1/cmd/args"},
},
{
"ExecutorConfig.Sessions.*.Cmd.Args.*",
[]string{"executorconfig/sessions" + Separator + "Session1/cmd/args~"},
},
{
"ExecutorConfig.Sessions.*.Cmd.Args.0",
[]string{"executorconfig/sessions" + Separator + "Session1/cmd/args~"},
},
{
"Array.0.I",
[]string{visibleRW("array" + Separator + "0/I")},
},
{
"Array.*",
[]string{visibleRW("array" + Separator + "0")},
},
{
"Ptr.I",
[]string{visibleRO("ptr/I")},
},
{
"Str",
[]string{visibleRO("str")},
},
{
"Bytes",
[]string{visibleRW("bytes")},
},
{
"Bytes.0",
[]string{visibleRW("bytes")},
},
{
"Bytes.*",
[]string{visibleRW("bytes")},
},
}
for _, te := range tests {
keys := CalculateKeys(ec, te.in, "")
assert.Equal(t, te.out, keys)
}
panicTests := []string{
"Array.1.I",
"Array.0.i",
"Array.f.i",
"ExecutorConfig.foo",
"foo",
"ExecutorConfig.Sessions.foo",
"Str.*",
"Str.foo",
}
for _, te := range panicTests {
assert.Panics(t, func() {
CalculateKeys(ec, te, "")
})
}
}

View File

@@ -0,0 +1,127 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
log "github.com/Sirupsen/logrus"
"golang.org/x/crypto/nacl/secretbox"
)
// The value of this key is hidden from API requests, but visible within the guest
// #nosec: Potential hardcoded credentials
const GuestInfoSecretKey = "guestinfo.ovfEnv"
// SecretKey provides helpers to encrypt/decrypt extraconfig values
type SecretKey struct {
key [32]byte
}
// NewSecretKey generates a new secret key
func NewSecretKey() (*SecretKey, error) {
s := new(SecretKey)
if _, err := rand.Read(s.key[:]); err != nil {
return nil, err
}
return s, nil
}
// FromString base64 decodes an existing SecretKey
func (s *SecretKey) FromString(key string) error {
b, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return err
}
if len(b) != 32 {
return errors.New("invalid secret key")
}
copy(s.key[:], b)
return nil
}
// String base64 encodes a SecretKey
func (s *SecretKey) String() string {
return base64.StdEncoding.EncodeToString(s.key[:])
}
// Source wraps the given DataSource, decrypting any secret values
func (s *SecretKey) Source(ds DataSource) DataSource {
// If GuestInfoSecretKey has a value, it should be our secret key.
// #nosec: Errors unhandled.
if val, _ := ds(GuestInfoSecretKey); val != "" {
if err := s.FromString(val); err != nil {
log.Errorf("failed to decode %s: %s", GuestInfoSecretKey, err)
} else {
log.Debugf("secret key decoded from %s", GuestInfoSecretKey)
}
}
return func(key string) (string, error) {
val, err := ds(key)
if err == nil && isSecret(key) {
b, err := base64.StdEncoding.DecodeString(val)
if err != nil {
return "", err
}
var nonce [24]byte
copy(nonce[:], b[:24])
plaintext, ok := secretbox.Open([]byte{}, b[24:], &nonce, &s.key)
if !ok {
return "", fmt.Errorf("failed to decrypt value for %s", key)
}
val = string(plaintext)
}
return val, err
}
}
// Sink wraps the given DataSink, encrypting any secret values
func (s *SecretKey) Sink(ds DataSink) DataSink {
// Store our secret key.
if err := ds(GuestInfoSecretKey, s.String()); err != nil {
log.Errorf("failed to store %s: %s", GuestInfoSecretKey, err)
}
return func(key, value string) error {
if isSecret(key) {
var nonce [24]byte
if _, err := rand.Read(nonce[:]); err != nil {
return err
}
ciphertext := secretbox.Seal(nonce[:], []byte(value), &nonce, &s.key)
value = base64.StdEncoding.EncodeToString(ciphertext)
}
return ds(key, value)
}
}

View File

@@ -0,0 +1,67 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSecretFields(t *testing.T) {
type tell struct {
Who string `vic:"0.1" scope:"secret" key:"who"`
}
type stuff struct {
Username string `vic:"0.1" scope:"read-only" key:"username"`
Password string `vic:"0.1" scope:"secret" key:"password"`
Tell tell
}
config := stuff{
Username: "root",
Password: "super-s@fe-passw0rd",
Tell: tell{"noone"},
}
out, err := NewSecretKey()
if err != nil {
t.Fatal(err)
}
encoded := map[string]string{}
Encode(out.Sink(MapSink(encoded)), config)
password := encoded["guestinfo.vice./password"+suffixSeparator+secretSuffix]
assert.NotEmpty(t, password, "encrypted password")
assert.NotEqual(t, password, config.Password, "encrypted password")
for _, expectEq := range []bool{true, false} {
var in SecretKey
var decoded stuff
Decode(in.Source(MapSource(encoded)), &decoded)
if expectEq {
assert.Equal(t, config, decoded, "Encoded and decoded does not match")
} else {
assert.NotEqual(t, config, decoded, "Encoded and decoded should not not match")
}
// second time should fail to decrypt w/o GuestInfoSecretKey
delete(encoded, GuestInfoSecretKey)
}
}

View File

@@ -0,0 +1,56 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extraconfig
import (
"sync"
)
// Store provides combined DataSource and DataSink.
type Store interface {
Get(string) (string, error)
Put(string, string) error
}
type MapStore struct {
mutex sync.Mutex
store map[string]string
}
func New() *MapStore {
return &MapStore{
store: make(map[string]string),
}
}
func (t *MapStore) Get(key string) (string, error) {
t.mutex.Lock()
defer t.mutex.Unlock()
val, ok := t.store[key]
if !ok {
return "", ErrKeyNotFound
}
return val, nil
}
func (t *MapStore) Put(key, value string) error {
t.mutex.Lock()
defer t.mutex.Unlock()
t.store[key] = value
return nil
}

View File

@@ -0,0 +1,55 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package vmomi
import (
"testing"
"reflect"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi/vim25/types"
)
func TestDelta(t *testing.T) {
new := map[string]string{
"hello": "goodbye",
"cruel": "world",
"is": "not",
"enough": "already",
}
existing := []types.BaseOptionValue{
&types.OptionValue{Key: "hello", Value: "goodbye"},
&types.OptionValue{Key: "is", Value: "always"},
&types.OptionValue{Key: "present", Value: "regardless"},
}
updatesSlice := OptionValueUpdatesFromMap(existing, new)
expected := map[string]string{
"enough": "already", // added
"cruel": "world", // added
"is": "not", // changed
}
// turn them back into maps for equality check
updates := OptionValueMap(updatesSlice)
if !assert.True(t, reflect.DeepEqual(expected, updates), "DeepEqual says they do not match") {
t.Fatalf("Expected: %+q \nActual: %+q\n", expected, updates)
}
}

View File

@@ -0,0 +1,140 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package vmomi is in a separate package to avoid the transitive inclusion of govmomi
// as a fundamental dependency of the main extraconfig
package vmomi
import (
"fmt"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
)
// OptionValueMap returns a map from array of OptionValues
func OptionValueMap(src []types.BaseOptionValue) map[string]string {
// create the key/value store from the extraconfig slice for lookups
kv := make(map[string]string)
for i := range src {
k := src[i].GetOptionValue().Key
v := src[i].GetOptionValue().Value.(string)
kv[k] = unescapeNil(v)
}
return kv
}
// OptionValueSource is a convenience method to generate a MapSource source from
// and array of OptionValue's
func OptionValueSource(src []types.BaseOptionValue) extraconfig.DataSource {
kv := OptionValueMap(src)
return extraconfig.MapSource(kv)
}
// OptionValueFromMap is a convenience method to convert a map into a BaseOptionValue array
// escapeNil - if true a nil string is replaced with "<nil>". Allows us to distinguish between
// deletion and nil as a value
func OptionValueFromMap(data map[string]string, escape bool) []types.BaseOptionValue {
if len(data) == 0 {
return nil
}
array := make([]types.BaseOptionValue, len(data))
i := 0
for k, v := range data {
if escape {
v = escapeNil(v)
}
array[i] = &types.OptionValue{Key: k, Value: v}
i++
}
return array
}
// OptionValueArrayToString translates the options array in to a Go formatted structure dump
func OptionValueArrayToString(options []types.BaseOptionValue) string {
// create the key/value store from the extraconfig slice for lookups
kv := make(map[string]string)
for i := range options {
k := options[i].GetOptionValue().Key
v := options[i].GetOptionValue().Value.(string)
kv[k] = v
}
return fmt.Sprintf("%#v", kv)
}
// OptionValueUpdatesFromMap generates an optionValue array for those entries in the map that do not
// already exist, are changed from the reference array, or a removed
// A removed entry will have a nil string for the value
// NOTE: DOES NOT CURRENTLY SUPPORT DELETION OF KEYS - KEYS MISSING FROM NEW MAP ARE IGNORED
func OptionValueUpdatesFromMap(existing []types.BaseOptionValue, new map[string]string) []types.BaseOptionValue {
e := len(existing)
if e == 0 {
return OptionValueFromMap(new, true)
}
n := len(new)
updates := make(map[string]string, n+e)
unchanged := make(map[string]struct{}, n+e)
// first the existing keys
for i := range existing {
v := existing[i].GetOptionValue()
if nV, ok := new[v.Key]; ok && nV == v.Value.(string) {
unchanged[v.Key] = struct{}{}
// no change
continue
} else if ok {
// changed
updates[v.Key] = escapeNil(nV)
} else {
// deletion
// NOTE: ignored as this also deletes non VIC entries currently
// there's no prefix for the non-guestinfo keys so cannot easily filter
// updates[v.Key] = ""
}
}
// now the new keys
for k, v := range new {
if _, ok := unchanged[k]; ok {
continue
}
if _, ok := updates[k]; !ok {
updates[k] = escapeNil(v)
}
}
return OptionValueFromMap(updates, false)
}
func escapeNil(input string) string {
if input == "" {
return "<nil>"
}
return input
}
func unescapeNil(input string) string {
if input == "<nil>" {
return ""
}
return input
}

View File

@@ -0,0 +1,75 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package guest
import (
"fmt"
"io/ioutil"
"strings"
"context"
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/vm"
)
const (
UUIDPath = "/sys/class/dmi/id/product_serial"
UUIDPrefix = "VMware-"
)
// UUID gets the BIOS UUID via the sys interface. This UUID is known by vphsere
func UUID() (string, error) {
id, err := ioutil.ReadFile(UUIDPath)
if err != nil {
return "", fmt.Errorf("error retrieving vm uuid: %s", err)
}
uuidstr := string(id[:])
// check the uuid starts with "VMware-"
if !strings.HasPrefix(uuidstr, UUIDPrefix) {
return "", fmt.Errorf("cannot find this VM's UUID")
}
// Strip the prefix, white spaces, and the trailing '\n'
uuidstr = strings.Replace(uuidstr[len(UUIDPrefix):(len(uuidstr)-1)], " ", "", -1)
// need to add dashes, e.g. "564d395e-d807-e18a-cb25-b79f65eb2b9f"
uuidstr = fmt.Sprintf("%s-%s-%s-%s", uuidstr[0:8], uuidstr[8:12], uuidstr[12:21], uuidstr[21:])
return uuidstr, nil
}
// GetSelf gets VirtualMachine reference for the VM this process is running on
func GetSelf(ctx context.Context, s *session.Session) (*vm.VirtualMachine, error) {
u, err := UUID()
if err != nil {
return nil, err
}
search := object.NewSearchIndex(s.Vim25())
ref, err := search.FindByUuid(ctx, s.Datacenter, u, true, nil)
if err != nil {
return nil, err
}
if ref == nil {
return nil, fmt.Errorf("can't find the hosting vm")
}
return vm.NewVirtualMachine(ctx, s, ref.Reference()), nil
}

View File

@@ -0,0 +1,37 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !linux
package guest
import (
"fmt"
"runtime"
"context"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/vm"
)
// UUID gets the BIOS UUID via the sys interface. This UUID is known by vphsere
func UUID() (string, error) {
return "", fmt.Errorf("unimplemented on %s", runtime.GOOS)
}
// GetSelf gets VirtualMachine reference for the VM this process is running on
func GetSelf(ctx context.Context, s *session.Session) (*vm.VirtualMachine, error) {
return nil, fmt.Errorf("unimplemented on %s", runtime.GOOS)
}

View File

@@ -0,0 +1,49 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package guest
import (
"os/user"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/vmw-guestinfo/vmcheck"
)
func TestUUID(t *testing.T) {
if isVM, err := vmcheck.IsVirtualWorld(); !isVM || err != nil {
t.Skip("can get uuid if not running on a vm")
}
// need to be root and on esx to run this test
u, err := user.Current()
if !assert.NoError(t, err) {
return
}
if u.Uid != "0" {
t.SkipNow()
return
}
s, err := UUID()
if !assert.NoError(t, err) {
return
}
if !assert.NotNil(t, s) {
return
}
}

View File

@@ -0,0 +1,58 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package optmanager provides govmomi helpers for the OptionManager.
package optmanager
import (
"context"
"fmt"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/vsphere/session"
)
// QueryOptionValue uses the session client and OptionManager to look up the input option
// and return the received value.
func QueryOptionValue(ctx context.Context, s *session.Session, option string) (string, error) {
client := s.Vim25()
optMgr := object.NewOptionManager(client, *client.ServiceContent.Setting)
opts, err := optMgr.Query(ctx, option)
if err != nil {
return "", fmt.Errorf("error querying option %q: %s", option, err)
}
if len(opts) == 1 {
return fmt.Sprintf("%v", opts[0].GetOptionValue().Value), nil
}
return "", fmt.Errorf("%d values querying option %q", len(opts), option)
}
// UpdateOptionValue uses the session client and OptionManager to set the input option
func UpdateOptionValue(ctx context.Context, s *session.Session, option string, value string) error {
client := s.Vim25()
optMgr := object.NewOptionManager(client, *client.ServiceContent.Setting)
var opts []types.BaseOptionValue
opts = append(opts, &types.OptionValue{
Key: option,
Value: value,
})
err := optMgr.Update(ctx, opts)
if err != nil {
return fmt.Errorf("error setting option %q: %s", option, err)
}
return nil
}

View File

@@ -0,0 +1,65 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package optmanager
import (
"context"
"testing"
"github.com/vmware/govmomi/simulator"
"github.com/vmware/vic/pkg/vsphere/test"
)
func TestQueryOptionValue(t *testing.T) {
ctx := context.Background()
model := simulator.VPX()
defer model.Remove()
err := model.Create()
if err != nil {
t.Fatal(err)
}
server := model.Service.NewServer()
defer server.Close()
s, err := test.SessionWithVPX(ctx, server.URL.String())
if err != nil {
t.Fatal(err)
}
// Multiple value error
optValue, err := QueryOptionValue(ctx, s, "")
if err == nil {
t.Fatal("expected multiple value error")
}
// Invalid option
optValue, err = QueryOptionValue(ctx, s, "foo-bar")
if err == nil {
t.Fatal("expected invalid query error")
}
// Valid option
adminOptKey := "config.vpxd.sso.default.admin"
adminOptVal := "Administrator@vsphere.local"
optValue, err = QueryOptionValue(ctx, s, "config.vpxd.sso.default.admin")
if err != nil {
t.Fatalf("expected nil error, got: %s", err)
}
if optValue != adminOptVal {
t.Fatalf("expected value %s for query %q, got: %s", adminOptVal, adminOptKey, optValue)
}
}

View File

@@ -0,0 +1,446 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package performance
import (
"context"
"strconv"
"sync"
"time"
"github.com/vmware/govmomi/performance"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/session"
)
const (
// number of samples per collection
sampleSize = int32(2)
// number of seconds between sample collection
sampleInterval = int32(20)
// vSphere recommomends a maxiumum of 50 entities per query
maxEntityQuery = 50
)
// CPUUsage provides individual CPU metrics
type CPUUsage struct {
// processor id (0,1,2)
ID int
// MhzUsage is the MhZ consumed by a specific processor
MhzUsage int64
}
// CPUMetrics encapsulates available vm CPU metrics
type CPUMetrics struct {
// CPUs are the individual CPU metrics
CPUs []CPUUsage
// Usage is the percentage of total vm CPU usage
Usage float32
}
// MemoryMetrics encapsulates available vm memory metrics
type MemoryMetrics struct {
// Consumed memory of vm in bytes
Consumed int64
// Active memory of vm in bytes
Active int64
// Provisioned memory of vm in bytes
Provisioned int64
}
// NetworkUsage provides detailed network stats
type NetworkUsage struct {
Bytes uint64 // total bytes
Kbps int64 // KiloBytesPerSecond
Packets int64 // total packet count
Errors int64 // NOT CURRENTLY IMPLEMENTED
Dropped int64 // total dropped packet count
}
// Network provides metrics for individual network devices
type Network struct {
Name string
Rx NetworkUsage
Tx NetworkUsage
}
// DiskUsage provides detailed disk stats
type DiskUsage struct {
Bytes uint64 // total bytes for interval
Kbps int64 // KiloBytesPerSecond for interval
Op uint64 // Operation count for interval
Ops int64 // Operations per second
}
// VirtualDisk provides metrics for individual disks
type VirtualDisk struct {
Name string
Write DiskUsage
Read DiskUsage
}
// VMMetrics encapsulates the available metrics
type VMMetrics struct {
CPU CPUMetrics
Memory MemoryMetrics
Networks []Network
Disks []VirtualDisk
SampleTime time.Time
// interval of collection in seconds
Interval int32
}
// VMCollector is the VM metrics collector
type VMCollector struct {
perfMgr *performance.Manager
session *session.Session
timer *time.Ticker
stopper chan struct{}
// subscribers to streaming
mu sync.RWMutex
subs map[types.ManagedObjectReference]*vmSubscription
}
// newVMCollector will instantiate a new collector responsible for
// gathering VM metrics
func NewVMCollector(session *session.Session) *VMCollector {
return &VMCollector{
subs: make(map[types.ManagedObjectReference]*vmSubscription),
perfMgr: performance.NewManager(session.Vim25()),
session: session,
}
}
// Start will begin the collection polling process
func (vmc *VMCollector) Start() {
// create timer with sampleInterval alignment
vmc.timer = time.NewTicker(time.Duration(int64(sampleInterval)) * time.Second)
//create the stopper channel
vmc.stopper = make(chan struct{})
// loop on the channel
go func() {
ctx := context.Background()
collectorOp := trace.NewOperation(ctx, "VM metrics collector")
go vmc.collect(collectorOp)
for {
select {
case <-vmc.timer.C:
// collect metrics for current subscribers
vmc.collect(collectorOp)
case <-vmc.stopper:
// Ticker has been stopped, exit routine
collectorOp.Debugf("VM metrics collector complete")
return
}
}
}()
}
// Stop will stop the collection polling process
func (vmc *VMCollector) Stop() {
vmc.timer.Stop()
close(vmc.stopper)
}
// collect will query vSphere for VM metrics and return to the subscribers
func (vmc *VMCollector) collect(op trace.Operation) {
// gather the chunked morefs
vmReferences := vmc.subscriberReferences(op)
// iterate over the chunked references and sample vSphere
for i := range vmReferences {
// create a new operation so we can effectively log and measure the sample
sop := trace.NewOperation(op.Context, "sample operation")
sop.Debugf("parentOp[%s] sample(%d/%d) with %d morefs", op.ID(), i+1, len(vmReferences), len(vmReferences[i]))
go vmc.sample(sop, vmReferences[i])
}
}
// subscriberReferences will return a two dimensional array of VM managed object references. The
// references are chunked based on the maxEntityQuery limit.
func (vmc *VMCollector) subscriberReferences(op trace.Operation) [][]types.ManagedObjectReference {
var mos []types.ManagedObjectReference
var chunked [][]types.ManagedObjectReference
vmc.mu.Lock()
defer vmc.mu.Unlock()
op.Debugf("begin subscriberReferences: %d", len(vmc.subs))
// populate a two dimensional array chunked by the maxEntityQueryLimit
for mo := range vmc.subs {
mos = append(mos, mo)
if len(mos) == maxEntityQuery {
chunked = append(chunked, mos)
mos = make([]types.ManagedObjectReference, 0, maxEntityQuery)
}
}
// the initial loop will potentially miss the "remainder" morefs
if len(mos) > 0 && len(mos) < maxEntityQuery {
chunked = append(chunked, mos)
}
op.Debugf("end subscriberReferences: %d", len(chunked))
return chunked
}
// sample will query the vSphere performanceManager and publish the gather metrics
func (vmc *VMCollector) sample(op trace.Operation, mos []types.ManagedObjectReference) {
op.Debugf("begin sample for %d morefs", len(mos))
defer op.Debugf("end sample for %d morefs", len(mos))
// vSphere counters we are currently interested in monitoring
counters := []string{"cpu.usagemhz.average", "mem.active.average",
"virtualDisk.write.average", "virtualDisk.read.average",
"virtualDisk.numberReadAveraged.average", "virtualDisk.numberWriteAveraged.average",
"net.bytesRx.average", "net.bytesTx.average",
"net.droppedRx.summation", "net.droppedTx.summation",
"net.packetsRx.summation", "net.packetsTx.summation"}
// create the spec
spec := types.PerfQuerySpec{
Format: string(types.PerfFormatNormal),
MaxSample: sampleSize,
IntervalId: sampleInterval,
}
// retrieve sample based on counter names
sample, err := vmc.perfMgr.SampleByName(op.Context, spec, counters, mos)
if err != nil {
op.Errorf("unable to get metric sample: %s", err)
return
}
// convert to metrics
result, err := vmc.perfMgr.ToMetricSeries(op.Context, sample)
if err != nil {
op.Errorf("unable to convert metric sample to metric series: %s", err)
return
}
// iterate over results, convert to vic metrics and publish to subscribers
for i := range result {
met := result[i]
sub, exists := vmc.subs[met.Entity]
if !exists {
// the subscription is no longer valid go to the next result
continue
}
// convert the sample to a metric and publish
for s := range met.SampleInfo {
metric := &VMMetrics{
CPU: CPUMetrics{
CPUs: []CPUUsage{},
},
Memory: MemoryMetrics{},
SampleTime: met.SampleInfo[s].Timestamp,
Interval: sampleInterval,
}
// the series will have values for each sample
for _, v := range met.Value {
// skip the aggregate metric (empty string) and any negative values
if v.Instance == "" && v.Name != "mem.active.average" || v.Value[s] < 0 {
continue
}
switch v.Name {
case "cpu.usagemhz.average":
// convert cpu instance to int
cpu, err := instanceID(op, sub.ID(), v.Instance)
if err != nil {
// skipping this instance
continue
}
// specific vCPU metric
vcpu := CPUUsage{
ID: cpu,
MhzUsage: v.Value[s],
}
metric.CPU.CPUs = append(metric.CPU.CPUs, vcpu)
case "mem.active.average":
metric.Memory.Active = v.Value[s]
// DISK
case "virtualDisk.read.average":
disk := findDisk(metric, v.Instance)
// perfManager returns kbps -- convert to sum of bytes
sum := summation(v.Value[s]) * 1024
metric.Disks[disk].Read.Bytes = sum
metric.Disks[disk].Read.Kbps = v.Value[s]
case "virtualDisk.write.average":
disk := findDisk(metric, v.Instance)
// perfManager returns kbps -- convert to sum of bytes
sum := summation(v.Value[s]) * 1024
metric.Disks[disk].Write.Bytes = sum
metric.Disks[disk].Write.Kbps = v.Value[s]
case "virtualDisk.numberReadAveraged.average":
disk := findDisk(metric, v.Instance)
// sum of iop read average
metric.Disks[disk].Read.Op = summation(v.Value[s])
metric.Disks[disk].Read.Ops = v.Value[s]
case "virtualDisk.numberWriteAveraged.average":
disk := findDisk(metric, v.Instance)
// sum of iop write average
metric.Disks[disk].Write.Op = summation(v.Value[s])
metric.Disks[disk].Write.Ops = v.Value[s]
// NET
default:
name := sub.DeviceName(v.Instance)
// if we have a name gather the stats
if name != "" {
// get the network to update
net := findNetwork(metric, name)
switch v.Name {
case "net.bytesRx.average":
// perfManager returns kbps -- convert to sum of bytes
sum := summation(v.Value[s]) * 1024
metric.Networks[net].Rx.Bytes = sum
metric.Networks[net].Rx.Kbps = v.Value[s]
case "net.bytesTx.average":
// perfManager returns kbps -- convert to sum of bytes
sum := summation(v.Value[s]) * 1024
metric.Networks[net].Tx.Bytes = sum
metric.Networks[net].Tx.Kbps = v.Value[s]
case "net.droppedRx.summation":
metric.Networks[net].Rx.Dropped = v.Value[s]
case "net.droppedTx.summation":
metric.Networks[net].Tx.Dropped = v.Value[s]
case "net.packetsRx.summation":
metric.Networks[net].Rx.Packets = v.Value[s]
case "net.packetsTx.summation":
metric.Networks[net].Tx.Packets = v.Value[s]
}
}
}
}
sub.Publish(metric)
}
}
}
// Subscribe to a vm metric subscription
func (vmc *VMCollector) Subscribe(op trace.Operation, moref types.ManagedObjectReference, id string) (chan interface{}, error) {
vmc.mu.Lock()
defer vmc.mu.Unlock()
// used at end of func
subscriptionCount := len(vmc.subs)
// do we already have this subscription?
_, exists := vmc.subs[moref]
if !exists {
op.Debugf("Creating new subscription(%s)", id)
sub, err := newVMSubscription(op, vmc.session, moref, id)
if err != nil {
return nil, err
}
vmc.subs[moref] = sub
}
// get a subscriber channel
ch := vmc.subs[moref].Channel()
// This is first subscription so start collection
if subscriptionCount == 0 {
vmc.Start()
}
return ch, nil
}
// Unsubscribe from a vm metric subscription. The subscriber channel will
// be evicted and when no subscribers remain the subscription will be removed.
func (vmc *VMCollector) Unsubscribe(op trace.Operation, moref types.ManagedObjectReference, ch chan interface{}) {
vmc.mu.Lock()
defer vmc.mu.Unlock()
sub, exists := vmc.subs[moref]
if exists {
// remove the communication channel
sub.Evict(ch)
// do we have any subscribers to this subscription
if sub.Publishers() == 0 {
op.Debugf("Deleting metric subscription(%s)", sub.ID())
delete(vmc.subs, moref)
}
op.Debugf("Unsubscribed %s from metrics", sub.ID())
}
// no subscriptions, so stop the collection
if len(vmc.subs) == 0 {
vmc.Stop()
}
}
// instanceID coverts the ID or Key of a metric from a string to int.
func instanceID(op trace.Operation, subscriptionID string, instance string) (int, error) {
converted, err := strconv.Atoi(instance)
if err != nil {
// I don't expect this to ever happen, but if it does log and don't publish
op.Errorf("metrics failed to convert the subscription(%s) device id to an int - value(%#v): %s", subscriptionID, instance, err)
return converted, err
}
return converted, nil
}
// summation returns the product of the average * interval
func summation(avg int64) uint64 {
return uint64(avg) * uint64(sampleInterval)
}
// findDisk will iterate over the Disks and return the DiskUsage ordinal position
func findDisk(metric *VMMetrics, name string) int {
// find by name
for i := range metric.Disks {
if metric.Disks[i].Name == name {
return i
}
}
// Let's create the disk
d := VirtualDisk{
Name: name,
Read: DiskUsage{},
Write: DiskUsage{},
}
metric.Disks = append(metric.Disks, d)
return len(metric.Disks) - 1
}
// findNetwork will iterate over the Networks and return the NetworkUsage ordinal position
func findNetwork(metric *VMMetrics, name string) int {
// find by name
for i := range metric.Networks {
if metric.Networks[i].Name == name {
return i
}
}
// Let's create the disk
net := Network{
Name: name,
Rx: NetworkUsage{},
Tx: NetworkUsage{},
}
metric.Networks = append(metric.Networks, net)
return len(metric.Networks) - 1
}

View File

@@ -0,0 +1,186 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package performance
import (
"fmt"
"strconv"
"time"
"github.com/docker/docker/pkg/pubsub"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/vm"
)
// vmSubscription is a 1:1 relationship to a Virtual Machine and a
// 1:M relationship to subscribers
type vmSubscription struct {
vm *vm.VirtualMachine
id string
pub *pubsub.Publisher
devices object.VirtualDeviceList
deviceInstanceToKey map[string]string
diskNames []string // container's virtualDisk names
networkNames []string // container's network names
}
// DeviceName will return the name associated with the metric instance id
func (sub *vmSubscription) DeviceName(instance string) string {
var name string
// did we previously find this device
if name, exists := sub.deviceInstanceToKey[instance]; exists {
return name
}
// convert instance to key - we are expecting regular failures, so no logging
// or returning of error
key, err := strconv.Atoi(instance)
if err != nil {
// this is not a key, so return an empty string
return name
}
// find the device and get the name
device := sub.devices.FindByKey(int32(key))
if device != nil {
// get the name
name = sub.devices.Name(device)
// populate map
sub.deviceInstanceToKey[instance] = name
}
return name
}
// ID returns the subscription's id
func (sub *vmSubscription) ID() string {
return sub.id
}
// DeviceList retrieves the VMs devices and builds slices of the disk and network
// names
func (sub *vmSubscription) DeviceList(op trace.Operation) error {
list, err := sub.vm.Device(op.Context)
if err != nil {
op.Errorf("vm stats subscription(%s) unable to load devices: %s", sub.ID(), err)
return err
}
// populate slice for disk and network names
for i := range list {
switch list.Type(list[i]) {
case object.DeviceTypeDisk:
// disk names are presented by the performanceManager based on their relationship to the
// controller. So we need to create a disk name that aligns with the performance manager
// naming pattern
disk := list[i].GetVirtualDevice()
switch c := list.FindByKey(disk.ControllerKey).(type) {
case types.BaseVirtualSCSIController:
sub.diskNames = append(sub.diskNames, fmt.Sprintf("%s%d:%d", "scsi", c.GetVirtualSCSIController().BusNumber, *disk.UnitNumber))
}
case object.DeviceTypeEthernet:
sub.networkNames = append(sub.networkNames, list.Name(list[i]))
}
}
sub.devices = list
return nil
}
// Disks returns an initialized slice of the containers VirtualDisks
func (sub *vmSubscription) Disks() []VirtualDisk {
var disks []VirtualDisk
for i := range sub.diskNames {
d := VirtualDisk{
Name: sub.diskNames[i],
Read: DiskUsage{},
Write: DiskUsage{},
}
disks = append(disks, d)
}
return disks
}
// Networks returns an initialized slice of the containers networks
func (sub *vmSubscription) Networks() []Network {
var networks []Network
for i := range sub.networkNames {
n := Network{
Name: sub.networkNames[i],
Rx: NetworkUsage{},
Tx: NetworkUsage{},
}
networks = append(networks, n)
}
return networks
}
// Publish sends the metric to all channels subscribed
func (sub *vmSubscription) Publish(metric *VMMetrics) {
// if no disk / network reported then add the defaults
if len(metric.Disks) == 0 {
metric.Disks = sub.Disks()
}
if len(metric.Networks) == 0 {
metric.Networks = sub.Networks()
}
sub.pub.Publish(metric)
}
// Publishers returns the number of channels subscribed to the container
func (sub *vmSubscription) Publishers() int {
return sub.pub.Len()
}
// Channel provides the communication chan for metrics subscriptions
func (sub *vmSubscription) Channel() chan interface{} {
return sub.pub.Subscribe()
}
// Evict will remove the channel from the publisher ending the subscription
func (sub *vmSubscription) Evict(ch chan interface{}) {
sub.pub.Evict(ch)
}
// newVMSubscription is a helper func to convert the interface to a subscription
func newVMSubscription(op trace.Operation, session *session.Session, moref types.ManagedObjectReference, id string) (*vmSubscription, error) {
// ensure we have a valid moRef..we won't worry about inspecting the details
if moref.String() == "" {
err := fmt.Errorf("no vm associated with new stats subscription: %s", id)
op.Errorf("%s", err)
return nil, err
}
sub := &vmSubscription{
vm: vm.NewVirtualMachine(op.Context, session, moref),
deviceInstanceToKey: make(map[string]string),
}
err := sub.DeviceList(op)
if err != nil {
return nil, err
}
// create the publisher
sub.pub = pubsub.NewPublisher(100*time.Millisecond, 0)
return sub, nil
}

423
vendor/github.com/vmware/vic/pkg/vsphere/rbac/rbac.go generated vendored Normal file
View File

@@ -0,0 +1,423 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"context"
"fmt"
"reflect"
"strings"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/methods"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/trace"
)
const (
VCenter = iota
DatacenterReadOnly
Datacenter
Cluster
DatastoreFolder
Datastore
VSANDatastore
Network
Endpoint
)
type NameToRef map[string]types.ManagedObjectReference
type AuthzManager struct {
authzManager *object.AuthorizationManager
client *vim25.Client
resources map[int8]*Resource
TargetRoles []types.AuthorizationRole
RolePrefix string
Principal string
Config *Config
}
type Resource struct {
Type int8
Propagate bool
Role types.AuthorizationRole
}
type Config struct {
Resources []Resource
}
type PermissionList []types.Permission
type ResourcePermission struct {
RType int8
Reference types.ManagedObjectReference
Permission types.Permission
}
func NewAuthzManager(ctx context.Context, client *vim25.Client) *AuthzManager {
authManager := object.NewAuthorizationManager(client)
mgr := &AuthzManager{
client: client,
authzManager: authManager,
}
return mgr
}
func (am *AuthzManager) InitConfig(principal string, rolePrefix string, config *Config) {
am.Principal = principal
am.RolePrefix = rolePrefix
am.Config = config
am.initTargetRoles()
am.initResourceMap()
}
func (am *AuthzManager) CreateRoles(ctx context.Context) (int, error) {
return am.createOrRepairRoles(ctx)
}
func (am *AuthzManager) DeleteRoles(ctx context.Context) (int, error) {
return am.deleteRoles(ctx)
}
func (am *AuthzManager) RoleList(ctx context.Context) (object.AuthorizationRoleList, error) {
return am.getRoleList(ctx)
}
func (am *AuthzManager) IsPrincipalAnAdministrator(ctx context.Context) (bool, error) {
// Check if the principal belongs to the Administrators group
res, err := am.PrincipalBelongsToGroup(ctx, "Administrators")
if err != nil {
return false, err
}
if res {
return res, nil
}
// Check if the principal has an Admin Role
res, err = am.PrincipalHasRole(ctx, "Admin")
if err != nil {
return false, err
}
return res, nil
}
func (am *AuthzManager) PrincipalBelongsToGroup(ctx context.Context, group string) (bool, error) {
op := trace.FromContext(ctx, "PrincipalBelongsToGroup")
ref := *am.client.ServiceContent.UserDirectory
components := strings.Split(am.Principal, "@")
var domain string
name := components[0]
if len(components) < 2 {
domain = ""
} else {
domain = components[1]
}
req := types.RetrieveUserGroups{
This: ref,
Domain: domain,
SearchStr: name,
ExactMatch: true,
BelongsToGroup: group,
FindUsers: true,
FindGroups: false,
}
results, err := methods.RetrieveUserGroups(ctx, am.client, &req)
// This is to work around a bug in vSphere, when AD is added to
// the identity source list, the API returns Object Not Found,
// In this case, we ignore the error and return false (BUG: 2037706)
if err != nil && (isNotSupportedError(ctx, err) || isNotFoundError(ctx, err)) {
op.Debugf("Received Error (%s) from PrincipalBelongsToGroup(), could not verify user %s is not a member of the Administrators group", err.Error(), am.Principal)
op.Warnf("If ops-user (%s) belongs to the Administrators group, permissions on some resources might have been restricted", am.Principal)
return false, nil
}
if err != nil {
op.Debugf("Error from PrincipalBelongsToGroup: %s", err.Error())
return false, err
}
if len(results.Returnval) > 0 {
return true, nil
}
return false, nil
}
func (am *AuthzManager) PrincipalHasRole(ctx context.Context, roleName string) (bool, error) {
// Build expected representation of the ops-user
principal := strings.ToLower(am.Principal)
// Get role id for admin Role
roleList, err := am.RoleList(ctx)
if err != nil {
return false, err
}
role := roleList.ByName(roleName)
allPerms, err := am.authzManager.RetrieveAllPermissions(ctx)
if err != nil {
return false, err
}
for _, perm := range allPerms {
if perm.RoleId != role.RoleId {
continue
}
fPrincipal := am.formatPrincipal(perm.Principal)
if fPrincipal == principal {
return true, nil
}
}
return false, nil
}
func (am *AuthzManager) GetPermissions(ctx context.Context,
ref types.ManagedObjectReference) ([]types.Permission, error) {
// Get current Permissions
return am.authzManager.RetrieveEntityPermissions(ctx, ref, false)
}
func (am *AuthzManager) AddPermission(ctx context.Context, ref types.ManagedObjectReference, resourceType int8, isGroup bool) (*ResourcePermission, error) {
resource := am.getResource(resourceType)
if resource == nil {
return nil, fmt.Errorf("cannot find resource of type %d", resourceType)
}
// Collect the new roles, possibly cache the result in the Authz manager
roleList, err := am.getRoleList(ctx)
if err != nil {
return nil, err
}
// Locate target role
role := roleList.ByName(am.getRoleName(resource))
if role == nil {
return nil, fmt.Errorf("cannot find role: %s", resource.Role.Name)
}
// Get current Permissions
permissions, err := am.authzManager.RetrieveEntityPermissions(ctx, ref, false)
if err != nil {
return nil, err
}
for _, permission := range permissions {
if permission.Principal == am.Principal &&
permission.RoleId == role.RoleId &&
permission.Propagate == resource.Propagate {
return nil, nil
}
}
// No match found, create new permission
permission := types.Permission{
Principal: am.Principal,
RoleId: role.RoleId,
Propagate: resource.Propagate,
Group: isGroup,
}
permissions = append(permissions, permission)
if err = am.authzManager.SetEntityPermissions(ctx, ref, permissions); err != nil {
return nil, err
}
resourcePermission := &ResourcePermission{
Permission: permission,
Reference: ref,
RType: resourceType,
}
return resourcePermission, nil
}
func (am *AuthzManager) createOrRepairRoles(ctx context.Context) (int, error) {
// Get all the existing roles
mgr := am.authzManager
roleList, err := mgr.RoleList(ctx)
if err != nil {
return 0, err
}
var count int
for _, targetRole := range am.TargetRoles {
foundRole := roleList.ByName(targetRole.Name)
if foundRole != nil {
isMod, err := am.checkAndRepairRole(ctx, &targetRole, foundRole)
if isMod && err == nil {
count++
}
} else {
_, err = mgr.AddRole(ctx, targetRole.Name, targetRole.Privilege)
if err == nil {
count++
}
}
if err != nil {
return count, err
}
}
return count, nil
}
func (am *AuthzManager) deleteRoles(ctx context.Context) (int, error) {
mgr := am.authzManager
// Get all the existing roles
roleList, err := mgr.RoleList(ctx)
if err != nil {
return 0, err
}
var count int
for _, targetRole := range am.TargetRoles {
foundRole := roleList.ByName(targetRole.Name)
if foundRole != nil {
err = mgr.RemoveRole(ctx, foundRole.RoleId, true)
if err == nil {
count++
}
}
}
return count, nil
}
func (am *AuthzManager) getRoleList(ctx context.Context) (object.AuthorizationRoleList, error) {
return am.authzManager.RoleList(ctx)
}
func (am *AuthzManager) checkAndRepairRole(ctx context.Context, tRole *types.AuthorizationRole, fRole *types.AuthorizationRole) (bool, error) {
mgr := am.authzManager
// Check that the privileges list in Target Role is a subset of the list in Found role
fSet := make(map[string]bool)
for _, p := range fRole.Privilege {
fSet[p] = true
}
var isModified bool
for _, p := range tRole.Privilege {
if _, found := fSet[p]; !found {
// Privilege not found
// Add it to the found Role
fRole.Privilege = append(fRole.Privilege, p)
isModified = true
}
}
if !isModified {
return false, nil
}
// Not a subset need to call go-vmomi to set the new privileges
err := mgr.UpdateRole(ctx, fRole.RoleId, fRole.Name, fRole.Privilege)
return true, err
}
func (am *AuthzManager) initTargetRoles() {
count := len(am.Config.Resources)
roles := make([]types.AuthorizationRole, 0, count)
dSet := make(map[string]bool)
for index, resource := range am.Config.Resources {
name := am.getRoleName(&am.Config.Resources[index])
// Discard duplicates
if _, found := dSet[name]; !found {
role := new(types.AuthorizationRole)
*role = resource.Role
role.Name = name
dSet[name] = true
roles = append(roles, *role)
}
}
am.TargetRoles = roles
}
func (am *AuthzManager) initResourceMap() {
am.resources = make(map[int8]*Resource)
for i, resource := range am.Config.Resources {
am.resources[resource.Type] = &am.Config.Resources[i]
}
}
func (am *AuthzManager) getResource(resourceType int8) *Resource {
resource, ok := am.resources[resourceType]
if !ok {
panic(errors.Errorf("Cannot find RBAC resource type: %d", resourceType))
}
return resource
}
func (am *AuthzManager) formatPrincipal(principal string) string {
components := strings.Split(principal, "\\")
if len(components) != 2 {
return strings.ToLower(principal)
}
ret := strings.ToLower(components[1]) + "@" + strings.ToLower(components[0])
return ret
}
func (am *AuthzManager) getRoleName(resource *Resource) string {
switch resource.Type {
case DatacenterReadOnly:
return resource.Role.Name
default:
return am.RolePrefix + resource.Role.Name
}
}
func isNotSupportedError(ctx context.Context, err error) bool {
op := trace.FromContext(ctx, "isNotSupportedError")
if soap.IsSoapFault(err) {
vimFault := soap.ToSoapFault(err).VimFault()
op.Debugf("Error type: %s", reflect.TypeOf(vimFault))
_, ok := soap.ToSoapFault(err).VimFault().(types.NotSupported)
return ok
}
return false
}
func isNotFoundError(ctx context.Context, err error) bool {
op := trace.FromContext(ctx, "isNotFoundError")
if soap.IsSoapFault(err) {
vimFault := soap.ToSoapFault(err).VimFault()
op.Debugf("Error type: %s", reflect.TypeOf(vimFault))
_, ok := soap.ToSoapFault(err).VimFault().(types.NotFound)
return ok
}
return false
}

View File

@@ -0,0 +1,240 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/vmware/govmomi/simulator"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/test/env"
)
var Role1 = types.AuthorizationRole{
Name: "vcenter",
Privilege: []string{
"Datastore.Config",
},
}
var Role2 = types.AuthorizationRole{
Name: "datacenter",
Privilege: []string{
"Datastore.Config",
"Datastore.FileManagement",
"VirtualMachine.Config.AddNewDisk",
"VirtualMachine.Config.AdvancedConfig",
"VirtualMachine.Config.RemoveDisk",
"VirtualMachine.Inventory.Create",
"VirtualMachine.Inventory.Delete",
},
}
var Role3 = types.AuthorizationRole{
Name: "cluster",
Privilege: []string{
"Datastore.AllocateSpace",
"Datastore.Browse",
"Datastore.Config",
"Datastore.DeleteFile",
"Datastore.FileManagement",
"Host.Config.SystemManagement",
},
}
// Configuration for the ops-user
var testRBACConfig = Config{
Resources: []Resource{
{
Type: VCenter,
Propagate: false,
Role: Role1,
},
{
Type: Datacenter,
Propagate: true,
Role: Role2,
},
{
Type: Cluster,
Propagate: true,
Role: Role3,
},
},
}
var testRolePrefix = "test-role-prefix"
var testUser = "test-user"
func TestRolesSimulatorVPX(t *testing.T) {
ctx := context.Background()
m := simulator.VPX()
defer m.Remove()
err := m.Create()
require.NoError(t, err, "Cannot create VPX Simulator")
s := m.Service.NewServer()
defer s.Close()
config := &session.Config{
Service: s.URL.String(),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
}
sess, err := session.NewSession(config).Connect(ctx)
require.NoError(t, err, "Cannot connect to VPX Simulator")
am := NewAuthzManager(ctx, sess.Vim25())
am.InitConfig(testUser, testRolePrefix, &testRBACConfig)
var testRoleNames = []string{
"datacenter",
"cluster",
}
var testRolePrivileges = []string{
"VirtualMachine.Config.AddNewDisk",
"Host.Config.SystemManagement",
}
DoTestRoles(ctx, t, am, testRoleNames, testRolePrivileges)
}
func TestRolesVCenter(t *testing.T) {
ctx := context.Background()
config := &session.Config{
Service: env.URL(t),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
}
sess, err := session.NewSession(config).Connect(ctx)
if err != nil {
t.SkipNow()
}
am := NewAuthzManager(ctx, sess.Vim25())
am.InitConfig(testUser, testRolePrefix, &testRBACConfig)
var testRoleNames = []string{
"datacenter",
"cluster",
}
var testRolePrivileges = []string{
"VirtualMachine.Config.AddNewDisk",
"Host.Config.SystemManagement",
}
DoTestRoles(ctx, t, am, testRoleNames, testRolePrivileges)
}
func TestAdminSimulatorVPX(t *testing.T) {
ctx := context.Background()
m := simulator.VPX()
defer m.Remove()
err := m.Create()
require.NoError(t, err, "Cannot create VPX Simulator")
s := m.Service.NewServer()
defer s.Close()
config := &session.Config{
Service: s.URL.String(),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
}
sess, err := session.NewSession(config).Connect(ctx)
require.NoError(t, err, "Cannot connect to VPX Simulator")
am := NewAuthzManager(ctx, sess.Vim25())
am.InitConfig("admin", "test-role-prefix", &testRBACConfig)
// Unfortunately the Sim does not have support for looking up group membership
// therefore we can only test the presence of the Admin role
res, err := am.PrincipalHasRole(ctx, "Admin")
require.NoError(t, err, "Failed to verify Admin Privileges")
require.True(t, res, "User Administrator@vsphere.local should have an Admin role")
// Negative test, principal does not have that role
res, err = am.PrincipalHasRole(ctx, "NoAccess")
require.NoError(t, err, "Failed to verify Admin Privileges")
require.False(t, res, "User Administrator@vsphere.local should have an NoAccess role")
// Check regular user
am.Principal = "nouser@vshpere.local"
res, err = am.PrincipalHasRole(ctx, "Admin")
require.NoError(t, err, "Failed to verify Admin Privileges")
require.False(t, res, "User nouser@vsphere.local should not have an Admin role")
}
func TestAdminVCenter(t *testing.T) {
ctx := context.Background()
config := &session.Config{
Service: env.URL(t),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
}
sess, err := session.NewSession(config).Connect(ctx)
if err != nil {
t.SkipNow()
}
am := NewAuthzManager(ctx, sess.Vim25())
am.InitConfig("Administrator@vsphere.local", "test-role-prefix", &testRBACConfig)
res, err := am.PrincipalBelongsToGroup(ctx, "Administrators")
require.NoError(t, err, "Failed to verify Admin Privileges")
require.True(t, res, "User Administrator@vsphere.local should be a member of Administrators")
res, err = am.PrincipalHasRole(ctx, "Admin")
require.NoError(t, err, "Failed to verify Admin Privileges")
require.True(t, res, "User Administrator@vsphere.local should have an Admin role")
// Negative test, principal does not belong
res, err = am.PrincipalBelongsToGroup(ctx, "TestUsers")
require.NoError(t, err, "Failed to verify Admin Privileges")
require.False(t, res, "User Administrator@vsphere.local should not be a member of TestUsers")
// Negative test, principal does not have that role
res, err = am.PrincipalHasRole(ctx, "NoAccess")
require.NoError(t, err, "Failed to verify Admin Privileges")
require.False(t, res, "User Administrator@vsphere.local should have an NoAccess role")
// Check regular user
am.Principal = "nouser@vshpere.local"
res, err = am.PrincipalHasRole(ctx, "Admin")
require.NoError(t, err, "Failed to verify Admin Privileges")
require.False(t, res, "User nouser@vsphere.local should not have an Admin role")
// Check regular user
am.Principal = "nouser"
res, err = am.PrincipalHasRole(ctx, "Admin")
require.NoError(t, err, "Failed to verify Admin Privileges")
require.False(t, res, "User nouser@vsphere.local should not have an Admin role")
}

View File

@@ -0,0 +1,120 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rbac
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/vmware/govmomi/vim25/types"
)
func DoTestRoles(ctx context.Context, t *testing.T, am *AuthzManager, testRoleNames []string, testRolePrivileges []string) {
var roleCount = len(am.TargetRoles)
count := InitRoles(ctx, t, am)
defer Cleanup(ctx, t, am, true)
require.Equal(t, roleCount, count, "Incorrect number of roles: expected %d, actual %d", roleCount, count)
// Test correct role validation, it should return 0
roleCount = 0
count, err := am.createOrRepairRoles(ctx)
require.NoError(t, err, "Failed to create roles")
require.Equal(t, roleCount, count, "Incorrect number of roles: expected %d, actual %d", roleCount, count)
// Remove two Privileges from two roles
roles, err := am.getRoleList(ctx)
fmt.Println(err)
fmt.Println(roles)
for i, name := range testRoleNames {
testRoleNames[i] = am.RolePrefix + name
}
for _, role := range roles {
if role.Name == testRoleNames[0] {
removePrivilege(&role, testRolePrivileges[0])
am.authzManager.UpdateRole(ctx, role.RoleId, role.Name, role.Privilege)
}
if role.Name == testRoleNames[1] {
removePrivilege(&role, testRolePrivileges[1])
am.authzManager.UpdateRole(ctx, role.RoleId, role.Name, role.Privilege)
}
}
// Test
roleCount = 2
count, err = am.createOrRepairRoles(ctx)
require.NoError(t, err, "Failed to repair roles 1")
require.Equal(t, roleCount, count, "Incorrect number of roles: expected %d, actual %d", roleCount, count)
// Test correct role validation, it should return 0
roleCount = 0
count, err = am.createOrRepairRoles(ctx)
require.NoError(t, err, "Failed to repair roles 2")
require.Equal(t, roleCount, count, "Incorrect number of roles: expected %d, actual %d", roleCount, count)
}
func VerifyResourcePermissions(ctx context.Context, t *testing.T, am *AuthzManager, retPerms []ResourcePermission) {
for _, retPerm := range retPerms {
// Validate returned permission against the configured permission
configPerm := am.getResource(retPerm.RType)
require.Equal(t, am.Principal, retPerm.Permission.Principal)
require.Equal(t, configPerm.Propagate, retPerm.Permission.Propagate)
actPerms, err := am.GetPermissions(ctx, retPerm.Reference)
require.NoError(t, err)
for _, actPerm := range actPerms {
if actPerm.Principal != am.Principal {
continue
}
// RoleId must be the same
require.Equal(t, retPerm.Permission.RoleId, actPerm.RoleId)
}
}
}
func InitRoles(ctx context.Context, t *testing.T, am *AuthzManager) int {
Cleanup(ctx, t, am, false)
count, err := am.createOrRepairRoles(ctx)
require.NoError(t, err, "Failed to initialize Roles")
return count
}
func Cleanup(ctx context.Context, t *testing.T, am *AuthzManager, checkCount bool) {
var roleCount = len(am.TargetRoles)
count, err := am.deleteRoles(ctx)
require.NoError(t, err, "Failed to delete roles")
if checkCount && count != roleCount {
t.Fatalf("Incorrect number of roles: expcted %d, actual %d", roleCount, count)
}
}
func removePrivilege(role *types.AuthorizationRole, privilege string) {
for i, priv := range role.Privilege {
if priv == privilege {
role.Privilege = append(role.Privilege[:i], role.Privilege[i+1:]...)
return
}
}
}

View File

@@ -0,0 +1,47 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package session
import "fmt"
// SDKURLError is returned when the soap SDK URL cannot be parsed
type SDKURLError struct {
Service string
Err error
}
func (e SDKURLError) Error() string {
return fmt.Sprintf("SDK URL (%s) could not be parsed: %s", e.Service, e.Err)
}
// SoapClientError is returned when we're unable to obtain a vim client
type SoapClientError struct {
Host string
Err error
}
func (e SoapClientError) Error() string {
return fmt.Sprintf("Failed to connect to %s: %s", e.Host, e.Err)
}
// UserPassLoginError is returned when login via username/password is unsuccessful
type UserPassLoginError struct {
Host string
Err error
}
func (e UserPassLoginError) Error() string {
return fmt.Sprintf("Failed to log in to %s: %s", e.Host, e.Err)
}

View File

@@ -0,0 +1,397 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package session caches vSphere objects to avoid having to repeatedly
// make govmomi client calls.
//
// To obtain a Session, call Create with a Config. The config
// contains the SDK URL (Service) and the desired vSphere resources.
// Create then connects to Service and stores govmomi objects for
// each corresponding value in Config. The Session is returned and
// the user can use the cached govmomi objects in the exported fields of
// Session instead of directly using a govmomi Client.
//
package session
import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/session"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/methods"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/config"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/extraconfig"
)
const (
defaultMaxInFlight = 32
tlsHandshakeTimeout = 30 * time.Second
)
// Config contains the configuration used to create a Session.
type Config struct {
// SDK URL or proxy
Service string
// Credentials
User *url.Userinfo
// CloneTicket is used to clone an existing session
CloneTicket string
// Allow insecure connection to Service
Insecure bool
// Target thumbprint
Thumbprint string
// Keep alive duration
Keepalive time.Duration
// User-Agent to identify login sessions (see: govc session.ls)
UserAgent string
ClusterPath string
DatacenterPath string
DatastorePath string
HostPath string
PoolPath string
}
// Session caches vSphere objects obtained by querying the SDK.
type Session struct {
*govmomi.Client
*Config
Cluster *object.ComputeResource
Datacenter *object.Datacenter
Datastore *object.Datastore
Host *object.HostSystem
Pool *object.ResourcePool
VMFolder *object.Folder
Finder *find.Finder
}
// RoundTripFunc alias
type RoundTripFunc func(*http.Request) (*http.Response, error)
// RoundTrip method
func (rt RoundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
return rt(r)
}
// LimitConcurrency limits how many requests can be processed at once
func LimitConcurrency(rt http.RoundTripper, limit int) http.RoundTripper {
limiter := make(chan struct{}, limit)
return RoundTripFunc(func(r *http.Request) (*http.Response, error) {
// reserve a slot
limiter <- struct{}{}
// free the slot
defer func() {
<-limiter
}()
// use the given round tripper
return rt.RoundTrip(r)
})
}
// NewSession creates a new Session struct. If config is nil,
// it creates a Flags object from the command line arguments or
// environment, and uses that instead to create a Session.
func NewSession(config *Config) *Session {
return &Session{Config: config}
}
// Vim25 returns the vim25.Client to the caller
func (s *Session) Vim25() *vim25.Client {
return s.Client.Client
}
// IsVC returns whether the session is backed by VC
func (s *Session) IsVC() bool {
return s.Client.IsVC()
}
// IsVSAN returns whether the datastore used in the session is backed by VSAN
func (s *Session) IsVSAN(ctx context.Context) bool {
// #nosec: Errors unhandled.
dsType, _ := s.Datastore.Type(ctx)
return dsType == types.HostFileSystemVolumeFileSystemTypeVsan
}
// Create accepts a Config and returns a Session with the cached vSphere resources.
func (s *Session) Create(ctx context.Context) (*Session, error) {
op := trace.FromContext(ctx, "Create")
var vchConfig config.VirtualContainerHostConfigSpec
var connConfig config.Connection
source, err := extraconfig.GuestInfoSource()
if err != nil {
return nil, err
}
prefix := extraconfig.CalculateKeys(vchConfig, "Connection", "")
if len(prefix) != 1 {
return nil, fmt.Errorf("must be exactly one Connection defined in VCH configuration")
}
extraconfig.DecodeWithPrefix(source, &connConfig, prefix[0])
s.Service = connConfig.Target
s.User = url.UserPassword(connConfig.Username, connConfig.Token)
s.Thumbprint = connConfig.TargetThumbprint
_, err = s.Connect(op)
if err != nil {
return nil, err
}
// we're treating this as an atomic behaviour, so log out if we failed
defer func() {
if err != nil {
// #nosec: Errors unhandled.
s.Client.Logout(op)
}
}()
_, err = s.Populate(op)
if err != nil {
return nil, err
}
return s, nil
}
// Connect establishes the connection for the session but nothing more
func (s *Session) Connect(ctx context.Context) (*Session, error) {
op := trace.FromContext(ctx, "Connect")
op.Debugf("Creating VMOMI session with thumbprint %s", s.Thumbprint)
soapURL, err := soap.ParseURL(s.Service)
if soapURL == nil || err != nil {
return nil, SDKURLError{
Service: s.Service,
Err: err,
}
}
// Update the service URL with expanded defaults
s.Service = soapURL.String()
// VCH components do not include credentials within the target URL
if s.User != nil {
soapURL.User = s.User
}
soapClient := soap.NewClient(soapURL, s.Insecure)
soapClient.Version = "6.0" // Pin to 6.0 until we need 6.5+ specific API
var login func(context.Context) error
login = func(ctx context.Context) error {
return s.Client.Login(ctx, soapURL.User)
}
soapClient.UserAgent = s.UserAgent
soapClient.SetThumbprint(soapURL.Host, s.Thumbprint)
maxInFlight := defaultMaxInFlight
if e := os.Getenv("VIC_MAX_IN_FLIGHT"); e != "" {
if i, err := strconv.Atoi(e); err == nil {
maxInFlight = i
}
}
// Limit the concurrency of SOAP requests
if t, ok := soapClient.Transport.(*http.Transport); ok {
t.MaxIdleConnsPerHost = maxInFlight
t.TLSHandshakeTimeout = tlsHandshakeTimeout
}
soapClient.Transport = LimitConcurrency(soapClient.Transport, maxInFlight)
// TODO: option to set http.Client.Transport.TLSClientConfig.RootCAs
vimClient, err := vim25.NewClient(op, soapClient)
if err != nil {
return nil, SoapClientError{
Host: soapURL.Host,
Err: err,
}
}
if s.Keepalive != 0 {
vimClient.RoundTripper = session.KeepAliveHandler(soapClient, s.Keepalive,
func(roundTripper soap.RoundTripper) error {
cop := trace.FromOperation(op, "KeepAlive")
_, err := methods.GetCurrentTime(cop, roundTripper)
if err == nil {
return nil
}
cop.Warnf("session keepalive error: %s", err)
if isNotAuthenticated(err) {
if err = login(cop); err != nil {
cop.Errorf("session keepalive failed to re-authenticate: %s", err)
} else {
cop.Info("session keepalive re-authenticated")
}
}
return nil
})
}
// TODO: get rid of govmomi.Client usage, only provides a few helpers we don't need.
s.Client = &govmomi.Client{
Client: vimClient,
SessionManager: session.NewManager(vimClient),
}
if s.CloneTicket != "" {
// clone a user session if we have a ticket
err = s.SessionManager.CloneSession(op, s.CloneTicket)
} else {
// otherwise login to create a new one
err = login(op)
}
if err != nil {
return nil, UserPassLoginError{
Host: soapURL.Host,
Err: err,
}
}
s.Finder = find.NewFinder(s.Vim25(), false)
// log high-level environment information
s.logEnvironmentInfo(op)
return s, nil
}
// Populate resolves the set of cached resources that should be presented
// This returns accumulated error detail if there is ambiguity, but sets all
// unambiguous or correct resources.
func (s *Session) Populate(ctx context.Context) (*Session, error) {
op := trace.FromContext(ctx, "Populate")
// Populate s
var errs []string
var err error
finder := s.Finder
op.Debugf("vSphere resource cache populating...")
s.Datacenter, err = finder.DatacenterOrDefault(op, s.DatacenterPath)
if err != nil {
errs = append(errs, fmt.Sprintf("Failure finding dc (%s): %s", s.DatacenterPath, err.Error()))
} else {
finder.SetDatacenter(s.Datacenter)
op.Debugf("Cached dc: %s", s.DatacenterPath)
}
finder.SetDatacenter(s.Datacenter)
s.Cluster, err = finder.ComputeResourceOrDefault(op, s.ClusterPath)
if err != nil {
errs = append(errs, fmt.Sprintf("Failure finding cluster (%s): %s", s.ClusterPath, err.Error()))
} else {
op.Debugf("Cached cluster: %s", s.ClusterPath)
}
s.Datastore, err = finder.DatastoreOrDefault(op, s.DatastorePath)
if err != nil {
errs = append(errs, fmt.Sprintf("Failure finding ds (%s): %s", s.DatastorePath, err.Error()))
} else {
op.Debugf("Cached ds: %s", s.DatastorePath)
}
s.Host, err = finder.HostSystemOrDefault(op, s.HostPath)
if err != nil {
if _, ok := err.(*find.DefaultMultipleFoundError); !ok || !s.IsVC() {
errs = append(errs, fmt.Sprintf("Failure finding host (%s): %s", s.HostPath, err.Error()))
}
} else {
op.Debugf("Cached host: %s", s.HostPath)
}
s.Pool, err = finder.ResourcePoolOrDefault(op, s.PoolPath)
if err != nil {
errs = append(errs, fmt.Sprintf("Failure finding pool (%s): %s", s.PoolPath, err.Error()))
} else {
op.Debugf("Cached pool: %s", s.PoolPath)
}
if s.Datacenter != nil {
folders, err := s.Datacenter.Folders(op)
if err != nil {
errs = append(errs, fmt.Sprintf("Failure finding folders (%s): %s", s.DatacenterPath, err.Error()))
} else {
op.Debugf("Cached folders: %s", s.DatacenterPath)
}
s.VMFolder = folders.VmFolder
}
if len(errs) > 0 {
op.Debugf("Error count populating vSphere cache: (%d)", len(errs))
return nil, errors.New(strings.Join(errs, "\n"))
}
op.Debug("vSphere resource cache populated...")
return s, nil
}
func (s *Session) logEnvironmentInfo(op trace.Operation) {
a := s.ServiceContent.About
op.WithFields(logrus.Fields{
"Name": a.Name,
"Vendor": a.Vendor,
"Version": a.Version,
"Build": a.Build,
"OS Type": a.OsType,
"API Type": a.ApiType,
"API Version": a.ApiVersion,
"Product ID": a.ProductLineId,
"UUID": a.InstanceUuid,
}).Debug("Session Environment Info: ")
return
}
func isNotAuthenticated(err error) bool {
if soap.IsSoapFault(err) {
switch soap.ToSoapFault(err).VimFault().(type) {
case types.NotAuthenticated:
return true
}
}
return false
}

View File

@@ -0,0 +1,186 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package session
import (
"context"
"crypto/tls"
"strings"
"testing"
"time"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/simulator"
"github.com/vmware/vic/pkg/vsphere/test/env"
)
func TestSessionDefaults(t *testing.T) {
ctx := context.Background()
config := &Config{
Service: env.URL(t),
Insecure: true,
}
session, err := NewSession(config).Create(ctx)
if err != nil {
eStr := err.Error()
t.Logf("%+v", eStr)
// FIXME: See comments below
if strings.Contains(eStr, "resolves to multiple hosts") {
t.SkipNow()
}
t.Logf("%+v", eStr)
if _, ok := err.(*find.DefaultMultipleFoundError); !ok {
t.Errorf(eStr)
} else {
t.SkipNow()
}
}
if session != nil {
defer session.Logout(ctx)
}
t.Logf("%+v", session)
}
func TestSession(t *testing.T) {
ctx := context.Background()
config := &Config{
Service: env.URL(t),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
DatacenterPath: "",
DatastorePath: "/ha-datacenter/datastore/*",
HostPath: "/ha-datacenter/host/*/*",
PoolPath: "/ha-datacenter/host/*/Resources",
}
session, err := NewSession(config).Create(ctx)
if err != nil {
eStr := err.Error()
t.Logf("%+v", eStr)
// FIXME: session.Create incorporates Populate which loses the type of any original error from vmomi
// In the case where the test is run on a cluster with multiple hosts, find.MultipleFoundError
// gets rolled up into a generic error in Populate. As such, the best we can do is just grep for the string, which is lame
// The test shouldn't fail if it's run on a cluster with multiple hosts. However, it won't test for anything either.
if strings.Contains(eStr, "resolves to multiple hosts") {
t.SkipNow()
}
if _, ok := err.(*find.MultipleFoundError); !ok {
t.Errorf(eStr)
} else {
t.SkipNow()
}
}
if session != nil {
defer session.Logout(ctx)
t.Logf("Session: %+v", session)
t.Logf("IsVC: %t", session.IsVC())
t.Logf("IsVSAN: %t", session.IsVSAN(ctx))
}
}
func TestFolder(t *testing.T) {
ctx := context.Background()
config := &Config{
Service: env.URL(t),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
DatacenterPath: "",
DatastorePath: "/ha-datacenter/datastore/*",
HostPath: "/ha-datacenter/host/*/*",
PoolPath: "/ha-datacenter/host/*/Resources",
}
session, err := NewSession(config).Create(ctx)
if err != nil {
eStr := err.Error()
t.Logf("%+v", eStr)
// FIXME: See comments above
if strings.Contains(eStr, "resolves to multiple hosts") {
t.SkipNow()
}
if _, ok := err.(*find.MultipleFoundError); !ok {
t.Errorf(eStr)
} else {
t.SkipNow()
}
}
if session != nil {
defer session.Logout(ctx)
if session.VMFolder == nil {
t.Errorf("Get empty folder")
}
}
}
func TestConnect(t *testing.T) {
ctx := context.Background()
for _, model := range []*simulator.Model{simulator.ESX(), simulator.VPX()} {
defer model.Remove()
err := model.Create()
if err != nil {
t.Fatal(err)
}
model.Service.TLS = new(tls.Config)
s := model.Service.NewServer()
defer s.Close()
config := &Config{
Keepalive: time.Minute,
Service: s.URL.String(),
}
for _, thumbprint := range []string{"", s.CertificateInfo().ThumbprintSHA1} {
u := *s.URL
config.Service = u.String()
config.Thumbprint = thumbprint
_, err = NewSession(config).Connect(ctx)
if thumbprint == "" {
if err == nil {
t.Error("expected x509.UnknownAuthorityError error")
}
} else {
if err != nil {
t.Error(err)
}
}
u.User = nil
config.Service = u.String()
_, err = NewSession(config).Connect(ctx)
if err == nil {
t.Fatal("expected login error")
}
config.Service = ""
_, err = NewSession(config).Connect(ctx)
if err == nil {
t.Fatal("expected URL parse error")
}
}
}
}

View File

@@ -0,0 +1,49 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sys
import (
"fmt"
"io/ioutil"
"strings"
)
const (
UUIDPath = "/sys/class/dmi/id/product_serial"
UUIDPrefix = "VMware-"
)
// UUID gets the BIOS UUID via the sys interface. This UUID is known by vphsere
func UUID() (string, error) {
id, err := ioutil.ReadFile(UUIDPath)
if err != nil {
return "", fmt.Errorf("error retrieving vm uuid: %s", err)
}
uuidstr := string(id[:])
// check the uuid starts with "VMware-"
if !strings.HasPrefix(uuidstr, UUIDPrefix) {
return "", fmt.Errorf("cannot find this VM's UUID")
}
// Strip the prefix, white spaces, and the trailing '\n'
uuidstr = strings.Replace(uuidstr[len(UUIDPrefix):(len(uuidstr)-1)], " ", "", -1)
// need to add dashes, e.g. "564d395e-d807-e18a-cb25-b79f65eb2b9f"
uuidstr = fmt.Sprintf("%s-%s-%s-%s", uuidstr[0:8], uuidstr[8:12], uuidstr[12:21], uuidstr[21:])
return uuidstr, nil
}

View File

@@ -0,0 +1,24 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !linux
package sys
import "fmt"
// UUID gets the BIOS UUID via the sys interface. This UUID is known by vphsere
func UUID() (string, error) {
return "", fmt.Errorf("not implemented")
}

View File

@@ -0,0 +1,212 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tags
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/pkg/errors"
)
const (
CategoryURL = "/com/vmware/cis/tagging/category"
ErrAlreadyExists = "already_exists"
)
type CategoryCreateSpec struct {
CreateSpec CategoryCreate `json:"create_spec"`
}
type CategoryUpdateSpec struct {
UpdateSpec CategoryUpdate `json:"update_spec"`
}
type CategoryCreate struct {
AssociableTypes []string `json:"associable_types"`
Cardinality string `json:"cardinality"`
Description string `json:"description"`
Name string `json:"name"`
}
type CategoryUpdate struct {
AssociableTypes []string `json:"associable_types"`
Cardinality string `json:"cardinality"`
Description string `json:"description"`
Name string `json:"name"`
}
type Category struct {
ID string `json:"id"`
Description string `json:"description"`
Name string `json:"name"`
Cardinality string `json:"cardinality"`
AssociableTypes []string `json:"associable_types"`
UsedBy []string `json:"used_by"`
}
func (c *RestClient) CreateCategoryIfNotExist(ctx context.Context, name string, description string, categoryType string, multiValue bool) (*string, error) {
categories, err := c.GetCategoriesByName(ctx, name)
if err != nil {
return nil, errors.Wrapf(err, "failed to query category for %s", name)
}
if categories == nil {
var multiValueStr string
if multiValue {
multiValueStr = "MULTIPLE"
} else {
multiValueStr = "SINGLE"
}
categoryCreate := CategoryCreate{[]string{categoryType}, multiValueStr, description, name}
spec := CategoryCreateSpec{categoryCreate}
id, err := c.CreateCategory(ctx, &spec)
if err != nil {
// in case there are two docker daemon try to create inventory category, query the category once again
if strings.Contains(err.Error(), "ErrAlreadyExists") {
if categories, err = c.GetCategoriesByName(ctx, name); err != nil {
Logger.Debugf("Failed to get inventory category for %s", errors.WithStack(err))
return nil, errors.Wrap(err, "create inventory category failed")
}
} else {
Logger.Debugf("Failed to create inventory category for %s", errors.WithStack(err))
return nil, errors.Wrap(err, "create inventory category failed")
}
} else {
return id, nil
}
}
if categories != nil {
return &categories[0].ID, nil
}
// should not happen
Logger.Debugf("Failed to create inventory for it's existed, but could not query back. Please check system")
return nil, errors.Errorf("Failed to create inventory for it's existed, but could not query back. Please check system")
}
func (c *RestClient) CreateCategory(ctx context.Context, spec *CategoryCreateSpec) (*string, error) {
Logger.Debugf("Create category %v", spec)
stream, _, status, err := c.call(ctx, "POST", CategoryURL, spec, nil)
Logger.Debugf("Get status code: %d", status)
if status != http.StatusOK || err != nil {
Logger.Debugf("Create category failed with status code: %d, error message: %s", status, errors.WithStack(err))
return nil, errors.Wrapf(err, "Status code: %d", status)
}
type RespValue struct {
Value string
}
var pID RespValue
if err := json.NewDecoder(stream).Decode(&pID); err != nil {
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
return nil, errors.Wrap(err, "create category failed")
}
return &(pID.Value), nil
}
func (c *RestClient) GetCategory(ctx context.Context, id string) (*Category, error) {
Logger.Debugf("Get category %s", id)
stream, _, status, err := c.call(ctx, "GET", fmt.Sprintf("%s/id:%s", CategoryURL, id), nil, nil)
if status != http.StatusOK || err != nil {
Logger.Debugf("Get category failed with status code: %s, error message: %s", status, errors.WithStack(err))
return nil, errors.Errorf("Status code: %d, error: %s", status, err)
}
type RespValue struct {
Value Category
}
var pCategory RespValue
if err := json.NewDecoder(stream).Decode(&pCategory); err != nil {
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
return nil, errors.Wrapf(err, "get category %s failed", id)
}
return &(pCategory.Value), nil
}
func (c *RestClient) UpdateCategory(ctx context.Context, id string, spec *CategoryUpdateSpec) error {
Logger.Debugf("Update category %v", spec)
_, _, status, err := c.call(ctx, "PATCH", fmt.Sprintf("%s/id:%s", CategoryURL, id), spec, nil)
Logger.Debugf("Get status code: %d", status)
if status != http.StatusOK || err != nil {
Logger.Debugf("Update category failed with status code: %d, error message: %s", status, errors.WithStack(err))
return errors.Wrapf(err, "Status code: %d", status)
}
return nil
}
func (c *RestClient) DeleteCategory(ctx context.Context, id string) error {
Logger.Debugf("Delete category %s", id)
_, _, status, err := c.call(ctx, "DELETE", fmt.Sprintf("%s/id:%s", CategoryURL, id), nil, nil)
if status != http.StatusOK || err != nil {
Logger.Debugf("Delete category failed with status code: %s, error message: %s", status, errors.WithStack(err))
return errors.Errorf("Status code: %d, error: %s", status, err)
}
return nil
}
func (c *RestClient) ListCategories(ctx context.Context) ([]string, error) {
Logger.Debugf("List all categories")
stream, _, status, err := c.call(ctx, "GET", CategoryURL, nil, nil)
if status != http.StatusOK || err != nil {
Logger.Debugf("Get categories failed with status code: %s, error message: %s", status, errors.WithStack(err))
return nil, errors.Errorf("Status code: %d, error: %s", status, err)
}
type Categories struct {
Value []string
}
var pCategories Categories
if err := json.NewDecoder(stream).Decode(&pCategories); err != nil {
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
return nil, errors.Wrap(err, "list categories failed")
}
return pCategories.Value, nil
}
func (c *RestClient) GetCategoriesByName(ctx context.Context, name string) ([]Category, error) {
Logger.Debugf("Get category %s", name)
categoryIds, err := c.ListCategories(ctx)
if err != nil {
Logger.Debugf("Get category failed for: %s", errors.WithStack(err))
return nil, errors.Wrapf(err, "get categories by name %s failed", name)
}
var categories []Category
for _, cID := range categoryIds {
category, err := c.GetCategory(ctx, cID)
if err != nil {
Logger.Debugf("Get category %s failed for %s", cID, errors.WithStack(err))
}
if category.Name == name {
categories = append(categories, *category)
}
}
return categories, nil
}

View File

@@ -0,0 +1,202 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tags
import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/vmware/govmomi/vim25/soap"
)
const (
RestPrefix = "/rest"
loginURL = "/com/vmware/cis/session"
)
type RestClient struct {
mu sync.Mutex
host string
scheme string
endpoint *url.URL
user *url.Userinfo
HTTP *http.Client
cookies []*http.Cookie
}
func NewClient(u *url.URL, insecure bool, thumbprint string) *RestClient {
endpoint := &url.URL{}
*endpoint = *u
Logger.Debugf("Create rest client")
endpoint.Path = RestPrefix
sc := soap.NewClient(endpoint, insecure)
if thumbprint != "" {
sc.SetThumbprint(endpoint.Host, thumbprint)
}
user := endpoint.User
endpoint.User = nil
return &RestClient{
endpoint: endpoint,
user: user,
host: endpoint.Host,
scheme: endpoint.Scheme,
HTTP: &sc.Client,
}
}
func (c *RestClient) encodeData(data interface{}) (*bytes.Buffer, error) {
params := bytes.NewBuffer(nil)
if data != nil {
if err := json.NewEncoder(params).Encode(data); err != nil {
return nil, errors.Wrap(err, "failed to encode json data")
}
}
return params, nil
}
func (c *RestClient) call(ctx context.Context, method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) {
// Logger.Debugf("%s: %s, headers: %+v", method, path, headers)
params, err := c.encodeData(data)
if err != nil {
return nil, nil, -1, errors.Wrap(err, "call failed")
}
if data != nil {
if headers == nil {
headers = make(map[string][]string)
}
headers["Content-Type"] = []string{"application/json"}
}
body, hdr, statusCode, err := c.clientRequest(ctx, method, path, params, headers)
if statusCode == http.StatusUnauthorized && strings.Contains(err.Error(), "This method requires authentication") {
c.Login(ctx)
Logger.Debugf("Rerun request after login")
return c.clientRequest(ctx, method, path, params, headers)
}
return body, hdr, statusCode, errors.Wrap(err, "call failed")
}
func (c *RestClient) clientRequest(ctx context.Context, method, path string, in io.Reader, headers map[string][]string) (io.ReadCloser, http.Header, int, error) {
expectedPayload := (method == "POST" || method == "PUT")
if expectedPayload && in == nil {
in = bytes.NewReader([]byte{})
}
req, err := c.newRequest(method, path, in)
if err != nil {
return nil, nil, -1, errors.Wrap(err, "failed to create request")
}
req = req.WithContext(ctx)
c.mu.Lock()
if c.cookies != nil {
req.AddCookie(c.cookies[0])
}
c.mu.Unlock()
if headers != nil {
for k, v := range headers {
req.Header[k] = v
}
}
if expectedPayload && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Accept", "application/json")
resp, err := c.HTTP.Do(req)
return c.handleResponse(resp, err)
}
func (c *RestClient) handleResponse(resp *http.Response, err error) (io.ReadCloser, http.Header, int, error) {
statusCode := -1
if resp != nil {
statusCode = resp.StatusCode
}
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return nil, nil, statusCode, errors.Errorf("Cannot connect to endpoint %s. Is vCloud Suite API running on this server?", c.host)
}
return nil, nil, statusCode, errors.Wrap(err, "error occurred trying to connect")
}
if statusCode < http.StatusOK || statusCode >= http.StatusBadRequest {
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, nil, statusCode, errors.Wrap(err, "error reading response")
}
if len(body) == 0 {
return nil, nil, statusCode, errors.Errorf("Error: request returned %s", http.StatusText(statusCode))
}
Logger.Debugf("Error response: %s", bytes.TrimSpace(body))
return nil, nil, statusCode, errors.Errorf("Error response from vCloud Suite API: %s", bytes.TrimSpace(body))
}
return resp.Body, resp.Header, statusCode, nil
}
func (c *RestClient) Login(ctx context.Context) error {
c.mu.Lock()
defer c.mu.Unlock()
Logger.Debugf("Login to %s through rest API.", c.host)
request, err := c.newRequest("POST", loginURL, nil)
if err != nil {
return errors.Wrap(err, "login failed")
}
if c.user != nil {
password, _ := c.user.Password()
request.SetBasicAuth(c.user.Username(), password)
}
resp, err := c.HTTP.Do(request)
if err != nil {
return errors.Wrap(err, "login failed")
}
if resp == nil {
return errors.New("response is nil in Login")
}
if resp.StatusCode != http.StatusOK {
// #nosec: Errors unhandled.
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
return errors.Errorf("Login failed: body: %s, status: %s", bytes.TrimSpace(body), resp.Status)
}
c.cookies = resp.Cookies()
Logger.Debugf("Login succeeded")
return nil
}
func (c *RestClient) newRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
return http.NewRequest(method, c.endpoint.String()+urlStr, body)
}

View File

@@ -0,0 +1,135 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tags
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/pkg/errors"
)
const (
TagAssociationURL = "/com/vmware/cis/tagging/tag-association"
)
type AssociatedObject struct {
ID *string `json:"id"`
Type *string `json:"type"`
}
type TagAssociationSpec struct {
ObjectID *AssociatedObject `json:"object_id,omitempty"`
TagID *string `json:"tag_id,omitempty"`
}
func (c *RestClient) getAssociatedObject(objID *string, objType *string) *AssociatedObject {
if objID == nil && objType == nil {
return nil
}
object := AssociatedObject{
ID: objID,
Type: objType,
}
return &object
}
func (c *RestClient) getAssociationSpec(tagID *string, objID *string, objType *string) *TagAssociationSpec {
object := c.getAssociatedObject(objID, objType)
spec := TagAssociationSpec{
TagID: tagID,
ObjectID: object,
}
return &spec
}
func (c *RestClient) AttachTagToObject(ctx context.Context, tagID string, objID string, objType string) error {
Logger.Debugf("Attach Tag %s to object id: %s, type: %s", tagID, objID, objType)
spec := c.getAssociationSpec(&tagID, &objID, &objType)
_, _, status, err := c.call(ctx, "POST", fmt.Sprintf("%s?~action=attach", TagAssociationURL), *spec, nil)
Logger.Debugf("Get status code: %d", status)
if status != http.StatusOK || err != nil {
Logger.Debugf("Attach tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
return errors.Wrapf(err, "Get unexpected status code: %d", status)
}
return nil
}
func (c *RestClient) DetachTagFromObject(ctx context.Context, tagID string, objID string, objType string) error {
Logger.Debugf("Detach Tag %s to object id: %s, type: %s", tagID, objID, objType)
spec := c.getAssociationSpec(&tagID, &objID, &objType)
_, _, status, err := c.call(ctx, "POST", fmt.Sprintf("%s?~action=detach", TagAssociationURL), *spec, nil)
Logger.Debugf("Get status code: %d", status)
if status != http.StatusOK || err != nil {
Logger.Debugf("Detach tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
return errors.Wrapf(err, "Get unexpected status code: %d", status)
}
return nil
}
func (c *RestClient) ListAttachedTags(ctx context.Context, objID string, objType string) ([]string, error) {
Logger.Debugf("List attached tags of object id: %s, type: %s", objID, objType)
spec := c.getAssociationSpec(nil, &objID, &objType)
stream, _, status, err := c.call(ctx, "POST", fmt.Sprintf("%s?~action=list-attached-tags", TagAssociationURL), *spec, nil)
Logger.Debugf("Get status code: %d", status)
if status != http.StatusOK || err != nil {
Logger.Debugf("Detach tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
return nil, errors.Wrapf(err, "Get unexpected status code: %d", status)
}
type RespValue struct {
Value []string
}
var pTag RespValue
if err := json.NewDecoder(stream).Decode(&pTag); err != nil {
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
return nil, errors.Wrap(err, "list attached tags failed")
}
return pTag.Value, nil
}
func (c *RestClient) ListAttachedObjects(ctx context.Context, tagID string) ([]AssociatedObject, error) {
Logger.Debugf("List attached objects of tag: %s", tagID)
spec := c.getAssociationSpec(&tagID, nil, nil)
Logger.Debugf("List attached objects for tag %v", *spec)
// stream, _, status, err := c.call("POST", fmt.Sprintf("%s?~action=list-attached-objects", TagAssociationURL), *spec, nil)
stream, _, status, err := c.call(ctx, "POST", fmt.Sprintf("%s?~action=list-attached-objects", TagAssociationURL), *spec, nil)
Logger.Debugf("Get status code: %d", status)
if status != http.StatusOK || err != nil {
Logger.Debugf("List object failed with status code: %d, error message: %s", status, errors.WithStack(err))
return nil, errors.Wrapf(err, "Get unexpected status code: %d", status)
}
type RespValue struct {
Value []AssociatedObject
}
var pTag RespValue
if err := json.NewDecoder(stream).Decode(&pTag); err != nil {
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
return nil, errors.Wrap(err, "list attached tags failed")
}
return pTag.Value, nil
}

255
vendor/github.com/vmware/vic/pkg/vsphere/tags/tags.go generated vendored Normal file
View File

@@ -0,0 +1,255 @@
// Copyright 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tags
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"github.com/Sirupsen/logrus"
"github.com/pkg/errors"
)
const (
TagURL = "/com/vmware/cis/tagging/tag"
)
type TagCreateSpec struct {
CreateSpec TagCreate `json:"create_spec"`
}
type TagCreate struct {
CategoryID string `json:"category_id"`
Description string `json:"description"`
Name string `json:"name"`
}
type TagUpdateSpec struct {
UpdateSpec TagUpdate `json:"update_spec"`
}
type TagUpdate struct {
Description string `json:"description"`
Name string `json:"name"`
}
type Tag struct {
ID string `json:"id"`
Description string `json:"description"`
Name string `json:"name"`
CategoryID string `json:"category_id"`
UsedBy []string `json:"used_by"`
}
var Logger = logrus.New()
func (c *RestClient) CreateTagIfNotExist(ctx context.Context, name string, description string, categoryID string) (*string, error) {
tagCreate := TagCreate{categoryID, description, name}
spec := TagCreateSpec{tagCreate}
id, err := c.CreateTag(ctx, &spec)
if err == nil {
return id, nil
}
Logger.Debugf("Created tag %s failed for %s", errors.WithStack(err))
// if already exists, query back
if strings.Contains(err.Error(), ErrAlreadyExists) {
tagObjs, err := c.GetTagByNameForCategory(ctx, name, categoryID)
if err != nil {
return nil, errors.Wrapf(err, "failed to query tag %s for category %s", name, categoryID)
}
if tagObjs != nil {
return &tagObjs[0].ID, nil
}
// should not happen
return nil, errors.New("Failed to create tag for it's existed, but could not query back. Please check system")
}
return nil, errors.Wrap(err, "failed to create tag")
}
func (c *RestClient) DeleteTagIfNoObjectAttached(ctx context.Context, id string) error {
objs, err := c.ListAttachedObjects(ctx, id)
if err != nil {
return errors.Wrap(err, "failed to delete tag")
}
if objs != nil && len(objs) > 0 {
Logger.Debugf("tag %s related objects is not empty, do not delete it.", id)
return nil
}
return c.DeleteTag(ctx, id)
}
func (c *RestClient) CreateTag(ctx context.Context, spec *TagCreateSpec) (*string, error) {
Logger.Debugf("Create Tag %v", spec)
stream, _, status, err := c.call(ctx, "POST", TagURL, spec, nil)
Logger.Debugf("Get status code: %d", status)
if status != http.StatusOK || err != nil {
Logger.Debugf("Create tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
return nil, errors.Wrapf(err, "Status code: %d", status)
}
type RespValue struct {
Value string
}
var pID RespValue
if err := json.NewDecoder(stream).Decode(&pID); err != nil {
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
return nil, errors.Wrap(err, "create tag failed")
}
return &pID.Value, nil
}
func (c *RestClient) GetTag(ctx context.Context, id string) (*Tag, error) {
Logger.Debugf("Get tag %s", id)
stream, _, status, err := c.call(ctx, "GET", fmt.Sprintf("%s/id:%s", TagURL, id), nil, nil)
if status != http.StatusOK || err != nil {
Logger.Debugf("Get tag failed with status code: %s, error message: %s", status, errors.WithStack(err))
return nil, errors.Wrapf(err, "Status code: %d", status)
}
type RespValue struct {
Value Tag
}
var pTag RespValue
if err := json.NewDecoder(stream).Decode(&pTag); err != nil {
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
return nil, errors.Wrapf(err, "failed to get tag %s", id)
}
return &(pTag.Value), nil
}
func (c *RestClient) UpdateTag(ctx context.Context, id string, spec *TagUpdateSpec) error {
Logger.Debugf("Update tag %v", spec)
_, _, status, err := c.call(ctx, "PATCH", fmt.Sprintf("%s/id:%s", TagURL, id), spec, nil)
Logger.Debugf("Get status code: %d", status)
if status != http.StatusOK || err != nil {
Logger.Debugf("Update tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
return errors.Wrapf(err, "Status code: %d", status)
}
return nil
}
func (c *RestClient) DeleteTag(ctx context.Context, id string) error {
Logger.Debugf("Delete tag %s", id)
_, _, status, err := c.call(ctx, "DELETE", fmt.Sprintf("%s/id:%s", TagURL, id), nil, nil)
if status != http.StatusOK || err != nil {
Logger.Debugf("Delete tag failed with status code: %s, error message: %s", status, errors.WithStack(err))
return errors.Wrapf(err, "Status code: %d", status)
}
return nil
}
func (c *RestClient) ListTags(ctx context.Context) ([]string, error) {
Logger.Debugf("List all tags")
stream, _, status, err := c.call(ctx, "GET", TagURL, nil, nil)
if status != http.StatusOK || err != nil {
Logger.Debugf("Get tags failed with status code: %s, error message: %s", status, errors.WithStack(err))
return nil, errors.Wrapf(err, "Status code: %d", status)
}
return c.handleTagIDList(stream)
}
func (c *RestClient) ListTagsForCategory(ctx context.Context, id string) ([]string, error) {
Logger.Debugf("List tags for category: %s", id)
type PostCategory struct {
CId string `json:"category_id"`
}
spec := PostCategory{id}
stream, _, status, err := c.call(ctx, "POST", fmt.Sprintf("%s/id:%s?~action=list-tags-for-category", TagURL, id), spec, nil)
if status != http.StatusOK || err != nil {
Logger.Debugf("List tags for category failed with status code: %s, error message: %s", status, errors.WithStack(err))
return nil, errors.Wrapf(err, "Status code: %d", status)
}
return c.handleTagIDList(stream)
}
func (c *RestClient) handleTagIDList(stream io.ReadCloser) ([]string, error) {
type Tags struct {
Value []string
}
var pTags Tags
if err := json.NewDecoder(stream).Decode(&pTags); err != nil {
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
return nil, errors.Wrap(err, "failed to decode json")
}
return pTags.Value, nil
}
// Get tag through tag name and category id
func (c *RestClient) GetTagByNameForCategory(ctx context.Context, name string, id string) ([]Tag, error) {
Logger.Debugf("Get tag %s for category %s", name, id)
tagIds, err := c.ListTagsForCategory(ctx, id)
if err != nil {
Logger.Debugf("Get tag failed for %s", errors.WithStack(err))
return nil, errors.Wrapf(err, "get tag failed for name %s category %s", name, id)
}
var tags []Tag
for _, tID := range tagIds {
tag, err := c.GetTag(ctx, tID)
if err != nil {
Logger.Debugf("Get tag %s failed for %s", tID, errors.WithStack(err))
return nil, errors.Wrapf(err, "get tag failed for name %s category %s", name, id)
}
if tag.Name == name {
tags = append(tags, *tag)
}
}
return tags, nil
}
// Get attached tags through tag name pattern
func (c *RestClient) GetAttachedTagsByNamePattern(ctx context.Context, namePattern string, objID string, objType string) ([]Tag, error) {
tagIds, err := c.ListAttachedTags(ctx, objID, objType)
if err != nil {
Logger.Debugf("Get attached tags failed for %s", errors.WithStack(err))
return nil, errors.Wrap(err, "get attached tags failed")
}
var validName = regexp.MustCompile(namePattern)
var tags []Tag
for _, tID := range tagIds {
tag, err := c.GetTag(ctx, tID)
if err != nil {
Logger.Debugf("Get tag %s failed for %s", tID, errors.WithStack(err))
}
if validName.MatchString(tag.Name) {
tags = append(tags, *tag)
}
}
return tags, nil
}

View File

@@ -0,0 +1,207 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package tasks wraps the operation of VC. It will invoke the operation and wait
// until it's finished, and then return the execution result or error message.
package tasks
import (
"context"
"math/rand"
"time"
"github.com/vmware/govmomi/task"
"github.com/vmware/govmomi/vim25/progress"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/trace"
)
const (
maxBackoffFactor = int64(16)
)
//FIXME: remove this type and refactor to use object.Task from govmomi
// this will require a lot of code being touched in a lot of places.
type Task interface {
Wait(ctx context.Context) error
WaitForResult(ctx context.Context, s progress.Sinker) (*types.TaskInfo, error)
}
type temporary interface {
Temporary() bool
}
// Wait wraps govmomi operations and wait the operation to complete
// Sample usage:
// info, err := Wait(ctx, func(ctx), (*object.Reference, *TaskInfo, error) {
// return vm, vm.Reconfigure(ctx, config)
// })
func Wait(ctx context.Context, f func(context.Context) (Task, error)) error {
_, err := WaitForResult(ctx, f)
return err
}
// WaitForResult wraps govmomi operations and wait the operation to complete.
// Return the operation result
// Sample usage:
// info, err := WaitForResult(ctx, func(ctx) (*TaskInfo, error) {
// return vm, vm.Reconfigure(ctx, config)
// })
func WaitForResult(ctx context.Context, f func(context.Context) (Task, error)) (*types.TaskInfo, error) {
var err error
var backoffFactor int64 = 1
op := trace.FromContext(ctx, "WaitForResult")
for {
var t Task
var info *types.TaskInfo
if t, err = f(op); err == nil {
if info, err = t.WaitForResult(op, nil); err == nil {
return info, nil
}
}
if !IsRetryError(op, err) {
return info, err
}
sleepValue := time.Duration(backoffFactor * (rand.Int63n(100) + int64(50)))
select {
case <-time.After(sleepValue * time.Millisecond):
backoffFactor *= 2
if backoffFactor > maxBackoffFactor {
backoffFactor = maxBackoffFactor
}
case <-op.Done():
return info, op.Err()
}
op.Warnf("retrying task")
}
}
const (
vimFault = "vim"
soapFault = "soap"
taskFault = "task"
)
// IsRetryErrors will return true for vSphere errors, which can be fixed by retry.
// Currently the error includes TaskInProgress, NetworkDisruptedAndConfigRolledBack and InvalidArgument
// Retry on NetworkDisruptedAndConfigRolledBack is to workaround vSphere issue
// Retry on InvalidArgument(invlid path) is to workaround vSAN bug: https://bugzilla.eng.vmware.com/show_bug.cgi?id=1770798. TODO: Should remove it after vSAN fixed the bug
func IsRetryError(op trace.Operation, err error) bool {
if soap.IsSoapFault(err) {
switch f := soap.ToSoapFault(err).VimFault().(type) {
case types.TaskInProgress:
return true
case types.NetworkDisruptedAndConfigRolledBack:
logExpectedFault(op, soapFault, f)
return true
case types.InvalidArgument:
logExpectedFault(op, soapFault, f)
return true
case types.VAppTaskInProgress:
logExpectedFault(op, soapFault, f)
return true
case types.FailToLockFaultToleranceVMs:
logExpectedFault(op, soapFault, f)
return true
case types.HostCommunication:
logExpectedFault(op, soapFault, f)
return true
default:
logSoapFault(op, f)
return false
}
}
if soap.IsVimFault(err) {
switch f := soap.ToVimFault(err).(type) {
case *types.TaskInProgress:
return true
case *types.NetworkDisruptedAndConfigRolledBack:
logExpectedFault(op, vimFault, f)
return true
case *types.InvalidArgument:
logExpectedFault(op, vimFault, f)
return true
case *types.VAppTaskInProgress:
logExpectedFault(op, soapFault, f)
return true
case *types.FailToLockFaultToleranceVMs:
logExpectedFault(op, soapFault, f)
return true
case *types.HostCommunication:
logExpectedFault(op, soapFault, f)
return true
default:
logFault(op, f)
return false
}
}
switch err := err.(type) {
case task.Error:
switch f := err.Fault().(type) {
case *types.TaskInProgress:
return true
case *types.NetworkDisruptedAndConfigRolledBack:
logExpectedFault(op, taskFault, f)
return true
case *types.InvalidArgument:
logExpectedFault(op, taskFault, f)
return true
case *types.HostCommunication:
logExpectedFault(op, taskFault, f)
return true
default:
logFault(op, err.Fault())
return false
}
default:
// retry the temporary errors
t, ok := err.(temporary)
if ok && t.Temporary() {
logExpectedError(op, err)
return true
}
logError(op, err)
return false
}
}
// Helper Functions
func logFault(op trace.Operation, fault types.BaseMethodFault) {
op.Errorf("unexpected fault on task retry: %#v", fault)
}
func logSoapFault(op trace.Operation, fault types.AnyType) {
op.Debugf("unexpected soap fault on task retry: %s", fault)
}
func logError(op trace.Operation, err error) {
op.Debugf("unexpected error on task retry: %s", err)
}
func logExpectedFault(op trace.Operation, kind string, fault interface{}) {
op.Debugf("task retry on expected %s fault: %#v", kind, fault)
}
func logExpectedError(op trace.Operation, err error) {
op.Debugf("task retry on expected error %s", err)
}

View File

@@ -0,0 +1,440 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tasks
import (
"context"
"strings"
"testing"
"time"
log "github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/simulator"
"github.com/vmware/govmomi/task"
"github.com/vmware/govmomi/vim25/methods"
"github.com/vmware/govmomi/vim25/progress"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/errors"
"github.com/vmware/vic/pkg/trace"
)
func TestMain(m *testing.M) {
log.SetLevel(log.DebugLevel)
m.Run()
}
type MyTask struct {
success bool
}
func (t *MyTask) Wait(ctx context.Context) error {
_, err := t.WaitForResult(ctx, nil)
return err
}
func (t *MyTask) WaitForResult(ctx context.Context, s progress.Sinker) (*types.TaskInfo, error) {
if t.success {
return nil, nil
}
return nil, errors.Errorf("Wait failed")
}
func createFailedTask(context.Context) (Task, error) {
return nil, errors.Errorf("Create VM failed")
}
func createFailedResultWaiter(context.Context) (Task, error) {
task := &MyTask{
false,
}
return task, nil
}
func createResultWaiter(context.Context) (Task, error) {
task := &MyTask{
true,
}
return task, nil
}
func TestFailedInvokeResult(t *testing.T) {
ctx := context.TODO()
_, err := WaitForResult(ctx, func(ctx context.Context) (Task, error) {
return createFailedTask(ctx)
})
if err == nil || !strings.Contains(err.Error(), "Create VM failed") {
t.Errorf("Not expected error message")
}
}
func TestFailedWaitResult(t *testing.T) {
ctx := context.TODO()
_, err := WaitForResult(ctx, func(ctx context.Context) (Task, error) {
return createFailedResultWaiter(ctx)
})
log.Debugf("got error: %s", err.Error())
if err == nil || !strings.Contains(err.Error(), "Wait failed") {
t.Errorf("Not expected error message")
}
}
func TestSuccessWaitResult(t *testing.T) {
ctx := context.TODO()
_, err := WaitForResult(ctx, func(ctx context.Context) (Task, error) {
return createResultWaiter(ctx)
})
if err != nil {
t.Errorf("Unexpected error: %s", err.Error())
}
}
func createFailed(context.Context) (Task, error) {
return nil, errors.Errorf("Create VM failed")
}
func createFailedWaiter(context.Context) (Task, error) {
task := &MyTask{
false,
}
return task, nil
}
func createWaiter(context.Context) (Task, error) {
task := &MyTask{
true,
}
return task, nil
}
func TestFailedInvoke(t *testing.T) {
ctx := context.TODO()
err := Wait(ctx, func(ctx context.Context) (Task, error) {
return createFailed(ctx)
})
if err == nil || !strings.Contains(err.Error(), "Create VM failed") {
t.Errorf("Not expected error message")
}
}
func TestFailedWait(t *testing.T) {
ctx := context.TODO()
err := Wait(ctx, func(ctx context.Context) (Task, error) {
return createFailedWaiter(ctx)
})
log.Debugf("got error: %s", err.Error())
if err == nil || !strings.Contains(err.Error(), "Wait failed") {
t.Errorf("Not expected error message")
}
}
func TestSuccessWait(t *testing.T) {
ctx := context.TODO()
err := Wait(ctx, func(ctx context.Context) (Task, error) {
return createWaiter(ctx)
})
if err != nil {
t.Errorf("Unexpected error: %s", err.Error())
}
}
var taskInProgressFault = task.Error{
LocalizedMethodFault: &types.LocalizedMethodFault{
Fault: &types.TaskInProgress{},
},
}
type taskInProgressTask struct {
cur, max int
err error
info *types.TaskInfo
}
func (t *taskInProgressTask) Wait(ctx context.Context) error {
t.cur++
if t.cur == t.max {
return t.err
}
return taskInProgressFault
}
func (t *taskInProgressTask) WaitForResult(ctx context.Context, s progress.Sinker) (*types.TaskInfo, error) {
return t.info, t.Wait(ctx)
}
func mustRunInTime(t *testing.T, d time.Duration, f func()) {
done := make(chan bool)
go func() {
f()
close(done)
}()
ctx, cancel := context.WithTimeout(context.Background(), d)
defer cancel()
select {
case <-done: // ran within alloted time
case <-ctx.Done():
t.Fatalf("test did not run in alloted time %s", d)
}
}
func TestRetry(t *testing.T) {
mustRunInTime(t, 2*time.Second, func() {
ctx := context.Background()
i := 0
ti, err := WaitForResult(ctx, func(_ context.Context) (Task, error) {
i++
return nil, assert.AnError
})
assert.Nil(t, ti)
assert.Equal(t, i, 1)
assert.Error(t, err)
assert.Equal(t, err, assert.AnError)
// error != TaskInProgress during task creation
i = 0
e := &task.Error{
LocalizedMethodFault: &types.LocalizedMethodFault{
Fault: &types.RuntimeFault{}, // random fault != TaskInProgress
LocalizedMessage: "random fault",
},
}
ti, err = WaitForResult(ctx, func(_ context.Context) (Task, error) {
i++
return nil, e
})
assert.Nil(t, ti)
assert.Equal(t, i, 1)
assert.Error(t, err)
assert.Equal(t, err, e)
// context cancelled after two retries
i = 0
ctx, cancel := context.WithCancel(ctx)
ti, err = WaitForResult(ctx, func(_ context.Context) (Task, error) {
i++
if i == 2 {
cancel()
}
return nil, taskInProgressFault
})
assert.Nil(t, ti)
assert.Equal(t, i, 2)
assert.Error(t, err)
assert.Equal(t, err, ctx.Err())
// TaskInProgress from task creation for 2 iterations and
// then nil error
tsk := &taskInProgressTask{
max: 1,
info: &types.TaskInfo{
Task: types.ManagedObjectReference{
Type: "task",
Value: "foo",
},
},
}
i = 0
ti, err = WaitForResult(context.Background(), func(_ context.Context) (Task, error) {
i++
if i == 2 {
return tsk, nil
}
return nil, taskInProgressFault
})
assert.Equal(t, tsk.info, ti)
assert.Equal(t, i, 2)
assert.NoError(t, err)
// return TaskInPregress from task.WaitForResult for 2 iterations
// and then return assert.AnError
tsk = &taskInProgressTask{
max: 2,
err: assert.AnError,
info: &types.TaskInfo{
Task: types.ManagedObjectReference{
Type: "task",
Value: "foo",
},
},
}
ti, err = WaitForResult(context.Background(), func(_ context.Context) (Task, error) {
return tsk, nil
})
assert.Equal(t, tsk.info, ti)
assert.Equal(t, tsk.max, tsk.cur)
assert.Error(t, err)
assert.Equal(t, err, tsk.err)
// return TaskInPregress from task.WaitForResult for 2 iterations
// and then return nil error
tsk.cur = 0
tsk.err = nil
ti, err = WaitForResult(context.Background(), func(_ context.Context) (Task, error) {
return tsk, nil
})
assert.Equal(t, tsk.info, ti)
assert.Equal(t, tsk.info, ti)
assert.Equal(t, tsk.cur, tsk.max)
assert.NoError(t, err)
})
}
// faultyVirtualMachine wrap simulator.VirtualMachine with fault injection
type faultyVirtualMachine struct {
simulator.VirtualMachine
fault types.BaseMethodFault
}
// Run implements simulator.TaskRunner and always returns vm.fault
func (vm *faultyVirtualMachine) Run(task *simulator.Task) (types.AnyType, types.BaseMethodFault) {
return nil, vm.fault
}
// Override PowerOffVMTask to inject a fault
func (vm *faultyVirtualMachine) PowerOffVMTask(c *types.PowerOffVM_Task) soap.HasFault {
r := &methods.PowerOffVM_TaskBody{}
task := simulator.NewTask(vm)
r.Res = &types.PowerOffVM_TaskResponse{
Returnval: task.Self,
}
task.Run()
return r
}
// MarkAsTemplate implements a non-Task method to inject vm.fault
func (vm *faultyVirtualMachine) MarkAsTemplate(c *types.MarkAsTemplate) soap.HasFault {
return &methods.MarkAsTemplateBody{
Fault_: simulator.Fault("nope", vm.fault),
}
}
// TestSoapFaults covers the various soap fault checking paths
func TestSoapFaults(t *testing.T) {
op := trace.NewOperation(context.Background(), "TestSoapFaults")
// Nothing VC specific in this test, so we use the simpler ESX model
model := simulator.ESX()
model.Autostart = false
defer model.Remove()
err := model.Create()
if err != nil {
t.Fatal(err)
}
server := model.Service.NewServer()
defer server.Close()
client, err := govmomi.NewClient(op, server.URL, true)
if err != nil {
t.Fatal(err)
}
// Any VM will do
finder := find.NewFinder(client.Client, false)
vm, err := finder.VirtualMachine(op, "/ha-datacenter/vm/*_VM0")
if err != nil {
t.Fatal(err)
}
// Test the success path
err = Wait(op, func(ctx context.Context) (Task, error) {
return vm.PowerOn(ctx)
})
if err != nil {
t.Fatal(err)
}
// Wrap existing vm MO with faultyVirtualMachine
ref := simulator.Map.Get(vm.Reference())
fvm := &faultyVirtualMachine{*ref.(*simulator.VirtualMachine), nil}
simulator.Map.Put(fvm)
// Inject TaskInProgress fault
fvm.fault = new(types.TaskInProgress)
task, err := vm.PowerOff(op)
if err != nil {
t.Fatal(err)
}
// Test the task.Error path
res, err := task.WaitForResult(op, nil)
if !IsRetryError(op, err) {
t.Error(err)
}
// Test the soap.IsVimFault() path
if !IsRetryError(op, soap.WrapVimFault(res.Error.Fault)) {
t.Errorf("fault=%#v", res.Error.Fault)
}
// Test the soap.IsSoapFault() path
err = vm.MarkAsTemplate(op)
if !IsRetryError(op, err) {
t.Error(err)
}
// Test a fault other than TaskInProgress
fvm.fault = &types.QuestionPending{
Text: "now why would you want to do such a thing?",
}
err = Wait(op, func(ctx context.Context) (Task, error) {
return vm.PowerOff(ctx)
})
if err == nil {
t.Error("expected error")
}
if IsRetryError(op, err) {
t.Error(err)
}
// Test with retry
fvm.fault = new(types.TaskInProgress)
called := 0
err = Wait(op, func(ctx context.Context) (Task, error) {
called++
if called > 1 {
simulator.Map.Put(ref) // remove fault injection
}
return vm.PowerOff(ctx)
})
if err != nil {
t.Error(err)
}
if called != 2 {
t.Errorf("called=%d", called)
}
}

View File

@@ -0,0 +1,38 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package env
import (
"os"
"testing"
)
// URL checks whether or not we set VIC_ESX_TEST_URL environment variable,
// skipping the calling test if undefined
func URL(t *testing.T) string {
s := os.Getenv("VIC_ESX_TEST_URL")
if s == "" && t != nil {
t.Skip("Skipping: No test ESX URL defined")
}
return s
}
func DS(t *testing.T) string {
s := os.Getenv("VIC_ESX_TEST_DATASTORE")
if s == "" && t != nil {
t.Skip("Skipping: No test ESX DATASTORE defined")
}
return s
}

125
vendor/github.com/vmware/vic/pkg/vsphere/test/test.go generated vendored Normal file
View File

@@ -0,0 +1,125 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package test
import (
"fmt"
"math/rand"
"strings"
"testing"
"time"
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/lib/spec"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/test/env"
"context"
)
// Session returns a session.Session struct
func Session(ctx context.Context, t *testing.T) *session.Session {
config := &session.Config{
Service: env.URL(t),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
DatacenterPath: "",
DatastorePath: "/ha-datacenter/datastore/*",
HostPath: "/ha-datacenter/host/*/*",
PoolPath: "/ha-datacenter/host/*/Resources",
}
s, err := session.NewSession(config).Create(ctx)
if err != nil {
// FIXME: See session_test.go TestSession for detail. We never get to PickRandomHost in the case of multiple hosts
if strings.Contains(err.Error(), "resolves to multiple hosts") {
t.SkipNow()
} else {
t.Errorf("ERROR: %s", err)
t.SkipNow()
}
}
return s
}
// SessionWithESX returns a general-purpose ESX session for tests.
func SessionWithESX(ctx context.Context, service string) (*session.Session, error) {
config := &session.Config{
Service: service,
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
DatacenterPath: "/ha-datacenter",
ClusterPath: "*",
DatastorePath: "/ha-datacenter/datastore/LocalDS_0",
PoolPath: "/ha-datacenter/host/localhost.localdomain/Resources",
}
s, err := session.NewSession(config).Connect(ctx)
if err != nil {
return nil, err
}
if s, err = s.Populate(ctx); err != nil {
return nil, err
}
return s, nil
}
// SessionWithVPX returns a general-purpose VPX session for tests.
func SessionWithVPX(ctx context.Context, service string) (*session.Session, error) {
config := &session.Config{
Service: service,
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
DatacenterPath: "/DC0",
ClusterPath: "/DC0/host/DC0_C0",
DatastorePath: "/DC0/datastore/LocalDS_0",
PoolPath: "/DC0/host/DC0_C0/Resources",
}
s, err := session.NewSession(config).Connect(ctx)
if err != nil {
return nil, err
}
if s, err = s.Populate(ctx); err != nil {
return nil, err
}
return s, nil
}
// SpecConfig returns a spec.VirtualMachineConfigSpecConfig struct
func SpecConfig(session *session.Session, name string) *spec.VirtualMachineConfigSpecConfig {
return &spec.VirtualMachineConfigSpecConfig{
NumCPUs: 2,
MemoryMB: 2048,
VMForkEnabled: true,
ID: name,
Name: "zombie_attack",
BootMediaPath: session.Datastore.Path("brainz.iso"),
VMPathName: fmt.Sprintf("[%s]", session.Datastore.Name()),
}
}
// PickRandomHost returns a random object.HostSystem from the hosts attached to the datastore and also lives in the same cluster
func PickRandomHost(ctx context.Context, session *session.Session, t *testing.T) *object.HostSystem {
hosts, err := session.Datastore.AttachedClusterHosts(ctx, session.Cluster)
if err != nil {
t.Errorf("ERROR: %s", err)
t.SkipNow()
}
return hosts[rand.Intn(len(hosts))]
}

680
vendor/github.com/vmware/vic/pkg/vsphere/vm/vm.go generated vendored Normal file
View File

@@ -0,0 +1,680 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package vm
import (
"container/list"
"context"
"errors"
"fmt"
"net/url"
"path"
"strconv"
"strings"
"sync/atomic"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/property"
"github.com/vmware/govmomi/vim25/methods"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/extraconfig/vmomi"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/tasks"
)
const (
DestroyTask = "Destroy_Task"
UpdateStatus = "UpdateInProgress"
)
type InvalidState struct {
r types.ManagedObjectReference
}
func (i *InvalidState) Error() string {
return fmt.Sprintf("vm %s is invalid", i.r.String())
}
// VirtualMachine struct defines the VirtualMachine which provides additional
// VIC specific methods over object.VirtualMachine as well as keeps some state
type VirtualMachine struct {
// TODO: Wrap Internal VirtualMachine struct when we have it
// *internal.VirtualMachine
*object.VirtualMachine
*session.Session
// fxing is 1 means this vm is fixing for it's in invalid status. 0 means not in fixing status
fixing int32
}
// NewVirtualMachine returns a NewVirtualMachine object
func NewVirtualMachine(ctx context.Context, session *session.Session, moref types.ManagedObjectReference) *VirtualMachine {
return NewVirtualMachineFromVM(ctx, session, object.NewVirtualMachine(session.Vim25(), moref))
}
// NewVirtualMachineFromVM returns a NewVirtualMachine object
func NewVirtualMachineFromVM(ctx context.Context, session *session.Session, vm *object.VirtualMachine) *VirtualMachine {
return &VirtualMachine{
VirtualMachine: vm,
Session: session,
}
}
// VMPathNameAsURL returns the full datastore path of the VM as a url. The datastore name is in the host
// portion, the path is in the Path field, the scheme is set to "ds"
func (vm *VirtualMachine) VMPathNameAsURL(ctx context.Context) (url.URL, error) {
op := trace.FromContext(ctx, "VMPathNameAsURL")
var mvm mo.VirtualMachine
if err := vm.Properties(op, vm.Reference(), []string{"config.files.vmPathName"}, &mvm); err != nil {
op.Errorf("Unable to get managed config for VM: %s", err)
return url.URL{}, err
}
if mvm.Config == nil {
return url.URL{}, errors.New("failed to get datastore path - config not found")
}
path := path.Dir(mvm.Config.Files.VmPathName)
val := url.URL{
Scheme: "ds",
}
// split the dsPath into the url components
if ix := strings.Index(path, "] "); ix != -1 {
val.Host = path[strings.Index(path, "[")+1 : ix]
val.Path = path[ix+2:]
}
return val, nil
}
// FolderName returns the name of the namespace(vsan) or directory(vmfs) that holds the VM
// this equates to the normal directory that contains the vmx file, stripped of any parent path
func (vm *VirtualMachine) FolderName(ctx context.Context) (string, error) {
op := trace.FromContext(ctx, "FolderName")
u, err := vm.VMPathNameAsURL(op)
if err != nil {
return "", err
}
return path.Base(u.Path), nil
}
func (vm *VirtualMachine) getNetworkName(op trace.Operation, nic types.BaseVirtualEthernetCard) (string, error) {
if card, ok := nic.GetVirtualEthernetCard().Backing.(*types.VirtualEthernetCardDistributedVirtualPortBackingInfo); ok {
pg := card.Port.PortgroupKey
pgref := object.NewDistributedVirtualPortgroup(vm.Session.Vim25(), types.ManagedObjectReference{
Type: "DistributedVirtualPortgroup",
Value: pg,
})
var pgo mo.DistributedVirtualPortgroup
err := pgref.Properties(op, pgref.Reference(), []string{"config"}, &pgo)
if err != nil {
op.Errorf("Failed to query portgroup %s for %s", pg, err)
return "", err
}
return pgo.Config.Name, nil
}
return nic.GetVirtualEthernetCard().DeviceInfo.GetDescription().Summary, nil
}
func (vm *VirtualMachine) FetchExtraConfigBaseOptions(ctx context.Context) ([]types.BaseOptionValue, error) {
op := trace.FromContext(ctx, "FetchExtraConfigBaseOptions")
var err error
var mvm mo.VirtualMachine
if err = vm.Properties(op, vm.Reference(), []string{"config.extraConfig"}, &mvm); err != nil {
op.Errorf("Unable to get vm config: %s", err)
return nil, err
}
return mvm.Config.ExtraConfig, nil
}
func (vm *VirtualMachine) FetchExtraConfig(ctx context.Context) (map[string]string, error) {
op := trace.FromContext(ctx, "FetchExtraConfig")
info := make(map[string]string)
v, err := vm.FetchExtraConfigBaseOptions(op)
if err != nil {
return nil, err
}
for _, bov := range v {
ov := bov.GetOptionValue()
value, _ := ov.Value.(string)
info[ov.Key] = value
}
return info, nil
}
// WaitForExtraConfig waits until key shows up with the expected value inside the ExtraConfig
func (vm *VirtualMachine) WaitForExtraConfig(ctx context.Context, waitFunc func(pc []types.PropertyChange) bool) error {
op := trace.FromContext(ctx, "WaitForExtraConfig")
// Get the default collector
p := property.DefaultCollector(vm.Vim25())
// Wait on config.extraConfig
// https://www.vmware.com/support/developer/vc-sdk/visdk2xpubs/ReferenceGuide/vim.vm.ConfigInfo.html
return property.Wait(op, p, vm.Reference(), []string{"config.extraConfig", object.PropRuntimePowerState}, waitFunc)
}
func (vm *VirtualMachine) WaitForKeyInExtraConfig(ctx context.Context, key string) (string, error) {
op := trace.FromContext(ctx, "WaitForKeyInExtraConfig")
var detail string
var poweredOff error
waitFunc := func(pc []types.PropertyChange) bool {
for _, c := range pc {
if c.Op != types.PropertyChangeOpAssign {
continue
}
switch v := c.Val.(type) {
case types.ArrayOfOptionValue:
for _, value := range v.OptionValue {
// check the status of the key and return true if it's been set to non-nil
if key == value.GetOptionValue().Key {
detail = value.GetOptionValue().Value.(string)
if detail != "" && detail != "<nil>" {
// ensure we clear any tentative error
poweredOff = nil
return true
}
break // continue the outer loop as we may have a powerState change too
}
}
case types.VirtualMachinePowerState:
// Give up if the vm has powered off
if v != types.VirtualMachinePowerStatePoweredOn {
msg := "powered off"
if v == types.VirtualMachinePowerStateSuspended {
// Unlikely, but possible if the VM was suspended out-of-band
msg = string(v)
}
poweredOff = fmt.Errorf("container VM has unexpectedly %s", msg)
}
}
}
return poweredOff != nil
}
err := vm.WaitForExtraConfig(op, waitFunc)
if err == nil && poweredOff != nil {
err = poweredOff
}
if err != nil {
return "", err
}
return detail, nil
}
func (vm *VirtualMachine) Name(ctx context.Context) (string, error) {
op := trace.FromContext(ctx, "Name")
var err error
var mvm mo.VirtualMachine
if err = vm.Properties(op, vm.Reference(), []string{"summary.config"}, &mvm); err != nil {
op.Errorf("Unable to get vm summary.config property: %s", err)
return "", err
}
return mvm.Summary.Config.Name, nil
}
func (vm *VirtualMachine) UUID(ctx context.Context) (string, error) {
op := trace.FromContext(ctx, "UUID")
var err error
var mvm mo.VirtualMachine
if err = vm.Properties(op, vm.Reference(), []string{"summary.config"}, &mvm); err != nil {
op.Errorf("Unable to get vm summary.config property: %s", err)
return "", err
}
return mvm.Summary.Config.Uuid, nil
}
// DeleteExceptDisks destroys the VM after detaching all virtual disks
func (vm *VirtualMachine) DeleteExceptDisks(ctx context.Context) (*object.Task, error) {
op := trace.FromContext(ctx, "DeleteExceptDisks")
devices, err := vm.Device(op)
if err != nil {
return nil, err
}
disks := devices.SelectByType(&types.VirtualDisk{})
err = vm.RemoveDevice(op, true, disks...)
if err != nil {
return nil, err
}
return vm.Destroy(op)
}
func (vm *VirtualMachine) VMPathName(ctx context.Context) (string, error) {
op := trace.FromContext(ctx, "VMPathName")
var err error
var mvm mo.VirtualMachine
if err = vm.Properties(op, vm.Reference(), []string{"config.files.vmPathName"}, &mvm); err != nil {
op.Errorf("Unable to get vm config.files property: %s", err)
return "", err
}
return mvm.Config.Files.VmPathName, nil
}
// GetCurrentSnapshotTree returns current snapshot, with tree information
func (vm *VirtualMachine) GetCurrentSnapshotTree(ctx context.Context) (*types.VirtualMachineSnapshotTree, error) {
op := trace.FromContext(ctx, "GetCurrentSnapshotTree")
var err error
var mvm mo.VirtualMachine
if err = vm.Properties(op, vm.Reference(), []string{"snapshot"}, &mvm); err != nil {
op.Infof("Unable to get vm properties: %s", err)
return nil, err
}
if mvm.Snapshot == nil {
// no snapshot at all
return nil, nil
}
current := mvm.Snapshot.CurrentSnapshot
q := list.New()
for _, c := range mvm.Snapshot.RootSnapshotList {
q.PushBack(c)
}
compareID := func(node types.VirtualMachineSnapshotTree) bool {
if node.Snapshot == *current {
return true
}
return false
}
return vm.bfsSnapshotTree(q, compareID), nil
}
// GetCurrentSnapshotTreeByName returns current snapshot, with tree information
func (vm *VirtualMachine) GetSnapshotTreeByName(ctx context.Context, name string) (*types.VirtualMachineSnapshotTree, error) {
op := trace.FromContext(ctx, "GetSnapshotTreeByName")
var err error
var mvm mo.VirtualMachine
if err = vm.Properties(op, vm.Reference(), []string{"snapshot"}, &mvm); err != nil {
op.Infof("Unable to get vm properties: %s", err)
return nil, err
}
if mvm.Snapshot == nil {
// no snapshot at all
return nil, nil
}
q := list.New()
for _, c := range mvm.Snapshot.RootSnapshotList {
q.PushBack(c)
}
compareName := func(node types.VirtualMachineSnapshotTree) bool {
if node.Name == name {
return true
}
return false
}
return vm.bfsSnapshotTree(q, compareName), nil
}
// Finds a snapshot tree based on comparator function 'compare' via a breadth first search of the snapshot tree attached to the VM
func (vm *VirtualMachine) bfsSnapshotTree(q *list.List, compare func(node types.VirtualMachineSnapshotTree) bool) *types.VirtualMachineSnapshotTree {
if q.Len() == 0 {
return nil
}
e := q.Front()
tree := q.Remove(e).(types.VirtualMachineSnapshotTree)
if compare(tree) {
return &tree
}
for _, c := range tree.ChildSnapshotList {
q.PushBack(c)
}
return vm.bfsSnapshotTree(q, compare)
}
// IsConfigureSnapshot is the helper func that returns true if node is a snapshot with specified name prefix
func IsConfigureSnapshot(node *types.VirtualMachineSnapshotTree, prefix string) bool {
return node != nil && strings.HasPrefix(node.Name, prefix)
}
func (vm *VirtualMachine) registerVM(op trace.Operation, path, name string,
vapp, pool, host *types.ManagedObjectReference, vmfolder *object.Folder) (*object.Task, error) {
op.Debugf("Register VM %s", name)
if vapp == nil {
var hostObject *object.HostSystem
if host != nil {
hostObject = object.NewHostSystem(vm.Vim25(), *host)
}
poolObject := object.NewResourcePool(vm.Vim25(), *pool)
return vmfolder.RegisterVM(op, path, name, false, poolObject, hostObject)
}
req := types.RegisterChildVM_Task{
This: vapp.Reference(),
Path: path,
Host: host,
}
if name != "" {
req.Name = name
}
res, err := methods.RegisterChildVM_Task(op, vm.Vim25(), &req)
if err != nil {
return nil, err
}
return object.NewTask(vm.Vim25(), res.Returnval), nil
}
func (vm *VirtualMachine) IsFixing() bool {
return vm.fixing > 0
}
func (vm *VirtualMachine) EnterFixingState() {
atomic.AddInt32(&vm.fixing, 1)
}
func (vm *VirtualMachine) LeaveFixingState() {
atomic.StoreInt32(&vm.fixing, 0)
}
// FixInvalidState fix vm invalid state issue through unregister & register
func (vm *VirtualMachine) fixVM(op trace.Operation) error {
op.Debugf("Fix invalid state VM: %s", vm.Reference())
properties := []string{"summary.config", "summary.runtime.host", "resourcePool", "parentVApp"}
op.Debugf("Get vm properties %s", properties)
var mvm mo.VirtualMachine
if err := vm.VirtualMachine.Properties(op, vm.Reference(), properties, &mvm); err != nil {
op.Errorf("Unable to get vm properties: %s", err)
return err
}
name := mvm.Summary.Config.Name
op.Debugf("Unregister VM %s", name)
vm.EnterFixingState()
if err := vm.Unregister(op); err != nil {
op.Errorf("Unable to unregister vm %q: %s", name, err)
// Leave fixing state since it will not be reset in the remove event handler
vm.LeaveFixingState()
return err
}
task, err := vm.registerVM(op, mvm.Summary.Config.VmPathName, name, mvm.ParentVApp, mvm.ResourcePool, mvm.Summary.Runtime.Host, vm.Session.VMFolder)
if err != nil {
op.Errorf("Unable to register VM %q back: %s", name, err)
return err
}
info, err := task.WaitForResult(op, nil)
if err != nil {
return err
}
// re-register vm will change vm reference, so reset the object reference here
if info.Error != nil {
return errors.New(info.Error.LocalizedMessage)
}
// set new registered vm attribute back
newRef := info.Result.(types.ManagedObjectReference)
common := object.NewCommon(vm.Vim25(), newRef)
common.InventoryPath = vm.InventoryPath
vm.Common = common
return nil
}
func (vm *VirtualMachine) needsFix(op trace.Operation, err error) bool {
if err == nil {
return false
}
if vm.IsInvalidState(op) {
op.Debugf("vm %s is invalid", vm.Reference())
return true
}
return false
}
func (vm *VirtualMachine) IsInvalidState(ctx context.Context) bool {
op := trace.FromContext(ctx, "IsInvalidState")
var o mo.VirtualMachine
if err := vm.VirtualMachine.Properties(op, vm.Reference(), []string{"summary.runtime.connectionState"}, &o); err != nil {
op.Debugf("Failed to get vm properties: %s", err)
return false
}
if o.Summary.Runtime.ConnectionState == types.VirtualMachineConnectionStateInvalid {
return true
}
return false
}
// WaitForResult is designed to handle VM invalid state error for any VM operations.
// It will call tasks.WaitForResult to retry if there is task in progress error.
func (vm *VirtualMachine) WaitForResult(ctx context.Context, f func(context.Context) (tasks.Task, error)) (*types.TaskInfo, error) {
op := trace.FromContext(ctx, "WaitForResult")
info, err := tasks.WaitForResult(op, f)
if err == nil || !vm.needsFix(op, err) {
return info, err
}
op.Debugf("Try to fix task failure %s", err)
if nerr := vm.fixVM(op); nerr != nil {
op.Errorf("Failed to fix task failure: %s", nerr)
return info, err
}
op.Debug("Fixed")
return tasks.WaitForResult(op, f)
}
func (vm *VirtualMachine) Properties(ctx context.Context, r types.ManagedObjectReference, ps []string, o *mo.VirtualMachine) error {
// lets ensure we have an operation
op := trace.FromContext(ctx, "VM Properties")
defer trace.End(trace.Begin(fmt.Sprintf("VM(%s) Properties(%s)", r, ps), op))
contains := false
for _, v := range ps {
if v == "summary" || v == "summary.runtime" {
contains = true
break
}
}
if !contains {
ps = append(ps, "summary.runtime.connectionState")
}
op.Debugf("properties: %s", ps)
if err := vm.VirtualMachine.Properties(op, r, ps, o); err != nil {
return err
}
if o.Summary.Runtime.ConnectionState != types.VirtualMachineConnectionStateInvalid {
return nil
}
op.Infof("vm %s is in invalid state", r)
if err := vm.fixVM(op); err != nil {
op.Errorf("Failed to fix vm %s: %s", vm.Reference(), err)
return &InvalidState{r: vm.Reference()}
}
return vm.VirtualMachine.Properties(op, vm.Reference(), ps, o)
}
func (vm *VirtualMachine) Parent(ctx context.Context) (*types.ManagedObjectReference, error) {
op := trace.FromContext(ctx, "Parent")
var mvm mo.VirtualMachine
if err := vm.Properties(op, vm.Reference(), []string{"parentVApp", "resourcePool"}, &mvm); err != nil {
op.Errorf("Unable to get VM parent: %s", err)
return nil, err
}
if mvm.ParentVApp != nil {
return mvm.ParentVApp, nil
}
return mvm.ResourcePool, nil
}
func (vm *VirtualMachine) DatastoreReference(ctx context.Context) ([]types.ManagedObjectReference, error) {
op := trace.FromContext(ctx, "DatastoreReference")
var mvm mo.VirtualMachine
if err := vm.Properties(op, vm.Reference(), []string{"datastore"}, &mvm); err != nil {
op.Errorf("Unable to get VM datastore: %s", err)
return nil, err
}
return mvm.Datastore, nil
}
// VCHUpdateStatus tells if an upgrade/configure has already been started based on the UpdateInProgress flag in ExtraConfig
// It returns the error if the vm operation does not succeed
func (vm *VirtualMachine) VCHUpdateStatus(ctx context.Context) (bool, error) {
op := trace.FromContext(ctx, "VCHUpdateStatus")
info, err := vm.FetchExtraConfig(op)
if err != nil {
op.Errorf("Unable to get vm ExtraConfig: %s", err)
return false, err
}
if v, ok := info[UpdateStatus]; ok {
status, err := strconv.ParseBool(v)
if err != nil {
// If error occurs, the bool return value does not matter for the caller.
return false, fmt.Errorf("failed to parse %s to bool: %s", v, err)
}
return status, nil
}
// If UpdateStatus is not found, it might be the case that no upgrade/configure has been done to this VCH before
return false, nil
}
// SetVCHUpdateStatus sets the VCH update status in ExtraConfig
func (vm *VirtualMachine) SetVCHUpdateStatus(ctx context.Context, status bool) error {
op := trace.FromContext(ctx, "SetVCHUpdateStatus")
info := make(map[string]string)
info[UpdateStatus] = strconv.FormatBool(status)
s := &types.VirtualMachineConfigSpec{
ExtraConfig: vmomi.OptionValueFromMap(info, true),
}
_, err := vm.WaitForResult(op, func(op context.Context) (tasks.Task, error) {
return vm.Reconfigure(op, *s)
})
return err
}
// DisableDestroy attempts to disable the VirtualMachine.Destroy_Task method.
// When the method is disabled, the VC UI will disable/grey out the VM "delete" action.
// Requires the "Global.Disable" VC privilege.
// The VirtualMachine.Destroy_Task method can still be invoked via the API.
func (vm *VirtualMachine) DisableDestroy(ctx context.Context) {
if !vm.IsVC() {
return
}
op := trace.FromContext(ctx, "DisableDestroy")
m := object.NewAuthorizationManager(vm.Vim25())
method := []object.DisabledMethodRequest{
{
Method: DestroyTask,
Reason: "Managed by VIC Engine",
},
}
obj := []types.ManagedObjectReference{vm.Reference()}
err := m.DisableMethods(op, obj, method, "VIC")
if err != nil {
op.Warnf("Failed to disable method %s for %s: %s", method[0].Method, obj[0], err)
}
}
// EnableDestroy attempts to enable the VirtualMachine.Destroy_Task method
// so that the VC UI can enable the VM "delete" action.
func (vm *VirtualMachine) EnableDestroy(ctx context.Context) {
if !vm.IsVC() {
return
}
op := trace.FromContext(ctx, "EnableDestroy")
m := object.NewAuthorizationManager(vm.Vim25())
obj := []types.ManagedObjectReference{vm.Reference()}
err := m.EnableMethods(op, obj, []string{DestroyTask}, "VIC")
if err != nil {
op.Warnf("Failed to enable Destroy_Task for %s: %s", obj[0], err)
}
}
// RemoveSnapshot removes a snapshot by reference
func (vm *VirtualMachine) RemoveSnapshotByRef(ctx context.Context, snapshot *types.ManagedObjectReference, removeChildren bool, consolidate *bool) (*object.Task, error) {
req := types.RemoveSnapshot_Task{
This: snapshot.Reference(),
RemoveChildren: removeChildren,
Consolidate: consolidate,
}
res, err := methods.RemoveSnapshot_Task(ctx, vm.Vim25(), &req)
if err != nil {
return nil, err
}
return object.NewTask(vm.Vim25(), res.Returnval), nil
}

777
vendor/github.com/vmware/vic/pkg/vsphere/vm/vm_test.go generated vendored Normal file
View File

@@ -0,0 +1,777 @@
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package vm
import (
"container/list"
"context"
"fmt"
"math"
"math/rand"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/simulator"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/lib/guest"
"github.com/vmware/vic/pkg/trace"
"github.com/vmware/vic/pkg/vsphere/extraconfig/vmomi"
"github.com/vmware/vic/pkg/vsphere/session"
"github.com/vmware/vic/pkg/vsphere/sys"
"github.com/vmware/vic/pkg/vsphere/tasks"
"github.com/vmware/vic/pkg/vsphere/test"
)
func CreateVM(ctx context.Context, session *session.Session, host *object.HostSystem, name string) (*types.ManagedObjectReference, error) {
// Create the spec config
specconfig := test.SpecConfig(session, name)
// Create a linux guest
linux, err := guest.NewLinuxGuest(ctx, session, specconfig)
if err != nil {
return nil, err
}
// Create the vm
task, err := session.VMFolder.CreateVM(ctx, *linux.Spec().Spec(), session.Pool, host)
if err != nil {
return nil, err
}
info, err := task.WaitForResult(ctx, nil)
if err != nil {
return nil, err
}
moref := info.Result.(types.ManagedObjectReference)
// Return the moRef
return &moref, nil
}
func TestDeleteExceptDisk(t *testing.T) {
s := os.Getenv("DRONE")
if s != "" {
t.Skip("Skipping: test must be run in a VM")
}
ctx := context.Background()
session := test.Session(ctx, t)
defer session.Logout(ctx)
host := test.PickRandomHost(ctx, session, t)
uuid, err := sys.UUID()
if err != nil {
t.Fatalf("unable to get UUID for guest - used for VM name: %s", err)
}
name := fmt.Sprintf("%s-%d", uuid, rand.Intn(math.MaxInt32))
moref, err := CreateVM(ctx, session, host, name)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
// Wrap the result with our version of VirtualMachine
vm := NewVirtualMachine(ctx, session, *moref)
folder, err := vm.FolderName(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
// generate the disk name
diskName := fmt.Sprintf("%s/%s.vmdk", folder, folder)
// Delete the VM but not it's disk
task, err := vm.DeleteExceptDisks(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
_, err = task.WaitForResult(ctx, nil)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
// check that the disk still exists
session.Datastore.Stat(ctx, diskName)
if err != nil {
t.Fatalf("Disk does not exist")
}
// clean up
dm := object.NewVirtualDiskManager(session.Client.Client)
task, err = dm.DeleteVirtualDisk(context.TODO(), diskName, nil)
if err != nil {
t.Fatalf("Unable to locate orphan vmdk: %s", err)
}
if err = task.Wait(context.TODO()); err != nil {
t.Fatalf("Unable to remove orphan vmdk: %s", err)
}
}
func TestVM(t *testing.T) {
s := os.Getenv("DRONE")
if s != "" {
t.Skip("Skipping: test must be run in a VM")
}
ctx := context.Background()
session := test.Session(ctx, t)
defer session.Logout(ctx)
host := test.PickRandomHost(ctx, session, t)
uuid, err := sys.UUID()
if err != nil {
t.Fatalf("unable to get UUID for guest - used for VM name: %s", err)
return
}
name := fmt.Sprintf("%s-%d", uuid, rand.Intn(math.MaxInt32))
moref, err := CreateVM(ctx, session, host, name)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
// Wrap the result with our version of VirtualMachine
vm := NewVirtualMachine(ctx, session, *moref)
// Check the state
state, err := vm.PowerState(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
assert.Equal(t, types.VirtualMachinePowerStatePoweredOff, state)
// Check VM name
rname, err := vm.Name(ctx)
if err != nil {
t.Errorf("Failed to load VM name: %s", err)
}
assert.Equal(t, name, rname)
// Get VM UUID
ruuid, err := vm.UUID(ctx)
if err != nil {
t.Errorf("Failed to load VM UUID: %s", err)
}
t.Logf("Got UUID: %s", ruuid)
err = vm.fixVM(trace.FromContext(ctx, "TestVM"))
if err != nil {
t.Errorf("Failed to fix vm: %s", err)
}
newVM, err := session.Finder.VirtualMachine(ctx, name)
if err != nil {
t.Errorf("Failed to find fixed vm: %s", err)
}
assert.Equal(t, vm.Reference(), newVM.Reference())
// VM properties
var ovm mo.VirtualMachine
if err = vm.Properties(ctx, newVM.Reference(), []string{"config"}, &ovm); err != nil {
t.Errorf("Failed to get vm properties: %s", err)
}
// Destroy the vm
task, err := vm.Destroy(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
_, err = task.WaitForResult(ctx, nil)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
}
func TestVMFailureWithTimeout(t *testing.T) {
ctx := context.Background()
session := test.Session(ctx, t)
defer session.Logout(ctx)
host := test.PickRandomHost(ctx, session, t)
ctx, cancel := context.WithTimeout(ctx, 1*time.Microsecond)
defer cancel()
uuid, err := sys.UUID()
if err != nil {
t.Fatalf("unable to get UUID for guest - used for VM name: %s", err)
return
}
name := fmt.Sprintf("%s-%d", uuid, rand.Intn(math.MaxInt32))
_, err = CreateVM(ctx, session, host, name)
if err != nil && err != context.DeadlineExceeded {
t.Fatalf("ERROR: %s", err)
}
}
func TestVMAttributes(t *testing.T) {
ctx := context.Background()
session := test.Session(ctx, t)
defer session.Logout(ctx)
host := test.PickRandomHost(ctx, session, t)
uuid, err := sys.UUID()
if err != nil {
t.Fatalf("unable to get UUID for guest - used for VM name: %s", err)
return
}
ID := fmt.Sprintf("%s-%d", uuid, rand.Intn(math.MaxInt32))
moref, err := CreateVM(ctx, session, host, ID)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
// Wrap the result with our version of VirtualMachine
vm := NewVirtualMachine(ctx, session, *moref)
folder, err := vm.FolderName(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
name, err := vm.Name(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
assert.Equal(t, name, folder)
task, err := vm.PowerOn(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
_, err = task.WaitForResult(ctx, nil)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
if guest, err := vm.FetchExtraConfig(ctx); err != nil {
t.Fatalf("ERROR: %s", err)
} else {
assert.NotEmpty(t, guest)
}
defer func() {
// Destroy the vm
task, err := vm.PowerOff(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
_, err = task.WaitForResult(ctx, nil)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
task, err = vm.Destroy(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
_, err = task.WaitForResult(ctx, nil)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
}()
}
func TestWaitForKeyInExtraConfig(t *testing.T) {
ctx := context.Background()
m := simulator.ESX()
defer m.Remove()
err := m.Create()
if err != nil {
t.Fatal(err)
}
server := m.Service.NewServer()
defer server.Close()
config := &session.Config{
Service: server.URL.String(),
}
s, err := session.NewSession(config).Connect(ctx)
if err != nil {
t.Fatal(err)
}
if s, err = s.Populate(ctx); err != nil {
t.Fatal(err)
}
vms, err := s.Finder.VirtualMachineList(ctx, "*")
if err != nil {
t.Fatal(err)
}
vm := NewVirtualMachineFromVM(ctx, s, vms[0])
opt := &types.OptionValue{Key: "foo", Value: "bar"}
obj := simulator.Map.Get(vm.Reference()).(*simulator.VirtualMachine)
val, err := vm.WaitForKeyInExtraConfig(ctx, opt.Key)
if err == nil {
t.Error("expected error")
}
obj.Config.ExtraConfig = append(obj.Config.ExtraConfig, opt)
obj.Summary.Runtime.PowerState = types.VirtualMachinePowerStatePoweredOn
val, err = vm.WaitForKeyInExtraConfig(ctx, opt.Key)
if err != nil {
t.Fatal(err)
}
if val != opt.Value {
t.Errorf("%s != %s", val, opt.Value)
}
}
func createSnapshotTree(prefix string, deep int, wide int) []types.VirtualMachineSnapshotTree {
var result []types.VirtualMachineSnapshotTree
if deep == 0 {
return nil
}
for i := 1; i <= wide; i++ {
nodeID := fmt.Sprintf("%s%d", prefix, i)
node := types.VirtualMachineSnapshotTree{
Snapshot: types.ManagedObjectReference{
Type: "Snapshot",
Value: nodeID,
},
Name: nodeID,
}
node.ChildSnapshotList = createSnapshotTree(nodeID, deep-1, wide)
result = append(result, node)
}
return result
}
func TestBfsSnapshotTree(t *testing.T) {
ref := &types.ManagedObjectReference{
Type: "Snapshot",
Value: "12131",
}
rootList := createSnapshotTree("", 5, 5)
ctx := context.Background()
session := test.Session(ctx, t)
defer session.Logout(ctx)
vm := NewVirtualMachine(ctx, session, *ref)
q := list.New()
for _, c := range rootList {
q.PushBack(c)
}
compareID := func(node types.VirtualMachineSnapshotTree) bool {
if node.Snapshot == *ref {
t.Logf("Found match")
return true
}
return false
}
current := vm.bfsSnapshotTree(q, compareID)
if current == nil {
t.Errorf("Should found current snapshot")
}
q = list.New()
for _, c := range rootList {
q.PushBack(c)
}
ref = &types.ManagedObjectReference{
Type: "Snapshot",
Value: "185",
}
current = vm.bfsSnapshotTree(q, compareID)
if current != nil {
t.Errorf("Should not found snapshot")
}
name := "12131"
compareName := func(node types.VirtualMachineSnapshotTree) bool {
if node.Name == name {
t.Logf("Found match")
return true
}
return false
}
q = list.New()
for _, c := range rootList {
q.PushBack(c)
}
found := vm.bfsSnapshotTree(q, compareName)
if found == nil {
t.Errorf("Should found snapshot %q", name)
}
q = list.New()
for _, c := range rootList {
q.PushBack(c)
}
name = "185"
found = vm.bfsSnapshotTree(q, compareName)
if found != nil {
t.Errorf("Should not found snapshot")
}
}
// TestProperties test vm.properties happy path and fix vm path
func TestIsFixing(t *testing.T) {
mo := types.ManagedObjectReference{Type: "vm", Value: "12"}
v := object.NewVirtualMachine(nil, mo)
vm := NewVirtualMachineFromVM(nil, nil, v)
assert.False(t, vm.IsFixing(), "new vm should not in fixing status")
vm.EnterFixingState()
assert.True(t, vm.IsFixing(), "vm should be in fixing status")
vm.EnterFixingState()
assert.True(t, vm.IsFixing(), "vm should be in fixing status")
vm.LeaveFixingState()
assert.False(t, vm.IsFixing(), "vm should not be in fixing status")
}
// TestProperties test vm.properties happy path and fix vm path
func TestProperties(t *testing.T) {
ctx := context.Background()
// Nothing VC specific in this test, so we use the simpler ESX model
model := simulator.ESX()
model.Autostart = false
defer model.Remove()
err := model.Create()
if err != nil {
t.Fatal(err)
}
server := model.Service.NewServer()
defer server.Close()
client, err := govmomi.NewClient(ctx, server.URL, true)
if err != nil {
t.Fatal(err)
}
// Any VM will do
finder := find.NewFinder(client.Client, false)
vmo, err := finder.VirtualMachine(ctx, "/ha-datacenter/vm/*_VM0")
if err != nil {
t.Fatal(err)
}
config := &session.Config{
Service: server.URL.String(),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
DatacenterPath: "",
DatastorePath: "/ha-datacenter/datastore/*",
HostPath: "/ha-datacenter/host/*/*",
PoolPath: "/ha-datacenter/host/*/Resources",
}
s, err := session.NewSession(config).Connect(ctx)
if err != nil {
t.Fatal(err)
}
s.Populate(ctx)
vmm := NewVirtualMachine(ctx, s, vmo.Reference())
// Test the success path
var o mo.VirtualMachine
err = vmm.Properties(ctx, vmo.Reference(), []string{"config", "summary", "resourcePool", "parentVApp"}, &o)
if err != nil {
t.Fatal(err)
}
// // Inject invalid connection state to vm
ref := simulator.Map.Get(vmo.Reference()).(*simulator.VirtualMachine)
ref.Summary.Config.VmPathName = ref.Config.Files.VmPathName
ref.Summary.Runtime.ConnectionState = types.VirtualMachineConnectionStateInvalid
err = vmm.Properties(ctx, vmo.Reference(), []string{"config", "summary"}, &o)
if err != nil {
t.Fatal(err)
}
assert.True(t, o.Summary.Runtime.ConnectionState != types.VirtualMachineConnectionStateInvalid, "vm state should be fixed")
}
// TestWaitForResult covers the success path and invalid vm fix path
func TestWaitForResult(t *testing.T) {
ctx := context.Background()
// Nothing VC specific in this test, so we use the simpler ESX model
model := simulator.ESX()
model.Autostart = false
defer model.Remove()
err := model.Create()
if err != nil {
t.Fatal(err)
}
server := model.Service.NewServer()
defer server.Close()
client, err := govmomi.NewClient(ctx, server.URL, true)
if err != nil {
t.Fatal(err)
}
// Any VM will do
finder := find.NewFinder(client.Client, false)
vmo, err := finder.VirtualMachine(ctx, "/ha-datacenter/vm/*_VM0")
if err != nil {
t.Fatal(err)
}
config := &session.Config{
Service: server.URL.String(),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
DatacenterPath: "",
DatastorePath: "/ha-datacenter/datastore/*",
HostPath: "/ha-datacenter/host/*/*",
PoolPath: "/ha-datacenter/host/*/Resources",
}
s, err := session.NewSession(config).Connect(ctx)
if err != nil {
t.Fatal(err)
}
s.Populate(ctx)
vmm := NewVirtualMachine(ctx, s, vmo.Reference())
// Test the success path
_, err = vmm.WaitForResult(ctx, func(ctx context.Context) (tasks.Task, error) {
return vmm.PowerOn(ctx)
})
if err != nil {
t.Fatal(err)
}
_, err = vmm.WaitForResult(ctx, func(ctx context.Context) (tasks.Task, error) {
return vmm.PowerOff(ctx)
})
if err != nil {
t.Fatal(err)
}
ref := simulator.Map.Get(vmm.Reference()).(*simulator.VirtualMachine)
// Test task failed, but vm is not in invalid state
called := 0
_, err = vmm.WaitForResult(ctx, func(ctx context.Context) (tasks.Task, error) {
called++
return vmm.PowerOff(ctx)
})
if err == nil {
t.Fatal("Should have error")
}
assert.True(t, called == 1, "task should not be retried")
// Test task failure with invalid state vm
ref.Summary.Config.VmPathName = ref.Config.Files.VmPathName
ref.Summary.Runtime.ConnectionState = types.VirtualMachineConnectionStateInvalid
called = 0
_, err = vmm.WaitForResult(ctx, func(ctx context.Context) (tasks.Task, error) {
called++
return vmm.PowerOff(ctx)
})
assert.True(t, called == 2, "task should be retried once")
assert.True(t, !vmm.IsInvalidState(ctx), "vm state should be fixed")
}
// SetUpdateStatus sets the VCH upgrade/configure status.
func SetUpdateStatus(ctx context.Context, updateStatus string, vm *VirtualMachine) error {
info := make(map[string]string)
info[UpdateStatus] = updateStatus
s := &types.VirtualMachineConfigSpec{
ExtraConfig: vmomi.OptionValueFromMap(info, true),
}
_, err := vm.WaitForResult(ctx, func(ctx context.Context) (tasks.Task, error) {
return vm.Reconfigure(ctx, *s)
})
if err != nil {
return err
}
return nil
}
// TestVCHUpdateStatus tests if VCHUpdateStatus() could obtain the correct VCH upgrade/configure status
func TestVCHUpdateStatus(t *testing.T) {
ctx := context.Background()
// Nothing VC specific in this test, so we use the simpler ESX model
model := simulator.ESX()
defer model.Remove()
err := model.Create()
if err != nil {
t.Fatal(err)
}
server := model.Service.NewServer()
defer server.Close()
client, err := govmomi.NewClient(ctx, server.URL, true)
if err != nil {
t.Fatal(err)
}
// Any VM will do
finder := find.NewFinder(client.Client, false)
vmo, err := finder.VirtualMachine(ctx, "/ha-datacenter/vm/*_VM0")
if err != nil {
t.Fatal(err)
}
config := &session.Config{
Service: server.URL.String(),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
DatacenterPath: "",
DatastorePath: "/ha-datacenter/datastore/*",
HostPath: "/ha-datacenter/host/*/*",
PoolPath: "/ha-datacenter/host/*/Resources",
}
s, err := session.NewSession(config).Connect(ctx)
if err != nil {
t.Fatal(err)
}
s.Populate(ctx)
vmm := NewVirtualMachine(ctx, s, vmo.Reference())
updateStatus, err := vmm.VCHUpdateStatus(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
assert.False(t, updateStatus, "updateStatus should be false if UpdateInProgress is not set in the VCH's ExtraConfig")
// Set UpdateInProgress to false
SetUpdateStatus(ctx, "false", vmm)
updateStatus, err = vmm.VCHUpdateStatus(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
assert.False(t, updateStatus, "updateStatus should be false since UpdateInProgress is set to false")
// Set UpdateInProgress to true
SetUpdateStatus(ctx, "true", vmm)
updateStatus, err = vmm.VCHUpdateStatus(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
assert.True(t, updateStatus, "updateStatus should be true since UpdateInProgress is set to true")
// Set UpdateInProgress to NonBool
SetUpdateStatus(ctx, "NonBool", vmm)
updateStatus, err = vmm.VCHUpdateStatus(ctx)
if assert.Error(t, err, "An error was expected") {
assert.Contains(t, err.Error(), "failed to parse", "Error msg should contain 'failed to parse' since UpdateInProgress is set to NonBool")
}
}
// TestSetVCHUpdateStatus tests if SetVCHUpdateStatus() could set the VCH upgrade/configure status correctly
func TestSetVCHUpdateStatus(t *testing.T) {
ctx := context.Background()
// Nothing VC specific in this test, so we use the simpler ESX model
model := simulator.ESX()
defer model.Remove()
err := model.Create()
if err != nil {
t.Fatal(err)
}
server := model.Service.NewServer()
defer server.Close()
client, err := govmomi.NewClient(ctx, server.URL, true)
if err != nil {
t.Fatal(err)
}
// Any VM will do
finder := find.NewFinder(client.Client, false)
vmo, err := finder.VirtualMachine(ctx, "/ha-datacenter/vm/*_VM0")
if err != nil {
t.Fatal(err)
}
config := &session.Config{
Service: server.URL.String(),
Insecure: true,
Keepalive: time.Duration(5) * time.Minute,
DatacenterPath: "",
DatastorePath: "/ha-datacenter/datastore/*",
HostPath: "/ha-datacenter/host/*/*",
PoolPath: "/ha-datacenter/host/*/Resources",
}
s, err := session.NewSession(config).Connect(ctx)
if err != nil {
t.Fatal(err)
}
s.Populate(ctx)
vmm := NewVirtualMachine(ctx, s, vmo.Reference())
// Set UpdateInProgress to true and then check status
err = vmm.SetVCHUpdateStatus(ctx, true)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
info, err := vmm.FetchExtraConfig(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
v, ok := info[UpdateStatus]
if ok {
assert.Equal(t, "true", v, "UpdateInProgress should be true")
} else {
t.Fatal("ERROR: UpdateInProgress does not exist in ExtraConfig")
}
// Set UpdateInProgress to false and then check status
err = vmm.SetVCHUpdateStatus(ctx, false)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
info, err = vmm.FetchExtraConfig(ctx)
if err != nil {
t.Fatalf("ERROR: %s", err)
}
v, ok = info[UpdateStatus]
if ok {
assert.Equal(t, "false", v, "UpdateInProgress should be false")
} else {
t.Fatal("ERROR: UpdateInProgress does not exist in ExtraConfig")
}
}