Fix the dependency issue (#231)

This commit is contained in:
Robbie Zhang
2018-06-21 12:09:42 -07:00
committed by GitHub
parent 027b76651d
commit 6ec1098bb8
16629 changed files with 74837 additions and 4975021 deletions

View File

@@ -1,142 +0,0 @@
// 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

@@ -1,103 +0,0 @@
// 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

@@ -1,43 +0,0 @@
// 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

@@ -1,395 +0,0 @@
// 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

@@ -1,219 +0,0 @@
// 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

@@ -1,83 +0,0 @@
// 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
}

View File

@@ -1,152 +0,0 @@
// 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

@@ -1,117 +0,0 @@
// 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

@@ -1,39 +0,0 @@
// 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,
}
}

View File

@@ -1,102 +0,0 @@
// 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
}

View File

@@ -1,473 +0,0 @@
// 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

@@ -1,292 +0,0 @@
// 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

@@ -1,668 +0,0 @@
// 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

@@ -1,739 +0,0 @@
// 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

@@ -1,238 +0,0 @@
// 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")
}

View File

@@ -1,339 +0,0 @@
// 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

@@ -1,33 +0,0 @@
// 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)
}
}

View File

@@ -1,125 +0,0 @@
// 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

@@ -1,454 +0,0 @@
// 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

@@ -1,537 +0,0 @@
// 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

@@ -1,21 +0,0 @@
// 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

@@ -1,269 +0,0 @@
// 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

@@ -1,67 +0,0 @@
// 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

@@ -1,55 +0,0 @@
// 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

@@ -1,140 +0,0 @@
// 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

@@ -1,75 +0,0 @@
// 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

@@ -1,37 +0,0 @@
// 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

@@ -1,49 +0,0 @@
// 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

@@ -1,58 +0,0 @@
// 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

@@ -1,65 +0,0 @@
// 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,226 @@
// Copyright 2018 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"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/performance"
"github.com/vmware/govmomi/property"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/mo"
"github.com/vmware/govmomi/vim25/types"
"github.com/vmware/vic/pkg/trace"
)
const (
// cpuUsage measures the actively used CPU of the host, as a percentage of the total available CPU.
cpuUsage = "cpu.usage.average"
// memActive measures the sum of all active metrics for all powered-on VMs plus vSphere services on the host.
memActive = "mem.active.average"
// memConsumed measures the amount of machine memory used on the host, including vSphere services, VMkernel,
// the service console and the total consumed memory metrics for all running VMs.
memConsumed = "mem.consumed.average"
// memTotalCapacity measures the total amount of memory reservation used by and available for powered-on VMs
// and vSphere services on the host.
memTotalCapacity = "mem.totalCapacity.average"
// memOverhead measures the total of all overhead metrics for powered-on VMs, plus the overhead of running
// vSphere services on the host.
memOverhead = "mem.overhead.average"
)
// HostMemory stores an ESXi host's memory metrics.
type HostMemory struct {
ActiveKB int64
ConsumedKB int64
OverheadKB int64
TotalKB int64
}
// HostCPU stores an ESXi host's CPU metrics.
type HostCPU struct {
UsagePercent float64
}
// HostMetricsInfo stores an ESXi host's memory and CPU metrics.
type HostMetricsInfo struct {
Memory HostMemory
CPU HostCPU
}
// HostMetricsProvider returns CPU and memory metrics for all ESXi hosts in a cluster
// via implementation of the MetricsProvider interface.
type HostMetricsProvider struct {
*vim25.Client
}
// NewHostMetricsProvider returns a new instance of HostMetricsProvider.
func NewHostMetricsProvider(c *vim25.Client) *HostMetricsProvider {
return &HostMetricsProvider{Client: c}
}
// GetMetricsForComputeResource gathers host metrics from the supplied compute resource.
// Returned map is keyed on the host ManagedObjectReference in string form.
func (h *HostMetricsProvider) GetMetricsForComputeResource(op trace.Operation, cr *object.ComputeResource) (map[string]*HostMetricsInfo, error) {
// Gather hosts from the session cluster and then obtain their morefs.
hosts, err := cr.Hosts(op)
if err != nil {
return nil, fmt.Errorf("unable to obtain host morefs from compute resource: %s", err)
}
if hosts == nil {
return nil, fmt.Errorf("no hosts found in compute resource")
}
return h.GetMetricsForHosts(op, hosts)
}
// GetMetricsForHosts returns metrics pertaining to supplied ESX hosts.
// Returned map is keyed on the host ManagedObjectReference in string form.
func (h *HostMetricsProvider) GetMetricsForHosts(op trace.Operation, hosts []*object.HostSystem) (map[string]*HostMetricsInfo, error) {
if len(hosts) == 0 {
return nil, fmt.Errorf("no hosts provided")
}
if h.Client == nil {
return nil, fmt.Errorf("client not set")
}
// filter out hosts that are in maintenance mode or disconnected
hosts, err := filterHosts(op, h.Client, hosts)
if err != nil {
return nil, err
}
morefToHost := make(map[string]*object.HostSystem)
morefs := make([]types.ManagedObjectReference, len(hosts))
for i, host := range hosts {
moref := host.Reference()
morefToHost[moref.String()] = host
morefs[i] = moref
}
// Query CPU and memory metrics for the morefs.
spec := types.PerfQuerySpec{
Format: string(types.PerfFormatNormal),
IntervalId: sampleInterval,
}
counters := []string{cpuUsage, memActive, memConsumed, memTotalCapacity, memOverhead}
perfMgr := performance.NewManager(h.Client)
sample, err := perfMgr.SampleByName(op.Context, spec, counters, morefs)
if err != nil {
errStr := "unable to get metric sample: %s"
op.Errorf(errStr, err)
return nil, fmt.Errorf(errStr, err)
}
results, err := perfMgr.ToMetricSeries(op.Context, sample)
if err != nil {
errStr := "unable to convert metric sample to metric series: %s"
op.Errorf(errStr, err)
return nil, fmt.Errorf(errStr, err)
}
metrics := assembleMetrics(op, morefToHost, results)
return metrics, nil
}
// assembleMetrics processes the metric samples received from govmomi and returns a finalized metrics map
// keyed by the hosts.
func assembleMetrics(op trace.Operation, morefToHost map[string]*object.HostSystem,
results []performance.EntityMetric) map[string]*HostMetricsInfo {
metrics := make(map[string]*HostMetricsInfo)
for _, host := range morefToHost {
metrics[host.Reference().String()] = &HostMetricsInfo{}
}
for i := range results {
res := results[i]
host, exists := morefToHost[res.Entity.String()]
if !exists {
op.Warnf("moref %s does not exist in requested morefs, skipping", res.Entity.String())
continue
}
ref := host.Reference().String()
// Process each value and assign it directly to the corresponding metric field
// since there is only one sample.
for _, v := range res.Value {
// We don't need to collect non-aggregate (non-empty Instance) metrics.
if v.Instance != "" {
continue
}
if len(v.Value) == 0 {
op.Warnf("metric %s for moref %s has no value, skipping", v.Name, res.Entity.String())
continue
}
switch v.Name {
case cpuUsage:
// Convert percent units from 1/100th of a percent (100 = 1%) to a human-readable percentage.
metrics[ref].CPU.UsagePercent = float64(v.Value[0]) / 100.0
case memActive:
metrics[ref].Memory.ActiveKB = v.Value[0]
case memConsumed:
metrics[ref].Memory.ConsumedKB = v.Value[0]
case memOverhead:
metrics[ref].Memory.OverheadKB = v.Value[0]
case memTotalCapacity:
// Total capacity is in MB, convert to KB so as to have all memory values in KB.
metrics[ref].Memory.TotalKB = v.Value[0] * 1024
}
}
}
return metrics
}
// filterHosts removes candidate hosts who are either disconnected or in maintenance mode.
func filterHosts(op trace.Operation, client *vim25.Client, hosts []*object.HostSystem) ([]*object.HostSystem, error) {
if len(hosts) == 0 {
return nil, fmt.Errorf("no candidate hosts to filter check")
}
props := []string{"summary.runtime"}
refs := make([]types.ManagedObjectReference, 0, len(hosts))
for _, h := range hosts {
refs = append(refs, h.Reference())
}
hs := make([]mo.HostSystem, 0, len(hosts))
pc := property.DefaultCollector(client)
err := pc.Retrieve(op, refs, props, &hs)
if err != nil {
return nil, err
}
result := hosts[:0]
for i, h := range hs {
if h.Summary.Runtime.ConnectionState == types.HostSystemConnectionStateConnected && !h.Summary.Runtime.InMaintenanceMode {
result = append(result, hosts[i])
}
}
return result, nil
}

View File

@@ -0,0 +1,30 @@
// Copyright 2018 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 (
"github.com/vmware/govmomi/object"
"github.com/vmware/vic/pkg/trace"
)
// MetricsProvider defines the interface for providing metrics.
type MetricsProvider interface {
// GetMetricsForComputeResource returns metrics for a particular compute resource. The metrics are
// returned in a map of HostMetricsInfo keyed on host ManagedObjectReferences.
GetMetricsForComputeResource(trace.Operation, *object.ComputeResource) (map[string]*HostMetricsInfo, error)
// GetMetricsForHosts returns metrics pertaining to supplied ESX hosts.
GetMetricsForHosts(trace.Operation, []*object.HostSystem) (map[string]*HostMetricsInfo, error)
}

View File

@@ -26,13 +26,12 @@ import (
"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
vm *object.VirtualMachine
id string
pub *pubsub.Publisher
@@ -171,7 +170,7 @@ func newVMSubscription(op trace.Operation, session *session.Session, moref types
}
sub := &vmSubscription{
vm: vm.NewVirtualMachine(op.Context, session, moref),
vm: object.NewVirtualMachine(session.Vim25(), moref),
deviceInstanceToKey: make(map[string]string),
}

View File

@@ -1,423 +0,0 @@
// 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

@@ -1,240 +0,0 @@
// 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

@@ -1,120 +0,0 @@
// 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

@@ -1,4 +1,4 @@
// Copyright 2016 VMware, Inc. All Rights Reserved.
// Copyright 2016-2018 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.
@@ -91,9 +91,14 @@ type Session struct {
Host *object.HostSystem
Pool *object.ResourcePool
// Default vSphere VMFolder
VMFolder *object.Folder
// Folder where appliance is located
VCHFolder *object.Folder
Finder *find.Finder
DRSEnabled *bool
}
// RoundTripFunc alias
@@ -122,9 +127,7 @@ func LimitConcurrency(rt http.RoundTripper, limit int) http.RoundTripper {
})
}
// 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.
// NewSession creates a new Session struct.
func NewSession(config *Config) *Session {
return &Session{Config: config}
}
@@ -299,9 +302,12 @@ func (s *Session) Connect(ctx context.Context) (*Session, error) {
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.
// Populate caches resources on the session object. These resources
// are based off of the config provided at session creation.
//
// vic specific:
// The values that end in Path (DataCenterPath, ClusterPath, etc..) are
// either from the CLI or have been retreived from the appliance extraConfig
func (s *Session) Populate(ctx context.Context) (*Session, error) {
op := trace.FromContext(ctx, "Populate")
@@ -327,6 +333,14 @@ func (s *Session) Populate(ctx context.Context) (*Session, error) {
errs = append(errs, fmt.Sprintf("Failure finding cluster (%s): %s", s.ClusterPath, err.Error()))
} else {
op.Debugf("Cached cluster: %s", s.ClusterPath)
// if we have a cluster lets get DRS Status
if s.Cluster != nil && s.Cluster.Reference().Type == "ClusterComputeResource" {
cc := object.NewClusterComputeResource(s.Client.Client, s.Cluster.Reference())
clusterConfig, _ := cc.Configuration(op)
if clusterConfig != nil {
s.DRSEnabled = clusterConfig.DrsConfig.Enabled
}
}
}
s.Datastore, err = finder.DatastoreOrDefault(op, s.DatastorePath)
@@ -360,6 +374,14 @@ func (s *Session) Populate(ctx context.Context) (*Session, error) {
op.Debugf("Cached folders: %s", s.DatacenterPath)
}
s.VMFolder = folders.VmFolder
// We don't persist the VCH folder location so set
// the VCH folder to the default VM folder.
// The actual location of the VCH will be determined later
// and this folder ref will be updated accordingly.
//
// This will provide standalone ESXi and backwards
// compatibility to non-folder versions.
s.VCHFolder = folders.VmFolder
}
if len(errs) > 0 {

View File

@@ -1,186 +0,0 @@
// 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

@@ -1,212 +0,0 @@
// 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

@@ -1,202 +0,0 @@
// 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

@@ -1,135 +0,0 @@
// 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
}

View File

@@ -1,255 +0,0 @@
// 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

@@ -1,207 +0,0 @@
// 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

@@ -1,440 +0,0 @@
// 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

@@ -1,38 +0,0 @@
// 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
}

View File

@@ -1,125 +0,0 @@
// 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))]
}

View File

@@ -1,680 +0,0 @@
// 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
}

View File

@@ -1,777 +0,0 @@
// 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")
}
}