VMware vSphere Integrated Containers provider (#206)
* Add Virtual Kubelet provider for VIC Initial virtual kubelet provider for VMware VIC. This provider currently handles creating and starting of a pod VM via the VIC portlayer and persona server. Image store handling via the VIC persona server. This provider currently requires the feature/wolfpack branch of VIC. * Added pod stop and delete. Also added node capacity. Added the ability to stop and delete pod VMs via VIC. Also retrieve node capacity information from the VCH. * Cleanup and readme file Some file clean up and added a Readme.md markdown file for the VIC provider. * Cleaned up errors, added function comments, moved operation code 1. Cleaned up error handling. Set standard for creating errors. 2. Added method prototype comments for all interface functions. 3. Moved PodCreator, PodStarter, PodStopper, and PodDeleter to a new folder. * Add mocking code and unit tests for podcache, podcreator, and podstarter Used the unit test framework used in VIC to handle assertions in the provider's unit test. Mocking code generated using OSS project mockery, which is compatible with the testify assertion framework. * Vendored packages for the VIC provider Requires feature/wolfpack branch of VIC and a few specific commit sha of projects used within VIC. * Implementation of POD Stopper and Deleter unit tests (#4) * Updated files for initial PR
This commit is contained in:
142
vendor/github.com/vmware/vic/pkg/vsphere/compute/rp.go
generated
vendored
Normal file
142
vendor/github.com/vmware/vic/pkg/vsphere/compute/rp.go
generated
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package compute
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/vim25/mo"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
|
||||
"github.com/vmware/vic/pkg/errors"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/vm"
|
||||
)
|
||||
|
||||
// ResourcePool struct defines the ResourcePool which provides additional
|
||||
// VIC specific methods over object.ResourcePool as well as keeps some state
|
||||
type ResourcePool struct {
|
||||
*object.ResourcePool
|
||||
|
||||
*session.Session
|
||||
}
|
||||
|
||||
// NewResourcePool returns a New ResourcePool object
|
||||
func NewResourcePool(ctx context.Context, session *session.Session, moref types.ManagedObjectReference) *ResourcePool {
|
||||
return &ResourcePool{
|
||||
ResourcePool: object.NewResourcePool(
|
||||
session.Vim25(),
|
||||
moref,
|
||||
),
|
||||
Session: session,
|
||||
}
|
||||
}
|
||||
|
||||
func (rp *ResourcePool) GetChildrenVMs(ctx context.Context, s *session.Session) ([]*vm.VirtualMachine, error) {
|
||||
op := trace.FromContext(ctx, "GetChildrenVMs")
|
||||
|
||||
var err error
|
||||
var mrp mo.ResourcePool
|
||||
var vms []*vm.VirtualMachine
|
||||
|
||||
if err = rp.Properties(op, rp.Reference(), []string{"vm"}, &mrp); err != nil {
|
||||
op.Errorf("Unable to get children vm of resource pool %s: %s", rp.Name(), err)
|
||||
return vms, err
|
||||
}
|
||||
|
||||
for _, o := range mrp.Vm {
|
||||
v := vm.NewVirtualMachine(op, s, o)
|
||||
vms = append(vms, v)
|
||||
}
|
||||
return vms, nil
|
||||
}
|
||||
|
||||
func (rp *ResourcePool) GetChildVM(ctx context.Context, s *session.Session, name string) (*vm.VirtualMachine, error) {
|
||||
op := trace.FromContext(ctx, "GetChildVM")
|
||||
|
||||
searchIndex := object.NewSearchIndex(s.Client.Client)
|
||||
child, err := searchIndex.FindChild(op, rp.Reference(), name)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("Unable to find VM(%s): %s", name, err.Error())
|
||||
}
|
||||
if child == nil {
|
||||
return nil, nil
|
||||
}
|
||||
// instantiate the vm object
|
||||
return vm.NewVirtualMachine(op, s, child.Reference()), nil
|
||||
}
|
||||
|
||||
func (rp *ResourcePool) GetCluster(ctx context.Context) (*object.ComputeResource, error) {
|
||||
op := trace.FromContext(ctx, "GetCluster")
|
||||
|
||||
var err error
|
||||
var mrp mo.ResourcePool
|
||||
|
||||
if err = rp.Properties(op, rp.Reference(), []string{"owner"}, &mrp); err != nil {
|
||||
op.Errorf("Unable to get cluster of resource pool %s: %s", rp.Name(), err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return object.NewComputeResource(rp.Client.Client, mrp.Owner), nil
|
||||
}
|
||||
|
||||
func (rp *ResourcePool) GetDatacenter(ctx context.Context) (*object.Datacenter, error) {
|
||||
op := trace.FromContext(ctx, "GetDatacenter")
|
||||
|
||||
dcRef, err := rp.getLowestAncestor(op, "Datacenter")
|
||||
if err != nil || dcRef == nil {
|
||||
op.Errorf("Unable to get datacenter ancestor of rp %s: %s", rp.Name(), err)
|
||||
return nil, errors.Errorf("Unable to get datacenter ancestor of rp %s: %s", rp.Name(), err)
|
||||
}
|
||||
|
||||
return object.NewDatacenter(rp.Client.Client, *dcRef), nil
|
||||
}
|
||||
|
||||
func (rp *ResourcePool) getAncestors(op trace.Operation, inType string) ([]types.ManagedObjectReference, error) {
|
||||
client := rp.Session.Vim25()
|
||||
|
||||
ancestors, err := mo.Ancestors(op, client, client.ServiceContent.PropertyCollector, rp.Reference())
|
||||
if err != nil {
|
||||
op.Errorf("Unable to get ancestors of rp %s: %s", rp.Name(), err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outAncestors := make([]types.ManagedObjectReference, 0, len(ancestors))
|
||||
for _, ancestor := range ancestors {
|
||||
if ancestor.Self.Type == inType {
|
||||
a := ancestor.Self
|
||||
outAncestors = append(outAncestors, a)
|
||||
}
|
||||
}
|
||||
|
||||
return outAncestors, nil
|
||||
}
|
||||
|
||||
func (rp *ResourcePool) getLowestAncestor(op trace.Operation, inType string) (*types.ManagedObjectReference, error) {
|
||||
ancestors, err := rp.getAncestors(op, inType)
|
||||
if err != nil {
|
||||
op.Errorf("Unable to get ancestors of rp %s: %s", rp.Name(), err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ancestors) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
index := len(ancestors) - 1
|
||||
return &ancestors[index], nil
|
||||
}
|
||||
103
vendor/github.com/vmware/vic/pkg/vsphere/compute/rp_test.go
generated
vendored
Normal file
103
vendor/github.com/vmware/vic/pkg/vsphere/compute/rp_test.go
generated
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package compute
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/vmware/govmomi/simulator"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/test"
|
||||
)
|
||||
|
||||
func TestRp(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for i, model := range []*simulator.Model{simulator.ESX(), simulator.VPX()} {
|
||||
t.Logf("%d", i)
|
||||
defer model.Remove()
|
||||
err := model.Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := model.Service.NewServer()
|
||||
defer s.Close()
|
||||
|
||||
s.URL.User = url.UserPassword("user", "pass")
|
||||
t.Logf("server URL: %s", s.URL)
|
||||
|
||||
var sess *session.Session
|
||||
if i == 0 {
|
||||
sess, err = test.SessionWithESX(ctx, s.URL.String())
|
||||
} else {
|
||||
sess, err = test.SessionWithVPX(ctx, s.URL.String())
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sess.Logout(ctx)
|
||||
testGetChildrenVMs(ctx, sess, t)
|
||||
testGetChildVM(ctx, sess, t)
|
||||
testGetCluster(ctx, sess, t)
|
||||
testGetDatacenter(ctx, sess, t)
|
||||
}
|
||||
}
|
||||
|
||||
func testGetChildrenVMs(ctx context.Context, sess *session.Session, t *testing.T) {
|
||||
rp := NewResourcePool(ctx, sess, sess.Pool.Reference())
|
||||
vms, err := rp.GetChildrenVMs(ctx, sess)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get children vm of resource pool %s, %s", rp.Name(), err)
|
||||
}
|
||||
// if vms == nil || len(vms) == 0 {
|
||||
// t.Error("Didn't get children VM")
|
||||
// }
|
||||
for _, vm := range vms {
|
||||
t.Logf("vm: %s", vm)
|
||||
}
|
||||
}
|
||||
|
||||
func testGetChildVM(ctx context.Context, sess *session.Session, t *testing.T) {
|
||||
rp := NewResourcePool(ctx, sess, sess.Pool.Reference())
|
||||
vm, err := rp.GetChildVM(ctx, sess, "random")
|
||||
if err == nil && vm != nil {
|
||||
t.Logf("vm: %s", vm.Reference())
|
||||
t.Errorf("Should not find VM random")
|
||||
}
|
||||
}
|
||||
|
||||
func testGetCluster(ctx context.Context, sess *session.Session, t *testing.T) {
|
||||
rp := NewResourcePool(ctx, sess, sess.Pool.Reference())
|
||||
cluster, err := rp.GetCluster(ctx)
|
||||
if err != nil {
|
||||
t.Logf("Failed to owner cluster: %s", err)
|
||||
t.Errorf("Should get owner")
|
||||
}
|
||||
t.Logf("Cluster: %s", cluster)
|
||||
}
|
||||
|
||||
func testGetDatacenter(ctx context.Context, sess *session.Session, t *testing.T) {
|
||||
rp := NewResourcePool(ctx, sess, sess.Pool.Reference())
|
||||
datacenter, err := rp.GetDatacenter(ctx)
|
||||
if err != nil {
|
||||
t.Logf("Failed to find parent Datacenter: %s", err)
|
||||
t.Errorf("Should get Datacenter")
|
||||
}
|
||||
t.Logf("Datacenter: %s", datacenter)
|
||||
}
|
||||
43
vendor/github.com/vmware/vic/pkg/vsphere/compute/vapp.go
generated
vendored
Normal file
43
vendor/github.com/vmware/vic/pkg/vsphere/compute/vapp.go
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package compute
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
)
|
||||
|
||||
// VirtualApp struct defines the VirtualApp which provides additional
|
||||
// VIC specific methods over object.VirtualApp as well as keeps some state
|
||||
type VirtualApp struct {
|
||||
*object.VirtualApp
|
||||
|
||||
*session.Session
|
||||
}
|
||||
|
||||
// NewResourcePool returns a New ResourcePool object
|
||||
func NewVirtualApp(ctx context.Context, session *session.Session, moref types.ManagedObjectReference) *VirtualApp {
|
||||
return &VirtualApp{
|
||||
VirtualApp: object.NewVirtualApp(
|
||||
session.Vim25(),
|
||||
moref,
|
||||
),
|
||||
Session: session,
|
||||
}
|
||||
}
|
||||
395
vendor/github.com/vmware/vic/pkg/vsphere/datastore/datastore.go
generated
vendored
Normal file
395
vendor/github.com/vmware/vic/pkg/vsphere/datastore/datastore.go
generated
vendored
Normal file
@@ -0,0 +1,395 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/vim25/mo"
|
||||
"github.com/vmware/govmomi/vim25/soap"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/tasks"
|
||||
)
|
||||
|
||||
// Helper gives access to the datastore regardless of type (esx, esx + vc,
|
||||
// or esx + vc + vsan). Also wraps paths to a given root directory
|
||||
type Helper struct {
|
||||
// The Datastore API likes everything in "path/to/thing" format.
|
||||
ds *object.Datastore
|
||||
|
||||
s *session.Session
|
||||
|
||||
// The FileManager API likes everything in "[dsname] path/to/thing" format.
|
||||
fm *object.FileManager
|
||||
|
||||
// The datastore url (including root) in "[dsname] /path" format.
|
||||
RootURL object.DatastorePath
|
||||
}
|
||||
|
||||
// NewDatastore returns a Datastore.
|
||||
// ctx is a context,
|
||||
// s is an authenticated session
|
||||
// ds is the vsphere datastore
|
||||
// rootdir is the top level directory to root all data. If root does not exist,
|
||||
// it will be created. If it already exists, NOOP. This cannot be empty.
|
||||
func NewHelper(ctx context.Context, s *session.Session, ds *object.Datastore, rootdir string) (*Helper, error) {
|
||||
op := trace.FromContext(ctx, "NewHelper")
|
||||
|
||||
d := &Helper{
|
||||
ds: ds,
|
||||
s: s,
|
||||
fm: object.NewFileManager(s.Vim25()),
|
||||
}
|
||||
|
||||
if path.IsAbs(rootdir) {
|
||||
rootdir = rootdir[1:]
|
||||
}
|
||||
|
||||
if err := d.mkRootDir(op, rootdir); err != nil {
|
||||
op.Infof("error creating root directory %s: %s", rootdir, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d.RootURL.Path == "" {
|
||||
return nil, fmt.Errorf("failed to create root directory")
|
||||
}
|
||||
|
||||
op.Infof("Datastore path is %s", d.RootURL.String())
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func NewHelperFromURL(ctx context.Context, s *session.Session, u *url.URL) (*Helper, error) {
|
||||
fm := object.NewFileManager(s.Vim25())
|
||||
vsDs, err := s.Finder.DatastoreOrDefault(ctx, u.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := &Helper{
|
||||
ds: vsDs,
|
||||
s: s,
|
||||
fm: fm,
|
||||
}
|
||||
|
||||
d.RootURL.FromString(u.Path)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func NewHelperFromSession(ctx context.Context, s *session.Session) *Helper {
|
||||
return &Helper{
|
||||
ds: s.Datastore,
|
||||
s: s,
|
||||
fm: object.NewFileManager(s.Vim25()),
|
||||
}
|
||||
}
|
||||
|
||||
// GetDatastores returns a map of datastores given a map of names and urls
|
||||
func GetDatastores(ctx context.Context, s *session.Session, dsURLs map[string]*url.URL) (map[string]*Helper, error) {
|
||||
stores := make(map[string]*Helper)
|
||||
|
||||
for name, dsURL := range dsURLs {
|
||||
d, err := NewHelperFromURL(ctx, s, dsURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stores[name] = d
|
||||
}
|
||||
|
||||
return stores, nil
|
||||
}
|
||||
|
||||
func (d *Helper) Summary(ctx context.Context) (*types.DatastoreSummary, error) {
|
||||
|
||||
var mds mo.Datastore
|
||||
if err := d.ds.Properties(ctx, d.ds.Reference(), []string{"info", "summary"}, &mds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mds.Summary, nil
|
||||
}
|
||||
|
||||
func mkdir(op trace.Operation, sess *session.Session, fm *object.FileManager, createParentDirectories bool, path string) (string, error) {
|
||||
op.Infof("Creating directory %s", path)
|
||||
|
||||
if err := fm.MakeDirectory(op, path, sess.Datacenter, createParentDirectories); err != nil {
|
||||
if soap.IsSoapFault(err) {
|
||||
soapFault := soap.ToSoapFault(err)
|
||||
if _, ok := soapFault.VimFault().(types.FileAlreadyExists); ok {
|
||||
op.Debugf("File already exists: %s", path)
|
||||
return "", os.ErrExist
|
||||
}
|
||||
}
|
||||
op.Debugf("Creating %s error: %s", path, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Mkdir creates directories.
|
||||
func (d *Helper) Mkdir(ctx context.Context, createParentDirectories bool, dirs ...string) (string, error) {
|
||||
op := trace.FromContext(ctx, "Mkdir")
|
||||
|
||||
return mkdir(op, d.s, d.fm, createParentDirectories, path.Join(d.RootURL.String(), path.Join(dirs...)))
|
||||
}
|
||||
|
||||
// Ls returns a list of dirents at the given path (relative to root)
|
||||
//
|
||||
// A note aboutpaths and the datastore browser.
|
||||
// None of these work paths work
|
||||
// r, err := ds.Ls(ctx, "ds:///vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
|
||||
// r, err := ds.Ls(ctx, "[vsanDatastore] ds:///vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/")
|
||||
// r, err := ds.Ls(ctx, "[vsanDatastore] //vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/")
|
||||
// r, err := ds.Ls(ctx, "[] ds:///vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
|
||||
// r, err := ds.Ls(ctx, "[] /vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
|
||||
// r, err := ds.Ls(ctx, "[] ../vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
|
||||
// r, err := ds.Ls(ctx, "[] ./vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
|
||||
// r, err := ds.Ls(ctx, "[52a67632ac3497a3-411916fd50bedc27] /0ea65357-0494-d42d-2ede-000c292dc5b5")
|
||||
// r, err := ds.Ls(ctx, "[vsan:52a67632ac3497a3-411916fd50bedc27] /0ea65357-0494-d42d-2ede-000c292dc5b5")
|
||||
// r, err := ds.Ls(ctx, "[vsan:52a67632ac3497a3-411916fd50bedc27] 0ea65357-0494-d42d-2ede-000c292dc5b5")
|
||||
// r, err := ds.Ls(ctx, "[vsanDatastore] /vmfs/volumes/vsan:52a67632ac3497a3-411916fd50bedc27/0ea65357-0494-d42d-2ede-000c292dc5b5")
|
||||
|
||||
// The only URI that works on VC + VSAN.
|
||||
// r, err := ds.Ls(ctx, "[vsanDatastore] /0ea65357-0494-d42d-2ede-000c292dc5b5")
|
||||
//
|
||||
func (d *Helper) Ls(ctx context.Context, p string) (*types.HostDatastoreBrowserSearchResults, error) {
|
||||
spec := types.HostDatastoreBrowserSearchSpec{
|
||||
MatchPattern: []string{"*"},
|
||||
Details: &types.FileQueryFlags{
|
||||
FileType: true,
|
||||
FileOwner: types.NewBool(true),
|
||||
},
|
||||
}
|
||||
|
||||
b, err := d.ds.Browser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task, err := b.SearchDatastore(ctx, path.Join(d.RootURL.String(), p), &spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info, err := task.WaitForResult(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := info.Result.(types.HostDatastoreBrowserSearchResults)
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// LsDirs returns a list of dirents at the given path (relative to root)
|
||||
func (d *Helper) LsDirs(ctx context.Context, p string) (*types.ArrayOfHostDatastoreBrowserSearchResults, error) {
|
||||
spec := &types.HostDatastoreBrowserSearchSpec{
|
||||
MatchPattern: []string{"*"},
|
||||
Details: &types.FileQueryFlags{
|
||||
FileType: true,
|
||||
FileOwner: types.NewBool(true),
|
||||
},
|
||||
}
|
||||
|
||||
b, err := d.ds.Browser(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task, err := b.SearchDatastoreSubFolders(ctx, path.Join(d.RootURL.String(), p), spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info, err := task.WaitForResult(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := info.Result.(types.ArrayOfHostDatastoreBrowserSearchResults)
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (d *Helper) Upload(ctx context.Context, r io.Reader, pth string) error {
|
||||
return d.ds.Upload(ctx, r, path.Join(d.RootURL.Path, pth), &soap.DefaultUpload)
|
||||
}
|
||||
|
||||
func (d *Helper) Download(ctx context.Context, pth string) (io.ReadCloser, error) {
|
||||
rc, _, err := d.ds.Download(ctx, path.Join(d.RootURL.Path, pth), &soap.DefaultDownload)
|
||||
return rc, err
|
||||
}
|
||||
|
||||
func (d *Helper) Stat(ctx context.Context, pth string) (types.BaseFileInfo, error) {
|
||||
i, err := d.ds.Stat(ctx, path.Join(d.RootURL.Path, pth))
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case object.DatastoreNoSuchDirectoryError:
|
||||
return nil, os.ErrNotExist
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (d *Helper) Mv(ctx context.Context, fromPath, toPath string) error {
|
||||
op := trace.FromContext(ctx, "Mv")
|
||||
|
||||
from := path.Join(d.RootURL.String(), fromPath)
|
||||
to := path.Join(d.RootURL.String(), toPath)
|
||||
op.Infof("Moving %s to %s", from, to)
|
||||
err := tasks.Wait(ctx, func(context.Context) (tasks.Task, error) {
|
||||
return d.fm.MoveDatastoreFile(ctx, from, d.s.Datacenter, to, d.s.Datacenter, true)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Helper) Rm(ctx context.Context, pth string) error {
|
||||
op := trace.FromContext(ctx, "Rm")
|
||||
|
||||
f := path.Join(d.RootURL.String(), pth)
|
||||
op.Infof("Removing %s", pth)
|
||||
return d.ds.NewFileManager(d.s.Datacenter, true).Delete(ctx, f) // TODO: NewHelper should create the DatastoreFileManager
|
||||
}
|
||||
|
||||
func (d *Helper) IsVSAN(ctx context.Context) bool {
|
||||
// #nosec: Errors unhandled.
|
||||
dsType, _ := d.ds.Type(ctx)
|
||||
return dsType == types.HostFileSystemVolumeFileSystemTypeVsan
|
||||
}
|
||||
|
||||
// This creates the root directory in the datastore and sets the rooturl and
|
||||
// rootdir in the datastore struct so we can reuse it for other routines. This
|
||||
// handles vsan + vc, vsan + esx, and esx. The URI conventions are not the
|
||||
// same for each and this tries to create the directory and stash the relevant
|
||||
// result so the URI doesn't need to be recomputed for every datastore
|
||||
// operation.
|
||||
func (d *Helper) mkRootDir(op trace.Operation, rootdir string) error {
|
||||
if rootdir == "" {
|
||||
return fmt.Errorf("root directory is empty")
|
||||
}
|
||||
|
||||
if path.IsAbs(rootdir) {
|
||||
return fmt.Errorf("root directory (%s) must not be an absolute path", rootdir)
|
||||
}
|
||||
|
||||
// Handle vsan
|
||||
// Vsan will complain if the root dir exists. Just call it directly and
|
||||
// swallow the error if it's already there.
|
||||
if d.IsVSAN(op) {
|
||||
comps := strings.Split(rootdir, "/")
|
||||
|
||||
nm := object.NewDatastoreNamespaceManager(d.s.Vim25())
|
||||
|
||||
// This returns the vmfs path (including the datastore and directory
|
||||
// UUIDs). Use the directory UUID in future operations because it is
|
||||
// the stable path which we can use regardless of vsan state.
|
||||
uuid, err := nm.CreateDirectory(op, d.ds, comps[0], "")
|
||||
if err != nil {
|
||||
if !soap.IsSoapFault(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
soapFault := soap.ToSoapFault(err)
|
||||
if _, ok := soapFault.VimFault().(types.FileAlreadyExists); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// XXX UGLY HACK until we move this into the installer. Use the
|
||||
// display name if the dir exists since we can't get the UUID after the
|
||||
// directory is created.
|
||||
uuid = comps[0]
|
||||
err = nil
|
||||
}
|
||||
|
||||
rootdir = path.Join(path.Base(uuid), path.Join(comps[1:]...))
|
||||
}
|
||||
|
||||
rooturl := d.ds.Path(rootdir)
|
||||
|
||||
// create the rest of the root dir in case of vSAN, otherwise
|
||||
// create the full path
|
||||
if _, err := mkdir(op, d.s, d.fm, true, rooturl); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
op.Infof("datastore root %s already exists", rooturl)
|
||||
}
|
||||
|
||||
d.RootURL.FromString(rooturl)
|
||||
return nil
|
||||
}
|
||||
|
||||
func PathFromString(dsp string) (*object.DatastorePath, error) {
|
||||
var p object.DatastorePath
|
||||
if !p.FromString(dsp) {
|
||||
return nil, errors.New(dsp + " not a datastore path")
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// Parse the datastore format ([datastore1] /path/to/thing) to groups.
|
||||
var datastoreFormat = regexp.MustCompile(`^\[([\w\d\(\)-_\.\s]+)\]`)
|
||||
var pathFormat = regexp.MustCompile(`\s([\/\w-_\.]*$)`)
|
||||
|
||||
// Converts `[datastore] /path` to ds:// URL
|
||||
func ToURL(ds string) (*url.URL, error) {
|
||||
u := new(url.URL)
|
||||
var matches []string
|
||||
if matches = datastoreFormat.FindStringSubmatch(ds); len(matches) != 2 {
|
||||
return nil, fmt.Errorf("Ambiguous datastore hostname format encountered from input: %s.", ds)
|
||||
}
|
||||
u.Host = matches[1]
|
||||
if matches = pathFormat.FindStringSubmatch(ds); len(matches) != 2 {
|
||||
return nil, fmt.Errorf("Ambiguous datastore path format encountered from input: %s.", ds)
|
||||
}
|
||||
|
||||
u.Path = path.Clean(matches[1])
|
||||
u.Scheme = "ds"
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Converts ds:// URL for datastores to datastore format ([datastore1] /path/to/thing)
|
||||
func URLtoDatastore(u *url.URL) (string, error) {
|
||||
scheme := "ds"
|
||||
if u.Scheme != scheme {
|
||||
return "", fmt.Errorf("url (%s) is not a datastore", u.String())
|
||||
}
|
||||
return fmt.Sprintf("[%s] %s", u.Host, u.Path), nil
|
||||
}
|
||||
|
||||
// TestName builds a unique datastore name
|
||||
func TestName(suffix string) string {
|
||||
return uuid.New().String()[0:16] + "-" + suffix
|
||||
}
|
||||
219
vendor/github.com/vmware/vic/pkg/vsphere/datastore/datastore_test.go
generated
vendored
Normal file
219
vendor/github.com/vmware/vic/pkg/vsphere/datastore/datastore_test.go
generated
vendored
Normal file
@@ -0,0 +1,219 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// test if we can get a Datastore via the rooturl
|
||||
func TestDatastoreGetDatastores(t *testing.T) {
|
||||
ctx, ds, cleanupfunc := DSsetup(t)
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
defer cleanupfunc()
|
||||
|
||||
firstSummary, err := ds.Summary(ctx)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("Name:\t%s\n", firstSummary.Name)
|
||||
t.Logf(" Path:\t%s\n", ds.ds.InventoryPath)
|
||||
t.Logf(" Type:\t%s\n", firstSummary.Type)
|
||||
t.Logf(" URL:\t%s\n", firstSummary.Url)
|
||||
t.Logf(" Capacity:\t%.1f GB\n", float64(firstSummary.Capacity)/(1<<30))
|
||||
t.Logf(" Free:\t%.1f GB\n", float64(firstSummary.FreeSpace)/(1<<30))
|
||||
|
||||
inMap := make(map[string]*url.URL)
|
||||
|
||||
p, err := url.Parse(ds.RootURL.String())
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
inMap["foo"] = p
|
||||
|
||||
dstores, err := GetDatastores(context.TODO(), ds.s, inMap)
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, dstores) {
|
||||
return
|
||||
}
|
||||
|
||||
secondSummary, err := ds.Summary(ctx)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.Equal(t, firstSummary, secondSummary) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatastoreRestart(t *testing.T) {
|
||||
// creates a root in the datastore
|
||||
ctx, ds, cleanupfunc := DSsetup(t)
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
defer cleanupfunc()
|
||||
|
||||
// Create a nested dir in the root and use that as the datastore
|
||||
nestedRoot := path.Join(ds.RootURL.Path, "foo")
|
||||
ds, err := NewHelper(ctx, ds.s, ds.s.Datastore, nestedRoot)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// test we can ls the root
|
||||
_, err = ds.Ls(ctx, "")
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// create a dir
|
||||
_, err = ds.Mkdir(ctx, true, "baz")
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// create a new datastore object with the same path as the nested one
|
||||
ds, err = NewHelper(ctx, ds.s, ds.s.Datastore, nestedRoot)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// try to create the same baz dir, assert it exists
|
||||
_, err = ds.Mkdir(ctx, true, "baz")
|
||||
if !assert.Error(t, err) || !assert.True(t, os.IsExist(err)) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, ds.RootURL)
|
||||
}
|
||||
|
||||
func TestDatastoreCreateDir(t *testing.T) {
|
||||
ctx, ds, cleanupfunc := DSsetup(t)
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
defer cleanupfunc()
|
||||
|
||||
_, err := ds.Ls(ctx, "")
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// assert create dir of a dir that exists is os.ErrExists
|
||||
_, err = ds.Mkdir(ctx, true, "foo")
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = ds.Mkdir(ctx, true, "foo")
|
||||
if !assert.Error(t, err) || !assert.True(t, os.IsExist(err)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatastoreMkdirAndLs(t *testing.T) {
|
||||
ctx, ds, cleanupfunc := DSsetup(t)
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
defer cleanupfunc()
|
||||
|
||||
dirs := []string{"dir1", "dir1/child1"}
|
||||
|
||||
// create the dir then test it exists by calling ls
|
||||
for _, dir := range dirs {
|
||||
_, err := ds.Mkdir(ctx, true, dir)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = ds.Ls(ctx, dir)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatastoreToURLParsing(t *testing.T) {
|
||||
expectedURL := "ds://datastore1/path/to/thing"
|
||||
|
||||
input := [][]string{
|
||||
{"[datastore1] /path/to/thing", expectedURL},
|
||||
{"[datastore1] path/to/thing", expectedURL},
|
||||
{"[datastore1] ///path////to/thing", expectedURL},
|
||||
{"[Datastore (1)] /path/to/thing", "ds://Datastore%20(1)/path/to/thing"},
|
||||
{"[datastore1] path", "ds://datastore1/path"},
|
||||
{"[datastore1] pa-th", "ds://datastore1/pa-th"},
|
||||
{"[datastore1] pa_th", "ds://datastore1/pa_th"},
|
||||
{"[data_store1] pa_th", "ds://data_store1/pa_th"},
|
||||
}
|
||||
|
||||
dsoutputs := []string{
|
||||
"[datastore1] /path/to/thing",
|
||||
"[datastore1] path/to/thing",
|
||||
"[datastore1] /path/to/thing",
|
||||
"[Datastore (1)] /path/to/thing",
|
||||
"[datastore1] path",
|
||||
"[datastore1] pa-th",
|
||||
"[datastore1] pa_th",
|
||||
"[data_store1] pa_th",
|
||||
}
|
||||
|
||||
for i, in := range input {
|
||||
u, err := ToURL(in[0])
|
||||
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, u) {
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.Equal(t, in[1], u.String()) {
|
||||
return
|
||||
}
|
||||
|
||||
out, err := URLtoDatastore(u)
|
||||
if !assert.NoError(t, err) || !assert.True(t, len(out) > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.Equal(t, dsoutputs[i], out) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From https://siongui.github.io/2015/04/13/go-generate-random-string/
|
||||
func RandomString(strlen int) string {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
result := make([]byte, strlen)
|
||||
for i := 0; i < strlen; i++ {
|
||||
result[i] = chars[rand.Intn(len(chars))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
83
vendor/github.com/vmware/vic/pkg/vsphere/datastore/datastore_util.go
generated
vendored
Normal file
83
vendor/github.com/vmware/vic/pkg/vsphere/datastore/datastore_util.go
generated
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/tasks"
|
||||
"github.com/vmware/vic/pkg/vsphere/test/env"
|
||||
)
|
||||
|
||||
// Used in testing
|
||||
|
||||
func Session(ctx context.Context, t *testing.T) *session.Session {
|
||||
config := &session.Config{
|
||||
Service: env.URL(t),
|
||||
|
||||
/// XXX Why does this insist on having this field populated?
|
||||
DatastorePath: env.DS(t),
|
||||
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
}
|
||||
|
||||
s := session.NewSession(config)
|
||||
_, err := s.Connect(ctx)
|
||||
if err != nil {
|
||||
s.Client.Logout(ctx)
|
||||
t.Log(err.Error())
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
_, err = s.Populate(ctx)
|
||||
if err != nil {
|
||||
t.Log(err.Error())
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func DSsetup(t *testing.T) (context.Context, *Helper, func()) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
ctx := context.Background()
|
||||
sess := Session(ctx, t)
|
||||
|
||||
ds, err := NewHelper(ctx, sess, sess.Datastore, TestName("dstests"))
|
||||
if !assert.NoError(t, err) {
|
||||
return ctx, nil, nil
|
||||
}
|
||||
|
||||
f := func() {
|
||||
log.Infof("Removing test root %s", ds.RootURL.String())
|
||||
err := tasks.Wait(ctx, func(context.Context) (tasks.Task, error) {
|
||||
return ds.fm.DeleteDatastoreFile(ctx, ds.RootURL.String(), sess.Datacenter)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return ctx, ds, f
|
||||
}
|
||||
152
vendor/github.com/vmware/vic/pkg/vsphere/diag/diag.go
generated
vendored
Normal file
152
vendor/github.com/vmware/vic/pkg/vsphere/diag/diag.go
generated
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package diag
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
// StatusCodeFatalThreshold defines a threshold after which all codes can be treated as fatal.
|
||||
const StatusCodeFatalThreshold = 64
|
||||
|
||||
const (
|
||||
// VCStatusOK vSphere API is available.
|
||||
VCStatusOK = 0
|
||||
// VCStatusInvalidURL Provided vSphere API URL is wrong.
|
||||
VCStatusInvalidURL = 64
|
||||
// VCStatusErrorQuery Error happened trying to query vSphere API
|
||||
VCStatusErrorQuery = 65
|
||||
// VCStatusErrorResponse Received response doesn't contain expected data.
|
||||
VCStatusErrorResponse = 66
|
||||
// VCStatusIncorrectResponse Received in case if returned data from server is different from expected.
|
||||
VCStatusIncorrectResponse = 67
|
||||
// VCStatusNotXML Received response is not XML
|
||||
VCStatusNotXML = 68
|
||||
// VCStatusUnknownHost is returned in case if DNS failed to resolve name.
|
||||
VCStatusUnknownHost = 69
|
||||
// VCStatusHostIsNotReachable
|
||||
VCStatusHostIsNotReachable = 70
|
||||
)
|
||||
|
||||
// UserReadableVCAPITestDescription convert API test code into user readable text
|
||||
func UserReadableVCAPITestDescription(code int) string {
|
||||
switch code {
|
||||
case VCStatusOK:
|
||||
return "vSphere API target responds as expected"
|
||||
case VCStatusInvalidURL:
|
||||
return "vSphere API target url is invalid"
|
||||
case VCStatusErrorQuery:
|
||||
return "vSphere API target failed to respond to the query"
|
||||
case VCStatusIncorrectResponse:
|
||||
return "vSphere API target returns unexpected response"
|
||||
case VCStatusErrorResponse:
|
||||
return "vSphere API target returns error"
|
||||
case VCStatusNotXML:
|
||||
return "vSphere API target returns non XML response"
|
||||
case VCStatusUnknownHost:
|
||||
return "vSphere API target can not be resolved from VCH"
|
||||
case VCStatusHostIsNotReachable:
|
||||
return "vSphere API target is out of reach. Wrong routing table?"
|
||||
default:
|
||||
return "vSphere API target test returned unknown code"
|
||||
}
|
||||
}
|
||||
|
||||
// CheckAPIAvailability accesses vSphere API to ensure it is a correct end point that is up and running.
|
||||
func CheckAPIAvailability(targetURL string) int {
|
||||
op := trace.NewOperation(context.Background(), "api test")
|
||||
errorCode := VCStatusErrorQuery
|
||||
|
||||
u, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
return VCStatusInvalidURL
|
||||
}
|
||||
|
||||
u.Path = "/sdk/vimService.wsdl"
|
||||
apiURL := u.String()
|
||||
|
||||
op.Debugf("Checking access to: %s", apiURL)
|
||||
|
||||
for attempts := 5; errorCode != VCStatusOK && attempts > 0; attempts-- {
|
||||
|
||||
// #nosec: TLS InsecureSkipVerify set true
|
||||
c := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
// Is 20 seconds enough to receive any response from vSphere target server?
|
||||
Timeout: time.Second * 20,
|
||||
}
|
||||
errorCode = queryAPI(op, c.Get, apiURL)
|
||||
}
|
||||
return errorCode
|
||||
}
|
||||
|
||||
func queryAPI(op trace.Operation, getter func(string) (*http.Response, error), apiURL string) int {
|
||||
resp, err := getter(apiURL)
|
||||
if err != nil {
|
||||
errTxt := err.Error()
|
||||
op.Errorf("Query error: %s", err)
|
||||
if strings.Contains(errTxt, "no such host") {
|
||||
return VCStatusUnknownHost
|
||||
}
|
||||
if strings.Contains(errTxt, "no route to host") {
|
||||
return VCStatusHostIsNotReachable
|
||||
}
|
||||
if strings.Contains(errTxt, "host is down") {
|
||||
return VCStatusHostIsNotReachable
|
||||
}
|
||||
return VCStatusErrorQuery
|
||||
}
|
||||
|
||||
data := make([]byte, 65636)
|
||||
n, err := io.ReadFull(resp.Body, data)
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
op.Errorf("Query error: %s", err)
|
||||
return VCStatusErrorResponse
|
||||
}
|
||||
if n >= len(data) {
|
||||
// #nosec: Errors unhandled.
|
||||
io.Copy(ioutil.Discard, resp.Body)
|
||||
}
|
||||
// #nosec: Errors unhandled.
|
||||
resp.Body.Close()
|
||||
|
||||
contentType := strings.ToLower(resp.Header.Get("Content-Type"))
|
||||
if !strings.Contains(contentType, "text/xml") {
|
||||
op.Errorf("Unexpected content type %s, should be text/xml", contentType)
|
||||
op.Errorf("Response from the server: %s", string(data))
|
||||
return VCStatusNotXML
|
||||
}
|
||||
// we just want to make sure that response contains something familiar that we could
|
||||
// use as vSphere API marker.
|
||||
if !bytes.Contains(data, []byte("urn:vim25Service")) {
|
||||
op.Errorf("Server response doesn't contain 'urn:vim25Service': %s", string(data))
|
||||
return VCStatusIncorrectResponse
|
||||
}
|
||||
return VCStatusOK
|
||||
}
|
||||
117
vendor/github.com/vmware/vic/pkg/vsphere/diag/diag_test.go
generated
vendored
Normal file
117
vendor/github.com/vmware/vic/pkg/vsphere/diag/diag_test.go
generated
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package diag
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
func TestCheckAPIAvailability(t *testing.T) {
|
||||
assert.Equal(t, VCStatusErrorQuery, CheckAPIAvailability("http://127.0.0.1:65535"))
|
||||
assert.Equal(t, VCStatusErrorQuery, CheckAPIAvailability("http://127.0.0.1:65536"))
|
||||
}
|
||||
|
||||
func TestCheckAPIAvailabilityQueryWithGetterError(t *testing.T) {
|
||||
op := trace.NewOperation(context.Background(), "test")
|
||||
f := func(s string) (*http.Response, error) { return nil, errors.New("wrong query") }
|
||||
code := queryAPI(op, f, "testurl")
|
||||
assert.Equal(t, VCStatusErrorQuery, code)
|
||||
}
|
||||
|
||||
type readerWithError struct {
|
||||
err error
|
||||
data *bytes.Reader
|
||||
}
|
||||
|
||||
func (r *readerWithError) Read(b []byte) (int, error) {
|
||||
if r.err != nil {
|
||||
return 0, r.err
|
||||
}
|
||||
return r.data.Read(b)
|
||||
}
|
||||
|
||||
func (r *readerWithError) Close() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
func TestCheckAPIAvailabilityQueryReadError(t *testing.T) {
|
||||
op := trace.NewOperation(context.Background(), "test")
|
||||
f := func(s string) (*http.Response, error) {
|
||||
hr := &http.Response{
|
||||
Body: &readerWithError{
|
||||
err: errors.New("read error happened"),
|
||||
},
|
||||
}
|
||||
return hr, nil
|
||||
}
|
||||
code := queryAPI(op, f, "testurl")
|
||||
assert.Equal(t, VCStatusErrorResponse, code)
|
||||
}
|
||||
|
||||
func TestCheckAPIAvailabilityQueryIncorrectDataType(t *testing.T) {
|
||||
op := trace.NewOperation(context.Background(), "test")
|
||||
f := func(s string) (*http.Response, error) {
|
||||
hr := &http.Response{
|
||||
Body: &readerWithError{
|
||||
data: bytes.NewReader([]byte("some data")),
|
||||
},
|
||||
}
|
||||
return hr, nil
|
||||
}
|
||||
code := queryAPI(op, f, "testurl")
|
||||
assert.Equal(t, VCStatusNotXML, code)
|
||||
}
|
||||
|
||||
func TestCheckAPIAvailabilityQueryIncorrectData(t *testing.T) {
|
||||
op := trace.NewOperation(context.Background(), "test")
|
||||
f := func(s string) (*http.Response, error) {
|
||||
hr := &http.Response{
|
||||
Body: &readerWithError{
|
||||
data: bytes.NewReader([]byte("some data")),
|
||||
},
|
||||
Header: http.Header{"Content-Type": []string{"text/xml"}},
|
||||
}
|
||||
return hr, nil
|
||||
}
|
||||
code := queryAPI(op, f, "testurl")
|
||||
assert.Equal(t, VCStatusIncorrectResponse, code)
|
||||
}
|
||||
|
||||
func TestCheckAPIAvailabilityQueryCorrectData(t *testing.T) {
|
||||
op := trace.NewOperation(context.Background(), "test")
|
||||
f := func(s string) (*http.Response, error) {
|
||||
hr := &http.Response{
|
||||
Body: &readerWithError{
|
||||
data: bytes.NewReader([]byte("some urn:vim25Service data")),
|
||||
},
|
||||
Header: http.Header{"Content-Type": []string{"text/xml"}},
|
||||
}
|
||||
return hr, nil
|
||||
}
|
||||
code := queryAPI(op, f, "testurl")
|
||||
assert.Equal(t, VCStatusOK, code)
|
||||
}
|
||||
|
||||
func TestCheckAPIAvailabilityIncorrectDNSName(t *testing.T) {
|
||||
assert.Equal(t, VCStatusUnknownHost, CheckAPIAvailability("https://example.notexisting.domain"))
|
||||
}
|
||||
39
vendor/github.com/vmware/vic/pkg/vsphere/diagnostic/manager.go
generated
vendored
Normal file
39
vendor/github.com/vmware/vic/pkg/vsphere/diagnostic/manager.go
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package diagnostic
|
||||
|
||||
import (
|
||||
"github.com/vmware/govmomi/object"
|
||||
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
)
|
||||
|
||||
// Manager struct defines the Manager which provides additional
|
||||
// VIC specific methods over object.DiagnosticManager
|
||||
type Manager struct {
|
||||
*object.DiagnosticManager
|
||||
|
||||
*session.Session
|
||||
}
|
||||
|
||||
// NewDiagnosticManager returns a new DiagnosticManager object
|
||||
func NewDiagnosticManager(session *session.Session) *Manager {
|
||||
return &Manager{
|
||||
DiagnosticManager: object.NewDiagnosticManager(
|
||||
session.Vim25(),
|
||||
),
|
||||
Session: session,
|
||||
}
|
||||
}
|
||||
102
vendor/github.com/vmware/vic/pkg/vsphere/disk/config.go
generated
vendored
Normal file
102
vendor/github.com/vmware/vic/pkg/vsphere/disk/config.go
generated
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/pkg/fs"
|
||||
)
|
||||
|
||||
type VirtualDiskConfig struct {
|
||||
// The URI in the datastore this disk can be found with
|
||||
DatastoreURI *object.DatastorePath
|
||||
|
||||
// The URI in the datastore to the parent of this disk
|
||||
ParentDatastoreURI *object.DatastorePath
|
||||
|
||||
// The size of the disk
|
||||
CapacityInKB int64
|
||||
|
||||
// Underlying filesystem
|
||||
Filesystem Filesystem
|
||||
|
||||
// Base disk UUID
|
||||
UUID string
|
||||
|
||||
DiskMode types.VirtualDiskMode
|
||||
}
|
||||
|
||||
func NewPersistentDisk(URI *object.DatastorePath) *VirtualDiskConfig {
|
||||
return &VirtualDiskConfig{
|
||||
DatastoreURI: URI,
|
||||
DiskMode: types.VirtualDiskModeIndependent_persistent,
|
||||
Filesystem: fs.NewExt4(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewNonPersistentDisk(URI *object.DatastorePath) *VirtualDiskConfig {
|
||||
return &VirtualDiskConfig{
|
||||
DatastoreURI: URI,
|
||||
DiskMode: types.VirtualDiskModeIndependent_nonpersistent,
|
||||
Filesystem: fs.NewExt4(),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *VirtualDiskConfig) WithParent(parent *object.DatastorePath) *VirtualDiskConfig {
|
||||
d.ParentDatastoreURI = parent
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *VirtualDiskConfig) WithFilesystem(ftype FilesystemType) *VirtualDiskConfig {
|
||||
switch ftype {
|
||||
case Xfs:
|
||||
d.Filesystem = fs.NewXFS()
|
||||
default:
|
||||
d.Filesystem = fs.NewExt4()
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *VirtualDiskConfig) WithCapacity(capacity int64) *VirtualDiskConfig {
|
||||
d.CapacityInKB = capacity
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// WithUUID can only be set on the base disk layer due to disklib bug
|
||||
// TODO: add an error mechanism for validating conditional settings like this
|
||||
func (d *VirtualDiskConfig) WithUUID(uuid string) *VirtualDiskConfig {
|
||||
d.UUID = uuid
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *VirtualDiskConfig) Hash() uint64 {
|
||||
key := fmt.Sprintf("%s-%t", d.DatastoreURI, d.IsPersistent())
|
||||
|
||||
hash := fnv.New64a()
|
||||
hash.Write([]byte(key))
|
||||
|
||||
return hash.Sum64()
|
||||
}
|
||||
|
||||
func (d *VirtualDiskConfig) IsPersistent() bool {
|
||||
return d.DiskMode == types.VirtualDiskModeIndependent_persistent || d.DiskMode == types.VirtualDiskModePersistent
|
||||
}
|
||||
473
vendor/github.com/vmware/vic/pkg/vsphere/disk/disk.go
generated
vendored
Normal file
473
vendor/github.com/vmware/vic/pkg/vsphere/disk/disk.go
generated
vendored
Normal file
@@ -0,0 +1,473 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
// FilesystemType represents the filesystem in use by a virtual disk
|
||||
type FilesystemType uint8
|
||||
|
||||
const (
|
||||
// Ext4 represents the ext4 file system
|
||||
Ext4 FilesystemType = iota + 1
|
||||
|
||||
// Xfs represents the XFS file system
|
||||
Xfs
|
||||
|
||||
// Ntfs represents the NTFS file system
|
||||
Ntfs
|
||||
|
||||
// directory in which to perform the direct mount of disk for bind mount
|
||||
// to actual target
|
||||
diskBindBase = "/.filesystem-by-label/"
|
||||
|
||||
// used to isolate applications from the lost+found in the root of ext4
|
||||
VolumeDataDir = "/.vic.vol.data"
|
||||
)
|
||||
|
||||
// Filesystem defines the interface for handling an attached virtual disk
|
||||
type Filesystem interface {
|
||||
Mkfs(op trace.Operation, devPath, label string) error
|
||||
SetLabel(op trace.Operation, devPath, labelName string) error
|
||||
Mount(op trace.Operation, devPath, targetPath string, options []string) error
|
||||
Unmount(op trace.Operation, path string) error
|
||||
}
|
||||
|
||||
// Semaphore represents the number of references to a disk
|
||||
type Semaphore struct {
|
||||
resource string
|
||||
refname string
|
||||
count uint64
|
||||
}
|
||||
|
||||
// NewSemaphore creates and returns a Semaphore initialized to 0
|
||||
func NewSemaphore(r, n string) *Semaphore {
|
||||
return &Semaphore{
|
||||
resource: r,
|
||||
refname: n,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Increment increases the reference count by one
|
||||
func (r *Semaphore) Increment() uint64 {
|
||||
return atomic.AddUint64(&r.count, 1)
|
||||
}
|
||||
|
||||
// Decrement decreases the reference count by one
|
||||
func (r *Semaphore) Decrement() uint64 {
|
||||
return atomic.AddUint64(&r.count, ^uint64(0))
|
||||
}
|
||||
|
||||
// Count returns the current reference count
|
||||
func (r *Semaphore) Count() uint64 {
|
||||
return atomic.LoadUint64(&r.count)
|
||||
}
|
||||
|
||||
// InUseError is returned when a detach is attempted on a disk that is
|
||||
// still in use
|
||||
type InUseError struct {
|
||||
error
|
||||
}
|
||||
|
||||
// VirtualDisk represents a VMDK in the datastore, the device node it may be
|
||||
// attached at (if it's attached), the mountpoint it is mounted at (if
|
||||
// mounted), and other configuration.
|
||||
type VirtualDisk struct {
|
||||
*VirtualDiskConfig
|
||||
|
||||
// The device node the disk is attached to
|
||||
DevicePath string
|
||||
|
||||
// The path on the filesystem this device is attached to.
|
||||
mountPath string
|
||||
// The options that the disk is currently mounted with.
|
||||
mountOpts string
|
||||
|
||||
// To avoid attach/detach races, this lock serializes operations to the disk.
|
||||
l sync.Mutex
|
||||
|
||||
mountedRefs *Semaphore
|
||||
|
||||
attachedRefs *Semaphore
|
||||
}
|
||||
|
||||
// NewVirtualDisk creates and returns a new VirtualDisk object associated with the
|
||||
// given datastore formatted with the specified FilesystemType
|
||||
func NewVirtualDisk(op trace.Operation, config *VirtualDiskConfig, disks map[uint64]*VirtualDisk) (*VirtualDisk, error) {
|
||||
if !strings.HasSuffix(config.DatastoreURI.String(), ".vmdk") {
|
||||
return nil, fmt.Errorf("%s doesn't have a vmdk suffix", config.DatastoreURI.String())
|
||||
}
|
||||
|
||||
if d, ok := disks[config.Hash()]; ok {
|
||||
return d, nil
|
||||
}
|
||||
op.Debugf("Didn't find the disk %s in the DiskManager cache, creating it", config.DatastoreURI)
|
||||
|
||||
uri := config.DatastoreURI.String()
|
||||
d := &VirtualDisk{
|
||||
VirtualDiskConfig: config,
|
||||
mountedRefs: NewSemaphore(uri, "mount"),
|
||||
attachedRefs: NewSemaphore(uri, "attach"),
|
||||
}
|
||||
disks[config.Hash()] = d
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *VirtualDisk) setAttached(op trace.Operation, devicePath string) (err error) {
|
||||
if d.DevicePath == "" {
|
||||
// Question: what happens if this is called a second time with a different devicePath?
|
||||
d.DevicePath = devicePath
|
||||
}
|
||||
|
||||
count := d.attachedRefs.Increment()
|
||||
op.Debugf("incremented attach count for %s: %d", d.DatastoreURI, count)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *VirtualDisk) canBeDetached() error {
|
||||
if !d.attached() {
|
||||
return fmt.Errorf("%s is already detached", d.DatastoreURI)
|
||||
}
|
||||
|
||||
if d.mounted() {
|
||||
return fmt.Errorf("%s is mounted (%s)", d.DatastoreURI, d.mountPath)
|
||||
}
|
||||
|
||||
if d.inUseByOther() {
|
||||
return fmt.Errorf("Detach skipped - %s is still in use", d.DatastoreURI)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *VirtualDisk) setDetached(op trace.Operation, disks map[uint64]*VirtualDisk) {
|
||||
// we only call this when it's been detached, so always make the updates
|
||||
op.Debugf("Dropping %s from the DiskManager cache", d.DatastoreURI)
|
||||
d.DevicePath = ""
|
||||
delete(disks, d.Hash())
|
||||
}
|
||||
|
||||
// Mkfs formats the disk with Filesystem and sets the disk label
|
||||
func (d *VirtualDisk) Mkfs(op trace.Operation, labelName string) error {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
if !d.attached() {
|
||||
return fmt.Errorf("%s isn't attached", d.DatastoreURI)
|
||||
}
|
||||
|
||||
if d.mounted() {
|
||||
return fmt.Errorf("%s is still mounted (%s)", d.DatastoreURI, d.mountPath)
|
||||
}
|
||||
|
||||
return d.Filesystem.Mkfs(op, d.DevicePath, labelName)
|
||||
}
|
||||
|
||||
// SetLabel sets this disk's label
|
||||
func (d *VirtualDisk) SetLabel(op trace.Operation, labelName string) error {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
if !d.attached() {
|
||||
return fmt.Errorf("%s isn't attached", d.DatastoreURI)
|
||||
}
|
||||
|
||||
return d.Filesystem.SetLabel(op, d.DevicePath, labelName)
|
||||
}
|
||||
|
||||
func (d *VirtualDisk) attached() bool {
|
||||
return d.DevicePath != ""
|
||||
}
|
||||
|
||||
// Attached returns true if this disk is attached, false otherwise
|
||||
func (d *VirtualDisk) Attached() bool {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
return d.attached()
|
||||
}
|
||||
|
||||
func (d *VirtualDisk) attachedByOther() bool {
|
||||
return d.attachedRefs.Count() > 1
|
||||
}
|
||||
|
||||
// AttachedByOther returns true if the attached references are > 1
|
||||
func (d *VirtualDisk) AttachedByOther() bool {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
return d.attachedByOther()
|
||||
}
|
||||
|
||||
func (d *VirtualDisk) mountedByOther() bool {
|
||||
return d.mountedRefs.Count() > 1
|
||||
}
|
||||
|
||||
// MountedByOther returns true if the mounted references are > 1
|
||||
func (d *VirtualDisk) MountedByOther() bool {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
return d.mountedByOther()
|
||||
}
|
||||
|
||||
func (d *VirtualDisk) inUseByOther() bool {
|
||||
return d.mountedByOther() || d.attachedByOther()
|
||||
}
|
||||
|
||||
// InUseByOther returns true if the disk is currently attached or
|
||||
// mounted by someone else
|
||||
func (d *VirtualDisk) InUseByOther() bool {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
return d.inUseByOther()
|
||||
}
|
||||
|
||||
// Mount attempts to mount this disk. A NOP occurs if the disk is already mounted
|
||||
// It returns the path at which the disk is mounted
|
||||
// Enhancement: allow provision of mount path and refcount for:
|
||||
// specific mount point and options
|
||||
func (d *VirtualDisk) Mount(op trace.Operation, options []string) (string, error) {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
op.Debugf("Mounting %s", d.DatastoreURI)
|
||||
|
||||
if !d.attached() {
|
||||
err := fmt.Errorf("%s isn't attached", d.DatastoreURI)
|
||||
op.Error(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
opts := strings.Join(options, ";")
|
||||
if !d.mounted() {
|
||||
mntpath, err := ioutil.TempDir("", "mnt")
|
||||
if err != nil {
|
||||
err := fmt.Errorf("unable to create mountpint: %s", err)
|
||||
op.Error(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// get mount source, disk is already mounted if this func returns without error
|
||||
mntsrc, err := d.getMountSource(op, options)
|
||||
if err != nil {
|
||||
op.Error(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// then mount it at the correct source
|
||||
if strings.HasSuffix(mntsrc, VolumeDataDir) {
|
||||
// append bind mount options if we are masking lost+found
|
||||
options = append(options, "bind")
|
||||
}
|
||||
|
||||
if err = d.Filesystem.Mount(op, mntsrc, mntpath, options); err != nil {
|
||||
op.Errorf("Failed to mount disk: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
d.mountPath = mntpath
|
||||
d.mountOpts = opts
|
||||
} else {
|
||||
// basic santiy check for matching options - we don't want to share a r/o mount
|
||||
// if the request was for r/w. Ideally we'd just mount this at a different location with the
|
||||
// requested options but that requires separate ref counting.
|
||||
// TODO: support differing mount opts
|
||||
if d.mountOpts != opts {
|
||||
op.Errorf("Unable to use mounted disk due to differing options: %s != %s", d.mountOpts, opts)
|
||||
return "", fmt.Errorf("incompatible mount options for disk reuse")
|
||||
}
|
||||
}
|
||||
|
||||
count := d.mountedRefs.Increment()
|
||||
op.Debugf("incremented mount count for %s: %d", d.mountPath, count)
|
||||
|
||||
return d.mountPath, nil
|
||||
}
|
||||
|
||||
// getMountSource mounts the disk rootfs, checks if it has volumeDataDir, if so it returns volumeDataDir
|
||||
// as the mount source to mask the lost+found folder, otherwise it returns the device path
|
||||
// NOTE: this mount should not be counted in the ref counts, bindTarget will be unmounted when disk detaches.
|
||||
// TODO: if we support different mount opts, we can't use the same bindTarget anymore.
|
||||
// need to assign each opt a different name, we can add a field in VirtualDisk that tracks bindTarget
|
||||
func (d *VirtualDisk) getMountSource(op trace.Operation, options []string) (string, error) {
|
||||
// need to first mount the disk under the diskBindBase
|
||||
bindTarget := path.Join(diskBindBase, d.DevicePath)
|
||||
|
||||
// sanity check to make sure previous bindTarget is cleaned up properly
|
||||
var e1, e2 error
|
||||
_, e1 = os.Stat(bindTarget)
|
||||
if e1 == nil {
|
||||
// bindTarget exists, check whether or not bindTarget is a mount point
|
||||
e2 = os.Remove(bindTarget)
|
||||
}
|
||||
|
||||
// we don't want to remount under the same mountpoint, so we only mounts under the following cases
|
||||
// first case: bindTarget exists but not a mountpoint
|
||||
// second case: bindTarget doesn't exist
|
||||
if (e1 == nil && e2 == nil) || os.IsNotExist(e1) {
|
||||
// #nosec
|
||||
if err := os.MkdirAll(bindTarget, 0744); err != nil {
|
||||
err = fmt.Errorf("unable to create mount point %s: %s", bindTarget, err)
|
||||
op.Error(err)
|
||||
return "", err
|
||||
}
|
||||
if err := d.Filesystem.Mount(op, d.DevicePath, bindTarget, options); err != nil {
|
||||
op.Errorf("Failed to mount disk: %s", err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
mntsrc := path.Join(bindTarget, VolumeDataDir)
|
||||
// if the volume contains a volumeDataDir directory then mount that instead of the root of the filesystem
|
||||
// if we cannot read it we go with the root of the filesystem
|
||||
_, err := os.Stat(mntsrc)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// if there's no such directory then revert to using the device directly
|
||||
op.Infof("No " + VolumeDataDir + " data directory in volume, mounting filesystem directly")
|
||||
mntsrc = d.DevicePath
|
||||
} else {
|
||||
return "", fmt.Errorf("unable to determine whether lost+found masking is required: %s", err)
|
||||
}
|
||||
}
|
||||
return mntsrc, nil
|
||||
}
|
||||
|
||||
// Unmount attempts to unmount a virtual disk
|
||||
func (d *VirtualDisk) Unmount(op trace.Operation) error {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
if !d.mounted() {
|
||||
return fmt.Errorf("%s already unmounted", d.DatastoreURI)
|
||||
}
|
||||
|
||||
count := d.mountedRefs.Decrement()
|
||||
op.Debugf("decremented mount count for %s: %d", d.mountPath, count)
|
||||
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// no more mount references to this disk, so actually unmount
|
||||
if err := d.Filesystem.Unmount(op, d.mountPath); err != nil {
|
||||
err := fmt.Errorf("failed to unmount disk: %s", err)
|
||||
op.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// only remove the mount directory - if we've succeeded in the unmount there won't be anything in it
|
||||
// if we somehow get here and there is content we do NOT want to delete it
|
||||
if err := os.Remove(d.mountPath); err != nil {
|
||||
err := fmt.Errorf("failed to clean up mount point: %s", err)
|
||||
op.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
d.mountPath = ""
|
||||
|
||||
// mountpath is cleaned, we need to clean up the bindTarget as well
|
||||
bindTarget := path.Join(diskBindBase, d.DevicePath)
|
||||
if err := d.Filesystem.Unmount(op, bindTarget); err != nil {
|
||||
return fmt.Errorf("failed to clean up actual mount point on device: %s", err)
|
||||
}
|
||||
|
||||
// only remove the mount directory - if we've succeeded in the unmount there won't be anything in it
|
||||
// if we somehow get here and there is content we do NOT want to delete it
|
||||
if err := os.Remove(bindTarget); err != nil {
|
||||
err := fmt.Errorf("failed to clean up actual mount point: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *VirtualDisk) mountPathFn() (string, error) {
|
||||
if !d.mounted() {
|
||||
return "", fmt.Errorf("%s isn't mounted", d.DatastoreURI)
|
||||
}
|
||||
|
||||
return d.mountPath, nil
|
||||
}
|
||||
|
||||
// MountPath returns the path on which the virtual disk is mounted,
|
||||
// or an error if the disk is not mounted
|
||||
func (d *VirtualDisk) MountPath() (string, error) {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
return d.mountPathFn()
|
||||
}
|
||||
|
||||
// DiskPath returns a URL referencing the path of the virtual disk
|
||||
// on the datastore
|
||||
func (d *VirtualDisk) DiskPath() url.URL {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
return url.URL{
|
||||
Scheme: "ds",
|
||||
Path: d.DatastoreURI.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *VirtualDisk) mounted() bool {
|
||||
return d.mountPath != ""
|
||||
}
|
||||
|
||||
// Mounted returns true if the virtual disk is mounted, false otherwise
|
||||
func (d *VirtualDisk) Mounted() bool {
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
return d.mounted()
|
||||
}
|
||||
|
||||
func (d *VirtualDisk) canBeUnmounted() error {
|
||||
if !d.attached() {
|
||||
return fmt.Errorf("%s is detached", d.DatastoreURI)
|
||||
}
|
||||
|
||||
if !d.mounted() {
|
||||
return fmt.Errorf("%s is unmounted", d.DatastoreURI)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *VirtualDisk) setUmounted() error {
|
||||
if !d.mounted() {
|
||||
return fmt.Errorf("%s already unmounted", d.DatastoreURI)
|
||||
}
|
||||
|
||||
d.mountPath = ""
|
||||
return nil
|
||||
}
|
||||
292
vendor/github.com/vmware/vic/pkg/vsphere/disk/disk_ext4_test.go
generated
vendored
Normal file
292
vendor/github.com/vmware/vic/pkg/vsphere/disk/disk_ext4_test.go
generated
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"context"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/pkg/mount"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/vic/lib/guest"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/datastore"
|
||||
)
|
||||
|
||||
// Create a disk, make an ext filesystem on it, set the label, mount it,
|
||||
// unmount it, then clean up.
|
||||
func TestCreateFS(t *testing.T) {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
if testing.Verbose() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
session := Session(context.Background(), t)
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
op := trace.NewOperation(context.TODO(), t.Name())
|
||||
|
||||
vchvm, err := guest.GetSelf(op, session)
|
||||
if err != nil {
|
||||
t.Skip("Not in a vm")
|
||||
}
|
||||
view := ContainerView(op, session, vchvm)
|
||||
if view == nil {
|
||||
t.Skip("Can't create a view")
|
||||
}
|
||||
|
||||
imagestore := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: datastore.TestName(t.Name()),
|
||||
}
|
||||
|
||||
// file manager
|
||||
fm := object.NewFileManager(session.Vim25())
|
||||
// create a directory in the datastore
|
||||
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Nuke the image store
|
||||
defer func() {
|
||||
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
_, err = task.WaitForResult(context.TODO(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// create a diskmanager
|
||||
vdm, err := NewDiskManager(op, session, view)
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
|
||||
return
|
||||
}
|
||||
|
||||
diskSize := int64(1 << 10)
|
||||
scratch := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: path.Join(imagestore.Path, "scratch.vmdk"),
|
||||
}
|
||||
|
||||
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
|
||||
d, err := vdm.CreateAndAttach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// make the filesysetem
|
||||
if err = d.Mkfs(op, "foo"); !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// set the label
|
||||
if err = d.SetLabel(op, "foo"); !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// do the mount
|
||||
dir, err := d.Mount(op, nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// boom
|
||||
if mounted, err := mount.Mounted(dir); !assert.NoError(t, err) || !assert.True(t, mounted) {
|
||||
return
|
||||
}
|
||||
|
||||
// clean up
|
||||
err = d.Unmount(op)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
err = vdm.Detach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachFS(t *testing.T) {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
if testing.Verbose() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
session := Session(context.Background(), t)
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
op := trace.NewOperation(context.TODO(), t.Name())
|
||||
|
||||
vchvm, err := guest.GetSelf(op, session)
|
||||
if err != nil {
|
||||
t.Skip("Not in a vm")
|
||||
}
|
||||
view := ContainerView(op, session, vchvm)
|
||||
if view == nil {
|
||||
t.Skip("Can't create a view")
|
||||
}
|
||||
|
||||
imagestore := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: datastore.TestName(t.Name()),
|
||||
}
|
||||
|
||||
// file manager
|
||||
fm := object.NewFileManager(session.Vim25())
|
||||
// create a directory in the datastore
|
||||
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Nuke the image store
|
||||
defer func() {
|
||||
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
_, err = task.WaitForResult(context.TODO(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// create a diskmanager
|
||||
vdm, err := NewDiskManager(op, session, view)
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
|
||||
return
|
||||
}
|
||||
|
||||
diskSize := int64(1 << 10)
|
||||
scratch := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: path.Join(imagestore.Path, "scratch.vmdk"),
|
||||
}
|
||||
|
||||
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
|
||||
d, err := vdm.CreateAndAttach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// make the filesysetem
|
||||
if err = d.Mkfs(op, "foo"); !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// set the label
|
||||
if err = d.SetLabel(op, "foo"); !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// do the mount
|
||||
dir, err := d.Mount(op, nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// boom
|
||||
if mounted, err := mount.Mounted(dir); !assert.NoError(t, err) || !assert.True(t, mounted) {
|
||||
return
|
||||
}
|
||||
|
||||
// clean up
|
||||
err = d.Unmount(op)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
err = vdm.Detach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
child := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: path.Join(imagestore.Path, "child.vmdk"),
|
||||
}
|
||||
|
||||
config = NewPersistentDisk(child).WithParent(scratch)
|
||||
d, err = vdm.CreateAndAttach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// do the mount
|
||||
dir, err = d.Mount(op, nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// boom
|
||||
if mounted, err := mount.Mounted(dir); !assert.NoError(t, err) || !assert.True(t, mounted) {
|
||||
return
|
||||
}
|
||||
|
||||
// clean up
|
||||
err = d.Unmount(op)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
err = vdm.Detach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
config = NewPersistentDisk(child)
|
||||
d, err = vdm.CreateAndAttach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// do the mount
|
||||
dir, err = d.Mount(op, nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// boom
|
||||
if mounted, err := mount.Mounted(dir); !assert.NoError(t, err) || !assert.True(t, mounted) {
|
||||
return
|
||||
}
|
||||
|
||||
// clean up
|
||||
err = d.Unmount(op)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
err = vdm.Detach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
668
vendor/github.com/vmware/vic/pkg/vsphere/disk/disk_manager.go
generated
vendored
Normal file
668
vendor/github.com/vmware/vic/pkg/vsphere/disk/disk_manager.go
generated
vendored
Normal file
@@ -0,0 +1,668 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/view"
|
||||
"github.com/vmware/govmomi/vim25/mo"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/pkg/errors"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/datastore"
|
||||
"github.com/vmware/vic/pkg/vsphere/guest"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/tasks"
|
||||
"github.com/vmware/vic/pkg/vsphere/vm"
|
||||
)
|
||||
|
||||
const (
|
||||
// You can assign the device to (1:z ), where 1 is SCSI controller 1 and z is a virtual device node from 0 to 15.
|
||||
// https://pubs.vmware.com/vsphere-65/index.jsp#com.vmware.vsphere.vm_admin.doc/GUID-5872D173-A076-42FE-8D0B-9DB0EB0E7362.html
|
||||
MaxAttachedDisks = 16
|
||||
)
|
||||
|
||||
// Manager manages disks for the vm it runs on. The expectation is this is run
|
||||
// from a VM on a vsphere instance. This VM creates disks on ESX, attaches
|
||||
// them to itself, writes to them, then detaches them.
|
||||
type Manager struct {
|
||||
// We can't have more than this number of disks attached.
|
||||
maxAttached chan bool
|
||||
|
||||
// reference to the vm this is running on.
|
||||
vm *vm.VirtualMachine
|
||||
|
||||
// VirtualDiskManager that is used to create vmdks directly on datastore
|
||||
// from https://pubs.vmware.com/vsphere-65/index.jsp?topic=%2Fcom.vmware.vspsdk.apiref.doc%2Fvim.VirtualDiskManager.html
|
||||
// Most VirtualDiskManager APIs will be DEPRECATED as of vSphere 6.5. Please use VStorageObjectManager APIs to manage Virtual disks.
|
||||
vdMngr *object.VirtualDiskManager
|
||||
|
||||
// ContainerView - https://pubs.vmware.com/vsphere-6-0/index.jsp#com.vmware.wssdk.apiref.doc/vim.view.ContainerView.html
|
||||
view *view.ContainerView
|
||||
|
||||
// The controller on this vm.
|
||||
controller *types.ParaVirtualSCSIController
|
||||
|
||||
// The PCI + SCSI device /dev node string format the disks can be attached with
|
||||
byPathFormat string
|
||||
|
||||
// serialize reconfigure operations
|
||||
mu sync.Mutex
|
||||
|
||||
// map of URIs to VirtualDisk structs so that we can return the same instance to the caller, required for ref counting
|
||||
Disks map[uint64]*VirtualDisk
|
||||
// used for locking the disk cache
|
||||
disksLock sync.Mutex
|
||||
}
|
||||
|
||||
// NewDiskManager creates a new Manager instance associated with the caller VM
|
||||
func NewDiskManager(op trace.Operation, session *session.Session, v *view.ContainerView) (*Manager, error) {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
vm, err := guest.GetSelf(op, session)
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
|
||||
// create handle to the docker daemon VM as we need to mount disks on it
|
||||
controller, byPathFormat, err := verifyParavirtualScsiController(op, vm)
|
||||
if err != nil {
|
||||
op.Errorf("scsi controller verification failed: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
maxAttached: make(chan bool, MaxAttachedDisks),
|
||||
vm: vm,
|
||||
vdMngr: object.NewVirtualDiskManager(vm.Vim25()),
|
||||
view: v,
|
||||
controller: controller,
|
||||
byPathFormat: byPathFormat,
|
||||
Disks: make(map[uint64]*VirtualDisk),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// toSpec converts the given config to VirtualDisk spec
|
||||
func (m *Manager) toSpec(config *VirtualDiskConfig) *types.VirtualDisk {
|
||||
backing := &types.VirtualDiskFlatVer2BackingInfo{
|
||||
DiskMode: string(config.DiskMode),
|
||||
ThinProvisioned: types.NewBool(true),
|
||||
VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
|
||||
FileName: config.DatastoreURI.String(),
|
||||
},
|
||||
}
|
||||
|
||||
if config.UUID != "" {
|
||||
backing.Uuid = config.UUID
|
||||
}
|
||||
|
||||
disk := &types.VirtualDisk{
|
||||
VirtualDevice: types.VirtualDevice{
|
||||
Key: -1,
|
||||
ControllerKey: m.controller.Key,
|
||||
UnitNumber: new(int32),
|
||||
Backing: backing,
|
||||
},
|
||||
// As of vSphere API 5.5 capacityInKB is deprecated. Documentation suggest using capacityInBytes but we can't unset CapacityInKB and its default value 0 causes problems
|
||||
// ... Exception thrown during reconfigure: (vim.vm.ConfigSpec) {
|
||||
// ...
|
||||
// --> unitNumber = -1,
|
||||
// --> capacityInKB = 0,
|
||||
// --> capacityInBytes = 8192000000,
|
||||
// --> shares = (vim.SharesInfo) null,
|
||||
// ...
|
||||
CapacityInBytes: config.CapacityInKB * 1024,
|
||||
CapacityInKB: config.CapacityInKB,
|
||||
}
|
||||
|
||||
if config.ParentDatastoreURI != nil {
|
||||
backing.Parent = &types.VirtualDiskFlatVer2BackingInfo{
|
||||
VirtualDeviceFileBackingInfo: types.VirtualDeviceFileBackingInfo{
|
||||
FileName: config.ParentDatastoreURI.String(),
|
||||
},
|
||||
}
|
||||
|
||||
// Capacity needs to be 0 as we inherit it from the parent
|
||||
disk.CapacityInBytes = 0
|
||||
disk.CapacityInKB = 0
|
||||
}
|
||||
|
||||
// It's possible the VCH has a disk already attached.
|
||||
*disk.VirtualDevice.UnitNumber = -1
|
||||
|
||||
return disk
|
||||
}
|
||||
|
||||
// CreateAndAttach creates a new VMDK, attaches it and ensures that the device becomes visible to the caller.
|
||||
// Returns a VirtualDisk corresponding to the created and attached disk.
|
||||
func (m *Manager) CreateAndAttach(op trace.Operation, config *VirtualDiskConfig) (*VirtualDisk, error) {
|
||||
defer trace.End(trace.Begin(config.DatastoreURI.String()))
|
||||
|
||||
// Get or create entry in disk cache
|
||||
m.disksLock.Lock()
|
||||
d, err := NewVirtualDisk(op, config, m.Disks)
|
||||
if err != nil {
|
||||
m.disksLock.Unlock()
|
||||
|
||||
op.Errorf("Unable to create disk entry: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
// take disk lock before we release the cache lock - this prevents the disk being removed from the cache
|
||||
// before we get a chance to adjust refcounts
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
m.disksLock.Unlock()
|
||||
|
||||
// check if the disk is attached from the perspective of the cache entry
|
||||
if d.DevicePath != "" {
|
||||
// this is a horrificaly misnamed call - it's incrementing the reference count
|
||||
d.setAttached(op, "")
|
||||
return d, nil
|
||||
}
|
||||
|
||||
op.Infof("Create/attach vmdk %s from parent %s", config.DatastoreURI, config.ParentDatastoreURI)
|
||||
|
||||
// we use findDiskByFilename to check if the disk is already attached
|
||||
// if it is then it's indicative of an error because it wasn't found in the cache, but this lets us recover
|
||||
_, ferr := findDiskByFilename(op, m.vm, d.DatastoreURI.String(), d.IsPersistent())
|
||||
if os.IsNotExist(ferr) {
|
||||
if err := m.attach(op, config); err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
} else {
|
||||
op.Errorf("Failed to determine if disk is already attached: %s", err)
|
||||
// this will be tidied up if/when the waitForDevice fails
|
||||
}
|
||||
|
||||
op.Debugf("Mapping vmdk to pci device %s", config.DatastoreURI)
|
||||
devicePath, err := m.devicePathByURI(op, config.DatastoreURI, d.IsPersistent())
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
|
||||
blockDev, err := waitForDevice(op, devicePath)
|
||||
if err != nil {
|
||||
op.Errorf("waitForDevice failed for %s with %s", d.DatastoreURI, errors.ErrorStack(err))
|
||||
// ensure that the disk is detached if it's the publish that's failed
|
||||
|
||||
disk, findErr := findDiskByFilename(op, m.vm, d.DatastoreURI.String(), d.IsPersistent())
|
||||
if findErr != nil {
|
||||
op.Debugf("findDiskByFilename(%s) failed with %s", d.DatastoreURI, errors.ErrorStack(findErr))
|
||||
}
|
||||
|
||||
if detachErr := m.detach(op, disk); detachErr != nil {
|
||||
op.Debugf("detach(%s) failed with %s", d.DatastoreURI, errors.ErrorStack(detachErr))
|
||||
}
|
||||
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
|
||||
err = d.setAttached(op, blockDev)
|
||||
|
||||
return d, err
|
||||
}
|
||||
|
||||
// Create creates a disk without a parent (and doesn't attach it).
|
||||
func (m *Manager) Create(op trace.Operation, config *VirtualDiskConfig) (*VirtualDisk, error) {
|
||||
defer trace.End(trace.Begin(config.DatastoreURI.String()))
|
||||
|
||||
var err error
|
||||
|
||||
d, err := NewVirtualDisk(op, config, m.Disks)
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
spec := &types.FileBackedVirtualDiskSpec{
|
||||
VirtualDiskSpec: types.VirtualDiskSpec{
|
||||
DiskType: string(types.VirtualDiskTypeThin),
|
||||
AdapterType: string(types.VirtualDiskAdapterTypeLsiLogic),
|
||||
},
|
||||
CapacityKb: config.CapacityInKB,
|
||||
}
|
||||
|
||||
op.Infof("Creating vmdk for layer or volume %s", d.DatastoreURI)
|
||||
err = tasks.Wait(op, func(ctx context.Context) (tasks.Task, error) {
|
||||
return m.vdMngr.CreateVirtualDisk(ctx, d.DatastoreURI.String(), nil, spec)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Gets a disk given a datastore path URI to the vmdk
|
||||
func (m *Manager) Get(op trace.Operation, config *VirtualDiskConfig) (*VirtualDisk, error) {
|
||||
defer trace.End(trace.Begin(config.DatastoreURI.String()))
|
||||
|
||||
d, err := NewVirtualDisk(op, config, m.Disks)
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
d.ParentDatastoreURI, err = m.DiskParent(op, config)
|
||||
return d, err
|
||||
}
|
||||
|
||||
// DiskParent returns the parent for an existing disk, based on the disk datastore URI in the config,
|
||||
// and ignoring any parent specified in the config.
|
||||
// datastore path will be nil if the disk has no parent
|
||||
func (m *Manager) DiskParent(op trace.Operation, config *VirtualDiskConfig) (*object.DatastorePath, error) {
|
||||
defer trace.End(trace.Begin(config.DatastoreURI.String()))
|
||||
|
||||
info, err := m.vdMngr.QueryVirtualDiskInfo(op, config.DatastoreURI.String(), m.vm.Datacenter, true)
|
||||
if err != nil {
|
||||
op.Errorf("Error querying parents (%s): %s", config.DatastoreURI, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// the last elem in the info list is the disk we just looked up.
|
||||
p := info[len(info)-1]
|
||||
|
||||
if p.Parent != "" {
|
||||
ppth, err := datastore.PathFromString(p.Parent)
|
||||
if err != nil {
|
||||
op.Errorf("Error converting parent to datastore URI (%s): %s", p.Parent, err)
|
||||
return nil, err
|
||||
}
|
||||
return ppth, nil
|
||||
}
|
||||
|
||||
// no parent
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TODO(FA) this doesn't work since delta disks get set with `deletable =
|
||||
// false` when they become parents. This needs some thought and will require
|
||||
// some answers from a larger context.
|
||||
//func (m *DiskManager) Delete(ctx context.Context, d *VirtualDisk) error {
|
||||
// defer trace.End(trace.Begin(d.DatastoreURI))
|
||||
//
|
||||
// log.Infof("Deleting %s", d.DatastoreURI)
|
||||
//
|
||||
// d.l.Lock()
|
||||
// defer d.l.Unlock()
|
||||
//
|
||||
// if d.isAttached() {
|
||||
// return fmt.Errorf("cannot delete %s, still attached (%s)", d.DatastoreURI, d.devicePath)
|
||||
// }
|
||||
//
|
||||
// // TODO(FA) Check if disk is a parent.
|
||||
//
|
||||
// vdm := object.NewVirtualDiskManager(m.vm.Client())
|
||||
// task, err := vdm.DeleteVirtualDisk(ctx, d.DatastoreURI, nil)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// err = task.Wait(ctx)
|
||||
// if err != nil {
|
||||
// return errors.Trace(err)
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// Attach attempts to attach a virtual disk
|
||||
func (m *Manager) attach(op trace.Operation, config *VirtualDiskConfig) error {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
disk := m.toSpec(config)
|
||||
|
||||
deviceList := object.VirtualDeviceList{}
|
||||
deviceList = append(deviceList, disk)
|
||||
|
||||
changeSpec, err := deviceList.ConfigSpec(types.VirtualDeviceConfigSpecOperationAdd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
machineSpec := types.VirtualMachineConfigSpec{}
|
||||
machineSpec.DeviceChange = append(machineSpec.DeviceChange, changeSpec...)
|
||||
|
||||
// ensure we abide by max attached disks limits
|
||||
m.maxAttached <- true
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// make sure the op is still valid as the above line could block for a long time
|
||||
select {
|
||||
case <-op.Done():
|
||||
return op.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
_, err = m.vm.WaitForResult(op, func(ctx context.Context) (tasks.Task, error) {
|
||||
t, er := m.vm.Reconfigure(ctx, machineSpec)
|
||||
|
||||
if t != nil {
|
||||
op.Debugf("Attach reconfigure task=%s", t.Reference())
|
||||
}
|
||||
|
||||
return t, er
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
select {
|
||||
case <-m.maxAttached:
|
||||
default:
|
||||
}
|
||||
|
||||
op.Errorf("vmdk storage driver failed to attach disk: %s", errors.ErrorStack(err))
|
||||
return errors.Trace(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Detach attempts to detach a virtual disk
|
||||
func (m *Manager) Detach(op trace.Operation, config *VirtualDiskConfig) error {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
// we have to hold the cache lock until we're done deleting the cache entry
|
||||
// or until we know we're not going to delete the entry
|
||||
m.disksLock.Lock()
|
||||
defer m.disksLock.Unlock()
|
||||
|
||||
d, err := NewVirtualDisk(op, config, m.Disks)
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
|
||||
d.l.Lock()
|
||||
defer d.l.Unlock()
|
||||
|
||||
count := d.attachedRefs.Decrement()
|
||||
op.Debugf("decremented attach count for %s: %d", d.DatastoreURI, count)
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := d.canBeDetached(); err != nil {
|
||||
op.Errorf("disk needs to be detached but is still in use: %s", err)
|
||||
return errors.Trace(err)
|
||||
}
|
||||
|
||||
op.Infof("Detaching disk %s", d.DevicePath)
|
||||
|
||||
disk, err := findDiskByFilename(op, m.vm, d.DatastoreURI.String(), d.IsPersistent())
|
||||
if err != nil {
|
||||
return errors.Trace(err)
|
||||
}
|
||||
|
||||
if err = m.detach(op, disk); err != nil {
|
||||
op.Errorf("detach for %s failed with %s", d.DevicePath, errors.ErrorStack(err))
|
||||
return errors.Trace(err)
|
||||
}
|
||||
|
||||
// this deletes the disk from the disk cache
|
||||
d.setDetached(op, m.Disks)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) DetachAll(op trace.Operation) error {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
disks, err := findAllDisks(op, m.vm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, disk := range disks {
|
||||
if err2 := m.detach(op, disk); err != nil {
|
||||
op.Errorf("error detaching disk: %s", err2.Error())
|
||||
// return the last error on the return of this function
|
||||
err = err2
|
||||
// if we failed here that means we have a disk attached, ensure we abide by max attached disks limits
|
||||
m.maxAttached <- true
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) detach(op trace.Operation, disk *types.VirtualDisk) error {
|
||||
config := []types.BaseVirtualDeviceConfigSpec{
|
||||
&types.VirtualDeviceConfigSpec{
|
||||
Device: disk,
|
||||
Operation: types.VirtualDeviceConfigSpecOperationRemove,
|
||||
},
|
||||
}
|
||||
|
||||
spec := types.VirtualMachineConfigSpec{}
|
||||
spec.DeviceChange = config
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
_, err := m.vm.WaitForResult(op, func(ctx context.Context) (tasks.Task, error) {
|
||||
t, er := m.vm.Reconfigure(ctx, spec)
|
||||
|
||||
if t != nil {
|
||||
op.Debugf("Detach reconfigure task=%s", t.Reference())
|
||||
}
|
||||
|
||||
return t, er
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
select {
|
||||
case <-m.maxAttached:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) devicePathByURI(op trace.Operation, datastoreURI *object.DatastorePath, persistent bool) (string, error) {
|
||||
disk, err := findDiskByFilename(op, m.vm, datastoreURI.String(), persistent)
|
||||
if err != nil {
|
||||
op.Errorf("findDisk failed for %s with %s", datastoreURI.String(), errors.ErrorStack(err))
|
||||
return "", errors.Trace(err)
|
||||
}
|
||||
|
||||
sysPath := fmt.Sprintf(m.byPathFormat, *disk.UnitNumber)
|
||||
|
||||
return sysPath, nil
|
||||
}
|
||||
|
||||
// AttachAndMount creates and attaches a vmdk as a non-persistent disk, mounts it, and returns the mount path.
|
||||
func (m *Manager) AttachAndMount(op trace.Operation, datastoreURI *object.DatastorePath, persistent bool) (string, error) {
|
||||
var config *VirtualDiskConfig
|
||||
|
||||
op.Infof("Attach/Mount %s", datastoreURI.String())
|
||||
|
||||
if !persistent {
|
||||
config = NewNonPersistentDisk(datastoreURI)
|
||||
} else {
|
||||
config = NewPersistentDisk(datastoreURI)
|
||||
}
|
||||
|
||||
d, err := m.CreateAndAttach(op, config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// don't update access time - that would cause the diff operation to mutate the filesystem
|
||||
opts := []string{"noatime"}
|
||||
|
||||
if !persistent {
|
||||
opts = append(opts, "ro")
|
||||
}
|
||||
|
||||
return d.Mount(op, opts)
|
||||
|
||||
}
|
||||
|
||||
// UnmountAndDetach unmounts and detaches a disk, subsequently cleaning the mount path
|
||||
func (m *Manager) UnmountAndDetach(op trace.Operation, datastoreURI *object.DatastorePath, persistent bool) error {
|
||||
var config *VirtualDiskConfig
|
||||
|
||||
if !persistent {
|
||||
config = NewNonPersistentDisk(datastoreURI)
|
||||
} else {
|
||||
config = NewPersistentDisk(datastoreURI)
|
||||
}
|
||||
|
||||
d, err := NewVirtualDisk(op, config, m.Disks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op.Infof("Unmount and Detach %s:%s", d.mountPath, d.DatastoreURI)
|
||||
|
||||
err = d.Unmount(op)
|
||||
derr := m.Detach(op, config)
|
||||
|
||||
if err != nil || derr != nil {
|
||||
op.Errorf("Error during unmount or detach, unmount: %s, detach: %s", err, derr)
|
||||
// prioritize first error
|
||||
if err == nil {
|
||||
err = derr
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) InUse(op trace.Operation, config *VirtualDiskConfig, filter func(vm *mo.VirtualMachine) bool) ([]*vm.VirtualMachine, error) {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
mngr := view.NewManager(m.vm.Vim25())
|
||||
|
||||
// Create view of VirtualMachine objects under the VCH's resource pool
|
||||
view2, err := mngr.CreateContainerView(op, m.vm.Session.Pool.Reference(), []string{"VirtualMachine"}, true)
|
||||
if err != nil {
|
||||
op.Errorf("failed to create view: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer view2.Destroy(op)
|
||||
|
||||
var mos []mo.VirtualMachine
|
||||
// Retrieve needed properties of all machines under this view
|
||||
err = view2.Retrieve(op, []string{"VirtualMachine"}, []string{"name", "config.hardware", "runtime.powerState"}, &mos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var vms []*vm.VirtualMachine
|
||||
// iterate over them to see whether they have the disk we want
|
||||
for i := range mos {
|
||||
mo := mos[i]
|
||||
op.Debugf("Working on vm %q", mo.Name)
|
||||
|
||||
if !filter(&mo) {
|
||||
op.Debugf("Filtering out vm %q", mo.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, device := range mo.Config.Hardware.Device {
|
||||
label := device.GetVirtualDevice().DeviceInfo.GetDescription().Label
|
||||
db := device.GetVirtualDevice().Backing
|
||||
if db == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch t := db.(type) {
|
||||
case types.BaseVirtualDeviceFileBackingInfo:
|
||||
if config.DatastoreURI.String() == t.GetVirtualDeviceFileBackingInfo().FileName {
|
||||
op.Infof("Found active user of target disk %s: %q", label, mo.Name)
|
||||
vms = append(vms, vm.NewVirtualMachine(context.Background(), m.vm.Session, mo.Reference()))
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
return vms, nil
|
||||
}
|
||||
|
||||
func (m *Manager) DiskFinder(op trace.Operation, filter func(p string) bool) (string, error) {
|
||||
defer trace.End(trace.Begin(""))
|
||||
|
||||
mngr := view.NewManager(m.vm.Vim25())
|
||||
|
||||
// Create view of VirtualMachine objects under the VCH's resource pool
|
||||
view2, err := mngr.CreateContainerView(op, m.vm.Session.Pool.Reference(), []string{"VirtualMachine"}, true)
|
||||
if err != nil {
|
||||
op.Errorf("failed to create view: %s", err)
|
||||
return "", err
|
||||
}
|
||||
defer view2.Destroy(op)
|
||||
|
||||
var mos []mo.VirtualMachine
|
||||
// Retrieve needed properties of all machines under this view
|
||||
err = view2.Retrieve(op, []string{"VirtualMachine"}, []string{"name", "config.hardware", "runtime.powerState"}, &mos)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// iterate over them to see whether they have the disk we want
|
||||
for i := range mos {
|
||||
mo := mos[i]
|
||||
|
||||
op.Debugf("Working on vm %q", mo.Name)
|
||||
|
||||
// observed empty fields here when copying to all 14 volumes on a cVM so being paranoid
|
||||
if mo.Config == nil || mo.Config.Hardware.Device == nil {
|
||||
op.Warnf("Skipping disk presence check for %q: failed to retrieve vm config", mo.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, device := range mo.Config.Hardware.Device {
|
||||
label := device.GetVirtualDevice().DeviceInfo.GetDescription().Label
|
||||
db := device.GetVirtualDevice().Backing
|
||||
if db == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch t := db.(type) {
|
||||
case types.BaseVirtualDeviceFileBackingInfo:
|
||||
diskPath := t.GetVirtualDeviceFileBackingInfo().FileName
|
||||
if filter(diskPath) {
|
||||
op.Infof("Found disk matching filter: (label: %s), %q", label, diskPath)
|
||||
return diskPath, nil
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errors.New("Not found")
|
||||
}
|
||||
|
||||
func (m *Manager) Owners(op trace.Operation, url *url.URL, filter func(vm *mo.VirtualMachine) bool) ([]*vm.VirtualMachine, error) {
|
||||
dsPath, err := datastore.PathFromString(url.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.InUse(op, NewPersistentDisk(dsPath), filter)
|
||||
}
|
||||
739
vendor/github.com/vmware/vic/pkg/vsphere/disk/disk_manager_test.go
generated
vendored
Normal file
739
vendor/github.com/vmware/vic/pkg/vsphere/disk/disk_manager_test.go
generated
vendored
Normal file
@@ -0,0 +1,739 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/view"
|
||||
"github.com/vmware/govmomi/vim25/mo"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/lib/guest"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/datastore"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/tasks"
|
||||
"github.com/vmware/vic/pkg/vsphere/test/env"
|
||||
)
|
||||
|
||||
func Session(ctx context.Context, t *testing.T) *session.Session {
|
||||
config := &session.Config{
|
||||
Service: env.URL(t),
|
||||
|
||||
DatastorePath: env.DS(t),
|
||||
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
}
|
||||
|
||||
s := session.NewSession(config)
|
||||
|
||||
_, err := s.Connect(ctx)
|
||||
if err != nil {
|
||||
t.Skip(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// we're treating this as an atomic behaviour, so log out if we failed
|
||||
defer func() {
|
||||
if err != nil {
|
||||
t.Skip(err.Error())
|
||||
s.Client.Logout(ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = s.Populate(ctx)
|
||||
if err != nil {
|
||||
t.Skip(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Vsan has special UUID / URI handling of top level directories which
|
||||
// we've implemented at another level. We can't import them here or it'd
|
||||
// be a circular dependency. Also, we already have tests that test these
|
||||
// cases but from a higher level.
|
||||
if err != nil || s.IsVSAN(ctx) {
|
||||
t.Logf("error: %s", err.Error())
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func ContainerView(ctx context.Context, session *session.Session, vm *object.VirtualMachine) *view.ContainerView {
|
||||
mngr := view.NewManager(session.Vim25())
|
||||
|
||||
pool, err := vm.ResourcePool(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create view of VirtualMachine objects under the VCH's resource pool
|
||||
v, err := mngr.CreateContainerView(ctx, pool.Reference(), []string{"VirtualMachine"}, true)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Create a lineage of disks inheriting from eachother, write portion of a
|
||||
// string to each, the confirm the result is the whole string
|
||||
func TestCreateAndDetach(t *testing.T) {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
if testing.Verbose() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
session := Session(context.Background(), t)
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
op := trace.NewOperation(context.TODO(), t.Name())
|
||||
|
||||
vchvm, err := guest.GetSelf(op, session)
|
||||
if err != nil {
|
||||
t.Skip("Not in a vm")
|
||||
}
|
||||
view := ContainerView(op, session, vchvm)
|
||||
if view == nil {
|
||||
t.Skip("Can't create a view")
|
||||
}
|
||||
|
||||
imagestore := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: datastore.TestName(t.Name()),
|
||||
}
|
||||
|
||||
// file manager
|
||||
fm := object.NewFileManager(session.Vim25())
|
||||
// create a directory in the datastore
|
||||
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Nuke the image store
|
||||
defer func() {
|
||||
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
_, err = task.WaitForResult(context.TODO(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// create a diskmanager
|
||||
vdm, err := NewDiskManager(op, session, view)
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
|
||||
return
|
||||
}
|
||||
|
||||
diskSize := int64(1 << 10)
|
||||
scratch := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: path.Join(imagestore.Path, "scratch.vmdk"),
|
||||
}
|
||||
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
|
||||
parent, err := vdm.Create(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
numChildren := 3
|
||||
children := make([]*VirtualDisk, numChildren)
|
||||
|
||||
testString := "Ground control to Major Tom"
|
||||
writeSize := len(testString) / numChildren
|
||||
// Create children which inherit from each other
|
||||
for i := 0; i < numChildren; i++ {
|
||||
|
||||
p := &object.DatastorePath{
|
||||
Datastore: imagestore.Datastore,
|
||||
Path: path.Join(imagestore.Path, fmt.Sprintf("child%d.vmdk", i)),
|
||||
}
|
||||
|
||||
config := NewPersistentDisk(p).WithParent(parent.DatastoreURI)
|
||||
child, cerr := vdm.CreateAndAttach(op, config)
|
||||
if !assert.NoError(t, cerr) {
|
||||
return
|
||||
}
|
||||
refs := child.attachedRefs.Count()
|
||||
assert.EqualValues(t, 1, refs, "Expected %d attach references, found %d", refs)
|
||||
|
||||
// attempt to recreate and attach the already attached disk
|
||||
config = NewPersistentDisk(p).WithParent(parent.DatastoreURI)
|
||||
child, cerr = vdm.CreateAndAttach(op, config)
|
||||
if !assert.NoError(t, cerr) {
|
||||
return
|
||||
}
|
||||
refs = child.attachedRefs.Count()
|
||||
assert.EqualValues(t, 2, refs, "Expected %d attach references, found %d", refs)
|
||||
|
||||
// attempt detach
|
||||
cerr = vdm.Detach(op, config)
|
||||
if !assert.NoError(t, cerr) {
|
||||
return
|
||||
}
|
||||
// should not actually detach, and should still have 1 reference
|
||||
refs = child.attachedRefs.Count()
|
||||
assert.EqualValues(t, 1, refs, "Expected %d attach references, found %d", refs)
|
||||
|
||||
children[i] = child
|
||||
|
||||
// Write directly to the disk
|
||||
f, cerr := os.OpenFile(child.DevicePath, os.O_RDWR, os.FileMode(0777))
|
||||
if !assert.NoError(t, cerr) {
|
||||
return
|
||||
}
|
||||
|
||||
start := i * writeSize
|
||||
end := start + writeSize
|
||||
|
||||
if i == numChildren-1 {
|
||||
// last chunk, write to the end.
|
||||
_, cerr = f.WriteAt([]byte(testString[start:]), int64(start))
|
||||
if !assert.NoError(t, cerr) || !assert.NoError(t, f.Sync()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to read the whole string
|
||||
b := make([]byte, len(testString))
|
||||
f.Seek(0, 0)
|
||||
_, cerr = f.Read(b)
|
||||
if !assert.NoError(t, cerr) {
|
||||
return
|
||||
}
|
||||
|
||||
//check against the test string
|
||||
if !assert.Equal(t, testString, string(b)) {
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
_, cerr = f.WriteAt([]byte(testString[start:end]), int64(start))
|
||||
if !assert.NoError(t, cerr) || !assert.NoError(t, f.Sync()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
f.Close()
|
||||
|
||||
cerr = vdm.Detach(op, config)
|
||||
if !assert.NoError(t, cerr) {
|
||||
return
|
||||
}
|
||||
|
||||
// use this image as the next parent
|
||||
parent = child
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefCounting(t *testing.T) {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
if testing.Verbose() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
session := Session(context.Background(), t)
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
op := trace.NewOperation(context.TODO(), t.Name())
|
||||
|
||||
vchvm, err := guest.GetSelf(op, session)
|
||||
if err != nil {
|
||||
t.Skip("Not in a vm")
|
||||
}
|
||||
view := ContainerView(op, session, vchvm)
|
||||
if view == nil {
|
||||
t.Skip("Can't create a view")
|
||||
}
|
||||
|
||||
imagestore := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: datastore.TestName(t.Name()),
|
||||
}
|
||||
|
||||
// file manager
|
||||
fm := object.NewFileManager(session.Vim25())
|
||||
// create a directory in the datastore
|
||||
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Nuke the image store
|
||||
defer func() {
|
||||
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
_, err = task.WaitForResult(context.TODO(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// create a diskmanager
|
||||
vdm, err := NewDiskManager(op, session, view)
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
|
||||
return
|
||||
}
|
||||
|
||||
diskSize := int64(1 << 10)
|
||||
scratch := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: path.Join(imagestore.Path, "scratch.vmdk"),
|
||||
}
|
||||
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
|
||||
p, err := vdm.Create(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.False(t, p.Attached(), "%s is attached but should not be", p.DatastoreURI)
|
||||
|
||||
child := &object.DatastorePath{
|
||||
Datastore: imagestore.Datastore,
|
||||
Path: path.Join(imagestore.Path, "testDisk.vmdk"),
|
||||
}
|
||||
config = NewPersistentDisk(child).WithParent(scratch)
|
||||
|
||||
// attempt attach
|
||||
assert.NoError(t, vdm.attach(op, config), "Error attempting to attach %s", config)
|
||||
|
||||
devicePath, err := vdm.devicePathByURI(op, child, config.IsPersistent())
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
d, err := NewVirtualDisk(op, config, vdm.Disks)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
blockDev, err := waitForDevice(op, devicePath)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.False(t, d.Attached(), "%s is attached but should not be", d.DatastoreURI)
|
||||
|
||||
// Attach the disk
|
||||
assert.NoError(t, d.setAttached(op, blockDev), "Error attempting to mark %s as attached", d.DatastoreURI)
|
||||
|
||||
assert.True(t, d.Attached(), "%s is not attached but should be", d.DatastoreURI)
|
||||
assert.NoError(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
|
||||
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
|
||||
assert.Equal(t, 1, d.attachedRefs.Count(), "%s has %d attach references but should have 1", d.DatastoreURI, d.attachedRefs.Count())
|
||||
|
||||
// attempt another attach at disk level to increase reference count
|
||||
// TODO(jzt): This should probably eventually use the attach code coming in
|
||||
// https://github.com/vmware/vic/issues/5422
|
||||
assert.NoError(t, d.setAttached(op, blockDev), "Error attempting to mark %s as attached", d.DatastoreURI)
|
||||
|
||||
assert.True(t, d.Attached(), "%s is not attached but should be", d.DatastoreURI)
|
||||
assert.Error(t, d.canBeDetached(), "%s should not be detachable but is", d.DatastoreURI)
|
||||
assert.True(t, d.InUseByOther(), "%s is not in use but should be", d.DatastoreURI)
|
||||
assert.Equal(t, 2, d.attachedRefs.Count(), "%s has %d attach references but should have 2", d.DatastoreURI, d.attachedRefs.Count())
|
||||
|
||||
// reduce reference count by calling detach
|
||||
d.setDetached(op, vdm.Disks)
|
||||
|
||||
assert.True(t, d.Attached(), "%s is not attached but should be", d.DatastoreURI)
|
||||
assert.NoError(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
|
||||
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
|
||||
assert.Equal(t, 1, d.attachedRefs.Count(), "%s has %d attach references but should have 1", d.DatastoreURI, d.attachedRefs.Count())
|
||||
|
||||
// test mount reference counting
|
||||
assert.NoError(t, d.Mkfs(op, "testDisk"), "Error attempting to format %s", d.DatastoreURI)
|
||||
|
||||
// create temp mount path
|
||||
dir, err := ioutil.TempDir("", "mnt")
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// cleanup
|
||||
defer func() {
|
||||
assert.NoError(t, os.RemoveAll(dir), "Error cleaning up mount path %s", dir)
|
||||
}()
|
||||
|
||||
// initial mount
|
||||
dir, err = d.Mount(op, nil)
|
||||
assert.NoError(t, err, "Error attempting to mount %s at %s", d.DatastoreURI, dir)
|
||||
|
||||
mountPath, err := d.MountPath()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.True(t, d.Mounted(), "%s is not mounted but should be", d.DatastoreURI)
|
||||
assert.Error(t, d.canBeDetached(), "%s should not be detachable but is", d.DatastoreURI)
|
||||
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
|
||||
assert.Equal(t, 1, d.mountedRefs.Count(), "%s has %d mount references but should have 1", d.DatastoreURI, d.mountedRefs.Count())
|
||||
assert.Equal(t, dir, mountPath, "%s is mounted at %s but should be mounted at %s", d.DatastoreURI, mountPath, dir)
|
||||
|
||||
// attempt another mount
|
||||
dir, err = d.Mount(op, nil)
|
||||
assert.NoError(t, err, "Error attempting to mount %s at %s", d.DatastoreURI, dir)
|
||||
|
||||
assert.True(t, d.Mounted(), "%s is not mounted but should be", d.DatastoreURI)
|
||||
assert.Error(t, d.canBeDetached(), "%s should not be detachable but is", d.DatastoreURI)
|
||||
assert.True(t, d.InUseByOther(), "%s is not in use but should be", d.DatastoreURI)
|
||||
assert.Equal(t, 2, d.mountedRefs.Count(), "%s has %d mount references but should have 2", d.DatastoreURI, d.mountedRefs.Count())
|
||||
|
||||
// attempt unmount
|
||||
assert.NoError(t, d.Unmount(op), "Error attempting to unmount %s", d.DatastoreURI)
|
||||
|
||||
assert.True(t, d.Mounted(), "%s is not mounted but should be", d.DatastoreURI)
|
||||
assert.Error(t, d.canBeDetached(), "%s should not be detachable but is", d.DatastoreURI)
|
||||
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
|
||||
assert.Equal(t, 1, d.mountedRefs.Count(), "%s has %d mount references but should have 1", d.DatastoreURI, d.mountedRefs.Count())
|
||||
|
||||
// actually unmount
|
||||
assert.NoError(t, d.Unmount(op), "Error attempting to unmount %s", d.DatastoreURI)
|
||||
|
||||
assert.False(t, d.Mounted(), "%s is mounted but should not be", d.DatastoreURI)
|
||||
assert.NoError(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
|
||||
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
|
||||
assert.Equal(t, 0, d.mountedRefs.Count(), "%s has %d mount references but should have 0", d.DatastoreURI, d.mountedRefs.Count())
|
||||
|
||||
// detach
|
||||
assert.NoError(t, vdm.Detach(op, config), "Error attempting to detach %s", d.DatastoreURI)
|
||||
|
||||
assert.False(t, d.Attached(), "%s is attached but should not be", d.DatastoreURI)
|
||||
assert.False(t, d.Mounted(), "%s is mounted but should not be", d.DatastoreURI)
|
||||
assert.Error(t, d.canBeDetached(), "%s should not be detachable but is", d.DatastoreURI)
|
||||
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
|
||||
assert.Equal(t, 0, d.attachedRefs.Count(), "%s has %d attach references but should have 0", d.DatastoreURI, d.attachedRefs.Count())
|
||||
assert.Equal(t, 0, d.mountedRefs.Count(), "%s has %d mount references but should have 0", d.DatastoreURI, d.mountedRefs.Count())
|
||||
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefCountingParallel(t *testing.T) {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
if testing.Verbose() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
session := Session(context.Background(), t)
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
op := trace.NewOperation(context.TODO(), t.Name())
|
||||
|
||||
vchvm, err := guest.GetSelf(op, session)
|
||||
if err != nil {
|
||||
t.Skip("Not in a vm")
|
||||
}
|
||||
view := ContainerView(op, session, vchvm)
|
||||
if view == nil {
|
||||
t.Skip("Can't create a view")
|
||||
}
|
||||
|
||||
imagestore := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: datastore.TestName(t.Name()),
|
||||
}
|
||||
|
||||
// file manager
|
||||
fm := object.NewFileManager(session.Vim25())
|
||||
// create a directory in the datastore
|
||||
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Nuke the image store
|
||||
defer func() {
|
||||
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
_, err = task.WaitForResult(context.TODO(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// create a diskmanager
|
||||
vdm, err := NewDiskManager(op, session, view)
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
|
||||
return
|
||||
}
|
||||
|
||||
diskSize := int64(1 << 10)
|
||||
uri := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: path.Join(imagestore.Path, "test.vmdk"),
|
||||
}
|
||||
config := NewPersistentDisk(uri).WithCapacity(diskSize)
|
||||
d, err := vdm.CreateAndAttach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.True(t, d.Attached(), "%s is not attached but should be", d.DatastoreURI)
|
||||
assert.NoError(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
|
||||
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
|
||||
assert.EqualValues(t, 1, d.attachedRefs.Count(), "%s has %d attach references but should have 1", d.DatastoreURI, d.attachedRefs.Count())
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
var err error
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < 5; j++ {
|
||||
|
||||
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
|
||||
|
||||
d, err = vdm.CreateAndAttach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
|
||||
|
||||
err = vdm.Detach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
assert.True(t, d.Attached(), "%s is not attached but should be", d.DatastoreURI)
|
||||
assert.NoError(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
|
||||
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
|
||||
assert.EqualValues(t, 1, d.attachedRefs.Count(), "%s has %d attach references but should have 1", d.DatastoreURI, d.attachedRefs.Count())
|
||||
|
||||
err = vdm.Detach(op, config)
|
||||
if !assert.NoError(t, err) {
|
||||
log.Error("Error detaching disk: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.False(t, d.Attached(), "%s is attached but should not be", d.DatastoreURI)
|
||||
assert.Error(t, d.canBeDetached(), "%s should be detachable but is not", d.DatastoreURI)
|
||||
assert.False(t, d.InUseByOther(), "%s is in use but should not be", d.DatastoreURI)
|
||||
assert.EqualValues(t, 0, d.attachedRefs.Count(), "%s has %d attach references but should have 0", d.DatastoreURI, d.attachedRefs.Count())
|
||||
}
|
||||
|
||||
func TestInUse(t *testing.T) {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
if testing.Verbose() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
session := Session(context.Background(), t)
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
op := trace.NewOperation(context.TODO(), t.Name())
|
||||
|
||||
vchvm, err := guest.GetSelf(op, session)
|
||||
if err != nil {
|
||||
t.Skip("Not in a vm")
|
||||
}
|
||||
view := ContainerView(op, session, vchvm)
|
||||
if view == nil {
|
||||
t.Skip("Can't create a view")
|
||||
}
|
||||
|
||||
imagestore := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: datastore.TestName(t.Name()),
|
||||
}
|
||||
|
||||
// file manager
|
||||
fm := object.NewFileManager(session.Vim25())
|
||||
// create a directory in the datastore
|
||||
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Nuke the image store
|
||||
defer func() {
|
||||
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
_, err = task.WaitForResult(context.TODO(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// create a diskmanager
|
||||
vdm, err := NewDiskManager(op, session, view)
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
|
||||
return
|
||||
}
|
||||
|
||||
// helper fn
|
||||
reconfigure := func(changes []types.BaseVirtualDeviceConfigSpec) error {
|
||||
t.Logf("Calling reconfigure")
|
||||
|
||||
machineSpec := types.VirtualMachineConfigSpec{}
|
||||
machineSpec.DeviceChange = changes
|
||||
|
||||
_, err := vdm.vm.WaitForResult(op, func(ctx context.Context) (tasks.Task, error) {
|
||||
t, er := vdm.vm.Reconfigure(ctx, machineSpec)
|
||||
|
||||
if t != nil {
|
||||
op.Debugf("reconfigure task=%s", t.Reference())
|
||||
}
|
||||
|
||||
return t, er
|
||||
})
|
||||
return err
|
||||
}
|
||||
// 1MB
|
||||
diskSize := int64(1 << 10)
|
||||
scratch := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: path.Join(imagestore.Path, "scratch.vmdk"),
|
||||
}
|
||||
// config
|
||||
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
|
||||
|
||||
// attach + create spec (scratch)
|
||||
spec := []types.BaseVirtualDeviceConfigSpec{
|
||||
&types.VirtualDeviceConfigSpec{
|
||||
Device: vdm.toSpec(config),
|
||||
Operation: types.VirtualDeviceConfigSpecOperationAdd,
|
||||
FileOperation: types.VirtualDeviceConfigSpecFileOperationCreate,
|
||||
},
|
||||
}
|
||||
|
||||
// filter powered off vms
|
||||
filter := func(vm *mo.VirtualMachine) bool {
|
||||
return vm.Runtime.PowerState != types.VirtualMachinePowerStatePoweredOn
|
||||
}
|
||||
|
||||
vms, err := vdm.InUse(op, config, filter)
|
||||
if !assert.NoError(t, err) && !assert.Len(t, vms, 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// reconfigure
|
||||
err = reconfigure(spec)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
t.Logf("scratch created and attached")
|
||||
|
||||
vms, err = vdm.InUse(op, config, filter)
|
||||
if !assert.NoError(t, err) && !assert.Len(t, vms, 1) {
|
||||
return
|
||||
}
|
||||
t.Logf("InUse by %s", vms)
|
||||
|
||||
// ref to scratch (needed for detach as initial spec's Key and UnitNumber was unset)
|
||||
disk, err := findDiskByFilename(op, vdm.vm, scratch.Path, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// DO NOT DETACH AND START WORKING ON THE CHILD
|
||||
|
||||
// child
|
||||
child := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: path.Join(imagestore.Path, "child.vmdk"),
|
||||
}
|
||||
// config
|
||||
config = NewPersistentDisk(child).WithParent(scratch)
|
||||
|
||||
// detach (scratch) AND attach + create (child) spec
|
||||
spec = []types.BaseVirtualDeviceConfigSpec{
|
||||
&types.VirtualDeviceConfigSpec{
|
||||
Device: disk,
|
||||
Operation: types.VirtualDeviceConfigSpecOperationRemove,
|
||||
},
|
||||
&types.VirtualDeviceConfigSpec{
|
||||
Device: vdm.toSpec(config),
|
||||
Operation: types.VirtualDeviceConfigSpecOperationAdd,
|
||||
FileOperation: types.VirtualDeviceConfigSpecFileOperationCreate,
|
||||
},
|
||||
}
|
||||
|
||||
// reconfigure
|
||||
err = reconfigure(spec)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
t.Logf("scratch detached, child created and attached")
|
||||
|
||||
vms, err = vdm.InUse(op, config, filter)
|
||||
if !assert.NoError(t, err) && !assert.Len(t, vms, 1) {
|
||||
return
|
||||
}
|
||||
t.Logf("InUse by %s", vms)
|
||||
|
||||
// ref to child (needed for detach as initial spec's Key and UnitNumber was unset)
|
||||
disk, err = findDiskByFilename(op, vdm.vm, child.Path, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// detach spec (child)
|
||||
spec = []types.BaseVirtualDeviceConfigSpec{
|
||||
&types.VirtualDeviceConfigSpec{
|
||||
Device: disk,
|
||||
Operation: types.VirtualDeviceConfigSpecOperationRemove,
|
||||
},
|
||||
}
|
||||
// reconfigure
|
||||
err = reconfigure(spec)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
t.Logf("child detached")
|
||||
}
|
||||
238
vendor/github.com/vmware/vic/pkg/vsphere/disk/lazy_detach_test.go
generated
vendored
Normal file
238
vendor/github.com/vmware/vic/pkg/vsphere/disk/lazy_detach_test.go
generated
vendored
Normal file
@@ -0,0 +1,238 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/lib/guest"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/datastore"
|
||||
"github.com/vmware/vic/pkg/vsphere/tasks"
|
||||
)
|
||||
|
||||
// TestLazyDetach tests lazy detach functionality to make sure that every ESXi version shows this behaviour
|
||||
// https://github.com/vmware/vic/issues/5565
|
||||
func TestLazyDetach(t *testing.T) {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
if testing.Verbose() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
session := Session(context.Background(), t)
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
|
||||
op := trace.NewOperation(context.TODO(), t.Name())
|
||||
|
||||
vchvm, err := guest.GetSelf(op, session)
|
||||
if err != nil {
|
||||
t.Skip("Not in a vm")
|
||||
}
|
||||
view := ContainerView(op, session, vchvm)
|
||||
if view == nil {
|
||||
t.Skip("Can't create a view")
|
||||
}
|
||||
|
||||
imagestore := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: datastore.TestName(t.Name()),
|
||||
}
|
||||
|
||||
// file manager
|
||||
fm := object.NewFileManager(session.Vim25())
|
||||
// create a directory in the datastore
|
||||
err = fm.MakeDirectory(context.TODO(), imagestore.String(), nil, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Nuke the image store
|
||||
defer func() {
|
||||
task, err := fm.DeleteDatastoreFile(context.TODO(), imagestore.String(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
_, err = task.WaitForResult(context.TODO(), nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// create a diskmanager
|
||||
vdm, err := NewDiskManager(op, session, view)
|
||||
if !assert.NoError(t, err) || !assert.NotNil(t, vdm) {
|
||||
return
|
||||
}
|
||||
|
||||
// helper fn
|
||||
reconfigure := func(changes []types.BaseVirtualDeviceConfigSpec) error {
|
||||
t.Logf("Calling reconfigure")
|
||||
|
||||
machineSpec := types.VirtualMachineConfigSpec{}
|
||||
machineSpec.DeviceChange = changes
|
||||
|
||||
_, err := vdm.vm.WaitForResult(op, func(ctx context.Context) (tasks.Task, error) {
|
||||
t, er := vdm.vm.Reconfigure(ctx, machineSpec)
|
||||
|
||||
if t != nil {
|
||||
op.Debugf("reconfigure task=%s", t.Reference())
|
||||
}
|
||||
|
||||
return t, er
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
oddity := "Ground control to Major Tom"
|
||||
operation := func(path *object.DatastorePath, read bool) error {
|
||||
// this is fundamentally checking persistent disks
|
||||
devicePath, err := vdm.devicePathByURI(op, path, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blockDev, err := waitForDevice(op, devicePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(blockDev, os.O_RDWR, os.FileMode(0777))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if read {
|
||||
// Try to read the whole string
|
||||
b := make([]byte, len(oddity))
|
||||
_, err = f.Read(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Check against the test string
|
||||
if oddity != string(b) {
|
||||
return fmt.Errorf("Read string is not the same one we wrote")
|
||||
}
|
||||
} else {
|
||||
// Write directly to the disk
|
||||
_, err = f.Write([]byte(oddity))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return f.Sync()
|
||||
}
|
||||
|
||||
// 1MB
|
||||
diskSize := int64(1 << 10)
|
||||
scratch := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: path.Join(imagestore.Path, "scratch.vmdk"),
|
||||
}
|
||||
// config
|
||||
config := NewPersistentDisk(scratch).WithCapacity(diskSize)
|
||||
|
||||
// attach + create spec (scratch)
|
||||
spec := []types.BaseVirtualDeviceConfigSpec{
|
||||
&types.VirtualDeviceConfigSpec{
|
||||
Device: vdm.toSpec(config),
|
||||
Operation: types.VirtualDeviceConfigSpecOperationAdd,
|
||||
FileOperation: types.VirtualDeviceConfigSpecFileOperationCreate,
|
||||
},
|
||||
}
|
||||
// reconfigure
|
||||
err = reconfigure(spec)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
t.Logf("scratch created and attached")
|
||||
|
||||
// ref to scratch (needed for detach as initial spec's Key and UnitNumber was unset)
|
||||
disk, err := findDiskByFilename(op, vdm.vm, scratch.Path, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
err = operation(scratch, false)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// DO NOT DETACH AND START WORKING ON THE CHILD
|
||||
|
||||
// child
|
||||
child := &object.DatastorePath{
|
||||
Datastore: session.Datastore.Name(),
|
||||
Path: path.Join(imagestore.Path, "child.vmdk"),
|
||||
}
|
||||
// config
|
||||
config = NewPersistentDisk(child).WithParent(scratch)
|
||||
|
||||
// detach (scratch) AND attach + create (child) spec
|
||||
spec = []types.BaseVirtualDeviceConfigSpec{
|
||||
&types.VirtualDeviceConfigSpec{
|
||||
Device: disk,
|
||||
Operation: types.VirtualDeviceConfigSpecOperationRemove,
|
||||
},
|
||||
&types.VirtualDeviceConfigSpec{
|
||||
Device: vdm.toSpec(config),
|
||||
Operation: types.VirtualDeviceConfigSpecOperationAdd,
|
||||
FileOperation: types.VirtualDeviceConfigSpecFileOperationCreate,
|
||||
},
|
||||
}
|
||||
// reconfigure
|
||||
err = reconfigure(spec)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
t.Logf("scratch detached, child created and attached")
|
||||
|
||||
err = operation(child, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// ref to child (needed for detach as initial spec's Key and UnitNumber was unset)
|
||||
disk, err = findDiskByFilename(op, vdm.vm, child.Path, true)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// detach spec (child)
|
||||
spec = []types.BaseVirtualDeviceConfigSpec{
|
||||
&types.VirtualDeviceConfigSpec{
|
||||
Device: disk,
|
||||
Operation: types.VirtualDeviceConfigSpecOperationRemove,
|
||||
},
|
||||
}
|
||||
// reconfigure
|
||||
err = reconfigure(spec)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
t.Logf("child detached")
|
||||
}
|
||||
339
vendor/github.com/vmware/vic/pkg/vsphere/disk/util.go
generated
vendored
Normal file
339
vendor/github.com/vmware/vic/pkg/vsphere/disk/util.go
generated
vendored
Normal file
@@ -0,0 +1,339 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/pkg/errors"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/vm"
|
||||
)
|
||||
|
||||
const (
|
||||
// The duration waitForPath will tolerate before timing out.
|
||||
// TODO FIXME see GH issues 2340 and 2385
|
||||
// TODO We need to add a vSphere cancellation step to cancel calls that are taking too long
|
||||
// TODO Remove these TODOs after 2385 is completed
|
||||
pathTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
// scsiScan tells the kernel to rescan the scsi bus.
|
||||
func scsiScan() error {
|
||||
root := "/sys/class/scsi_host"
|
||||
|
||||
dirs, err := ioutil.ReadDir(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
file := path.Join(root, dir.Name(), "scan")
|
||||
// Channel, SCSI target ID, and LUN: "-" == rescan all
|
||||
err = ioutil.WriteFile(file, []byte("- - -"), 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Waits for a device to appear in the given directory and returns the
|
||||
// resultant dev path (e.g /dev/sda). For instance, if sysPath is
|
||||
// /sys/bus/pci/devices/0000:03:00.0/host0/subsystem/devices/0:0:0:0/block, the
|
||||
// directory to appear in block will be mapped to /dev/<device>. waitForDevice
|
||||
// will wait for the entry to appear as a scsi target AND the blockdev to exist
|
||||
// in /dev/, returning the path to /dev/<blockdev>.
|
||||
func waitForDevice(op trace.Operation, sysPath string) (string, error) {
|
||||
defer trace.End(trace.Begin(sysPath))
|
||||
|
||||
var err error
|
||||
op, _ = trace.WithTimeout(&op, time.Duration(pathTimeout), "waitForDevice(%s)", sysPath)
|
||||
errCh := make(chan error)
|
||||
|
||||
var blockDev string
|
||||
go func() {
|
||||
t := time.NewTicker(200 * time.Microsecond)
|
||||
defer t.Stop()
|
||||
defer close(errCh)
|
||||
|
||||
for range t.C {
|
||||
// We've timed out.
|
||||
if op.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Syspath includes the scsi target itself. Wait for it and try
|
||||
// again before trying to identify the device node it maps to.
|
||||
dirents, err := ioutil.ReadDir(sysPath)
|
||||
if err != nil {
|
||||
|
||||
// try again
|
||||
if os.IsNotExist(err) {
|
||||
op.Debugf("Expected %s to appear. Trying again.", sysPath)
|
||||
continue
|
||||
}
|
||||
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
if len(dirents) > 1 {
|
||||
errCh <- fmt.Errorf("too many devices returned: %#v", dirents)
|
||||
return
|
||||
}
|
||||
|
||||
if len(dirents) == 1 {
|
||||
blockDev = "/dev/" + dirents[0].Name()
|
||||
|
||||
// check it exists
|
||||
if _, err := os.Stat(blockDev); err != nil {
|
||||
|
||||
// try again
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
// happy path
|
||||
return
|
||||
}
|
||||
|
||||
// run a manual scan of the scsi bus
|
||||
if serr := scsiScan(); serr != nil {
|
||||
op.Warnf("scsi scan: %s", serr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
op.Debugf("Waiting for attached disk to appear in %s, or timeout", sysPath)
|
||||
select {
|
||||
case err = <-errCh:
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
op.Infof("Attached disk present at %s", blockDev)
|
||||
case <-op.Done():
|
||||
if op.Err() != nil {
|
||||
return "", errors.Errorf("timeout waiting for layer to present in %s", sysPath)
|
||||
}
|
||||
}
|
||||
|
||||
return blockDev, nil
|
||||
}
|
||||
|
||||
// Ensures that a paravirtual scsi controller is present and determines the
|
||||
// base path of disks attached to it returns a handle to the controller and a
|
||||
// format string, with a single decimal for the disk unit number which will
|
||||
// result in the /sys/bus/pci/devices/{pci id like
|
||||
// 0000:03:00.0}/host{N provided by kernel}/subsystem/devices/N:0:{disk id like 0}:0/block/sd{Y provided by kernel} path.
|
||||
// The directory inside block isn't a devnode, but it's name can be mapped to
|
||||
// its /dev/ path.
|
||||
func verifyParavirtualScsiController(op trace.Operation, vm *vm.VirtualMachine) (*types.ParaVirtualSCSIController, string, error) {
|
||||
devices, err := vm.Device(op)
|
||||
if err != nil {
|
||||
op.Errorf("vmware driver failed to retrieve device list for VM %s: %s", vm, errors.ErrorStack(err))
|
||||
return nil, "", errors.Trace(err)
|
||||
}
|
||||
|
||||
controller, ok := devices.PickController((*types.ParaVirtualSCSIController)(nil)).(*types.ParaVirtualSCSIController)
|
||||
if controller == nil || !ok {
|
||||
err = errors.Errorf("vmware driver failed to find a paravirtual SCSI controller - ensure setup ran correctly")
|
||||
op.Errorf(err.Error())
|
||||
return nil, "", errors.Trace(err)
|
||||
}
|
||||
|
||||
// build the base path
|
||||
// first we determine which label we're looking for (requires VMW hardware version >=10)
|
||||
controllerLabel := fmt.Sprintf("SCSI%d", controller.BusNumber)
|
||||
op.Debugf("Looking for scsi controller with label %s", controllerLabel)
|
||||
|
||||
pciDevicesDir := "/sys/bus/pci/devices"
|
||||
pciBus, err := os.Open(pciDevicesDir)
|
||||
if err != nil {
|
||||
op.Errorf("Failed to open %s for reading: %s", pciDevicesDir, errors.ErrorStack(err))
|
||||
return controller, "", errors.Trace(err)
|
||||
}
|
||||
defer pciBus.Close()
|
||||
|
||||
pciDevices, err := pciBus.Readdirnames(0)
|
||||
if err != nil {
|
||||
op.Errorf("Failed to read contents of %s: %s", pciDevicesDir, errors.ErrorStack(err))
|
||||
return controller, "", errors.Trace(err)
|
||||
}
|
||||
|
||||
var controllerName string
|
||||
|
||||
for _, pciDev := range pciDevices {
|
||||
labelPath := path.Join(pciDevicesDir, pciDev, "label")
|
||||
flabel, err := os.Open(labelPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
op.Errorf("Unable to read label from %s: %s", labelPath, errors.ErrorStack(err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
defer flabel.Close()
|
||||
|
||||
buf := make([]byte, len(controllerLabel))
|
||||
_, err = flabel.Read(buf)
|
||||
if err != nil {
|
||||
op.Errorf("Unable to read label from %s: %s", labelPath, errors.ErrorStack(err))
|
||||
continue
|
||||
}
|
||||
|
||||
if controllerLabel == string(buf) {
|
||||
// we've found our controller
|
||||
controllerName = pciDev
|
||||
op.Debugf("Found pvscsi controller directory: %s", controllerName)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if controllerName == "" {
|
||||
err := errors.Errorf("Failed to locate pvscsi controller directory")
|
||||
return controller, "", errors.Trace(err)
|
||||
}
|
||||
|
||||
// Use the block subsystem directly.
|
||||
// /sys/bus/pci/devices/0000:03:00.0/host0/subsystem/devices/0:0:0:0/block
|
||||
// hostN (host0 in this case) is provided to us by the kernel.
|
||||
// N:0:0:X where N is from above X is provided by vsphere
|
||||
|
||||
// Glob for the scsi host
|
||||
matches, err := filepath.Glob(path.Join(pciDevicesDir, controllerName, "host*"))
|
||||
if err != nil {
|
||||
return controller, "", fmt.Errorf("scsi host glob failed: %s", err.Error())
|
||||
}
|
||||
|
||||
if len(matches) != 1 {
|
||||
return controller, "", fmt.Errorf("too many scsi hosts")
|
||||
}
|
||||
|
||||
// Get the number on the end
|
||||
hostStr := matches[0]
|
||||
hostidx := string(hostStr[len(hostStr)-1])
|
||||
|
||||
// First param in the X:X:X:X path is the host id. So `host2` will mean all devices will start with 2.
|
||||
formatString := path.Join(hostStr, fmt.Sprintf("subsystem/devices/%s:0:%%d:0/block/", hostidx))
|
||||
op.Debugf("Disk location format: %s", formatString)
|
||||
return controller, formatString, nil
|
||||
}
|
||||
|
||||
func findDisk(op trace.Operation, vm *vm.VirtualMachine, filter func(diskName string, mode string) bool) ([]*types.VirtualDisk, error) {
|
||||
defer trace.End(trace.Begin(vm.String()))
|
||||
|
||||
devices, err := vm.Device(op)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to refresh devices for vm: %s", errors.ErrorStack(err))
|
||||
}
|
||||
|
||||
candidates := devices.Select(func(device types.BaseVirtualDevice) bool {
|
||||
db := device.GetVirtualDevice().Backing
|
||||
if db == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
backing, ok := device.GetVirtualDevice().Backing.(*types.VirtualDiskFlatVer2BackingInfo)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
backingFileName := backing.VirtualDeviceFileBackingInfo.FileName
|
||||
mode := backing.DiskMode
|
||||
op.Debugf("backing file name %s, mode: %s", backingFileName, mode)
|
||||
|
||||
return filter(backingFileName, mode)
|
||||
})
|
||||
|
||||
if len(candidates) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
disks := make([]*types.VirtualDisk, len(candidates))
|
||||
for idx, disk := range candidates {
|
||||
disks[idx] = disk.(*types.VirtualDisk)
|
||||
}
|
||||
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
// Find the disk by name attached to the given vm.
|
||||
func findDiskByFilename(op trace.Operation, vm *vm.VirtualMachine, name string, persistent bool) (*types.VirtualDisk, error) {
|
||||
defer trace.End(trace.Begin(vm.String()))
|
||||
|
||||
op.Debugf("Looking for attached disk matching filename %s", name)
|
||||
|
||||
candidates, err := findDisk(op, vm, func(diskName string, mode string) bool {
|
||||
if persistent != (mode == string(types.VirtualDiskModePersistent) || mode == string(types.VirtualDiskModeIndependent_persistent)) {
|
||||
return false
|
||||
}
|
||||
|
||||
match := strings.HasSuffix(diskName, name)
|
||||
if match {
|
||||
op.Debugf("Found candidate disk for %s at %s", name, diskName)
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
op.Errorf("error finding disk: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(candidates) == 0 {
|
||||
op.Infof("No disks match name and persistence: %s, %t", name, persistent)
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
if len(candidates) > 1 {
|
||||
op.Errorf("Multiple disks match name: %s", name)
|
||||
// returning the first allows doing something with it
|
||||
return candidates[0], errors.Errorf("multiple disks match name: %s", name)
|
||||
}
|
||||
|
||||
return candidates[0], nil
|
||||
}
|
||||
|
||||
func findAllDisks(op trace.Operation, vm *vm.VirtualMachine) ([]*types.VirtualDisk, error) {
|
||||
defer trace.End(trace.Begin(vm.String()))
|
||||
|
||||
op.Debugf("Looking for all attached disks")
|
||||
|
||||
disks, err := findDisk(op, vm, func(diskName string, mode string) bool {
|
||||
return true
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
op.Errorf("error finding disk: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return disks, nil
|
||||
}
|
||||
33
vendor/github.com/vmware/vic/pkg/vsphere/disk/util_test.go
generated
vendored
Normal file
33
vendor/github.com/vmware/vic/pkg/vsphere/disk/util_test.go
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScsiScan(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
err := scsiScan()
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
// ignoring "permission denied" or "read-only file system" errors
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
125
vendor/github.com/vmware/vic/pkg/vsphere/disk/vmdk.go
generated
vendored
Normal file
125
vendor/github.com/vmware/vic/pkg/vsphere/disk/vmdk.go
generated
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package disk
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/vmware/govmomi/task"
|
||||
"github.com/vmware/govmomi/vim25/mo"
|
||||
"github.com/vmware/govmomi/vim25/soap"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/pkg/errors"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/datastore"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
)
|
||||
|
||||
// Vmdk is intended to be embedded by stores that manage VMDK-based data resources
|
||||
type Vmdk struct {
|
||||
*Manager
|
||||
*datastore.Helper
|
||||
*session.Session
|
||||
}
|
||||
|
||||
const (
|
||||
DiskBackendKey = "msg.disk.noBackEnd"
|
||||
LockedFileKey = "msg.fileio.lock"
|
||||
)
|
||||
|
||||
// Mount mounts the disk, returning the mount path and the function used to unmount/detaches
|
||||
// when no longer in use
|
||||
func (v *Vmdk) Mount(op trace.Operation, uri *url.URL, persistent bool) (string, func(), error) {
|
||||
if uri.Scheme != "ds" {
|
||||
return "", nil, errors.New("vmdk path must be a datastore url with \"ds\" scheme")
|
||||
}
|
||||
|
||||
dsPath, err := datastore.PathFromString(uri.Path)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
cleanFunc := func() {
|
||||
if err := v.UnmountAndDetach(op, dsPath, persistent); err != nil {
|
||||
op.Errorf("Error cleaning up disk: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
mountPath, err := v.AttachAndMount(op, dsPath, persistent)
|
||||
return mountPath, cleanFunc, err
|
||||
}
|
||||
|
||||
func LockedVMDKFilter(vm *mo.VirtualMachine) bool {
|
||||
if vm == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return vm.Runtime.PowerState == types.VirtualMachinePowerStatePoweredOn
|
||||
}
|
||||
|
||||
// IsLockedError will determine if the error received is:
|
||||
// a. related to a vmdk
|
||||
// b. due to the vmdk being locked
|
||||
// It will return false in absence of confirmation, meaning incomplete vim errors
|
||||
// will return false
|
||||
func IsLockedError(err error) bool {
|
||||
disks := LockedDisks(err)
|
||||
//if device is locked, disks will not be nil
|
||||
return len(disks) > 0
|
||||
}
|
||||
|
||||
// LockedDisks returns locked devices path in the error if it's device lock error
|
||||
func LockedDisks(err error) []string {
|
||||
var faultMessage []types.LocalizableMessage
|
||||
|
||||
if soap.IsSoapFault(err) {
|
||||
switch f := soap.ToSoapFault(err).VimFault().(type) {
|
||||
case *types.GenericVmConfigFault:
|
||||
faultMessage = f.FaultMessage
|
||||
}
|
||||
} else if soap.IsVimFault(err) {
|
||||
faultMessage = soap.ToVimFault(err).GetMethodFault().FaultMessage
|
||||
} else {
|
||||
switch err := err.(type) {
|
||||
case task.Error:
|
||||
faultMessage = err.Fault().GetMethodFault().FaultMessage
|
||||
}
|
||||
}
|
||||
|
||||
if faultMessage == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lockedFile := false
|
||||
var devices []string
|
||||
for _, message := range faultMessage {
|
||||
switch message.Key {
|
||||
case LockedFileKey:
|
||||
lockedFile = true
|
||||
case DiskBackendKey:
|
||||
for _, arg := range message.Arg {
|
||||
if device, ok := arg.Value.(string); ok {
|
||||
devices = append(devices, device)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if lockedFile {
|
||||
// make sure locked devices are returned only when both keys appear in the error
|
||||
return devices
|
||||
}
|
||||
return nil
|
||||
}
|
||||
60
vendor/github.com/vmware/vic/pkg/vsphere/disk/vmdk_test.go
generated
vendored
Normal file
60
vendor/github.com/vmware/vic/pkg/vsphere/disk/vmdk_test.go
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
454
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/basic_test.go
generated
vendored
Normal file
454
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/basic_test.go
generated
vendored
Normal file
@@ -0,0 +1,454 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// [BEGIN] SLIMMED DOWNED and MODIFIED VERSION of github.com/vmware/vic/lib/metadata
|
||||
type Common struct {
|
||||
ExecutionEnvironment string `vic:"0.1" recurse:"depth=0"`
|
||||
|
||||
ID string `vic:"0.1" scope:"read-only" key:"id"`
|
||||
|
||||
Name string `vic:"0.1" scope:"read-only" key:"name"`
|
||||
|
||||
Notes string `vic:"0.1" scope:"read-only" key:"notes"`
|
||||
}
|
||||
|
||||
type ContainerVM struct {
|
||||
Common `vic:"0.1" scope:"read-only" key:"common"`
|
||||
|
||||
Version string `vic:"0.1" scope:"hidden" key:"version"`
|
||||
|
||||
Aliases map[string]string `vic:"0.1" recurse:"depth=0"`
|
||||
|
||||
Interaction url.URL `vic:"0.1" recurse:"depth=0"`
|
||||
|
||||
AgentKey []byte `vic:"0.1" recurse:"depth=0"`
|
||||
}
|
||||
|
||||
type ExecutorConfig struct {
|
||||
Common `vic:"0.1" scope:"read-only" key:"common"`
|
||||
|
||||
Sessions map[string]SessionConfig `vic:"0.1" scope:"hidden" key:"sessions"`
|
||||
|
||||
Key string `json:"string"`
|
||||
}
|
||||
|
||||
type ExecutorConfigPointers struct {
|
||||
Common `vic:"0.1" scope:"read-only" key:"common"`
|
||||
|
||||
Sessions map[string]*SessionConfig `vic:"0.1" scope:"hidden" key:"sessions"`
|
||||
|
||||
Key string `json:"string"` // will inherit parent vic attributes
|
||||
}
|
||||
|
||||
type Cmd struct {
|
||||
Path string `vic:"0.1" scope:"hidden" key:"path"`
|
||||
|
||||
Args []string `vic:"0.1" scope:"hidden" key:"args"`
|
||||
|
||||
Env []string `vic:"0.1" scope:"hidden" key:"env"`
|
||||
|
||||
Dir string `vic:"0.1" scope:"hidden" key:"dir"`
|
||||
|
||||
Cmd *exec.Cmd `vic:"0.1" scope:"hidden" key:"cmd" recurse:"depth=0"`
|
||||
}
|
||||
|
||||
type SessionConfig struct {
|
||||
Common `vic:"0.1" scope:"hidden" key:"common" json:"page"`
|
||||
|
||||
Cmd Cmd `vic:"0.1" scope:"hidden" key:"cmd"`
|
||||
|
||||
Tty bool `vic:"0.1" scope:"hidden" key:"tty"`
|
||||
}
|
||||
|
||||
type ExecutorConfigPointersVisible struct {
|
||||
Sessions map[string]*VisibleSessionConfig `vic:"0.1" scope:"read-only" key:"sessions"`
|
||||
}
|
||||
|
||||
type VisibleSessionConfig struct {
|
||||
Cmd Cmd `vic:"0.1" scope:"read-only" key:"cmd"`
|
||||
|
||||
Tty bool `vic:"0.1" scope:"read-only" key:"tty"`
|
||||
}
|
||||
|
||||
// [END] SLIMMED VERSION of github.com/vmware/vic/lib/metadata
|
||||
|
||||
// make it verbose during testing
|
||||
func init() {
|
||||
logger.Level = logrus.DebugLevel
|
||||
}
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
type Type struct {
|
||||
Int int `vic:"0.1" scope:"read-write" key:"int"`
|
||||
Bool bool `vic:"0.1" scope:"read-write" key:"bool"`
|
||||
Float float64 `vic:"0.1" scope:"read-write" key:"float"`
|
||||
String string `vic:"0.1" scope:"read-write" key:"string"`
|
||||
}
|
||||
|
||||
Struct := Type{
|
||||
42,
|
||||
true,
|
||||
3.14,
|
||||
"Grrr",
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), Struct)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRW("int"): "42",
|
||||
visibleRW("bool"): "true",
|
||||
visibleRW("float"): "3.14E+00",
|
||||
visibleRW("string"): "Grrr",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestBasicMap(t *testing.T) {
|
||||
type Type struct {
|
||||
IntMap map[string]int `vic:"0.1" scope:"read-only" key:"intmap"`
|
||||
}
|
||||
|
||||
// key is not present
|
||||
var decoded Type
|
||||
Decode(MapSource(nil), &decoded)
|
||||
assert.NotNil(t, decoded.IntMap)
|
||||
assert.Empty(t, decoded.IntMap)
|
||||
|
||||
IntMap := Type{
|
||||
map[string]int{
|
||||
"1st": 12345,
|
||||
"2nd": 67890,
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), IntMap)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("intmap" + Separator + "1st"): "12345",
|
||||
visibleRO("intmap" + Separator + "2nd"): "67890",
|
||||
visibleRO("intmap"): "1st" + Separator + "2nd",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
// Decode to new variable
|
||||
decoded = Type{}
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, IntMap, decoded, "Encoded and decoded does not match")
|
||||
|
||||
// Decode to already existing variable
|
||||
IntMapOptimusPrime := Type{
|
||||
map[string]int{
|
||||
"first": 1,
|
||||
"second": 2,
|
||||
"1st": 0,
|
||||
},
|
||||
}
|
||||
Decode(MapSource(encoded), &IntMapOptimusPrime)
|
||||
|
||||
// We expect a merge and over-write
|
||||
expectedOptimusPrime := Type{
|
||||
map[string]int{
|
||||
"1st": 12345,
|
||||
"2nd": 67890,
|
||||
"first": 1,
|
||||
"second": 2,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, IntMapOptimusPrime, expectedOptimusPrime, "Decoded and expected does not match")
|
||||
|
||||
}
|
||||
|
||||
func TestBasicSlice(t *testing.T) {
|
||||
type Type struct {
|
||||
IntSlice []int `vic:"0.1" scope:"read-only" key:"intslice"`
|
||||
}
|
||||
|
||||
IntSlice := Type{
|
||||
[]int{1, 2, 3, 4, 5},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), IntSlice)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("intslice~"): "1" + Separator + "2" + Separator + "3" + Separator + "4" + Separator + "5",
|
||||
visibleRO("intslice"): "4",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
decoded.IntSlice = make([]int, 1)
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, IntSlice, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestStruct(t *testing.T) {
|
||||
type Type struct {
|
||||
Common Common `vic:"0.1" scope:"read-only" key:"common"`
|
||||
}
|
||||
|
||||
Struct := Type{
|
||||
Common: Common{
|
||||
ID: "0xDEADBEEF",
|
||||
Name: "Struct",
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), Struct)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("common/id"): "0xDEADBEEF",
|
||||
visibleRO("common/name"): "Struct",
|
||||
visibleRO("common/notes"): "",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestTime(t *testing.T) {
|
||||
type Type struct {
|
||||
Time time.Time `vic:"0.1" scope:"read-only" key:"time"`
|
||||
}
|
||||
|
||||
Time := Type{
|
||||
Time: time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC),
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), Time)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("time"): "2009-11-10 23:00:00 +0000 UTC",
|
||||
}
|
||||
assert.Equal(t, encoded, expected, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Time, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestNet(t *testing.T) {
|
||||
type Type struct {
|
||||
Net net.IPNet `vic:"0.1" scope:"read-only" key:"net"`
|
||||
}
|
||||
|
||||
// 127.0.0.1/8
|
||||
n := net.IPNet{IP: net.IP{0x7f, 0x0, 0x0, 0x1}, Mask: net.IPMask{0xff, 0x0, 0x0, 0x0}}
|
||||
Net := Type{
|
||||
Net: n,
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), Net)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("net/IP"): base64.StdEncoding.EncodeToString(n.IP),
|
||||
visibleRO("net/Mask"): base64.StdEncoding.EncodeToString(n.Mask),
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Net, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestNilNetPointer(t *testing.T) {
|
||||
type Type struct {
|
||||
Net *net.IPNet `vic:"0.1" scope:"read-only" key:"net"`
|
||||
}
|
||||
|
||||
Net := Type{
|
||||
Net: nil,
|
||||
}
|
||||
|
||||
// Net should be nil - pointers are supposed to be nil if the referenced tree is zero valued
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), Net)
|
||||
|
||||
expected := map[string]string{}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Net, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestPointer(t *testing.T) {
|
||||
type Type struct {
|
||||
Pointer *ContainerVM `vic:"0.1" scope:"hidden" key:"pointer"`
|
||||
PointerOmitnested *ContainerVM `vic:"0.1" scope:"non-persistent" key:"pointeromitnested" recurse:"depth=0"`
|
||||
}
|
||||
|
||||
Pointer := Type{
|
||||
Pointer: &ContainerVM{Version: "0.1"},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), Pointer)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("pointer/common/id"): "",
|
||||
visibleRO("pointer/common/name"): "",
|
||||
visibleRO("pointer/common/notes"): "",
|
||||
"pointer/version": "0.1",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Pointer, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestInheritenceOfNonPersistence(t *testing.T) {
|
||||
type CommonPersistence struct {
|
||||
ExecutionEnvironment string `vic:"0.1" recurse:"depth=0"`
|
||||
|
||||
ID string `vic:"0.1" scope:"read-only" key:"id"`
|
||||
|
||||
Name string `vic:"0.1" scope:"read-only" key:"name"`
|
||||
|
||||
Notes string `vic:"0.1" scope:"hidden" key:"notes"`
|
||||
}
|
||||
|
||||
type Type struct {
|
||||
Common CommonPersistence `vic:"0.1" scope:"read-write,non-persistent" key:"common"`
|
||||
}
|
||||
|
||||
Struct := Type{
|
||||
Common: CommonPersistence{
|
||||
ID: "0xDEADBEEF",
|
||||
Name: "Struct",
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
filterSink := ScopeFilterSink(NonPersistent|Hidden, MapSink(encoded))
|
||||
Encode(filterSink, Struct)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRONonpersistent("common/id"): "0xDEADBEEF",
|
||||
visibleRONonpersistent("common/name"): "Struct",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestInheritenceOfNonPersistenceWithPointer(t *testing.T) {
|
||||
|
||||
type Persistence struct {
|
||||
ExecutorConfigPointersVisible `vic:"0.1" scope:"read-only,non-persistent" key:"pointers"`
|
||||
}
|
||||
|
||||
Struct := Persistence{
|
||||
ExecutorConfigPointersVisible: ExecutorConfigPointersVisible{
|
||||
Sessions: map[string]*VisibleSessionConfig{
|
||||
"primary": {
|
||||
Tty: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
filterSink := ScopeFilterSink(NonPersistent|Hidden, MapSink(encoded))
|
||||
Encode(filterSink, Struct)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRONonpersistent("pointers/sessions"): "primary",
|
||||
visibleRONonpersistent("pointers/sessions" + Separator + "primary/tty"): "true",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Persistence
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestFilterSink(t *testing.T) {
|
||||
type CommonPersistence struct {
|
||||
ExecutionEnvironment string `vic:"0.1" recurse:"depth=0"`
|
||||
|
||||
ID string `vic:"0.1" scope:"read-only" key:"id"`
|
||||
|
||||
Name string `vic:"0.1" scope:"read-only,non-persistent" key:"name"`
|
||||
|
||||
Notes string `vic:"0.1" scope:"read-only" key:"notes"`
|
||||
}
|
||||
|
||||
type Type struct {
|
||||
Common CommonPersistence `vic:"0.1" scope:"read-write" key:"common"`
|
||||
}
|
||||
|
||||
Struct := Type{
|
||||
Common: CommonPersistence{
|
||||
ID: "0xDEADBEEF",
|
||||
Name: "Struct",
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
filterSink := ScopeFilterSink(NonPersistent|Hidden, MapSink(encoded))
|
||||
Encode(filterSink, Struct)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRONonpersistent("common/name"): "Struct",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
// strip ID as that would be filtered out
|
||||
Struct.Common.ID = ""
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Struct, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
537
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/clean_test.go
generated
vendored
Normal file
537
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/clean_test.go
generated
vendored
Normal file
@@ -0,0 +1,537 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// make it verbose during testing
|
||||
func init() {
|
||||
logger.Level = logrus.DebugLevel
|
||||
}
|
||||
|
||||
func TestEmbedded(t *testing.T) {
|
||||
|
||||
type Type struct {
|
||||
Common `vic:"0.1" scope:"read-only" key:"common"`
|
||||
}
|
||||
|
||||
Embedded := Type{
|
||||
Common: Common{
|
||||
ID: "0xDEADBEEF",
|
||||
Name: "Embedded",
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), Embedded)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("common/id"): "0xDEADBEEF",
|
||||
visibleRO("common/name"): "Embedded",
|
||||
visibleRO("common/notes"): "",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Embedded, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestNetPointer(t *testing.T) {
|
||||
type Type struct {
|
||||
Net *net.IPNet `vic:"0.1" scope:"read-only" key:"net"`
|
||||
}
|
||||
|
||||
// 127.0.0.1/8
|
||||
n := net.IPNet{IP: net.IP{0x7f, 0x0, 0x0, 0x1}, Mask: net.IPMask{0xff, 0x0, 0x0, 0x0}}
|
||||
Net := Type{
|
||||
Net: &n,
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), Net)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("net/IP"): base64.StdEncoding.EncodeToString(n.IP),
|
||||
visibleRO("net/Mask"): base64.StdEncoding.EncodeToString(n.Mask),
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Net, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestTimePointer(t *testing.T) {
|
||||
d := time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC)
|
||||
|
||||
type Type struct {
|
||||
Time *time.Time `vic:"0.1" scope:"read-only" key:"time"`
|
||||
}
|
||||
|
||||
Time := Type{
|
||||
Time: &d,
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), Time)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("time"): "2009-11-10 23:00:00 +0000 UTC",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, Time, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestStructMap(t *testing.T) {
|
||||
type Type struct {
|
||||
StructMap map[string]Common `vic:"0.1" scope:"read-only" key:"map"`
|
||||
}
|
||||
|
||||
StructMap := Type{
|
||||
map[string]Common{
|
||||
"Key1": {
|
||||
ID: "0xDEADBEEF",
|
||||
Name: "beef",
|
||||
},
|
||||
"Key2": {
|
||||
ID: "0x8BADF00D",
|
||||
Name: "food",
|
||||
},
|
||||
"Key3": {
|
||||
ID: "0xDEADF00D",
|
||||
Name: "dead",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), StructMap)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("map" + Separator + "Key1/id"): "0xDEADBEEF",
|
||||
visibleRO("map" + Separator + "Key1/name"): "beef",
|
||||
visibleRO("map" + Separator + "Key1/notes"): "",
|
||||
visibleRO("map" + Separator + "Key2/id"): "0x8BADF00D",
|
||||
visibleRO("map" + Separator + "Key2/name"): "food",
|
||||
visibleRO("map" + Separator + "Key2/notes"): "",
|
||||
visibleRO("map" + Separator + "Key3/id"): "0xDEADF00D",
|
||||
visibleRO("map" + Separator + "Key3/name"): "dead",
|
||||
visibleRO("map" + Separator + "Key3/notes"): "",
|
||||
visibleRO("map"): "Key1" + Separator + "Key2" + Separator + "Key3",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, StructMap, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestIntStructMap(t *testing.T) {
|
||||
type Type struct {
|
||||
StructMap map[int]Common `vic:"0.1" scope:"read-only" key:"map"`
|
||||
}
|
||||
|
||||
StructMap := Type{
|
||||
map[int]Common{
|
||||
1: {
|
||||
ID: "0xDEADBEEF",
|
||||
Name: "beef",
|
||||
},
|
||||
2: {
|
||||
ID: "0x8BADF00D",
|
||||
Name: "food",
|
||||
},
|
||||
3: {
|
||||
ID: "0xDEADF00D",
|
||||
Name: "dead",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), StructMap)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("map" + Separator + "1/id"): "0xDEADBEEF",
|
||||
visibleRO("map" + Separator + "1/name"): "beef",
|
||||
visibleRO("map" + Separator + "1/notes"): "",
|
||||
visibleRO("map" + Separator + "2/id"): "0x8BADF00D",
|
||||
visibleRO("map" + Separator + "2/name"): "food",
|
||||
visibleRO("map" + Separator + "2/notes"): "",
|
||||
visibleRO("map" + Separator + "3/id"): "0xDEADF00D",
|
||||
visibleRO("map" + Separator + "3/name"): "dead",
|
||||
visibleRO("map" + Separator + "3/notes"): "",
|
||||
visibleRO("map"): "1" + Separator + "2" + Separator + "3",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, StructMap, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestStructSlice(t *testing.T) {
|
||||
type Type struct {
|
||||
StructSlice []Common `vic:"0.1" scope:"read-only" key:"slice"`
|
||||
}
|
||||
|
||||
StructSlice := Type{
|
||||
[]Common{
|
||||
{
|
||||
ID: "0xDEADFEED",
|
||||
Name: "feed",
|
||||
},
|
||||
{
|
||||
ID: "0xFACEFEED",
|
||||
Name: "face",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), StructSlice)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("slice"): "1",
|
||||
visibleRO("slice" + Separator + "0/id"): "0xDEADFEED",
|
||||
visibleRO("slice" + Separator + "0/name"): "feed",
|
||||
visibleRO("slice" + Separator + "0/notes"): "",
|
||||
visibleRO("slice" + Separator + "1/id"): "0xFACEFEED",
|
||||
visibleRO("slice" + Separator + "1/name"): "face",
|
||||
visibleRO("slice" + Separator + "1/notes"): "",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, StructSlice, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestMultipleScope(t *testing.T) {
|
||||
MultipleScope := struct {
|
||||
MultipleScope string `vic:"0.1" scope:"read-only,hidden,non-persistent" key:"multiscope"`
|
||||
}{
|
||||
"MultipleScope",
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), MultipleScope)
|
||||
|
||||
expected := map[string]string{}
|
||||
assert.Equal(t, expected, encoded, "Not equal")
|
||||
}
|
||||
|
||||
func TestUnknownScope(t *testing.T) {
|
||||
UnknownScope := struct {
|
||||
UnknownScope int `vic:"0.1" scope:"unknownscope" key:"unknownscope"`
|
||||
}{
|
||||
42,
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), UnknownScope)
|
||||
|
||||
expected := map[string]string{}
|
||||
assert.Equal(t, encoded, expected, "Not equal")
|
||||
}
|
||||
|
||||
func TestUnknownProperty(t *testing.T) {
|
||||
UnknownProperty := struct {
|
||||
UnknownProperty int `vic:"0.1" scope:"hidden" key:"unknownproperty" recurse:"unknownproperty"`
|
||||
}{
|
||||
42,
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), UnknownProperty)
|
||||
|
||||
expected := map[string]string{
|
||||
hidden("unknownproperty"): "42",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Not equal")
|
||||
}
|
||||
|
||||
func TestOmitNested(t *testing.T) {
|
||||
OmitNested := struct {
|
||||
Time time.Time `vic:"0.1" scope:"volatile" key:"time" recurse:"depth=0"`
|
||||
CurrentTime time.Time `vic:"0.1" scope:"volatile" key:"time"`
|
||||
}{
|
||||
Time: time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC),
|
||||
CurrentTime: time.Date(2009, 11, 10, 23, 00, 00, 0, time.UTC),
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), OmitNested)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("time"): "2009-11-10 23:00:00 +0000 UTC",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and decoded does not match")
|
||||
|
||||
}
|
||||
|
||||
func TestComplex(t *testing.T) {
|
||||
type Type struct {
|
||||
ExecutorConfig ExecutorConfig `vic:"0.1" scope:"hidden" key:"executorconfig"`
|
||||
}
|
||||
|
||||
ExecutorConfig := Type{
|
||||
ExecutorConfig{
|
||||
Sessions: map[string]SessionConfig{
|
||||
"Session1": {
|
||||
Common: Common{
|
||||
ID: "SessionID",
|
||||
Name: "SessionName",
|
||||
},
|
||||
Tty: true,
|
||||
Cmd: Cmd{
|
||||
Path: "/vmware",
|
||||
Args: []string{"/bin/imagec", "-standalone"},
|
||||
Env: []string{"PATH=/bin", "USER=imagec"},
|
||||
Dir: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), ExecutorConfig)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("executorconfig/common/id"): "",
|
||||
visibleRO("executorconfig/common/name"): "",
|
||||
visibleRO("executorconfig/common/notes"): "",
|
||||
visibleRO("executorconfig/sessions" + Separator + "Session1/common/id"): "SessionID",
|
||||
visibleRO("executorconfig/sessions" + Separator + "Session1/common/name"): "SessionName",
|
||||
visibleRO("executorconfig/sessions" + Separator + "Session1/common/notes"): "",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/path"): "/vmware",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args~"): "/bin/imagec" + Separator + "-standalone",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args"): "1",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env~"): "PATH=/bin" + Separator + "USER=imagec",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env"): "1",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/dir"): "/",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/tty"): "true",
|
||||
hidden("executorconfig/sessions"): "Session1",
|
||||
hidden("executorconfig/Key"): "",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, ExecutorConfig, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
func TestComplexPointer(t *testing.T) {
|
||||
type Type struct {
|
||||
ExecutorConfig *ExecutorConfig `vic:"0.1" scope:"hidden" key:"executorconfig"`
|
||||
}
|
||||
|
||||
ExecutorConfig := Type{
|
||||
&ExecutorConfig{
|
||||
Sessions: map[string]SessionConfig{
|
||||
"Session1": {
|
||||
Common: Common{
|
||||
ID: "SessionID",
|
||||
Name: "SessionName",
|
||||
},
|
||||
Tty: true,
|
||||
Cmd: Cmd{
|
||||
Path: "/vmware",
|
||||
Args: []string{"/bin/imagec", "-standalone"},
|
||||
Env: []string{"PATH=/bin", "USER=imagec"},
|
||||
Dir: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), ExecutorConfig)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("executorconfig/common/id"): "",
|
||||
visibleRO("executorconfig/common/name"): "",
|
||||
visibleRO("executorconfig/common/notes"): "",
|
||||
visibleRO("executorconfig/sessions" + Separator + "Session1/common/id"): "SessionID",
|
||||
visibleRO("executorconfig/sessions" + Separator + "Session1/common/name"): "SessionName",
|
||||
visibleRO("executorconfig/sessions" + Separator + "Session1/common/notes"): "",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/path"): "/vmware",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args~"): "/bin/imagec" + Separator + "-standalone",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/args"): "1",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env~"): "PATH=/bin" + Separator + "USER=imagec",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/env"): "1",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/cmd/dir"): "/",
|
||||
hidden("executorconfig/sessions" + Separator + "Session1/tty"): "true",
|
||||
hidden("executorconfig/sessions"): "Session1",
|
||||
hidden("executorconfig/Key"): "",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Type
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, ExecutorConfig, decoded, "Encoded and decoded does not match")
|
||||
}
|
||||
|
||||
// TestPointerDecode tests the translation from a type where the sessions are direct values to
|
||||
// one where they are pointers
|
||||
func TestPointerDecode(t *testing.T) {
|
||||
reference := ExecutorConfig{
|
||||
Sessions: map[string]SessionConfig{
|
||||
"Session1": {
|
||||
Common: Common{
|
||||
ID: "SessionID",
|
||||
Name: "SessionName",
|
||||
},
|
||||
Tty: true,
|
||||
Cmd: Cmd{
|
||||
Path: "/vmware",
|
||||
Args: []string{"/bin/imagec", "-standalone"},
|
||||
Env: []string{"PATH=/bin", "USER=imagec"},
|
||||
Dir: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), reference)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("common/id"): "",
|
||||
visibleRO("common/name"): "",
|
||||
visibleRO("common/notes"): "",
|
||||
visibleRO("sessions" + Separator + "Session1/common/id"): "SessionID",
|
||||
visibleRO("sessions" + Separator + "Session1/common/name"): "SessionName",
|
||||
visibleRO("sessions" + Separator + "Session1/common/notes"): "",
|
||||
hidden("sessions" + Separator + "Session1/cmd/path"): "/vmware",
|
||||
hidden("sessions" + Separator + "Session1/cmd/args~"): "/bin/imagec" + Separator + "-standalone",
|
||||
hidden("sessions" + Separator + "Session1/cmd/args"): "1",
|
||||
hidden("sessions" + Separator + "Session1/cmd/env~"): "PATH=/bin" + Separator + "USER=imagec",
|
||||
hidden("sessions" + Separator + "Session1/cmd/env"): "1",
|
||||
hidden("sessions" + Separator + "Session1/cmd/dir"): "/",
|
||||
hidden("sessions" + Separator + "Session1/tty"): "true",
|
||||
hidden("sessions"): "Session1",
|
||||
hidden("Key"): "",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded ExecutorConfigPointers
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
// cannot assert equality at a high level because of the different structure types, but we can test the
|
||||
// common structure fragments
|
||||
assert.Equal(t, reference.Sessions["Session1"], *decoded.Sessions["Session1"], "Encoded and decoded sessions do not match")
|
||||
|
||||
}
|
||||
|
||||
func TestInsideOutside(t *testing.T) {
|
||||
type Inside struct {
|
||||
ID string `vic:"0.1" scope:"read-write" key:"id"`
|
||||
Name string `vic:"0.1" scope:"read-write" key:"name"`
|
||||
}
|
||||
|
||||
type Outside struct {
|
||||
Inside Inside `vic:"0.1" scope:"read-only" key:"inside"`
|
||||
ID string `vic:"0.1" scope:"read-write" key:"id"`
|
||||
Name string `vic:"0.1" scope:"read-write" key:"name"`
|
||||
}
|
||||
outside := Outside{
|
||||
Inside: Inside{
|
||||
ID: "inside",
|
||||
Name: "Inside",
|
||||
},
|
||||
ID: "outside",
|
||||
Name: "Outside",
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(MapSink(encoded), outside)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRW("inside.id"): "inside",
|
||||
visibleRW("inside.name"): "Inside",
|
||||
visibleRW("id"): "outside",
|
||||
visibleRW("name"): "Outside",
|
||||
}
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected does not match")
|
||||
|
||||
var decoded Outside
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
|
||||
assert.Equal(t, outside, decoded, "Encoded and decoded does not match")
|
||||
|
||||
}
|
||||
|
||||
func TestIPSlice(t *testing.T) {
|
||||
type Slice struct {
|
||||
Slice []net.IP `vic:"0.1" scope:"read-only" key:"slice"`
|
||||
}
|
||||
|
||||
ips := []net.IP{
|
||||
net.ParseIP("10.10.10.10"),
|
||||
net.ParseIP("10.10.10.1"),
|
||||
}
|
||||
|
||||
encodedIPs := make([]string, len(ips))
|
||||
for i := range ips {
|
||||
Encode(func(key, value string) error {
|
||||
encodedIPs[i] = value
|
||||
return nil
|
||||
}, ips[i])
|
||||
}
|
||||
|
||||
s := Slice{
|
||||
Slice: ips,
|
||||
}
|
||||
|
||||
encoded := make(map[string]string)
|
||||
Encode(MapSink(encoded), s)
|
||||
|
||||
expected := map[string]string{
|
||||
visibleRO("slice"): fmt.Sprintf("%d", len(ips)-1),
|
||||
}
|
||||
for i := range encodedIPs {
|
||||
expected[visibleRO(fmt.Sprintf("slice"+Separator+"%d", i))] = encodedIPs[i]
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, encoded, "Encoded and expected do not match")
|
||||
|
||||
var decoded Slice
|
||||
Decode(MapSource(encoded), &decoded)
|
||||
assert.Equal(t, s, decoded, "Encoded and decoded do not match")
|
||||
}
|
||||
477
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/decode.go
generated
vendored
Normal file
477
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/decode.go
generated
vendored
Normal file
@@ -0,0 +1,477 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
nilValue = reflect.ValueOf(nil)
|
||||
)
|
||||
|
||||
type decoder func(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error)
|
||||
|
||||
var (
|
||||
kindDecoders map[reflect.Kind]decoder
|
||||
intfDecoders map[reflect.Type]decoder
|
||||
)
|
||||
|
||||
func init() {
|
||||
kindDecoders = map[reflect.Kind]decoder{
|
||||
reflect.String: decodeString,
|
||||
reflect.Struct: decodeStruct,
|
||||
reflect.Slice: decodeSlice,
|
||||
reflect.Array: decodeSlice,
|
||||
reflect.Map: decodeMap,
|
||||
reflect.Ptr: decodePtr,
|
||||
reflect.Int: decodePrimitive,
|
||||
reflect.Int8: decodePrimitive,
|
||||
reflect.Int16: decodePrimitive,
|
||||
reflect.Int32: decodePrimitive,
|
||||
reflect.Int64: decodePrimitive,
|
||||
reflect.Bool: decodePrimitive,
|
||||
reflect.Float32: decodePrimitive,
|
||||
reflect.Float64: decodePrimitive,
|
||||
}
|
||||
|
||||
intfDecoders = map[reflect.Type]decoder{
|
||||
reflect.TypeOf(time.Time{}): decodeTime,
|
||||
}
|
||||
}
|
||||
|
||||
// decode is the generic switcher that decides which decoder to use for a field
|
||||
func decode(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
|
||||
// if depth has reached zero, we skip decoding entirely
|
||||
if depth.depth == 0 {
|
||||
return dest, nil
|
||||
}
|
||||
depth.depth--
|
||||
|
||||
// obtain the handler from the map, checking for the more specific interfaces first
|
||||
dec, ok := intfDecoders[dest.Type()]
|
||||
if ok {
|
||||
return dec(src, dest, prefix, depth)
|
||||
}
|
||||
|
||||
dec, ok = kindDecoders[dest.Kind()]
|
||||
if ok {
|
||||
return dec(src, dest, prefix, depth)
|
||||
}
|
||||
|
||||
logger.Debugf("Skipping unsupported field, interface: %T, kind %s", dest, dest.Kind())
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// decodeString is the degenerative case where what we get is what we need
|
||||
func decodeString(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
|
||||
v, err := src(prefix)
|
||||
if err != nil {
|
||||
logger.Debugf("No value found in data source for string at key %q", prefix)
|
||||
return nilValue, err
|
||||
}
|
||||
|
||||
return reflect.ValueOf(v), nil
|
||||
}
|
||||
|
||||
// decodePrimitive wraps the fromString primitive decoding in a manner that can be called via decode
|
||||
func decodePrimitive(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
|
||||
var this reflect.Value
|
||||
if !dest.CanAddr() {
|
||||
logger.Debugf("Making new primitive for %s", prefix)
|
||||
ptr := reflect.New(dest.Type())
|
||||
this = ptr.Elem()
|
||||
} else {
|
||||
logger.Debugf("Reusing existing struct for %s", prefix)
|
||||
this = dest
|
||||
}
|
||||
|
||||
// see if there's a value to decode
|
||||
v, err := src(prefix)
|
||||
if err != nil {
|
||||
logger.Debugf("No value available for key to primitive %s", prefix)
|
||||
return nilValue, err
|
||||
}
|
||||
|
||||
t := this.Type()
|
||||
this.Set(fromString(reflect.Zero(t), v))
|
||||
|
||||
return this, nil
|
||||
}
|
||||
|
||||
func decodePtr(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
|
||||
// if we're not following pointers, then return immediately
|
||||
if !depth.follow {
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// value representing the run-time data
|
||||
logger.Debugf("Decoding pointer into object: %#v", dest)
|
||||
|
||||
// if the pointer is nil we need to create the destination type
|
||||
target := dest
|
||||
if dest.IsNil() {
|
||||
target = reflect.New(dest.Type().Elem())
|
||||
}
|
||||
|
||||
// check to see if the resulting object is not nil
|
||||
// If it is nil, then there was nothing to decode and the pointer remains nil
|
||||
result, err := decode(src, target.Elem(), prefix, depth)
|
||||
logger.Debugf("target is now %#v, %+q ", target, target.Type())
|
||||
if !result.IsValid() || err == ErrKeyNotFound {
|
||||
// leave the pointer as nil if the result is zero type or invalid
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// neither pointer, nor zero
|
||||
// NOTE: if the returned result is not addressable this can panic - that generally
|
||||
// indicates an incorrect implementation of a decodeX method... those should always
|
||||
// return addressable Values. See decodeByteSlice as an example - this uses make([]byte)
|
||||
// rather than built in string(bytes) conversion specifically to get an addressable return
|
||||
if dest.IsNil() {
|
||||
dest = target
|
||||
}
|
||||
|
||||
dest.Elem().Set(result)
|
||||
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
var typeType = reflect.TypeOf((*reflect.Type)(nil)).Elem()
|
||||
|
||||
func decodeStruct(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
|
||||
// value representing the run-time data
|
||||
logger.Debugf("Decoding struct into object: %#v, type: %s", dest, dest.Type().Name())
|
||||
|
||||
var this reflect.Value
|
||||
if !dest.CanAddr() {
|
||||
logger.Debugf("Making new struct for %s", prefix)
|
||||
ptr := reflect.New(dest.Type())
|
||||
this = ptr.Elem()
|
||||
} else {
|
||||
logger.Debugf("Reusing existing struct for %s", prefix)
|
||||
this = dest
|
||||
}
|
||||
|
||||
// do we have any data for this struct at all
|
||||
var valid bool
|
||||
var err error
|
||||
noKeysFound := true
|
||||
|
||||
// iterate through every field in the struct
|
||||
for i := 0; i < this.NumField(); i++ {
|
||||
field := this.Field(i)
|
||||
key, fdepth := calculateKeyFromField(this.Type().Field(i), prefix, depth)
|
||||
if key == "" {
|
||||
// this is either a malformed key or explicitly skipped
|
||||
continue
|
||||
}
|
||||
|
||||
// Dump what we have so far
|
||||
logger.Debugf("Key: %s, Kind: %s Value: %s", key, field.Kind(), field.String())
|
||||
|
||||
// check to see if the resulting object is not nil
|
||||
// If it is nil, then there was nothing to decode
|
||||
var result reflect.Value
|
||||
result, err = decode(src, field, key, fdepth)
|
||||
if result.IsValid() {
|
||||
logger.Debugf("Setting field %s to %#v", this.Type().Field(i).Name, result)
|
||||
field.Set(result)
|
||||
valid = true
|
||||
if err != ErrKeyNotFound {
|
||||
noKeysFound = false
|
||||
}
|
||||
} else {
|
||||
logger.Debugf("Invalid result for field %s", this.Type().Field(i).Name)
|
||||
}
|
||||
}
|
||||
|
||||
if !valid || noKeysFound {
|
||||
logger.Debugf("No valid result, returning nil value")
|
||||
return nilValue, err
|
||||
}
|
||||
|
||||
logger.Debugf("Return decoded structure for %s: %#v", prefix, this)
|
||||
return this, nil
|
||||
}
|
||||
|
||||
func decodeByteSlice(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
|
||||
logger.Debugf("Converting string to []byte")
|
||||
base, err := src(prefix)
|
||||
if err != nil {
|
||||
logger.Debugf("No value found in data source for []byte %q", prefix)
|
||||
return nilValue, err
|
||||
}
|
||||
|
||||
bytes, err := base64.StdEncoding.DecodeString(base)
|
||||
if err != nil {
|
||||
logger.Debugf("Expected base64 encoded string for []byte %q: %s", prefix, err)
|
||||
return nilValue, err
|
||||
}
|
||||
|
||||
length := len(bytes)
|
||||
|
||||
// we don't even try to merge byte arrays - no idea how to get append behaviour
|
||||
// correct with reflection
|
||||
// use make([]byte) rather than built in string(bytes) conversion to get an addressable return value
|
||||
logger.Debugf("Making new slice for %s", prefix)
|
||||
this := make([]byte, length, length)
|
||||
|
||||
copy(this, bytes)
|
||||
|
||||
return reflect.ValueOf(this), nil
|
||||
}
|
||||
|
||||
func decodeSlice(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
|
||||
// value representing the run-time data
|
||||
logger.Debugf("Decoding struct into object: %#v", dest)
|
||||
kind := dest.Type().Elem().Kind()
|
||||
|
||||
if kind == reflect.Uint8 {
|
||||
return decodeByteSlice(src, dest, prefix, depth)
|
||||
}
|
||||
|
||||
// do we have any data for this struct at all
|
||||
length := 0
|
||||
curLen := 0
|
||||
|
||||
// get the length of the array
|
||||
len, err := src(prefix)
|
||||
if err != nil || len == "" {
|
||||
logger.Debugf("No value available for key %s - will create empty array if needed", prefix)
|
||||
} else {
|
||||
// if there's any data at all then we can assume we need to be extant
|
||||
lengthValue := fromString(reflect.ValueOf(0), len)
|
||||
length = int(lengthValue.Int()) + 1
|
||||
}
|
||||
|
||||
var this reflect.Value
|
||||
if !dest.IsValid() || dest.IsNil() || length > dest.Cap() {
|
||||
logger.Debugf("Making new slice for %s", prefix)
|
||||
this = reflect.MakeSlice(dest.Type(), length, length)
|
||||
} else {
|
||||
this = dest
|
||||
this.SetLen(length)
|
||||
curLen = this.Len()
|
||||
}
|
||||
|
||||
// determine the key given the array type
|
||||
if kind == reflect.Struct || isEncodableSliceElemType(dest.Type().Elem()) {
|
||||
for i := 0; i < length; i++ {
|
||||
// convert key to name|index format
|
||||
key := appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
|
||||
|
||||
// if there's already a struct in the array at this index then we pass that as the current
|
||||
// value
|
||||
var cur reflect.Value
|
||||
if i < curLen {
|
||||
cur = this.Index(i)
|
||||
} else {
|
||||
cur = reflect.Zero(dest.Type().Elem())
|
||||
}
|
||||
|
||||
var result reflect.Value
|
||||
// #nosec: Errors unhandled.
|
||||
result, _ = decode(src, cur, key, depth)
|
||||
if result.IsValid() {
|
||||
this.Index(i).Set(result)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return this, nil
|
||||
}
|
||||
|
||||
// convert key to name|index format
|
||||
key := appendToPrefix(prefix, "", "~")
|
||||
kval, err := src(key)
|
||||
if err != nil {
|
||||
logger.Debugf("No value found in data source for key %q", key)
|
||||
return this, err
|
||||
}
|
||||
|
||||
// lookup the key and split it
|
||||
values := strings.Split(kval, Separator)
|
||||
for i := 0; i < length; i++ {
|
||||
v := values[i]
|
||||
t := this.Type().Elem()
|
||||
k := fromString(reflect.Zero(t), v)
|
||||
// set the i'th slice item
|
||||
this.Index(i).Set(k)
|
||||
}
|
||||
|
||||
return this, nil
|
||||
}
|
||||
|
||||
func decodeMap(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
|
||||
// value representing the run-time data
|
||||
logger.Debugf("Decoding struct into object: %#v", dest)
|
||||
|
||||
// if the value is the zero type, we have to create ourselves
|
||||
var this reflect.Value
|
||||
if !dest.IsValid() || dest.IsNil() {
|
||||
logger.Debugf("Making new maps for %s", prefix)
|
||||
this = reflect.MakeMap(dest.Type())
|
||||
} else {
|
||||
this = dest
|
||||
}
|
||||
|
||||
mapkeys, err := src(prefix)
|
||||
if mapkeys == "" || err != nil {
|
||||
logger.Debugf("No value found in data source for maps keys %q", prefix)
|
||||
return this, err
|
||||
}
|
||||
|
||||
keytype := this.Type().Key()
|
||||
valtype := this.Type().Elem()
|
||||
|
||||
// split the list of map keys and iterate
|
||||
for _, value := range strings.Split(mapkeys, Separator) {
|
||||
k := fromString(reflect.Zero(keytype), value)
|
||||
target := this.MapIndex(k)
|
||||
if !target.IsValid() {
|
||||
target = reflect.Zero(valtype)
|
||||
}
|
||||
|
||||
key := appendToPrefix(prefix, Separator, value)
|
||||
|
||||
// check to see if the resulting object is not nil
|
||||
// If it is nil, then there was nothing to decode and the pointer remains nil
|
||||
// #nosec: Errors unhandled.
|
||||
result, _ := decode(src, target, key, depth)
|
||||
if result.IsValid() {
|
||||
this.SetMapIndex(k, result)
|
||||
}
|
||||
}
|
||||
|
||||
return this, nil
|
||||
}
|
||||
|
||||
func decodeTime(src DataSource, dest reflect.Value, prefix string, depth recursion) (reflect.Value, error) {
|
||||
v, err := src(prefix)
|
||||
if err != nil {
|
||||
logger.Debugf("No value found in data source for time %q", prefix)
|
||||
return nilValue, err
|
||||
}
|
||||
|
||||
t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", v)
|
||||
if err != nil {
|
||||
logger.Debugf("Failed to convert value %q to time", v)
|
||||
}
|
||||
|
||||
return reflect.ValueOf(t), nil
|
||||
}
|
||||
|
||||
// fromString converts string representation of a basic type to basic type
|
||||
func fromString(field reflect.Value, value string) reflect.Value {
|
||||
// handle the zero value
|
||||
// TODO: can probably handle this more efficiently with a nil pointer return
|
||||
// as whatever we're populating with primitives will already have their zero
|
||||
// value.
|
||||
if value == "" {
|
||||
return reflect.Zero(field.Type())
|
||||
}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
s, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to convert value %#v (%s) to int: %s", value, field.Kind(), err.Error())
|
||||
return field
|
||||
}
|
||||
return reflect.ValueOf(s).Convert(field.Type())
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
s, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to convert value %#v (%s) to uint: %s", value, field.Kind(), err.Error())
|
||||
return field
|
||||
}
|
||||
return reflect.ValueOf(s).Convert(field.Type())
|
||||
|
||||
case reflect.Bool:
|
||||
s, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to convert value %#v (%s) to bool: %s", value, field.Kind(), err.Error())
|
||||
return field
|
||||
}
|
||||
return reflect.ValueOf(s)
|
||||
|
||||
case reflect.String:
|
||||
return reflect.ValueOf(value)
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
s, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to convert value %#v (%s) to float: %s", value, field.Kind(), err.Error())
|
||||
return field
|
||||
}
|
||||
return reflect.ValueOf(s)
|
||||
|
||||
}
|
||||
logger.Debugf("Invalid Kind: %s (%#v)", field.Kind(), value)
|
||||
|
||||
return field
|
||||
}
|
||||
|
||||
// DataSource provides a function that, give a key will return a value
|
||||
// this is to be used during extraConfig decode to obtain values. Should
|
||||
// return ErrKeyNotFound if the key does not exist in the data source.
|
||||
type DataSource func(string) (string, error)
|
||||
|
||||
// Decode populates a destination with data from the supplied data source
|
||||
func Decode(src DataSource, dest interface{}) interface{} {
|
||||
if src == nil {
|
||||
logger.Warnf("Decode source is nil - unable to continue")
|
||||
return dest
|
||||
}
|
||||
|
||||
// #nosec: Errors unhandled.
|
||||
value, _ := decode(src, reflect.ValueOf(dest), DefaultPrefix, Unbounded)
|
||||
|
||||
return value.Interface()
|
||||
}
|
||||
|
||||
// DecodeWithPrefix populates a destination with data from the supplied data source, using
|
||||
// the specified prefix - this allows for decode into substructres.
|
||||
func DecodeWithPrefix(src DataSource, dest interface{}, prefix string) interface{} {
|
||||
if src == nil {
|
||||
logger.Warnf("Decode source is nil - unable to continue")
|
||||
return dest
|
||||
}
|
||||
|
||||
// #nosec: Errors unhandled.
|
||||
value, _ := decode(src, reflect.ValueOf(dest), prefix, Unbounded)
|
||||
|
||||
return value.Interface()
|
||||
}
|
||||
|
||||
// MapSource takes a key/value map and uses that as the datasource for decoding into
|
||||
// target structures
|
||||
func MapSource(src map[string]string) DataSource {
|
||||
return func(key string) (string, error) {
|
||||
val, ok := src[key]
|
||||
if !ok {
|
||||
return "", ErrKeyNotFound
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
}
|
||||
30
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/decode_darwin.go
generated
vendored
Normal file
30
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/decode_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import "errors"
|
||||
|
||||
// GuestInfoSource uses the rpcvmx mechanism to access the guestinfo key/value map as
|
||||
// the datasource for decoding into target structures
|
||||
func GuestInfoSource() (DataSource, error) {
|
||||
return GuestInfoSourceWithPrefix("")
|
||||
}
|
||||
|
||||
// GuestInfoSourceWithPrefix adds a prefix to all keys accessed. The key must not have leading
|
||||
// or trailing separator characters, but may have separators in other positions. The separator
|
||||
// (either . or /) will be replaced with the appropriate value for the key in question.
|
||||
func GuestInfoSourceWithPrefix(prefix string) (DataSource, error) {
|
||||
return nil, errors.New("Not implemented on OSX")
|
||||
}
|
||||
63
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/decode_linux.go
generated
vendored
Normal file
63
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/decode_linux.go
generated
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
"github.com/vmware/vmw-guestinfo/rpcvmx"
|
||||
"github.com/vmware/vmw-guestinfo/vmcheck"
|
||||
)
|
||||
|
||||
// GuestInfoSource uses the rpcvmx mechanism to access the guestinfo key/value map as
|
||||
// the datasource for decoding into target structures
|
||||
func GuestInfoSource() (DataSource, error) {
|
||||
return GuestInfoSourceWithPrefix("")
|
||||
}
|
||||
|
||||
// GuestInfoSourceWithPrefix adds a prefix to all keys accessed. The key must not have leading
|
||||
// or trailing separator characters, but may have separators in other positions. The separator
|
||||
// (either . or /) will be replaced with the appropriate value for the key in question.
|
||||
func GuestInfoSourceWithPrefix(prefix string) (DataSource, error) {
|
||||
// Check we're using a vcpu (which doesn't assume this is UID 0).
|
||||
if !vmcheck.IsVirtualCPU() {
|
||||
return nil, fmt.Errorf("not in a virtual world")
|
||||
}
|
||||
|
||||
guestinfo := rpcvmx.NewConfig()
|
||||
|
||||
source := func(key string) (string, error) {
|
||||
if key != GuestInfoSecretKey {
|
||||
key = addPrefixToKey(DefaultGuestInfoPrefix, prefix, key)
|
||||
}
|
||||
|
||||
value, err := guestinfo.String(key, "")
|
||||
if value == "" {
|
||||
err = ErrKeyNotFound
|
||||
} else if value == "<nil>" {
|
||||
value = ""
|
||||
}
|
||||
|
||||
if key != GuestInfoSecretKey { // don't log the secret key
|
||||
log.Debugf("GuestInfoSource: key: %s, value: %#v, error: %s", key, value, err)
|
||||
}
|
||||
|
||||
return value, err
|
||||
}
|
||||
|
||||
return new(SecretKey).Source(source), nil
|
||||
}
|
||||
32
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/decode_windows.go
generated
vendored
Normal file
32
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/decode_windows.go
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// GuestInfoSource uses the rpcvmx mechanism to access the guestinfo key/value map as
|
||||
// the datasource for decoding into target structures
|
||||
func GuestInfoSource() (DataSource, error) {
|
||||
return GuestInfoSourceWithPrefix("")
|
||||
}
|
||||
|
||||
// GuestInfoSourceWithPrefix adds a prefix to all keys accessed. The key must not have leading
|
||||
// or trailing separator characters, but may have separators in other positions. The separator
|
||||
// (either . or /) will be replaced with the appropriate value for the key in question.
|
||||
func GuestInfoSourceWithPrefix(prefix string) (DataSource, error) {
|
||||
return nil, errors.New("Not implemented on Windows")
|
||||
}
|
||||
47
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/doc.go
generated
vendored
Normal file
47
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/doc.go
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
/*
|
||||
Package extraconfig provides Encode/Decode methods to convert data between Go structs and VMware Extraconfig values.
|
||||
|
||||
The implementation understands the following set of annotations and map the fields to appropriate extraConfig keys - in the case where the key describes a boolean state, omitting the annotation implies the opposite:
|
||||
|
||||
hidden - hidden from GuestOS
|
||||
read-only - value can only be modified via vSphere APIs
|
||||
read-write - value can be modified
|
||||
non-persistent - value will be lost on VM reboot
|
||||
volatile - field is not exported directly, but via a function that freshens the value each time)
|
||||
|
||||
The struct fields are required to be annotated with the "vic" tag, otherwise extraconfig package simply skips them. Scope and key tags are also required.
|
||||
|
||||
Scope tag can contain multiple values (comma separated)
|
||||
Key tag can contain extra properties (comma separated) but the first element has to the name of the key.
|
||||
|
||||
type Example struct {
|
||||
// skipped - does not contain any tag
|
||||
Note string
|
||||
|
||||
// skipped - does not contain scope and key
|
||||
ID string `vic:"0.1"`
|
||||
|
||||
// valid - extraconfig will encode this using a read-only key (as instructed by scope)
|
||||
Name string `vic:"0.1" scope:"read-only" key:"name"`
|
||||
|
||||
// valid - but extraconfig won't nest into the struct (so it's value will be type's zero value)
|
||||
Time time.Time `vic:"0.1" scope:"volatile" key:"time,omitnested"`
|
||||
}
|
||||
|
||||
*/
|
||||
300
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/encode.go
generated
vendored
Normal file
300
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/encode.go
generated
vendored
Normal file
@@ -0,0 +1,300 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrKeyNotFound = errors.New("key not found")
|
||||
)
|
||||
|
||||
type encoder func(sink DataSink, src reflect.Value, prefix string, depth recursion)
|
||||
|
||||
var kindEncoders map[reflect.Kind]encoder
|
||||
var intfEncoders map[reflect.Type]encoder
|
||||
|
||||
func init() {
|
||||
kindEncoders = map[reflect.Kind]encoder{
|
||||
reflect.String: encodeString,
|
||||
reflect.Struct: encodeStruct,
|
||||
reflect.Slice: encodeSlice,
|
||||
reflect.Array: encodeSlice,
|
||||
reflect.Map: encodeMap,
|
||||
reflect.Ptr: encodePtr,
|
||||
reflect.Int: encodePrimitive,
|
||||
reflect.Int8: encodePrimitive,
|
||||
reflect.Int16: encodePrimitive,
|
||||
reflect.Int32: encodePrimitive,
|
||||
reflect.Int64: encodePrimitive,
|
||||
reflect.Bool: encodePrimitive,
|
||||
reflect.Float32: encodePrimitive,
|
||||
reflect.Float64: encodePrimitive,
|
||||
}
|
||||
|
||||
intfEncoders = map[reflect.Type]encoder{
|
||||
reflect.TypeOf(time.Time{}): encodeTime,
|
||||
}
|
||||
}
|
||||
|
||||
// decode is the generic switcher that decides which decoder to use for a field
|
||||
func encode(sink DataSink, src reflect.Value, prefix string, depth recursion) {
|
||||
// if depth has reached zero, we skip encoding entirely
|
||||
if depth.depth == 0 {
|
||||
return
|
||||
}
|
||||
depth.depth--
|
||||
|
||||
// obtain the handler from the map, checking for the more specific interfaces first
|
||||
enc, ok := intfEncoders[src.Type()]
|
||||
if ok {
|
||||
enc(sink, src, prefix, depth)
|
||||
return
|
||||
}
|
||||
|
||||
enc, ok = kindEncoders[src.Kind()]
|
||||
if ok {
|
||||
enc(sink, src, prefix, depth)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf("Skipping unsupported field, interface: %T, kind %s", src, src.Kind())
|
||||
}
|
||||
|
||||
// encodeString is the degenerative case where what we get is what we need
|
||||
func encodeString(sink DataSink, src reflect.Value, prefix string, depth recursion) {
|
||||
err := sink(prefix, src.String())
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to encode string for key %s: %s", prefix, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// encodePrimitive wraps the toString primitive encoding in a manner that can be called via encode
|
||||
func encodePrimitive(sink DataSink, src reflect.Value, prefix string, depth recursion) {
|
||||
err := sink(prefix, toString(src))
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to encode primitive for key %s: %s", prefix, err)
|
||||
}
|
||||
}
|
||||
|
||||
func encodePtr(sink DataSink, src reflect.Value, prefix string, depth recursion) {
|
||||
// if we're not following pointers, return immediately
|
||||
if !depth.follow {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf("Encoding object: %#v", src)
|
||||
|
||||
if src.IsNil() {
|
||||
// no need to attempt anything
|
||||
return
|
||||
}
|
||||
|
||||
encode(sink, src.Elem(), prefix, depth)
|
||||
}
|
||||
|
||||
func encodeStruct(sink DataSink, src reflect.Value, prefix string, depth recursion) {
|
||||
logger.Debugf("Encoding object: %#v", src)
|
||||
|
||||
// iterate through every field in the struct
|
||||
for i := 0; i < src.NumField(); i++ {
|
||||
field := src.Field(i)
|
||||
key, fdepth := calculateKeyFromField(src.Type().Field(i), prefix, depth)
|
||||
if key == "" {
|
||||
logger.Debugf("Skipping field %s with empty computed key", src.Type().Field(i).Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Dump what we have so far
|
||||
logger.Debugf("Key: %s, Kind: %s Value: %s", key, field.Kind(), field.String())
|
||||
|
||||
encode(sink, field, key, fdepth)
|
||||
}
|
||||
}
|
||||
|
||||
func isEncodableSliceElemType(t reflect.Type) bool {
|
||||
switch t {
|
||||
case reflect.TypeOf((net.IP)(nil)):
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func encodeSlice(sink DataSink, src reflect.Value, prefix string, depth recursion) {
|
||||
logger.Debugf("Encoding object: %#v", src)
|
||||
|
||||
length := src.Len()
|
||||
if length == 0 {
|
||||
logger.Debug("Skipping empty slice")
|
||||
return
|
||||
}
|
||||
|
||||
// determine the key given the array type
|
||||
kind := src.Type().Elem().Kind()
|
||||
if kind == reflect.Uint8 {
|
||||
// special []byte array handling
|
||||
|
||||
logger.Debugf("Converting []byte to base64 string")
|
||||
str := base64.StdEncoding.EncodeToString(src.Bytes())
|
||||
encode(sink, reflect.ValueOf(str), prefix, depth)
|
||||
return
|
||||
|
||||
} else if kind == reflect.Struct || isEncodableSliceElemType(src.Type().Elem()) {
|
||||
for i := 0; i < length; i++ {
|
||||
// convert key to name|index format
|
||||
key := appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
|
||||
encode(sink, src.Index(i), key, depth)
|
||||
}
|
||||
} else {
|
||||
// else assume it's primitive - we'll panic/recover and continue it not
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Errorf("unable to encode %s (slice) for %s: %s", src.Type(), prefix, err)
|
||||
}
|
||||
}()
|
||||
|
||||
values := make([]string, length)
|
||||
for i := 0; i < length; i++ {
|
||||
values[i] = toString(src.Index(i))
|
||||
}
|
||||
|
||||
// convert key to name|index format
|
||||
key := appendToPrefix(prefix, "", "~")
|
||||
err := sink(key, strings.Join(values, Separator))
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to encode slice data for key %s: %s", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
// prefix contains the length of the array
|
||||
// seems insane calling toString(ValueOf(..)) but it means we're using the same path for everything
|
||||
err := sink(prefix, toString(reflect.ValueOf(length-1)))
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to encode slice length for key %s: %s", prefix, err)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeMap(sink DataSink, src reflect.Value, prefix string, depth recursion) {
|
||||
logger.Debugf("Encoding object: %#v", src)
|
||||
|
||||
// iterate over keys and recurse
|
||||
mkeys := src.MapKeys()
|
||||
length := len(mkeys)
|
||||
if length == 0 {
|
||||
logger.Debug("Skipping empty map")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debugf("Encoding map entries based off prefix: %s", prefix)
|
||||
keys := make([]string, length)
|
||||
for i, v := range mkeys {
|
||||
keys[i] = toString(v)
|
||||
key := appendToPrefix(prefix, Separator, keys[i])
|
||||
encode(sink, src.MapIndex(v), key, depth)
|
||||
}
|
||||
|
||||
// sort the keys before joining - purely to make testing viable
|
||||
sort.Strings(keys)
|
||||
err := sink(prefix, strings.Join(keys, Separator))
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to encode map keys for key %s: %s", prefix, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func encodeTime(sink DataSink, src reflect.Value, prefix string, depth recursion) {
|
||||
err := sink(prefix, src.Interface().(time.Time).String())
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to encode time for key %s: %s", prefix, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// toString converts a basic type to its string representation
|
||||
func toString(field reflect.Value) string {
|
||||
switch field.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return strconv.FormatInt(field.Int(), 10)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return strconv.FormatUint(field.Uint(), 10)
|
||||
case reflect.Bool:
|
||||
return strconv.FormatBool(field.Bool())
|
||||
case reflect.String:
|
||||
return field.String()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return strconv.FormatFloat(field.Float(), 'E', -1, 64)
|
||||
default:
|
||||
panic(field.Type().String() + " is an unhandled type")
|
||||
}
|
||||
}
|
||||
|
||||
// DataSink provides a function that, given a key/value will persist that
|
||||
// in some manner suited for later retrieval
|
||||
type DataSink func(string, string) error
|
||||
|
||||
// Encode serializes the given type to the supplied data sink
|
||||
func Encode(sink DataSink, src interface{}) {
|
||||
encode(sink, reflect.ValueOf(src), DefaultPrefix, Unbounded)
|
||||
}
|
||||
|
||||
// EncodeWithPrefix serializes the given type to the supplied data sink, using
|
||||
// the supplied prefix - this allows for serialization of subsections of a
|
||||
// struct
|
||||
func EncodeWithPrefix(sink DataSink, src interface{}, prefix string) {
|
||||
encode(sink, reflect.ValueOf(src), prefix, Unbounded)
|
||||
}
|
||||
|
||||
// MapSink takes a map and populates it with key/value pairs from the encode
|
||||
func MapSink(sink map[string]string) DataSink {
|
||||
// this is a very basic mechanism of allowing serialized updates to a sink
|
||||
// a more involved approach is necessary if wanting to do concurrent read/write
|
||||
mutex := sync.Mutex{}
|
||||
|
||||
return func(key, value string) error {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
sink[key] = value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ScopeFilterSink will create a DataSink that only stores entries where the key scope
|
||||
// matches one or more scopes in the filter.
|
||||
// The filter is a bitwise composion of scope flags
|
||||
func ScopeFilterSink(filter uint, sink DataSink) DataSink {
|
||||
return func(key, value string) error {
|
||||
logger.Debugf("Filtering encode of %s with scopes: %v", key, calculateScopeFromKey(key))
|
||||
scope := calculateScope(calculateScopeFromKey(key))
|
||||
if scope&filter != 0 {
|
||||
sink(key, value)
|
||||
} else {
|
||||
logger.Debugf("Skipping encode of %s with scopes that do not match filter: %v", key, calculateScopeFromKey(key))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
30
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/encode_darwin.go
generated
vendored
Normal file
30
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/encode_darwin.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import "errors"
|
||||
|
||||
// GuestInfoSink uses the rpcvmx mechanism to update the guestinfo key/value map as
|
||||
// the datasink for encoding target structures
|
||||
func GuestInfoSink() (DataSink, error) {
|
||||
return GuestInfoSinkWithPrefix("")
|
||||
}
|
||||
|
||||
// GuestInfoSinkWithPrefix adds a prefix to all keys accessed. The key must not have leading
|
||||
// or trailing separator characters, but may have separators in other positions. The separator
|
||||
// (either . or /) will be replaced with the appropriate value for the key in question.
|
||||
func GuestInfoSinkWithPrefix(prefix string) (DataSink, error) {
|
||||
return nil, errors.New("Not implemented on OSX")
|
||||
}
|
||||
63
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/encode_linux.go
generated
vendored
Normal file
63
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/encode_linux.go
generated
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
"github.com/vmware/vmw-guestinfo/rpcvmx"
|
||||
"github.com/vmware/vmw-guestinfo/vmcheck"
|
||||
)
|
||||
|
||||
// GuestInfoSink uses the rpcvmx mechanism to update the guestinfo key/value map as
|
||||
// the datasink for encoding target structures
|
||||
func GuestInfoSink() (DataSink, error) {
|
||||
return GuestInfoSinkWithPrefix("")
|
||||
}
|
||||
|
||||
// GuestInfoSinkWithPrefix adds a prefix to all keys accessed. The key must not have leading
|
||||
// or trailing separator characters, but may have separators in other positions. The separator
|
||||
// (either . or /) will be replaced with the appropriate value for the key in question.
|
||||
func GuestInfoSinkWithPrefix(prefix string) (DataSink, error) {
|
||||
// Check we're using a vcpu (which doesn't assume this is UID 0).
|
||||
if !vmcheck.IsVirtualCPU() {
|
||||
return nil, fmt.Errorf("not in a virtual world")
|
||||
}
|
||||
|
||||
guestinfo := rpcvmx.NewConfig()
|
||||
|
||||
return func(key, value string) error {
|
||||
if strings.Contains(key, "/") {
|
||||
// quietly skip if it's a read-only key
|
||||
return nil
|
||||
}
|
||||
|
||||
key = addPrefixToKey(DefaultGuestInfoPrefix, prefix, key)
|
||||
|
||||
if value == "" {
|
||||
value = "<nil>"
|
||||
}
|
||||
|
||||
log.Debugf("GuestInfoSink: setting key: %s, value: %#v", key, value)
|
||||
err := guestinfo.SetString(key, value)
|
||||
if err != nil {
|
||||
log.Errorf("GuestInfoSink: error: %#v", err)
|
||||
}
|
||||
return err
|
||||
}, nil
|
||||
}
|
||||
30
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/encode_windows.go
generated
vendored
Normal file
30
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/encode_windows.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import "errors"
|
||||
|
||||
// GuestInfoSink uses the rpcvmx mechanism to update the guestinfo key/value map as
|
||||
// the datasink for encoding target structures
|
||||
func GuestInfoSink() (DataSink, error) {
|
||||
return GuestInfoSinkWithPrefix("")
|
||||
}
|
||||
|
||||
// GuestInfoSinkWithPrefix adds a prefix to all keys accessed. The key must not have leading
|
||||
// or trailing separator characters, but may have separators in other positions. The separator
|
||||
// (either . or /) will be replaced with the appropriate value for the key in question.
|
||||
func GuestInfoSinkWithPrefix(prefix string) (DataSink, error) {
|
||||
return nil, errors.New("Not implemented on Windows")
|
||||
}
|
||||
21
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/existing_test.go
generated
vendored
Normal file
21
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/existing_test.go
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestExistingBasic(t *testing.T) {
|
||||
t.Skip("decode into existing target tests not yet implemented")
|
||||
}
|
||||
510
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/keys.go
generated
vendored
Normal file
510
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/keys.go
generated
vendored
Normal file
@@ -0,0 +1,510 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
|
||||
"github.com/vmware/vic/pkg/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultTagName value
|
||||
DefaultTagName = "vic"
|
||||
// DefaultPrefix value
|
||||
DefaultPrefix = ""
|
||||
// DefaultGuestInfoPrefix value
|
||||
DefaultGuestInfoPrefix = "guestinfo.vice."
|
||||
//Separator for slice values and map keys
|
||||
Separator = "|"
|
||||
|
||||
// suffix separator character
|
||||
suffixSeparator = "@"
|
||||
// secret suffix
|
||||
secretSuffix = "secret"
|
||||
// non-persistent suffix
|
||||
nonpersistentSuffix = "non-persistent"
|
||||
)
|
||||
|
||||
const (
|
||||
// Invalid value
|
||||
Invalid = 1 << iota
|
||||
// Hidden value
|
||||
Hidden
|
||||
// ReadOnly value
|
||||
ReadOnly
|
||||
// ReadWrite value
|
||||
ReadWrite
|
||||
// NonPersistent value
|
||||
NonPersistent
|
||||
// Volatile value
|
||||
Volatile
|
||||
// Secret value
|
||||
Secret
|
||||
)
|
||||
|
||||
type recursion struct {
|
||||
// depth is a recursion depth, 0 equating to skip field
|
||||
depth int
|
||||
// follow controls whether we follow pointers
|
||||
follow bool
|
||||
}
|
||||
|
||||
// Unbounded is the value used for unbounded recursion
|
||||
var Unbounded = recursion{depth: -1, follow: true}
|
||||
|
||||
var logger = &logrus.Logger{
|
||||
Out: os.Stderr,
|
||||
// We're using our own text formatter to skip the \n and \t escaping logrus
|
||||
// was doing on non TTY Out (we redirect to a file) descriptors.
|
||||
Formatter: log.NewTextFormatter(),
|
||||
Hooks: make(logrus.LevelHooks),
|
||||
Level: logrus.InfoLevel,
|
||||
}
|
||||
|
||||
// SetLogLevel for the extraconfig package
|
||||
func SetLogLevel(level logrus.Level) {
|
||||
logger.Level = level
|
||||
}
|
||||
|
||||
// calculateScope returns the uint representation of scope tag
|
||||
func calculateScope(scopes []string) uint {
|
||||
var scope uint
|
||||
|
||||
empty := true
|
||||
for i := range scopes {
|
||||
if scopes[i] != "" {
|
||||
empty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if empty {
|
||||
return Hidden | ReadOnly
|
||||
}
|
||||
|
||||
for _, v := range scopes {
|
||||
switch v {
|
||||
case "hidden":
|
||||
scope |= Hidden
|
||||
case "read-only":
|
||||
scope |= ReadOnly
|
||||
case "read-write":
|
||||
scope |= ReadWrite
|
||||
case nonpersistentSuffix:
|
||||
scope |= NonPersistent
|
||||
case "volatile":
|
||||
scope |= Volatile
|
||||
case secretSuffix:
|
||||
scope |= Secret | ReadOnly
|
||||
default:
|
||||
return Invalid
|
||||
}
|
||||
}
|
||||
return scope
|
||||
}
|
||||
|
||||
func isSecret(key string) bool {
|
||||
suffix := strings.Split(key, suffixSeparator)
|
||||
if len(suffix) < 2 {
|
||||
// no @ separator
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range suffix[1:] {
|
||||
if suffix[i+1] == secretSuffix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isNonPersistent(key string) bool {
|
||||
suffix := strings.Split(key, suffixSeparator)
|
||||
if len(suffix) < 2 {
|
||||
// no @ separator
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range suffix[1:] {
|
||||
if suffix[i+1] == nonpersistentSuffix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func calculateScopeFromKey(key string) []string {
|
||||
scopes := []string{}
|
||||
|
||||
if !strings.HasPrefix(key, DefaultGuestInfoPrefix) {
|
||||
scopes = append(scopes, "hidden")
|
||||
}
|
||||
|
||||
if strings.Contains(key, "/") {
|
||||
scopes = append(scopes, "read-only")
|
||||
} else {
|
||||
scopes = append(scopes, "read-write")
|
||||
}
|
||||
|
||||
if isSecret(key) {
|
||||
scopes = append(scopes, secretSuffix)
|
||||
}
|
||||
|
||||
if isNonPersistent(key) {
|
||||
scopes = append(scopes, nonpersistentSuffix)
|
||||
}
|
||||
|
||||
return scopes
|
||||
}
|
||||
|
||||
func calculateKeyFromField(field reflect.StructField, prefix string, depth recursion) (string, recursion) {
|
||||
skip := recursion{}
|
||||
//skip unexported fields
|
||||
if field.PkgPath != "" {
|
||||
logger.Debugf("Skipping %s (not exported)", field.Name)
|
||||
return "", skip
|
||||
}
|
||||
|
||||
// get the annotations
|
||||
tags := field.Tag
|
||||
logger.Debugf("Tags: %#v", tags)
|
||||
|
||||
var key string
|
||||
var scopes []string
|
||||
var scope uint
|
||||
|
||||
fdepth := depth
|
||||
|
||||
prefixScopes := calculateScopeFromKey(prefix)
|
||||
prefixScope := calculateScope(prefixScopes)
|
||||
|
||||
// do we have DefaultTagName?
|
||||
if tags.Get(DefaultTagName) != "" {
|
||||
// get the scopes
|
||||
scopes = strings.Split(tags.Get("scope"), ",")
|
||||
logger.Debugf("Scopes: %#v", scopes)
|
||||
|
||||
// get the keys and split properties from it
|
||||
key = tags.Get("key")
|
||||
logger.Debugf("Key specified: %s", key)
|
||||
|
||||
// get the keys and split properties from it
|
||||
recurse := tags.Get("recurse")
|
||||
if recurse != "" {
|
||||
props := strings.Split(recurse, ",")
|
||||
// process properties
|
||||
for _, prop := range props {
|
||||
// determine recursion depth
|
||||
if strings.HasPrefix(prop, "depth") {
|
||||
parts := strings.Split(prop, "=")
|
||||
if len(parts) != 2 {
|
||||
logger.Warnf("Skipping field with incorrect recurse property: %s", prop)
|
||||
return "", skip
|
||||
}
|
||||
|
||||
val, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil {
|
||||
logger.Warnf("Skipping field with incorrect recurse value: %s", parts[1])
|
||||
return "", skip
|
||||
}
|
||||
fdepth.depth = int(val)
|
||||
} else if prop == "nofollow" {
|
||||
fdepth.follow = false
|
||||
} else if prop == "follow" {
|
||||
fdepth.follow = true
|
||||
} else {
|
||||
logger.Warnf("Ignoring unknown recurse property %s (%s)", key, prop)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Debugf("%s not tagged - inheriting parent scope", field.Name)
|
||||
scopes = prefixScopes
|
||||
}
|
||||
|
||||
if key == "" {
|
||||
logger.Debugf("%s does not specify key - defaulting to fieldname", field.Name)
|
||||
key = field.Name
|
||||
}
|
||||
|
||||
scope = calculateScope(scopes)
|
||||
|
||||
// non-persistent is inherited, even if other scopes are specified
|
||||
if prefixScope&NonPersistent != 0 {
|
||||
scope |= NonPersistent
|
||||
}
|
||||
|
||||
// re-calculate the key based on the scope and prefix
|
||||
if key = calculateKey(scope, prefix, key); key == "" {
|
||||
logger.Debugf("Skipping %s (unknown scope %s)", field.Name, scopes)
|
||||
return "", skip
|
||||
}
|
||||
|
||||
return key, fdepth
|
||||
}
|
||||
|
||||
// calculateKey calculates the key based on the scope and current prefix
|
||||
func calculateKey(scope uint, prefix string, key string) string {
|
||||
if scope&Invalid != 0 {
|
||||
logger.Debugf("invalid scope")
|
||||
return ""
|
||||
}
|
||||
|
||||
newSep := "/"
|
||||
oldSep := "."
|
||||
key = strings.TrimSpace(key)
|
||||
|
||||
hide := scope&Hidden != 0
|
||||
write := scope&ReadWrite != 0
|
||||
visible := strings.HasPrefix(prefix, DefaultGuestInfoPrefix)
|
||||
|
||||
if !hide && write {
|
||||
oldSep = "/"
|
||||
newSep = "."
|
||||
}
|
||||
|
||||
// strip any existing suffix from the prefix - it'll be re-added if still applicable
|
||||
suffix := strings.Index(prefix, suffixSeparator)
|
||||
if suffix != -1 {
|
||||
prefix = prefix[:suffix]
|
||||
}
|
||||
|
||||
// assemble the actual keypath with appropriate separators
|
||||
out := key
|
||||
if prefix != "" {
|
||||
out = strings.Join([]string{prefix, key}, newSep)
|
||||
}
|
||||
|
||||
if scope&Secret != 0 {
|
||||
out += suffixSeparator + secretSuffix
|
||||
}
|
||||
|
||||
if scope&NonPersistent != 0 {
|
||||
if hide {
|
||||
logger.Debugf("Unable to combine non-persistent and hidden scopes")
|
||||
return ""
|
||||
}
|
||||
out += suffixSeparator + nonpersistentSuffix
|
||||
}
|
||||
|
||||
// we don't care about existing separators when hiden
|
||||
if hide {
|
||||
if !visible {
|
||||
return out
|
||||
}
|
||||
|
||||
// strip the prefix and the leading r/w signifier
|
||||
return out[len(DefaultGuestInfoPrefix)+1:]
|
||||
}
|
||||
|
||||
// ensure that separators are correct
|
||||
out = strings.Replace(out, oldSep, newSep, -1)
|
||||
|
||||
// Assemble the base that controls key publishing in guest
|
||||
if !visible {
|
||||
return DefaultGuestInfoPrefix + newSep + out
|
||||
}
|
||||
|
||||
// prefix will have been mangled by strings.Replace
|
||||
return DefaultGuestInfoPrefix + out[len(DefaultGuestInfoPrefix):]
|
||||
}
|
||||
|
||||
// utility function to allow adding of arbitrary prefix into key
|
||||
// header is a leading segment that is preserved, prefix is injected after that
|
||||
func addPrefixToKey(header, prefix, key string) string {
|
||||
if prefix == "" {
|
||||
return key
|
||||
}
|
||||
|
||||
base := strings.TrimPrefix(key, header)
|
||||
separator := base[0]
|
||||
|
||||
var modifiedPrefix string
|
||||
if separator == '.' {
|
||||
modifiedPrefix = strings.Replace(prefix, "/", ".", -1)
|
||||
} else {
|
||||
modifiedPrefix = strings.Replace(prefix, ".", "/", -1)
|
||||
}
|
||||
|
||||
// we assume (given usage comment for WithPrefix) that there's no leading or trailing separator
|
||||
// on the prefix. base has a leading separator
|
||||
// guestinfoPrefix is const so adding it to the format string directly
|
||||
return fmt.Sprintf(header+"%c%s%s", separator, modifiedPrefix, base)
|
||||
}
|
||||
|
||||
// appendToPrefix will join the value to the prefix with the separator (if any) while ensuring that
|
||||
// any suffixes are moved to the end of the key
|
||||
func appendToPrefix(prefix, separator, value string) string {
|
||||
// strip any existing suffix from the prefix - it'll be re-added if still applicable
|
||||
index := strings.Index(prefix, suffixSeparator)
|
||||
suffix := ""
|
||||
if index != -1 {
|
||||
suffix = prefix[index:]
|
||||
prefix = prefix[:index]
|
||||
}
|
||||
|
||||
// suffix wil still include the suffix separator if present
|
||||
key := fmt.Sprintf("%s%s%s%s", prefix, separator, value, suffix)
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func calculateKeys(v reflect.Value, field string, prefix string) []string {
|
||||
logger.Debugf("v=%#v, field=%#v, prefix=%#v", v, field, prefix)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
return calculateKeys(v.Elem(), field, prefix)
|
||||
}
|
||||
|
||||
if field == "" {
|
||||
return []string{prefix}
|
||||
}
|
||||
|
||||
s := strings.SplitN(field, ".", 2)
|
||||
field = ""
|
||||
iterate := false
|
||||
if s[0] == "*" {
|
||||
iterate = true
|
||||
}
|
||||
|
||||
if len(s) > 1 {
|
||||
field = s[1]
|
||||
}
|
||||
|
||||
if !iterate {
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
found := false
|
||||
for _, k := range v.MapKeys() {
|
||||
sk := k.Convert(reflect.TypeOf(""))
|
||||
if sk.String() == s[0] {
|
||||
v = v.MapIndex(k)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
panic(fmt.Sprintf("could not find map key %s", s[0]))
|
||||
}
|
||||
prefix = appendToPrefix(prefix, Separator, s[0])
|
||||
case reflect.Array, reflect.Slice:
|
||||
i, err := strconv.Atoi(s[0])
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("bad array index %s: %s", s[0], err))
|
||||
}
|
||||
switch v.Type().Elem().Kind() {
|
||||
case reflect.Struct:
|
||||
prefix = appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
|
||||
case reflect.Uint8:
|
||||
return []string{prefix}
|
||||
default:
|
||||
prefix = appendToPrefix(prefix, "", "~")
|
||||
}
|
||||
v = v.Index(i)
|
||||
case reflect.Struct:
|
||||
f, found := v.Type().FieldByName(s[0])
|
||||
if !found {
|
||||
panic(fmt.Sprintf("could not find field %s", s[0]))
|
||||
}
|
||||
prefix, _ = calculateKeyFromField(f, prefix, recursion{})
|
||||
v = v.FieldByIndex(f.Index)
|
||||
default:
|
||||
panic(fmt.Sprintf("cannot get field from type %s", v.Type()))
|
||||
}
|
||||
|
||||
return calculateKeys(v, field, prefix)
|
||||
}
|
||||
|
||||
var out []string
|
||||
switch v.Kind() {
|
||||
case reflect.Map:
|
||||
for _, k := range v.MapKeys() {
|
||||
sk := k.Convert(reflect.TypeOf(""))
|
||||
prefix := appendToPrefix(prefix, Separator, sk.String())
|
||||
out = append(out, calculateKeys(v.MapIndex(k), field, prefix)...)
|
||||
}
|
||||
case reflect.Array, reflect.Slice:
|
||||
switch v.Type().Elem().Kind() {
|
||||
case reflect.Struct:
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
prefix := appendToPrefix(prefix, Separator, fmt.Sprintf("%d", i))
|
||||
out = append(out, calculateKeys(v.Index(i), field, prefix)...)
|
||||
}
|
||||
case reflect.Uint8:
|
||||
return []string{prefix}
|
||||
default:
|
||||
return []string{appendToPrefix(prefix, "", "~")}
|
||||
}
|
||||
case reflect.Struct:
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
prefix, _ := calculateKeyFromField(v.Type().Field(i), prefix, recursion{})
|
||||
out = append(out, calculateKeys(v.Field(i), field, prefix)...)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("can't iterate type %s", v.Type().String()))
|
||||
}
|
||||
|
||||
return out
|
||||
|
||||
}
|
||||
|
||||
// CalculateKeys gets the keys in extraconfig corresponding to the field
|
||||
// specification passed in for obj. Examples:
|
||||
//
|
||||
// type struct A {
|
||||
// I int `vic:"0.1" scope:"read-only" key:"i"`
|
||||
// Str string `vic:"0.1" scope:"read-only" key:"str"`
|
||||
// }
|
||||
//
|
||||
// type struct B {
|
||||
// A A `vic:"0.1" scope:"read-only" key:"a"`
|
||||
// Array []A `vic:"0.1" scope:"read-only" key:"array"`
|
||||
// Map map[string]string `vic:"0.1" scope:"read-only" key:"map"`
|
||||
// }
|
||||
//
|
||||
// b := B{}
|
||||
// b.Array = []A{A{}}
|
||||
// b.Map = map[string]string{"foo": "", "bar": ""}
|
||||
// // returns []string{"a/str"}
|
||||
// CalculateKeys(b, "A.Str", "")
|
||||
//
|
||||
// // returns []string{"array|0"}
|
||||
// CalculateKeys(b, "Array.0", "")
|
||||
//
|
||||
// // returns []string{"array|0"}
|
||||
// CalculateKeys(b, "Array.*", "")
|
||||
//
|
||||
// // returns []string{"map|foo", "map|bar"}
|
||||
// CalculateKeys(b, "Map.*", "")
|
||||
//
|
||||
// // returns []string{"map|foo"}
|
||||
// CalculateKeys(b, "Map.foo", "")
|
||||
//
|
||||
// // returns []string{"map|foo/str"}
|
||||
// CalculateKeys(b, "Map.foo.str", "")
|
||||
//
|
||||
func CalculateKeys(obj interface{}, field string, prefix string) []string {
|
||||
return calculateKeys(reflect.ValueOf(obj), field, prefix)
|
||||
}
|
||||
269
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/keys_test.go
generated
vendored
Normal file
269
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/keys_test.go
generated
vendored
Normal file
@@ -0,0 +1,269 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func visibleRO(key string) string {
|
||||
return calculateKey(calculateScope([]string{"read-only"}), "", key)
|
||||
}
|
||||
|
||||
func visibleRONonpersistent(key string) string {
|
||||
return calculateKey(calculateScope([]string{"read-only", "non-persistent"}), "", key)
|
||||
}
|
||||
|
||||
func visibleRW(key string) string {
|
||||
return calculateKey(calculateScope([]string{"read-write"}), "", key)
|
||||
}
|
||||
|
||||
func hidden(key string) string {
|
||||
return calculateKey(calculateScope([]string{"hidden"}), "", key)
|
||||
}
|
||||
|
||||
func TestHidden(t *testing.T) {
|
||||
scopes := []string{"hidden"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), "a/b", "c")
|
||||
|
||||
assert.Equal(t, "a/b/c", key, "Key should remain hidden")
|
||||
}
|
||||
|
||||
func TestHide(t *testing.T) {
|
||||
scopes := []string{"hidden"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+"/a/b", "c")
|
||||
|
||||
assert.Equal(t, "a/b/c", key, "Key should be hidden")
|
||||
}
|
||||
|
||||
func TestReveal(t *testing.T) {
|
||||
scopes := []string{"read-only"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), "a/b", "c")
|
||||
|
||||
assert.Equal(t, DefaultGuestInfoPrefix+"/a/b/c", key, "Key should be exposed")
|
||||
}
|
||||
|
||||
func TestVisibleReadOnly(t *testing.T) {
|
||||
scopes := []string{"read-only"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+"/a/b", "c")
|
||||
|
||||
assert.Equal(t, DefaultGuestInfoPrefix+"/a/b/c", key, "Key should be remain visible and read-only")
|
||||
}
|
||||
|
||||
func TestVisibleReadWrite(t *testing.T) {
|
||||
scopes := []string{"read-write"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
|
||||
|
||||
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c", key, "Key should be remain visible and read-write")
|
||||
}
|
||||
|
||||
func TestTopLevelReadOnly(t *testing.T) {
|
||||
scopes := []string{"read-only"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), "", "a")
|
||||
|
||||
assert.Equal(t, DefaultGuestInfoPrefix+"/a", key, "Key should be visible and read-only")
|
||||
}
|
||||
|
||||
func TestReadOnlyToReadWrite(t *testing.T) {
|
||||
scopes := []string{"read-write"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+"/a/b", "c")
|
||||
|
||||
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c", key, "Key should be visible and change to read-write")
|
||||
}
|
||||
|
||||
func TestReadWriteToReadOnly(t *testing.T) {
|
||||
scopes := []string{"read-only"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
|
||||
|
||||
assert.Equal(t, DefaultGuestInfoPrefix+"/a/b/c", key, "Key should be visible and change to read-only")
|
||||
}
|
||||
|
||||
func TestCompoundKey(t *testing.T) {
|
||||
scopes := []string{"read-write"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a", "b/c")
|
||||
|
||||
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c", key, "Key should be visible and read-write")
|
||||
}
|
||||
|
||||
func TestNoScopes(t *testing.T) {
|
||||
scopes := []string{}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a/b", "c")
|
||||
assert.Equal(t, "a/b/c", key, "Key should be completely proscriptive")
|
||||
|
||||
key = calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
|
||||
assert.Equal(t, "a.b/c", key, "Key should be hidden")
|
||||
|
||||
key = calculateKey(calculateScope(scopes), "a.b", "c")
|
||||
assert.Equal(t, "a.b/c", key, "Key should remain hidden")
|
||||
}
|
||||
|
||||
func TestSecret(t *testing.T) {
|
||||
scopes := []string{"secret", "read-write"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
|
||||
|
||||
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c"+suffixSeparator+secretSuffix, key, "Key should have secret suffix")
|
||||
}
|
||||
|
||||
func TestNonpersistent(t *testing.T) {
|
||||
scopes := []string{"non-persistent", "read-write"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
|
||||
|
||||
assert.Equal(t, DefaultGuestInfoPrefix+".a.b.c"+suffixSeparator+nonpersistentSuffix, key, "Key should have non-persistent suffix")
|
||||
}
|
||||
|
||||
func TestMultipleSuffixes(t *testing.T) {
|
||||
scopes := []string{"non-persistent", "secret", "read-write"}
|
||||
|
||||
key := calculateKey(calculateScope(scopes), DefaultGuestInfoPrefix+".a.b", "c")
|
||||
|
||||
assert.True(t, strings.Contains(key, suffixSeparator+secretSuffix) && strings.Contains(key, suffixSeparator+nonpersistentSuffix), "Key should contain both secret and non-persistent suffix")
|
||||
}
|
||||
|
||||
func TestCalculateKeys(t *testing.T) {
|
||||
type AStruct struct {
|
||||
I int
|
||||
}
|
||||
type Type struct {
|
||||
ExecutorConfig ExecutorConfig `vic:"0.1" scope:"hidden" key:"executorconfig"`
|
||||
Array []AStruct `vic:"0.1" scope:"read-write" key:"array"`
|
||||
Ptr *AStruct `vic:"0.1" scope:"read-only" key:"ptr"`
|
||||
Str string `vic:"0.1" scope:"read-only" key:"str"`
|
||||
Bytes []uint8 `vic:"0.1" scope:"read-write" key:"bytes"`
|
||||
}
|
||||
|
||||
ec := Type{
|
||||
ExecutorConfig: ExecutorConfig{
|
||||
Sessions: map[string]SessionConfig{
|
||||
"Session1": {
|
||||
Common: Common{
|
||||
ID: "SessionID",
|
||||
Name: "SessionName",
|
||||
},
|
||||
Tty: true,
|
||||
Cmd: Cmd{
|
||||
Path: "/vmware",
|
||||
Args: []string{"/bin/imagec", "-standalone"},
|
||||
Env: []string{"PATH=/bin", "USER=imagec"},
|
||||
Dir: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Array: []AStruct{
|
||||
{I: 0},
|
||||
},
|
||||
Ptr: &AStruct{
|
||||
I: 1,
|
||||
},
|
||||
Str: "foo",
|
||||
Bytes: []byte{0xd, 0xe, 0xa, 0xd, 0xb, 0xe, 0xe, 0xf},
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
in string
|
||||
out []string
|
||||
}{
|
||||
{
|
||||
"ExecutorConfig.*",
|
||||
[]string{
|
||||
visibleRO("executorconfig/common"),
|
||||
hidden("executorconfig/sessions"),
|
||||
"executorconfig/Key",
|
||||
},
|
||||
},
|
||||
{
|
||||
"ExecutorConfig.Sessions.*",
|
||||
[]string{"executorconfig/sessions" + Separator + "Session1"},
|
||||
},
|
||||
{
|
||||
"ExecutorConfig.Sessions.Session1.Cmd.Args",
|
||||
[]string{"executorconfig/sessions" + Separator + "Session1/cmd/args"},
|
||||
},
|
||||
{
|
||||
"ExecutorConfig.Sessions.*.Cmd.Args.*",
|
||||
[]string{"executorconfig/sessions" + Separator + "Session1/cmd/args~"},
|
||||
},
|
||||
{
|
||||
"ExecutorConfig.Sessions.*.Cmd.Args.0",
|
||||
[]string{"executorconfig/sessions" + Separator + "Session1/cmd/args~"},
|
||||
},
|
||||
{
|
||||
"Array.0.I",
|
||||
[]string{visibleRW("array" + Separator + "0/I")},
|
||||
},
|
||||
{
|
||||
"Array.*",
|
||||
[]string{visibleRW("array" + Separator + "0")},
|
||||
},
|
||||
{
|
||||
"Ptr.I",
|
||||
[]string{visibleRO("ptr/I")},
|
||||
},
|
||||
{
|
||||
"Str",
|
||||
[]string{visibleRO("str")},
|
||||
},
|
||||
{
|
||||
"Bytes",
|
||||
[]string{visibleRW("bytes")},
|
||||
},
|
||||
{
|
||||
"Bytes.0",
|
||||
[]string{visibleRW("bytes")},
|
||||
},
|
||||
{
|
||||
"Bytes.*",
|
||||
[]string{visibleRW("bytes")},
|
||||
},
|
||||
}
|
||||
|
||||
for _, te := range tests {
|
||||
keys := CalculateKeys(ec, te.in, "")
|
||||
assert.Equal(t, te.out, keys)
|
||||
}
|
||||
|
||||
panicTests := []string{
|
||||
"Array.1.I",
|
||||
"Array.0.i",
|
||||
"Array.f.i",
|
||||
"ExecutorConfig.foo",
|
||||
"foo",
|
||||
"ExecutorConfig.Sessions.foo",
|
||||
"Str.*",
|
||||
"Str.foo",
|
||||
}
|
||||
|
||||
for _, te := range panicTests {
|
||||
assert.Panics(t, func() {
|
||||
CalculateKeys(ec, te, "")
|
||||
})
|
||||
}
|
||||
}
|
||||
127
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/secret.go
generated
vendored
Normal file
127
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/secret.go
generated
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
)
|
||||
|
||||
// The value of this key is hidden from API requests, but visible within the guest
|
||||
// #nosec: Potential hardcoded credentials
|
||||
const GuestInfoSecretKey = "guestinfo.ovfEnv"
|
||||
|
||||
// SecretKey provides helpers to encrypt/decrypt extraconfig values
|
||||
type SecretKey struct {
|
||||
key [32]byte
|
||||
}
|
||||
|
||||
// NewSecretKey generates a new secret key
|
||||
func NewSecretKey() (*SecretKey, error) {
|
||||
s := new(SecretKey)
|
||||
|
||||
if _, err := rand.Read(s.key[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// FromString base64 decodes an existing SecretKey
|
||||
func (s *SecretKey) FromString(key string) error {
|
||||
b, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(b) != 32 {
|
||||
return errors.New("invalid secret key")
|
||||
}
|
||||
|
||||
copy(s.key[:], b)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String base64 encodes a SecretKey
|
||||
func (s *SecretKey) String() string {
|
||||
return base64.StdEncoding.EncodeToString(s.key[:])
|
||||
}
|
||||
|
||||
// Source wraps the given DataSource, decrypting any secret values
|
||||
func (s *SecretKey) Source(ds DataSource) DataSource {
|
||||
// If GuestInfoSecretKey has a value, it should be our secret key.
|
||||
// #nosec: Errors unhandled.
|
||||
if val, _ := ds(GuestInfoSecretKey); val != "" {
|
||||
if err := s.FromString(val); err != nil {
|
||||
log.Errorf("failed to decode %s: %s", GuestInfoSecretKey, err)
|
||||
} else {
|
||||
log.Debugf("secret key decoded from %s", GuestInfoSecretKey)
|
||||
}
|
||||
}
|
||||
|
||||
return func(key string) (string, error) {
|
||||
val, err := ds(key)
|
||||
|
||||
if err == nil && isSecret(key) {
|
||||
b, err := base64.StdEncoding.DecodeString(val)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var nonce [24]byte
|
||||
copy(nonce[:], b[:24])
|
||||
|
||||
plaintext, ok := secretbox.Open([]byte{}, b[24:], &nonce, &s.key)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to decrypt value for %s", key)
|
||||
}
|
||||
|
||||
val = string(plaintext)
|
||||
}
|
||||
|
||||
return val, err
|
||||
}
|
||||
}
|
||||
|
||||
// Sink wraps the given DataSink, encrypting any secret values
|
||||
func (s *SecretKey) Sink(ds DataSink) DataSink {
|
||||
// Store our secret key.
|
||||
if err := ds(GuestInfoSecretKey, s.String()); err != nil {
|
||||
log.Errorf("failed to store %s: %s", GuestInfoSecretKey, err)
|
||||
}
|
||||
|
||||
return func(key, value string) error {
|
||||
if isSecret(key) {
|
||||
var nonce [24]byte
|
||||
|
||||
if _, err := rand.Read(nonce[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ciphertext := secretbox.Seal(nonce[:], []byte(value), &nonce, &s.key)
|
||||
|
||||
value = base64.StdEncoding.EncodeToString(ciphertext)
|
||||
}
|
||||
|
||||
return ds(key, value)
|
||||
}
|
||||
}
|
||||
67
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/secret_test.go
generated
vendored
Normal file
67
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/secret_test.go
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSecretFields(t *testing.T) {
|
||||
type tell struct {
|
||||
Who string `vic:"0.1" scope:"secret" key:"who"`
|
||||
}
|
||||
|
||||
type stuff struct {
|
||||
Username string `vic:"0.1" scope:"read-only" key:"username"`
|
||||
Password string `vic:"0.1" scope:"secret" key:"password"`
|
||||
Tell tell
|
||||
}
|
||||
|
||||
config := stuff{
|
||||
Username: "root",
|
||||
Password: "super-s@fe-passw0rd",
|
||||
Tell: tell{"noone"},
|
||||
}
|
||||
|
||||
out, err := NewSecretKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
encoded := map[string]string{}
|
||||
Encode(out.Sink(MapSink(encoded)), config)
|
||||
|
||||
password := encoded["guestinfo.vice./password"+suffixSeparator+secretSuffix]
|
||||
assert.NotEmpty(t, password, "encrypted password")
|
||||
assert.NotEqual(t, password, config.Password, "encrypted password")
|
||||
|
||||
for _, expectEq := range []bool{true, false} {
|
||||
var in SecretKey
|
||||
|
||||
var decoded stuff
|
||||
Decode(in.Source(MapSource(encoded)), &decoded)
|
||||
|
||||
if expectEq {
|
||||
assert.Equal(t, config, decoded, "Encoded and decoded does not match")
|
||||
} else {
|
||||
assert.NotEqual(t, config, decoded, "Encoded and decoded should not not match")
|
||||
}
|
||||
|
||||
// second time should fail to decrypt w/o GuestInfoSecretKey
|
||||
delete(encoded, GuestInfoSecretKey)
|
||||
}
|
||||
}
|
||||
56
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/store.go
generated
vendored
Normal file
56
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/store.go
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package extraconfig
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Store provides combined DataSource and DataSink.
|
||||
type Store interface {
|
||||
Get(string) (string, error)
|
||||
Put(string, string) error
|
||||
}
|
||||
|
||||
type MapStore struct {
|
||||
mutex sync.Mutex
|
||||
|
||||
store map[string]string
|
||||
}
|
||||
|
||||
func New() *MapStore {
|
||||
return &MapStore{
|
||||
store: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MapStore) Get(key string) (string, error) {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
val, ok := t.store[key]
|
||||
if !ok {
|
||||
return "", ErrKeyNotFound
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (t *MapStore) Put(key, value string) error {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
t.store[key] = value
|
||||
return nil
|
||||
}
|
||||
55
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/vmomi/delta_test.go
generated
vendored
Normal file
55
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/vmomi/delta_test.go
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package vmomi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"reflect"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
)
|
||||
|
||||
func TestDelta(t *testing.T) {
|
||||
new := map[string]string{
|
||||
"hello": "goodbye",
|
||||
"cruel": "world",
|
||||
"is": "not",
|
||||
"enough": "already",
|
||||
}
|
||||
|
||||
existing := []types.BaseOptionValue{
|
||||
&types.OptionValue{Key: "hello", Value: "goodbye"},
|
||||
&types.OptionValue{Key: "is", Value: "always"},
|
||||
&types.OptionValue{Key: "present", Value: "regardless"},
|
||||
}
|
||||
|
||||
updatesSlice := OptionValueUpdatesFromMap(existing, new)
|
||||
|
||||
expected := map[string]string{
|
||||
"enough": "already", // added
|
||||
"cruel": "world", // added
|
||||
"is": "not", // changed
|
||||
}
|
||||
|
||||
// turn them back into maps for equality check
|
||||
updates := OptionValueMap(updatesSlice)
|
||||
|
||||
if !assert.True(t, reflect.DeepEqual(expected, updates), "DeepEqual says they do not match") {
|
||||
t.Fatalf("Expected: %+q \nActual: %+q\n", expected, updates)
|
||||
}
|
||||
}
|
||||
140
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/vmomi/optionvalue.go
generated
vendored
Normal file
140
vendor/github.com/vmware/vic/pkg/vsphere/extraconfig/vmomi/optionvalue.go
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package vmomi is in a separate package to avoid the transitive inclusion of govmomi
|
||||
// as a fundamental dependency of the main extraconfig
|
||||
package vmomi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/pkg/vsphere/extraconfig"
|
||||
)
|
||||
|
||||
// OptionValueMap returns a map from array of OptionValues
|
||||
func OptionValueMap(src []types.BaseOptionValue) map[string]string {
|
||||
// create the key/value store from the extraconfig slice for lookups
|
||||
kv := make(map[string]string)
|
||||
for i := range src {
|
||||
k := src[i].GetOptionValue().Key
|
||||
v := src[i].GetOptionValue().Value.(string)
|
||||
kv[k] = unescapeNil(v)
|
||||
}
|
||||
return kv
|
||||
}
|
||||
|
||||
// OptionValueSource is a convenience method to generate a MapSource source from
|
||||
// and array of OptionValue's
|
||||
func OptionValueSource(src []types.BaseOptionValue) extraconfig.DataSource {
|
||||
kv := OptionValueMap(src)
|
||||
return extraconfig.MapSource(kv)
|
||||
}
|
||||
|
||||
// OptionValueFromMap is a convenience method to convert a map into a BaseOptionValue array
|
||||
// escapeNil - if true a nil string is replaced with "<nil>". Allows us to distinguish between
|
||||
// deletion and nil as a value
|
||||
func OptionValueFromMap(data map[string]string, escape bool) []types.BaseOptionValue {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
array := make([]types.BaseOptionValue, len(data))
|
||||
|
||||
i := 0
|
||||
for k, v := range data {
|
||||
if escape {
|
||||
v = escapeNil(v)
|
||||
}
|
||||
array[i] = &types.OptionValue{Key: k, Value: v}
|
||||
i++
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
// OptionValueArrayToString translates the options array in to a Go formatted structure dump
|
||||
func OptionValueArrayToString(options []types.BaseOptionValue) string {
|
||||
// create the key/value store from the extraconfig slice for lookups
|
||||
kv := make(map[string]string)
|
||||
for i := range options {
|
||||
k := options[i].GetOptionValue().Key
|
||||
v := options[i].GetOptionValue().Value.(string)
|
||||
kv[k] = v
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%#v", kv)
|
||||
}
|
||||
|
||||
// OptionValueUpdatesFromMap generates an optionValue array for those entries in the map that do not
|
||||
// already exist, are changed from the reference array, or a removed
|
||||
// A removed entry will have a nil string for the value
|
||||
// NOTE: DOES NOT CURRENTLY SUPPORT DELETION OF KEYS - KEYS MISSING FROM NEW MAP ARE IGNORED
|
||||
func OptionValueUpdatesFromMap(existing []types.BaseOptionValue, new map[string]string) []types.BaseOptionValue {
|
||||
e := len(existing)
|
||||
if e == 0 {
|
||||
return OptionValueFromMap(new, true)
|
||||
}
|
||||
|
||||
n := len(new)
|
||||
updates := make(map[string]string, n+e)
|
||||
unchanged := make(map[string]struct{}, n+e)
|
||||
|
||||
// first the existing keys
|
||||
for i := range existing {
|
||||
v := existing[i].GetOptionValue()
|
||||
if nV, ok := new[v.Key]; ok && nV == v.Value.(string) {
|
||||
unchanged[v.Key] = struct{}{}
|
||||
// no change
|
||||
continue
|
||||
} else if ok {
|
||||
// changed
|
||||
updates[v.Key] = escapeNil(nV)
|
||||
} else {
|
||||
// deletion
|
||||
// NOTE: ignored as this also deletes non VIC entries currently
|
||||
// there's no prefix for the non-guestinfo keys so cannot easily filter
|
||||
// updates[v.Key] = ""
|
||||
}
|
||||
}
|
||||
|
||||
// now the new keys
|
||||
for k, v := range new {
|
||||
if _, ok := unchanged[k]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := updates[k]; !ok {
|
||||
updates[k] = escapeNil(v)
|
||||
}
|
||||
}
|
||||
|
||||
return OptionValueFromMap(updates, false)
|
||||
}
|
||||
|
||||
func escapeNil(input string) string {
|
||||
if input == "" {
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
func unescapeNil(input string) string {
|
||||
if input == "<nil>" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
75
vendor/github.com/vmware/vic/pkg/vsphere/guest/util_linux.go
generated
vendored
Normal file
75
vendor/github.com/vmware/vic/pkg/vsphere/guest/util_linux.go
generated
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package guest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"context"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/vm"
|
||||
)
|
||||
|
||||
const (
|
||||
UUIDPath = "/sys/class/dmi/id/product_serial"
|
||||
UUIDPrefix = "VMware-"
|
||||
)
|
||||
|
||||
// UUID gets the BIOS UUID via the sys interface. This UUID is known by vphsere
|
||||
func UUID() (string, error) {
|
||||
id, err := ioutil.ReadFile(UUIDPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error retrieving vm uuid: %s", err)
|
||||
}
|
||||
|
||||
uuidstr := string(id[:])
|
||||
|
||||
// check the uuid starts with "VMware-"
|
||||
if !strings.HasPrefix(uuidstr, UUIDPrefix) {
|
||||
return "", fmt.Errorf("cannot find this VM's UUID")
|
||||
}
|
||||
|
||||
// Strip the prefix, white spaces, and the trailing '\n'
|
||||
uuidstr = strings.Replace(uuidstr[len(UUIDPrefix):(len(uuidstr)-1)], " ", "", -1)
|
||||
|
||||
// need to add dashes, e.g. "564d395e-d807-e18a-cb25-b79f65eb2b9f"
|
||||
uuidstr = fmt.Sprintf("%s-%s-%s-%s", uuidstr[0:8], uuidstr[8:12], uuidstr[12:21], uuidstr[21:])
|
||||
|
||||
return uuidstr, nil
|
||||
}
|
||||
|
||||
// GetSelf gets VirtualMachine reference for the VM this process is running on
|
||||
func GetSelf(ctx context.Context, s *session.Session) (*vm.VirtualMachine, error) {
|
||||
u, err := UUID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
search := object.NewSearchIndex(s.Vim25())
|
||||
ref, err := search.FindByUuid(ctx, s.Datacenter, u, true, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ref == nil {
|
||||
return nil, fmt.Errorf("can't find the hosting vm")
|
||||
}
|
||||
|
||||
return vm.NewVirtualMachine(ctx, s, ref.Reference()), nil
|
||||
}
|
||||
37
vendor/github.com/vmware/vic/pkg/vsphere/guest/util_other.go
generated
vendored
Normal file
37
vendor/github.com/vmware/vic/pkg/vsphere/guest/util_other.go
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !linux
|
||||
|
||||
package guest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"context"
|
||||
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/vm"
|
||||
)
|
||||
|
||||
// UUID gets the BIOS UUID via the sys interface. This UUID is known by vphsere
|
||||
func UUID() (string, error) {
|
||||
return "", fmt.Errorf("unimplemented on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// GetSelf gets VirtualMachine reference for the VM this process is running on
|
||||
func GetSelf(ctx context.Context, s *session.Session) (*vm.VirtualMachine, error) {
|
||||
return nil, fmt.Errorf("unimplemented on %s", runtime.GOOS)
|
||||
}
|
||||
49
vendor/github.com/vmware/vic/pkg/vsphere/guest/util_test.go
generated
vendored
Normal file
49
vendor/github.com/vmware/vic/pkg/vsphere/guest/util_test.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package guest
|
||||
|
||||
import (
|
||||
"os/user"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/vmw-guestinfo/vmcheck"
|
||||
)
|
||||
|
||||
func TestUUID(t *testing.T) {
|
||||
if isVM, err := vmcheck.IsVirtualWorld(); !isVM || err != nil {
|
||||
t.Skip("can get uuid if not running on a vm")
|
||||
}
|
||||
// need to be root and on esx to run this test
|
||||
u, err := user.Current()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
if u.Uid != "0" {
|
||||
t.SkipNow()
|
||||
return
|
||||
}
|
||||
|
||||
s, err := UUID()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.NotNil(t, s) {
|
||||
return
|
||||
}
|
||||
}
|
||||
58
vendor/github.com/vmware/vic/pkg/vsphere/optmanager/optmanager.go
generated
vendored
Normal file
58
vendor/github.com/vmware/vic/pkg/vsphere/optmanager/optmanager.go
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package optmanager provides govmomi helpers for the OptionManager.
|
||||
package optmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
)
|
||||
|
||||
// QueryOptionValue uses the session client and OptionManager to look up the input option
|
||||
// and return the received value.
|
||||
func QueryOptionValue(ctx context.Context, s *session.Session, option string) (string, error) {
|
||||
client := s.Vim25()
|
||||
optMgr := object.NewOptionManager(client, *client.ServiceContent.Setting)
|
||||
opts, err := optMgr.Query(ctx, option)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error querying option %q: %s", option, err)
|
||||
}
|
||||
|
||||
if len(opts) == 1 {
|
||||
return fmt.Sprintf("%v", opts[0].GetOptionValue().Value), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%d values querying option %q", len(opts), option)
|
||||
}
|
||||
|
||||
// UpdateOptionValue uses the session client and OptionManager to set the input option
|
||||
func UpdateOptionValue(ctx context.Context, s *session.Session, option string, value string) error {
|
||||
client := s.Vim25()
|
||||
optMgr := object.NewOptionManager(client, *client.ServiceContent.Setting)
|
||||
var opts []types.BaseOptionValue
|
||||
opts = append(opts, &types.OptionValue{
|
||||
Key: option,
|
||||
Value: value,
|
||||
})
|
||||
err := optMgr.Update(ctx, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting option %q: %s", option, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
65
vendor/github.com/vmware/vic/pkg/vsphere/optmanager/optmanager_test.go
generated
vendored
Normal file
65
vendor/github.com/vmware/vic/pkg/vsphere/optmanager/optmanager_test.go
generated
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package optmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/vmware/govmomi/simulator"
|
||||
"github.com/vmware/vic/pkg/vsphere/test"
|
||||
)
|
||||
|
||||
func TestQueryOptionValue(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
model := simulator.VPX()
|
||||
defer model.Remove()
|
||||
err := model.Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server := model.Service.NewServer()
|
||||
defer server.Close()
|
||||
|
||||
s, err := test.SessionWithVPX(ctx, server.URL.String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Multiple value error
|
||||
optValue, err := QueryOptionValue(ctx, s, "")
|
||||
if err == nil {
|
||||
t.Fatal("expected multiple value error")
|
||||
}
|
||||
|
||||
// Invalid option
|
||||
optValue, err = QueryOptionValue(ctx, s, "foo-bar")
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid query error")
|
||||
}
|
||||
|
||||
// Valid option
|
||||
adminOptKey := "config.vpxd.sso.default.admin"
|
||||
adminOptVal := "Administrator@vsphere.local"
|
||||
optValue, err = QueryOptionValue(ctx, s, "config.vpxd.sso.default.admin")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, got: %s", err)
|
||||
}
|
||||
if optValue != adminOptVal {
|
||||
t.Fatalf("expected value %s for query %q, got: %s", adminOptVal, adminOptKey, optValue)
|
||||
}
|
||||
}
|
||||
446
vendor/github.com/vmware/vic/pkg/vsphere/performance/vm_collector.go
generated
vendored
Normal file
446
vendor/github.com/vmware/vic/pkg/vsphere/performance/vm_collector.go
generated
vendored
Normal file
@@ -0,0 +1,446 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package performance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/govmomi/performance"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
)
|
||||
|
||||
const (
|
||||
// number of samples per collection
|
||||
sampleSize = int32(2)
|
||||
// number of seconds between sample collection
|
||||
sampleInterval = int32(20)
|
||||
// vSphere recommomends a maxiumum of 50 entities per query
|
||||
maxEntityQuery = 50
|
||||
)
|
||||
|
||||
// CPUUsage provides individual CPU metrics
|
||||
type CPUUsage struct {
|
||||
// processor id (0,1,2)
|
||||
ID int
|
||||
// MhzUsage is the MhZ consumed by a specific processor
|
||||
MhzUsage int64
|
||||
}
|
||||
|
||||
// CPUMetrics encapsulates available vm CPU metrics
|
||||
type CPUMetrics struct {
|
||||
// CPUs are the individual CPU metrics
|
||||
CPUs []CPUUsage
|
||||
// Usage is the percentage of total vm CPU usage
|
||||
Usage float32
|
||||
}
|
||||
|
||||
// MemoryMetrics encapsulates available vm memory metrics
|
||||
type MemoryMetrics struct {
|
||||
// Consumed memory of vm in bytes
|
||||
Consumed int64
|
||||
// Active memory of vm in bytes
|
||||
Active int64
|
||||
// Provisioned memory of vm in bytes
|
||||
Provisioned int64
|
||||
}
|
||||
|
||||
// NetworkUsage provides detailed network stats
|
||||
type NetworkUsage struct {
|
||||
Bytes uint64 // total bytes
|
||||
Kbps int64 // KiloBytesPerSecond
|
||||
Packets int64 // total packet count
|
||||
Errors int64 // NOT CURRENTLY IMPLEMENTED
|
||||
Dropped int64 // total dropped packet count
|
||||
}
|
||||
|
||||
// Network provides metrics for individual network devices
|
||||
type Network struct {
|
||||
Name string
|
||||
Rx NetworkUsage
|
||||
Tx NetworkUsage
|
||||
}
|
||||
|
||||
// DiskUsage provides detailed disk stats
|
||||
type DiskUsage struct {
|
||||
Bytes uint64 // total bytes for interval
|
||||
Kbps int64 // KiloBytesPerSecond for interval
|
||||
Op uint64 // Operation count for interval
|
||||
Ops int64 // Operations per second
|
||||
}
|
||||
|
||||
// VirtualDisk provides metrics for individual disks
|
||||
type VirtualDisk struct {
|
||||
Name string
|
||||
Write DiskUsage
|
||||
Read DiskUsage
|
||||
}
|
||||
|
||||
// VMMetrics encapsulates the available metrics
|
||||
type VMMetrics struct {
|
||||
CPU CPUMetrics
|
||||
Memory MemoryMetrics
|
||||
Networks []Network
|
||||
Disks []VirtualDisk
|
||||
SampleTime time.Time
|
||||
// interval of collection in seconds
|
||||
Interval int32
|
||||
}
|
||||
|
||||
// VMCollector is the VM metrics collector
|
||||
type VMCollector struct {
|
||||
perfMgr *performance.Manager
|
||||
session *session.Session
|
||||
|
||||
timer *time.Ticker
|
||||
stopper chan struct{}
|
||||
|
||||
// subscribers to streaming
|
||||
mu sync.RWMutex
|
||||
subs map[types.ManagedObjectReference]*vmSubscription
|
||||
}
|
||||
|
||||
// newVMCollector will instantiate a new collector responsible for
|
||||
// gathering VM metrics
|
||||
func NewVMCollector(session *session.Session) *VMCollector {
|
||||
return &VMCollector{
|
||||
subs: make(map[types.ManagedObjectReference]*vmSubscription),
|
||||
perfMgr: performance.NewManager(session.Vim25()),
|
||||
session: session,
|
||||
}
|
||||
}
|
||||
|
||||
// Start will begin the collection polling process
|
||||
func (vmc *VMCollector) Start() {
|
||||
// create timer with sampleInterval alignment
|
||||
vmc.timer = time.NewTicker(time.Duration(int64(sampleInterval)) * time.Second)
|
||||
//create the stopper channel
|
||||
vmc.stopper = make(chan struct{})
|
||||
// loop on the channel
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
collectorOp := trace.NewOperation(ctx, "VM metrics collector")
|
||||
go vmc.collect(collectorOp)
|
||||
for {
|
||||
select {
|
||||
case <-vmc.timer.C:
|
||||
// collect metrics for current subscribers
|
||||
vmc.collect(collectorOp)
|
||||
case <-vmc.stopper:
|
||||
// Ticker has been stopped, exit routine
|
||||
collectorOp.Debugf("VM metrics collector complete")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop will stop the collection polling process
|
||||
func (vmc *VMCollector) Stop() {
|
||||
vmc.timer.Stop()
|
||||
close(vmc.stopper)
|
||||
}
|
||||
|
||||
// collect will query vSphere for VM metrics and return to the subscribers
|
||||
func (vmc *VMCollector) collect(op trace.Operation) {
|
||||
|
||||
// gather the chunked morefs
|
||||
vmReferences := vmc.subscriberReferences(op)
|
||||
|
||||
// iterate over the chunked references and sample vSphere
|
||||
for i := range vmReferences {
|
||||
// create a new operation so we can effectively log and measure the sample
|
||||
sop := trace.NewOperation(op.Context, "sample operation")
|
||||
sop.Debugf("parentOp[%s] sample(%d/%d) with %d morefs", op.ID(), i+1, len(vmReferences), len(vmReferences[i]))
|
||||
go vmc.sample(sop, vmReferences[i])
|
||||
}
|
||||
}
|
||||
|
||||
// subscriberReferences will return a two dimensional array of VM managed object references. The
|
||||
// references are chunked based on the maxEntityQuery limit.
|
||||
func (vmc *VMCollector) subscriberReferences(op trace.Operation) [][]types.ManagedObjectReference {
|
||||
var mos []types.ManagedObjectReference
|
||||
var chunked [][]types.ManagedObjectReference
|
||||
|
||||
vmc.mu.Lock()
|
||||
defer vmc.mu.Unlock()
|
||||
op.Debugf("begin subscriberReferences: %d", len(vmc.subs))
|
||||
|
||||
// populate a two dimensional array chunked by the maxEntityQueryLimit
|
||||
for mo := range vmc.subs {
|
||||
mos = append(mos, mo)
|
||||
if len(mos) == maxEntityQuery {
|
||||
chunked = append(chunked, mos)
|
||||
mos = make([]types.ManagedObjectReference, 0, maxEntityQuery)
|
||||
}
|
||||
}
|
||||
|
||||
// the initial loop will potentially miss the "remainder" morefs
|
||||
if len(mos) > 0 && len(mos) < maxEntityQuery {
|
||||
chunked = append(chunked, mos)
|
||||
}
|
||||
|
||||
op.Debugf("end subscriberReferences: %d", len(chunked))
|
||||
return chunked
|
||||
}
|
||||
|
||||
// sample will query the vSphere performanceManager and publish the gather metrics
|
||||
func (vmc *VMCollector) sample(op trace.Operation, mos []types.ManagedObjectReference) {
|
||||
op.Debugf("begin sample for %d morefs", len(mos))
|
||||
defer op.Debugf("end sample for %d morefs", len(mos))
|
||||
|
||||
// vSphere counters we are currently interested in monitoring
|
||||
counters := []string{"cpu.usagemhz.average", "mem.active.average",
|
||||
"virtualDisk.write.average", "virtualDisk.read.average",
|
||||
"virtualDisk.numberReadAveraged.average", "virtualDisk.numberWriteAveraged.average",
|
||||
"net.bytesRx.average", "net.bytesTx.average",
|
||||
"net.droppedRx.summation", "net.droppedTx.summation",
|
||||
"net.packetsRx.summation", "net.packetsTx.summation"}
|
||||
|
||||
// create the spec
|
||||
spec := types.PerfQuerySpec{
|
||||
Format: string(types.PerfFormatNormal),
|
||||
MaxSample: sampleSize,
|
||||
IntervalId: sampleInterval,
|
||||
}
|
||||
|
||||
// retrieve sample based on counter names
|
||||
sample, err := vmc.perfMgr.SampleByName(op.Context, spec, counters, mos)
|
||||
if err != nil {
|
||||
op.Errorf("unable to get metric sample: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// convert to metrics
|
||||
result, err := vmc.perfMgr.ToMetricSeries(op.Context, sample)
|
||||
if err != nil {
|
||||
op.Errorf("unable to convert metric sample to metric series: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// iterate over results, convert to vic metrics and publish to subscribers
|
||||
for i := range result {
|
||||
met := result[i]
|
||||
sub, exists := vmc.subs[met.Entity]
|
||||
if !exists {
|
||||
// the subscription is no longer valid go to the next result
|
||||
continue
|
||||
}
|
||||
// convert the sample to a metric and publish
|
||||
for s := range met.SampleInfo {
|
||||
|
||||
metric := &VMMetrics{
|
||||
CPU: CPUMetrics{
|
||||
CPUs: []CPUUsage{},
|
||||
},
|
||||
Memory: MemoryMetrics{},
|
||||
SampleTime: met.SampleInfo[s].Timestamp,
|
||||
Interval: sampleInterval,
|
||||
}
|
||||
|
||||
// the series will have values for each sample
|
||||
for _, v := range met.Value {
|
||||
// skip the aggregate metric (empty string) and any negative values
|
||||
if v.Instance == "" && v.Name != "mem.active.average" || v.Value[s] < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v.Name {
|
||||
case "cpu.usagemhz.average":
|
||||
// convert cpu instance to int
|
||||
cpu, err := instanceID(op, sub.ID(), v.Instance)
|
||||
if err != nil {
|
||||
// skipping this instance
|
||||
continue
|
||||
}
|
||||
// specific vCPU metric
|
||||
vcpu := CPUUsage{
|
||||
ID: cpu,
|
||||
MhzUsage: v.Value[s],
|
||||
}
|
||||
metric.CPU.CPUs = append(metric.CPU.CPUs, vcpu)
|
||||
case "mem.active.average":
|
||||
metric.Memory.Active = v.Value[s]
|
||||
// DISK
|
||||
case "virtualDisk.read.average":
|
||||
disk := findDisk(metric, v.Instance)
|
||||
// perfManager returns kbps -- convert to sum of bytes
|
||||
sum := summation(v.Value[s]) * 1024
|
||||
metric.Disks[disk].Read.Bytes = sum
|
||||
metric.Disks[disk].Read.Kbps = v.Value[s]
|
||||
case "virtualDisk.write.average":
|
||||
disk := findDisk(metric, v.Instance)
|
||||
// perfManager returns kbps -- convert to sum of bytes
|
||||
sum := summation(v.Value[s]) * 1024
|
||||
metric.Disks[disk].Write.Bytes = sum
|
||||
metric.Disks[disk].Write.Kbps = v.Value[s]
|
||||
case "virtualDisk.numberReadAveraged.average":
|
||||
disk := findDisk(metric, v.Instance)
|
||||
// sum of iop read average
|
||||
metric.Disks[disk].Read.Op = summation(v.Value[s])
|
||||
metric.Disks[disk].Read.Ops = v.Value[s]
|
||||
case "virtualDisk.numberWriteAveraged.average":
|
||||
disk := findDisk(metric, v.Instance)
|
||||
// sum of iop write average
|
||||
metric.Disks[disk].Write.Op = summation(v.Value[s])
|
||||
metric.Disks[disk].Write.Ops = v.Value[s]
|
||||
// NET
|
||||
default:
|
||||
name := sub.DeviceName(v.Instance)
|
||||
// if we have a name gather the stats
|
||||
if name != "" {
|
||||
// get the network to update
|
||||
net := findNetwork(metric, name)
|
||||
switch v.Name {
|
||||
case "net.bytesRx.average":
|
||||
// perfManager returns kbps -- convert to sum of bytes
|
||||
sum := summation(v.Value[s]) * 1024
|
||||
metric.Networks[net].Rx.Bytes = sum
|
||||
metric.Networks[net].Rx.Kbps = v.Value[s]
|
||||
case "net.bytesTx.average":
|
||||
// perfManager returns kbps -- convert to sum of bytes
|
||||
sum := summation(v.Value[s]) * 1024
|
||||
metric.Networks[net].Tx.Bytes = sum
|
||||
metric.Networks[net].Tx.Kbps = v.Value[s]
|
||||
case "net.droppedRx.summation":
|
||||
metric.Networks[net].Rx.Dropped = v.Value[s]
|
||||
case "net.droppedTx.summation":
|
||||
metric.Networks[net].Tx.Dropped = v.Value[s]
|
||||
case "net.packetsRx.summation":
|
||||
metric.Networks[net].Rx.Packets = v.Value[s]
|
||||
case "net.packetsTx.summation":
|
||||
metric.Networks[net].Tx.Packets = v.Value[s]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sub.Publish(metric)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to a vm metric subscription
|
||||
func (vmc *VMCollector) Subscribe(op trace.Operation, moref types.ManagedObjectReference, id string) (chan interface{}, error) {
|
||||
vmc.mu.Lock()
|
||||
defer vmc.mu.Unlock()
|
||||
|
||||
// used at end of func
|
||||
subscriptionCount := len(vmc.subs)
|
||||
|
||||
// do we already have this subscription?
|
||||
_, exists := vmc.subs[moref]
|
||||
if !exists {
|
||||
op.Debugf("Creating new subscription(%s)", id)
|
||||
sub, err := newVMSubscription(op, vmc.session, moref, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vmc.subs[moref] = sub
|
||||
}
|
||||
|
||||
// get a subscriber channel
|
||||
ch := vmc.subs[moref].Channel()
|
||||
|
||||
// This is first subscription so start collection
|
||||
if subscriptionCount == 0 {
|
||||
vmc.Start()
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// Unsubscribe from a vm metric subscription. The subscriber channel will
|
||||
// be evicted and when no subscribers remain the subscription will be removed.
|
||||
func (vmc *VMCollector) Unsubscribe(op trace.Operation, moref types.ManagedObjectReference, ch chan interface{}) {
|
||||
vmc.mu.Lock()
|
||||
defer vmc.mu.Unlock()
|
||||
|
||||
sub, exists := vmc.subs[moref]
|
||||
if exists {
|
||||
// remove the communication channel
|
||||
sub.Evict(ch)
|
||||
// do we have any subscribers to this subscription
|
||||
if sub.Publishers() == 0 {
|
||||
op.Debugf("Deleting metric subscription(%s)", sub.ID())
|
||||
delete(vmc.subs, moref)
|
||||
}
|
||||
op.Debugf("Unsubscribed %s from metrics", sub.ID())
|
||||
}
|
||||
|
||||
// no subscriptions, so stop the collection
|
||||
if len(vmc.subs) == 0 {
|
||||
vmc.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// instanceID coverts the ID or Key of a metric from a string to int.
|
||||
func instanceID(op trace.Operation, subscriptionID string, instance string) (int, error) {
|
||||
converted, err := strconv.Atoi(instance)
|
||||
if err != nil {
|
||||
// I don't expect this to ever happen, but if it does log and don't publish
|
||||
op.Errorf("metrics failed to convert the subscription(%s) device id to an int - value(%#v): %s", subscriptionID, instance, err)
|
||||
return converted, err
|
||||
}
|
||||
return converted, nil
|
||||
}
|
||||
|
||||
// summation returns the product of the average * interval
|
||||
func summation(avg int64) uint64 {
|
||||
return uint64(avg) * uint64(sampleInterval)
|
||||
}
|
||||
|
||||
// findDisk will iterate over the Disks and return the DiskUsage ordinal position
|
||||
func findDisk(metric *VMMetrics, name string) int {
|
||||
// find by name
|
||||
for i := range metric.Disks {
|
||||
if metric.Disks[i].Name == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
// Let's create the disk
|
||||
d := VirtualDisk{
|
||||
Name: name,
|
||||
Read: DiskUsage{},
|
||||
Write: DiskUsage{},
|
||||
}
|
||||
metric.Disks = append(metric.Disks, d)
|
||||
|
||||
return len(metric.Disks) - 1
|
||||
}
|
||||
|
||||
// findNetwork will iterate over the Networks and return the NetworkUsage ordinal position
|
||||
func findNetwork(metric *VMMetrics, name string) int {
|
||||
// find by name
|
||||
for i := range metric.Networks {
|
||||
if metric.Networks[i].Name == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
// Let's create the disk
|
||||
net := Network{
|
||||
Name: name,
|
||||
Rx: NetworkUsage{},
|
||||
Tx: NetworkUsage{},
|
||||
}
|
||||
metric.Networks = append(metric.Networks, net)
|
||||
|
||||
return len(metric.Networks) - 1
|
||||
}
|
||||
186
vendor/github.com/vmware/vic/pkg/vsphere/performance/vm_subscription.go
generated
vendored
Normal file
186
vendor/github.com/vmware/vic/pkg/vsphere/performance/vm_subscription.go
generated
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package performance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/pkg/pubsub"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/vm"
|
||||
)
|
||||
|
||||
// vmSubscription is a 1:1 relationship to a Virtual Machine and a
|
||||
// 1:M relationship to subscribers
|
||||
type vmSubscription struct {
|
||||
vm *vm.VirtualMachine
|
||||
id string
|
||||
|
||||
pub *pubsub.Publisher
|
||||
devices object.VirtualDeviceList
|
||||
deviceInstanceToKey map[string]string
|
||||
|
||||
diskNames []string // container's virtualDisk names
|
||||
networkNames []string // container's network names
|
||||
}
|
||||
|
||||
// DeviceName will return the name associated with the metric instance id
|
||||
func (sub *vmSubscription) DeviceName(instance string) string {
|
||||
var name string
|
||||
|
||||
// did we previously find this device
|
||||
if name, exists := sub.deviceInstanceToKey[instance]; exists {
|
||||
return name
|
||||
}
|
||||
|
||||
// convert instance to key - we are expecting regular failures, so no logging
|
||||
// or returning of error
|
||||
key, err := strconv.Atoi(instance)
|
||||
if err != nil {
|
||||
// this is not a key, so return an empty string
|
||||
return name
|
||||
}
|
||||
|
||||
// find the device and get the name
|
||||
device := sub.devices.FindByKey(int32(key))
|
||||
if device != nil {
|
||||
// get the name
|
||||
name = sub.devices.Name(device)
|
||||
// populate map
|
||||
sub.deviceInstanceToKey[instance] = name
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// ID returns the subscription's id
|
||||
func (sub *vmSubscription) ID() string {
|
||||
return sub.id
|
||||
}
|
||||
|
||||
// DeviceList retrieves the VMs devices and builds slices of the disk and network
|
||||
// names
|
||||
func (sub *vmSubscription) DeviceList(op trace.Operation) error {
|
||||
list, err := sub.vm.Device(op.Context)
|
||||
if err != nil {
|
||||
op.Errorf("vm stats subscription(%s) unable to load devices: %s", sub.ID(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
// populate slice for disk and network names
|
||||
for i := range list {
|
||||
switch list.Type(list[i]) {
|
||||
case object.DeviceTypeDisk:
|
||||
// disk names are presented by the performanceManager based on their relationship to the
|
||||
// controller. So we need to create a disk name that aligns with the performance manager
|
||||
// naming pattern
|
||||
disk := list[i].GetVirtualDevice()
|
||||
switch c := list.FindByKey(disk.ControllerKey).(type) {
|
||||
case types.BaseVirtualSCSIController:
|
||||
sub.diskNames = append(sub.diskNames, fmt.Sprintf("%s%d:%d", "scsi", c.GetVirtualSCSIController().BusNumber, *disk.UnitNumber))
|
||||
}
|
||||
case object.DeviceTypeEthernet:
|
||||
sub.networkNames = append(sub.networkNames, list.Name(list[i]))
|
||||
}
|
||||
}
|
||||
|
||||
sub.devices = list
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disks returns an initialized slice of the containers VirtualDisks
|
||||
func (sub *vmSubscription) Disks() []VirtualDisk {
|
||||
var disks []VirtualDisk
|
||||
for i := range sub.diskNames {
|
||||
d := VirtualDisk{
|
||||
Name: sub.diskNames[i],
|
||||
Read: DiskUsage{},
|
||||
Write: DiskUsage{},
|
||||
}
|
||||
disks = append(disks, d)
|
||||
}
|
||||
return disks
|
||||
}
|
||||
|
||||
// Networks returns an initialized slice of the containers networks
|
||||
func (sub *vmSubscription) Networks() []Network {
|
||||
var networks []Network
|
||||
for i := range sub.networkNames {
|
||||
n := Network{
|
||||
Name: sub.networkNames[i],
|
||||
Rx: NetworkUsage{},
|
||||
Tx: NetworkUsage{},
|
||||
}
|
||||
networks = append(networks, n)
|
||||
}
|
||||
return networks
|
||||
}
|
||||
|
||||
// Publish sends the metric to all channels subscribed
|
||||
func (sub *vmSubscription) Publish(metric *VMMetrics) {
|
||||
// if no disk / network reported then add the defaults
|
||||
if len(metric.Disks) == 0 {
|
||||
metric.Disks = sub.Disks()
|
||||
}
|
||||
if len(metric.Networks) == 0 {
|
||||
metric.Networks = sub.Networks()
|
||||
}
|
||||
sub.pub.Publish(metric)
|
||||
}
|
||||
|
||||
// Publishers returns the number of channels subscribed to the container
|
||||
func (sub *vmSubscription) Publishers() int {
|
||||
return sub.pub.Len()
|
||||
}
|
||||
|
||||
// Channel provides the communication chan for metrics subscriptions
|
||||
func (sub *vmSubscription) Channel() chan interface{} {
|
||||
return sub.pub.Subscribe()
|
||||
}
|
||||
|
||||
// Evict will remove the channel from the publisher ending the subscription
|
||||
func (sub *vmSubscription) Evict(ch chan interface{}) {
|
||||
sub.pub.Evict(ch)
|
||||
}
|
||||
|
||||
// newVMSubscription is a helper func to convert the interface to a subscription
|
||||
func newVMSubscription(op trace.Operation, session *session.Session, moref types.ManagedObjectReference, id string) (*vmSubscription, error) {
|
||||
// ensure we have a valid moRef..we won't worry about inspecting the details
|
||||
if moref.String() == "" {
|
||||
err := fmt.Errorf("no vm associated with new stats subscription: %s", id)
|
||||
op.Errorf("%s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sub := &vmSubscription{
|
||||
vm: vm.NewVirtualMachine(op.Context, session, moref),
|
||||
deviceInstanceToKey: make(map[string]string),
|
||||
}
|
||||
|
||||
err := sub.DeviceList(op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create the publisher
|
||||
sub.pub = pubsub.NewPublisher(100*time.Millisecond, 0)
|
||||
return sub, nil
|
||||
}
|
||||
423
vendor/github.com/vmware/vic/pkg/vsphere/rbac/rbac.go
generated
vendored
Normal file
423
vendor/github.com/vmware/vic/pkg/vsphere/rbac/rbac.go
generated
vendored
Normal file
@@ -0,0 +1,423 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/vim25"
|
||||
"github.com/vmware/govmomi/vim25/methods"
|
||||
"github.com/vmware/govmomi/vim25/soap"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
|
||||
"github.com/vmware/vic/pkg/errors"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
VCenter = iota
|
||||
DatacenterReadOnly
|
||||
Datacenter
|
||||
Cluster
|
||||
DatastoreFolder
|
||||
Datastore
|
||||
VSANDatastore
|
||||
Network
|
||||
Endpoint
|
||||
)
|
||||
|
||||
type NameToRef map[string]types.ManagedObjectReference
|
||||
|
||||
type AuthzManager struct {
|
||||
authzManager *object.AuthorizationManager
|
||||
client *vim25.Client
|
||||
resources map[int8]*Resource
|
||||
TargetRoles []types.AuthorizationRole
|
||||
RolePrefix string
|
||||
Principal string
|
||||
Config *Config
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
Type int8
|
||||
Propagate bool
|
||||
Role types.AuthorizationRole
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Resources []Resource
|
||||
}
|
||||
|
||||
type PermissionList []types.Permission
|
||||
|
||||
type ResourcePermission struct {
|
||||
RType int8
|
||||
Reference types.ManagedObjectReference
|
||||
Permission types.Permission
|
||||
}
|
||||
|
||||
func NewAuthzManager(ctx context.Context, client *vim25.Client) *AuthzManager {
|
||||
authManager := object.NewAuthorizationManager(client)
|
||||
mgr := &AuthzManager{
|
||||
client: client,
|
||||
authzManager: authManager,
|
||||
}
|
||||
return mgr
|
||||
}
|
||||
|
||||
func (am *AuthzManager) InitConfig(principal string, rolePrefix string, config *Config) {
|
||||
am.Principal = principal
|
||||
am.RolePrefix = rolePrefix
|
||||
am.Config = config
|
||||
am.initTargetRoles()
|
||||
am.initResourceMap()
|
||||
}
|
||||
|
||||
func (am *AuthzManager) CreateRoles(ctx context.Context) (int, error) {
|
||||
return am.createOrRepairRoles(ctx)
|
||||
}
|
||||
|
||||
func (am *AuthzManager) DeleteRoles(ctx context.Context) (int, error) {
|
||||
return am.deleteRoles(ctx)
|
||||
}
|
||||
|
||||
func (am *AuthzManager) RoleList(ctx context.Context) (object.AuthorizationRoleList, error) {
|
||||
return am.getRoleList(ctx)
|
||||
}
|
||||
|
||||
func (am *AuthzManager) IsPrincipalAnAdministrator(ctx context.Context) (bool, error) {
|
||||
// Check if the principal belongs to the Administrators group
|
||||
res, err := am.PrincipalBelongsToGroup(ctx, "Administrators")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if res {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Check if the principal has an Admin Role
|
||||
res, err = am.PrincipalHasRole(ctx, "Admin")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (am *AuthzManager) PrincipalBelongsToGroup(ctx context.Context, group string) (bool, error) {
|
||||
op := trace.FromContext(ctx, "PrincipalBelongsToGroup")
|
||||
|
||||
ref := *am.client.ServiceContent.UserDirectory
|
||||
|
||||
components := strings.Split(am.Principal, "@")
|
||||
var domain string
|
||||
name := components[0]
|
||||
if len(components) < 2 {
|
||||
domain = ""
|
||||
} else {
|
||||
domain = components[1]
|
||||
}
|
||||
|
||||
req := types.RetrieveUserGroups{
|
||||
This: ref,
|
||||
Domain: domain,
|
||||
SearchStr: name,
|
||||
ExactMatch: true,
|
||||
BelongsToGroup: group,
|
||||
FindUsers: true,
|
||||
FindGroups: false,
|
||||
}
|
||||
|
||||
results, err := methods.RetrieveUserGroups(ctx, am.client, &req)
|
||||
|
||||
// This is to work around a bug in vSphere, when AD is added to
|
||||
// the identity source list, the API returns Object Not Found,
|
||||
// In this case, we ignore the error and return false (BUG: 2037706)
|
||||
if err != nil && (isNotSupportedError(ctx, err) || isNotFoundError(ctx, err)) {
|
||||
op.Debugf("Received Error (%s) from PrincipalBelongsToGroup(), could not verify user %s is not a member of the Administrators group", err.Error(), am.Principal)
|
||||
op.Warnf("If ops-user (%s) belongs to the Administrators group, permissions on some resources might have been restricted", am.Principal)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
op.Debugf("Error from PrincipalBelongsToGroup: %s", err.Error())
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(results.Returnval) > 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (am *AuthzManager) PrincipalHasRole(ctx context.Context, roleName string) (bool, error) {
|
||||
// Build expected representation of the ops-user
|
||||
principal := strings.ToLower(am.Principal)
|
||||
|
||||
// Get role id for admin Role
|
||||
roleList, err := am.RoleList(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
role := roleList.ByName(roleName)
|
||||
|
||||
allPerms, err := am.authzManager.RetrieveAllPermissions(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, perm := range allPerms {
|
||||
if perm.RoleId != role.RoleId {
|
||||
continue
|
||||
}
|
||||
|
||||
fPrincipal := am.formatPrincipal(perm.Principal)
|
||||
if fPrincipal == principal {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (am *AuthzManager) GetPermissions(ctx context.Context,
|
||||
ref types.ManagedObjectReference) ([]types.Permission, error) {
|
||||
// Get current Permissions
|
||||
return am.authzManager.RetrieveEntityPermissions(ctx, ref, false)
|
||||
}
|
||||
|
||||
func (am *AuthzManager) AddPermission(ctx context.Context, ref types.ManagedObjectReference, resourceType int8, isGroup bool) (*ResourcePermission, error) {
|
||||
|
||||
resource := am.getResource(resourceType)
|
||||
if resource == nil {
|
||||
return nil, fmt.Errorf("cannot find resource of type %d", resourceType)
|
||||
}
|
||||
|
||||
// Collect the new roles, possibly cache the result in the Authz manager
|
||||
roleList, err := am.getRoleList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Locate target role
|
||||
role := roleList.ByName(am.getRoleName(resource))
|
||||
if role == nil {
|
||||
return nil, fmt.Errorf("cannot find role: %s", resource.Role.Name)
|
||||
}
|
||||
|
||||
// Get current Permissions
|
||||
permissions, err := am.authzManager.RetrieveEntityPermissions(ctx, ref, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, permission := range permissions {
|
||||
if permission.Principal == am.Principal &&
|
||||
permission.RoleId == role.RoleId &&
|
||||
permission.Propagate == resource.Propagate {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// No match found, create new permission
|
||||
permission := types.Permission{
|
||||
Principal: am.Principal,
|
||||
RoleId: role.RoleId,
|
||||
Propagate: resource.Propagate,
|
||||
Group: isGroup,
|
||||
}
|
||||
|
||||
permissions = append(permissions, permission)
|
||||
|
||||
if err = am.authzManager.SetEntityPermissions(ctx, ref, permissions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resourcePermission := &ResourcePermission{
|
||||
Permission: permission,
|
||||
Reference: ref,
|
||||
RType: resourceType,
|
||||
}
|
||||
|
||||
return resourcePermission, nil
|
||||
}
|
||||
|
||||
func (am *AuthzManager) createOrRepairRoles(ctx context.Context) (int, error) {
|
||||
// Get all the existing roles
|
||||
mgr := am.authzManager
|
||||
roleList, err := mgr.RoleList(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var count int
|
||||
for _, targetRole := range am.TargetRoles {
|
||||
foundRole := roleList.ByName(targetRole.Name)
|
||||
if foundRole != nil {
|
||||
isMod, err := am.checkAndRepairRole(ctx, &targetRole, foundRole)
|
||||
if isMod && err == nil {
|
||||
count++
|
||||
}
|
||||
} else {
|
||||
_, err = mgr.AddRole(ctx, targetRole.Name, targetRole.Privilege)
|
||||
if err == nil {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (am *AuthzManager) deleteRoles(ctx context.Context) (int, error) {
|
||||
mgr := am.authzManager
|
||||
// Get all the existing roles
|
||||
roleList, err := mgr.RoleList(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var count int
|
||||
for _, targetRole := range am.TargetRoles {
|
||||
foundRole := roleList.ByName(targetRole.Name)
|
||||
if foundRole != nil {
|
||||
err = mgr.RemoveRole(ctx, foundRole.RoleId, true)
|
||||
if err == nil {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (am *AuthzManager) getRoleList(ctx context.Context) (object.AuthorizationRoleList, error) {
|
||||
return am.authzManager.RoleList(ctx)
|
||||
}
|
||||
|
||||
func (am *AuthzManager) checkAndRepairRole(ctx context.Context, tRole *types.AuthorizationRole, fRole *types.AuthorizationRole) (bool, error) {
|
||||
mgr := am.authzManager
|
||||
// Check that the privileges list in Target Role is a subset of the list in Found role
|
||||
fSet := make(map[string]bool)
|
||||
for _, p := range fRole.Privilege {
|
||||
fSet[p] = true
|
||||
}
|
||||
|
||||
var isModified bool
|
||||
for _, p := range tRole.Privilege {
|
||||
if _, found := fSet[p]; !found {
|
||||
// Privilege not found
|
||||
// Add it to the found Role
|
||||
fRole.Privilege = append(fRole.Privilege, p)
|
||||
isModified = true
|
||||
}
|
||||
}
|
||||
|
||||
if !isModified {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Not a subset need to call go-vmomi to set the new privileges
|
||||
err := mgr.UpdateRole(ctx, fRole.RoleId, fRole.Name, fRole.Privilege)
|
||||
|
||||
return true, err
|
||||
}
|
||||
|
||||
func (am *AuthzManager) initTargetRoles() {
|
||||
count := len(am.Config.Resources)
|
||||
roles := make([]types.AuthorizationRole, 0, count)
|
||||
dSet := make(map[string]bool)
|
||||
for index, resource := range am.Config.Resources {
|
||||
name := am.getRoleName(&am.Config.Resources[index])
|
||||
// Discard duplicates
|
||||
if _, found := dSet[name]; !found {
|
||||
role := new(types.AuthorizationRole)
|
||||
*role = resource.Role
|
||||
role.Name = name
|
||||
dSet[name] = true
|
||||
roles = append(roles, *role)
|
||||
}
|
||||
}
|
||||
am.TargetRoles = roles
|
||||
}
|
||||
|
||||
func (am *AuthzManager) initResourceMap() {
|
||||
am.resources = make(map[int8]*Resource)
|
||||
for i, resource := range am.Config.Resources {
|
||||
am.resources[resource.Type] = &am.Config.Resources[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (am *AuthzManager) getResource(resourceType int8) *Resource {
|
||||
resource, ok := am.resources[resourceType]
|
||||
if !ok {
|
||||
panic(errors.Errorf("Cannot find RBAC resource type: %d", resourceType))
|
||||
}
|
||||
return resource
|
||||
}
|
||||
|
||||
func (am *AuthzManager) formatPrincipal(principal string) string {
|
||||
components := strings.Split(principal, "\\")
|
||||
if len(components) != 2 {
|
||||
return strings.ToLower(principal)
|
||||
}
|
||||
ret := strings.ToLower(components[1]) + "@" + strings.ToLower(components[0])
|
||||
return ret
|
||||
}
|
||||
|
||||
func (am *AuthzManager) getRoleName(resource *Resource) string {
|
||||
switch resource.Type {
|
||||
case DatacenterReadOnly:
|
||||
return resource.Role.Name
|
||||
default:
|
||||
return am.RolePrefix + resource.Role.Name
|
||||
}
|
||||
}
|
||||
|
||||
func isNotSupportedError(ctx context.Context, err error) bool {
|
||||
op := trace.FromContext(ctx, "isNotSupportedError")
|
||||
|
||||
if soap.IsSoapFault(err) {
|
||||
vimFault := soap.ToSoapFault(err).VimFault()
|
||||
op.Debugf("Error type: %s", reflect.TypeOf(vimFault))
|
||||
|
||||
_, ok := soap.ToSoapFault(err).VimFault().(types.NotSupported)
|
||||
return ok
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isNotFoundError(ctx context.Context, err error) bool {
|
||||
op := trace.FromContext(ctx, "isNotFoundError")
|
||||
|
||||
if soap.IsSoapFault(err) {
|
||||
vimFault := soap.ToSoapFault(err).VimFault()
|
||||
op.Debugf("Error type: %s", reflect.TypeOf(vimFault))
|
||||
|
||||
_, ok := soap.ToSoapFault(err).VimFault().(types.NotFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
240
vendor/github.com/vmware/vic/pkg/vsphere/rbac/rbac_test.go
generated
vendored
Normal file
240
vendor/github.com/vmware/vic/pkg/vsphere/rbac/rbac_test.go
generated
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/vmware/govmomi/simulator"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/test/env"
|
||||
)
|
||||
|
||||
var Role1 = types.AuthorizationRole{
|
||||
Name: "vcenter",
|
||||
Privilege: []string{
|
||||
"Datastore.Config",
|
||||
},
|
||||
}
|
||||
|
||||
var Role2 = types.AuthorizationRole{
|
||||
Name: "datacenter",
|
||||
Privilege: []string{
|
||||
"Datastore.Config",
|
||||
"Datastore.FileManagement",
|
||||
"VirtualMachine.Config.AddNewDisk",
|
||||
"VirtualMachine.Config.AdvancedConfig",
|
||||
"VirtualMachine.Config.RemoveDisk",
|
||||
"VirtualMachine.Inventory.Create",
|
||||
"VirtualMachine.Inventory.Delete",
|
||||
},
|
||||
}
|
||||
|
||||
var Role3 = types.AuthorizationRole{
|
||||
Name: "cluster",
|
||||
Privilege: []string{
|
||||
"Datastore.AllocateSpace",
|
||||
"Datastore.Browse",
|
||||
"Datastore.Config",
|
||||
"Datastore.DeleteFile",
|
||||
"Datastore.FileManagement",
|
||||
"Host.Config.SystemManagement",
|
||||
},
|
||||
}
|
||||
|
||||
// Configuration for the ops-user
|
||||
var testRBACConfig = Config{
|
||||
Resources: []Resource{
|
||||
{
|
||||
Type: VCenter,
|
||||
Propagate: false,
|
||||
Role: Role1,
|
||||
},
|
||||
{
|
||||
Type: Datacenter,
|
||||
Propagate: true,
|
||||
Role: Role2,
|
||||
},
|
||||
{
|
||||
Type: Cluster,
|
||||
Propagate: true,
|
||||
Role: Role3,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var testRolePrefix = "test-role-prefix"
|
||||
var testUser = "test-user"
|
||||
|
||||
func TestRolesSimulatorVPX(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
m := simulator.VPX()
|
||||
defer m.Remove()
|
||||
|
||||
err := m.Create()
|
||||
require.NoError(t, err, "Cannot create VPX Simulator")
|
||||
|
||||
s := m.Service.NewServer()
|
||||
defer s.Close()
|
||||
|
||||
config := &session.Config{
|
||||
Service: s.URL.String(),
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(config).Connect(ctx)
|
||||
require.NoError(t, err, "Cannot connect to VPX Simulator")
|
||||
|
||||
am := NewAuthzManager(ctx, sess.Vim25())
|
||||
am.InitConfig(testUser, testRolePrefix, &testRBACConfig)
|
||||
|
||||
var testRoleNames = []string{
|
||||
"datacenter",
|
||||
"cluster",
|
||||
}
|
||||
|
||||
var testRolePrivileges = []string{
|
||||
"VirtualMachine.Config.AddNewDisk",
|
||||
"Host.Config.SystemManagement",
|
||||
}
|
||||
|
||||
DoTestRoles(ctx, t, am, testRoleNames, testRolePrivileges)
|
||||
}
|
||||
|
||||
func TestRolesVCenter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
config := &session.Config{
|
||||
Service: env.URL(t),
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(config).Connect(ctx)
|
||||
if err != nil {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
am := NewAuthzManager(ctx, sess.Vim25())
|
||||
am.InitConfig(testUser, testRolePrefix, &testRBACConfig)
|
||||
|
||||
var testRoleNames = []string{
|
||||
"datacenter",
|
||||
"cluster",
|
||||
}
|
||||
|
||||
var testRolePrivileges = []string{
|
||||
"VirtualMachine.Config.AddNewDisk",
|
||||
"Host.Config.SystemManagement",
|
||||
}
|
||||
|
||||
DoTestRoles(ctx, t, am, testRoleNames, testRolePrivileges)
|
||||
}
|
||||
|
||||
func TestAdminSimulatorVPX(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
m := simulator.VPX()
|
||||
defer m.Remove()
|
||||
|
||||
err := m.Create()
|
||||
require.NoError(t, err, "Cannot create VPX Simulator")
|
||||
|
||||
s := m.Service.NewServer()
|
||||
defer s.Close()
|
||||
|
||||
config := &session.Config{
|
||||
Service: s.URL.String(),
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(config).Connect(ctx)
|
||||
require.NoError(t, err, "Cannot connect to VPX Simulator")
|
||||
|
||||
am := NewAuthzManager(ctx, sess.Vim25())
|
||||
am.InitConfig("admin", "test-role-prefix", &testRBACConfig)
|
||||
|
||||
// Unfortunately the Sim does not have support for looking up group membership
|
||||
// therefore we can only test the presence of the Admin role
|
||||
|
||||
res, err := am.PrincipalHasRole(ctx, "Admin")
|
||||
require.NoError(t, err, "Failed to verify Admin Privileges")
|
||||
require.True(t, res, "User Administrator@vsphere.local should have an Admin role")
|
||||
|
||||
// Negative test, principal does not have that role
|
||||
res, err = am.PrincipalHasRole(ctx, "NoAccess")
|
||||
require.NoError(t, err, "Failed to verify Admin Privileges")
|
||||
require.False(t, res, "User Administrator@vsphere.local should have an NoAccess role")
|
||||
|
||||
// Check regular user
|
||||
am.Principal = "nouser@vshpere.local"
|
||||
res, err = am.PrincipalHasRole(ctx, "Admin")
|
||||
require.NoError(t, err, "Failed to verify Admin Privileges")
|
||||
require.False(t, res, "User nouser@vsphere.local should not have an Admin role")
|
||||
}
|
||||
|
||||
func TestAdminVCenter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
config := &session.Config{
|
||||
Service: env.URL(t),
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(config).Connect(ctx)
|
||||
if err != nil {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
am := NewAuthzManager(ctx, sess.Vim25())
|
||||
am.InitConfig("Administrator@vsphere.local", "test-role-prefix", &testRBACConfig)
|
||||
|
||||
res, err := am.PrincipalBelongsToGroup(ctx, "Administrators")
|
||||
require.NoError(t, err, "Failed to verify Admin Privileges")
|
||||
require.True(t, res, "User Administrator@vsphere.local should be a member of Administrators")
|
||||
|
||||
res, err = am.PrincipalHasRole(ctx, "Admin")
|
||||
require.NoError(t, err, "Failed to verify Admin Privileges")
|
||||
require.True(t, res, "User Administrator@vsphere.local should have an Admin role")
|
||||
|
||||
// Negative test, principal does not belong
|
||||
res, err = am.PrincipalBelongsToGroup(ctx, "TestUsers")
|
||||
require.NoError(t, err, "Failed to verify Admin Privileges")
|
||||
require.False(t, res, "User Administrator@vsphere.local should not be a member of TestUsers")
|
||||
|
||||
// Negative test, principal does not have that role
|
||||
res, err = am.PrincipalHasRole(ctx, "NoAccess")
|
||||
require.NoError(t, err, "Failed to verify Admin Privileges")
|
||||
require.False(t, res, "User Administrator@vsphere.local should have an NoAccess role")
|
||||
|
||||
// Check regular user
|
||||
am.Principal = "nouser@vshpere.local"
|
||||
res, err = am.PrincipalHasRole(ctx, "Admin")
|
||||
require.NoError(t, err, "Failed to verify Admin Privileges")
|
||||
require.False(t, res, "User nouser@vsphere.local should not have an Admin role")
|
||||
|
||||
// Check regular user
|
||||
am.Principal = "nouser"
|
||||
res, err = am.PrincipalHasRole(ctx, "Admin")
|
||||
require.NoError(t, err, "Failed to verify Admin Privileges")
|
||||
require.False(t, res, "User nouser@vsphere.local should not have an Admin role")
|
||||
}
|
||||
120
vendor/github.com/vmware/vic/pkg/vsphere/rbac/rbac_test_util.go
generated
vendored
Normal file
120
vendor/github.com/vmware/vic/pkg/vsphere/rbac/rbac_test_util.go
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
)
|
||||
|
||||
func DoTestRoles(ctx context.Context, t *testing.T, am *AuthzManager, testRoleNames []string, testRolePrivileges []string) {
|
||||
var roleCount = len(am.TargetRoles)
|
||||
count := InitRoles(ctx, t, am)
|
||||
|
||||
defer Cleanup(ctx, t, am, true)
|
||||
require.Equal(t, roleCount, count, "Incorrect number of roles: expected %d, actual %d", roleCount, count)
|
||||
|
||||
// Test correct role validation, it should return 0
|
||||
roleCount = 0
|
||||
count, err := am.createOrRepairRoles(ctx)
|
||||
require.NoError(t, err, "Failed to create roles")
|
||||
require.Equal(t, roleCount, count, "Incorrect number of roles: expected %d, actual %d", roleCount, count)
|
||||
|
||||
// Remove two Privileges from two roles
|
||||
roles, err := am.getRoleList(ctx)
|
||||
fmt.Println(err)
|
||||
fmt.Println(roles)
|
||||
|
||||
for i, name := range testRoleNames {
|
||||
testRoleNames[i] = am.RolePrefix + name
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if role.Name == testRoleNames[0] {
|
||||
removePrivilege(&role, testRolePrivileges[0])
|
||||
am.authzManager.UpdateRole(ctx, role.RoleId, role.Name, role.Privilege)
|
||||
}
|
||||
if role.Name == testRoleNames[1] {
|
||||
removePrivilege(&role, testRolePrivileges[1])
|
||||
am.authzManager.UpdateRole(ctx, role.RoleId, role.Name, role.Privilege)
|
||||
}
|
||||
}
|
||||
|
||||
// Test
|
||||
roleCount = 2
|
||||
count, err = am.createOrRepairRoles(ctx)
|
||||
require.NoError(t, err, "Failed to repair roles 1")
|
||||
require.Equal(t, roleCount, count, "Incorrect number of roles: expected %d, actual %d", roleCount, count)
|
||||
|
||||
// Test correct role validation, it should return 0
|
||||
roleCount = 0
|
||||
count, err = am.createOrRepairRoles(ctx)
|
||||
require.NoError(t, err, "Failed to repair roles 2")
|
||||
require.Equal(t, roleCount, count, "Incorrect number of roles: expected %d, actual %d", roleCount, count)
|
||||
}
|
||||
|
||||
func VerifyResourcePermissions(ctx context.Context, t *testing.T, am *AuthzManager, retPerms []ResourcePermission) {
|
||||
for _, retPerm := range retPerms {
|
||||
|
||||
// Validate returned permission against the configured permission
|
||||
configPerm := am.getResource(retPerm.RType)
|
||||
require.Equal(t, am.Principal, retPerm.Permission.Principal)
|
||||
require.Equal(t, configPerm.Propagate, retPerm.Permission.Propagate)
|
||||
|
||||
actPerms, err := am.GetPermissions(ctx, retPerm.Reference)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, actPerm := range actPerms {
|
||||
if actPerm.Principal != am.Principal {
|
||||
continue
|
||||
}
|
||||
// RoleId must be the same
|
||||
require.Equal(t, retPerm.Permission.RoleId, actPerm.RoleId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func InitRoles(ctx context.Context, t *testing.T, am *AuthzManager) int {
|
||||
Cleanup(ctx, t, am, false)
|
||||
|
||||
count, err := am.createOrRepairRoles(ctx)
|
||||
require.NoError(t, err, "Failed to initialize Roles")
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func Cleanup(ctx context.Context, t *testing.T, am *AuthzManager, checkCount bool) {
|
||||
var roleCount = len(am.TargetRoles)
|
||||
count, err := am.deleteRoles(ctx)
|
||||
require.NoError(t, err, "Failed to delete roles")
|
||||
|
||||
if checkCount && count != roleCount {
|
||||
t.Fatalf("Incorrect number of roles: expcted %d, actual %d", roleCount, count)
|
||||
}
|
||||
}
|
||||
|
||||
func removePrivilege(role *types.AuthorizationRole, privilege string) {
|
||||
for i, priv := range role.Privilege {
|
||||
if priv == privilege {
|
||||
role.Privilege = append(role.Privilege[:i], role.Privilege[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
47
vendor/github.com/vmware/vic/pkg/vsphere/session/errors.go
generated
vendored
Normal file
47
vendor/github.com/vmware/vic/pkg/vsphere/session/errors.go
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package session
|
||||
|
||||
import "fmt"
|
||||
|
||||
// SDKURLError is returned when the soap SDK URL cannot be parsed
|
||||
type SDKURLError struct {
|
||||
Service string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e SDKURLError) Error() string {
|
||||
return fmt.Sprintf("SDK URL (%s) could not be parsed: %s", e.Service, e.Err)
|
||||
}
|
||||
|
||||
// SoapClientError is returned when we're unable to obtain a vim client
|
||||
type SoapClientError struct {
|
||||
Host string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e SoapClientError) Error() string {
|
||||
return fmt.Sprintf("Failed to connect to %s: %s", e.Host, e.Err)
|
||||
}
|
||||
|
||||
// UserPassLoginError is returned when login via username/password is unsuccessful
|
||||
type UserPassLoginError struct {
|
||||
Host string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e UserPassLoginError) Error() string {
|
||||
return fmt.Sprintf("Failed to log in to %s: %s", e.Host, e.Err)
|
||||
}
|
||||
397
vendor/github.com/vmware/vic/pkg/vsphere/session/session.go
generated
vendored
Normal file
397
vendor/github.com/vmware/vic/pkg/vsphere/session/session.go
generated
vendored
Normal file
@@ -0,0 +1,397 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package session caches vSphere objects to avoid having to repeatedly
|
||||
// make govmomi client calls.
|
||||
//
|
||||
// To obtain a Session, call Create with a Config. The config
|
||||
// contains the SDK URL (Service) and the desired vSphere resources.
|
||||
// Create then connects to Service and stores govmomi objects for
|
||||
// each corresponding value in Config. The Session is returned and
|
||||
// the user can use the cached govmomi objects in the exported fields of
|
||||
// Session instead of directly using a govmomi Client.
|
||||
//
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
|
||||
"github.com/vmware/govmomi"
|
||||
"github.com/vmware/govmomi/find"
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/session"
|
||||
"github.com/vmware/govmomi/vim25"
|
||||
"github.com/vmware/govmomi/vim25/methods"
|
||||
"github.com/vmware/govmomi/vim25/soap"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/lib/config"
|
||||
"github.com/vmware/vic/pkg/errors"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/extraconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMaxInFlight = 32
|
||||
tlsHandshakeTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// Config contains the configuration used to create a Session.
|
||||
type Config struct {
|
||||
// SDK URL or proxy
|
||||
Service string
|
||||
// Credentials
|
||||
User *url.Userinfo
|
||||
// CloneTicket is used to clone an existing session
|
||||
CloneTicket string
|
||||
// Allow insecure connection to Service
|
||||
Insecure bool
|
||||
// Target thumbprint
|
||||
Thumbprint string
|
||||
// Keep alive duration
|
||||
Keepalive time.Duration
|
||||
// User-Agent to identify login sessions (see: govc session.ls)
|
||||
UserAgent string
|
||||
|
||||
ClusterPath string
|
||||
DatacenterPath string
|
||||
DatastorePath string
|
||||
HostPath string
|
||||
PoolPath string
|
||||
}
|
||||
|
||||
// Session caches vSphere objects obtained by querying the SDK.
|
||||
type Session struct {
|
||||
*govmomi.Client
|
||||
|
||||
*Config
|
||||
|
||||
Cluster *object.ComputeResource
|
||||
Datacenter *object.Datacenter
|
||||
Datastore *object.Datastore
|
||||
Host *object.HostSystem
|
||||
Pool *object.ResourcePool
|
||||
|
||||
VMFolder *object.Folder
|
||||
|
||||
Finder *find.Finder
|
||||
}
|
||||
|
||||
// RoundTripFunc alias
|
||||
type RoundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
// RoundTrip method
|
||||
func (rt RoundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return rt(r)
|
||||
}
|
||||
|
||||
// LimitConcurrency limits how many requests can be processed at once
|
||||
func LimitConcurrency(rt http.RoundTripper, limit int) http.RoundTripper {
|
||||
limiter := make(chan struct{}, limit)
|
||||
|
||||
return RoundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
// reserve a slot
|
||||
limiter <- struct{}{}
|
||||
|
||||
// free the slot
|
||||
defer func() {
|
||||
<-limiter
|
||||
}()
|
||||
|
||||
// use the given round tripper
|
||||
return rt.RoundTrip(r)
|
||||
})
|
||||
}
|
||||
|
||||
// NewSession creates a new Session struct. If config is nil,
|
||||
// it creates a Flags object from the command line arguments or
|
||||
// environment, and uses that instead to create a Session.
|
||||
func NewSession(config *Config) *Session {
|
||||
return &Session{Config: config}
|
||||
}
|
||||
|
||||
// Vim25 returns the vim25.Client to the caller
|
||||
func (s *Session) Vim25() *vim25.Client {
|
||||
return s.Client.Client
|
||||
}
|
||||
|
||||
// IsVC returns whether the session is backed by VC
|
||||
func (s *Session) IsVC() bool {
|
||||
return s.Client.IsVC()
|
||||
}
|
||||
|
||||
// IsVSAN returns whether the datastore used in the session is backed by VSAN
|
||||
func (s *Session) IsVSAN(ctx context.Context) bool {
|
||||
// #nosec: Errors unhandled.
|
||||
dsType, _ := s.Datastore.Type(ctx)
|
||||
|
||||
return dsType == types.HostFileSystemVolumeFileSystemTypeVsan
|
||||
}
|
||||
|
||||
// Create accepts a Config and returns a Session with the cached vSphere resources.
|
||||
func (s *Session) Create(ctx context.Context) (*Session, error) {
|
||||
op := trace.FromContext(ctx, "Create")
|
||||
|
||||
var vchConfig config.VirtualContainerHostConfigSpec
|
||||
var connConfig config.Connection
|
||||
|
||||
source, err := extraconfig.GuestInfoSource()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prefix := extraconfig.CalculateKeys(vchConfig, "Connection", "")
|
||||
if len(prefix) != 1 {
|
||||
return nil, fmt.Errorf("must be exactly one Connection defined in VCH configuration")
|
||||
}
|
||||
extraconfig.DecodeWithPrefix(source, &connConfig, prefix[0])
|
||||
|
||||
s.Service = connConfig.Target
|
||||
s.User = url.UserPassword(connConfig.Username, connConfig.Token)
|
||||
s.Thumbprint = connConfig.TargetThumbprint
|
||||
|
||||
_, err = s.Connect(op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// we're treating this as an atomic behaviour, so log out if we failed
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// #nosec: Errors unhandled.
|
||||
s.Client.Logout(op)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = s.Populate(op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Connect establishes the connection for the session but nothing more
|
||||
func (s *Session) Connect(ctx context.Context) (*Session, error) {
|
||||
op := trace.FromContext(ctx, "Connect")
|
||||
|
||||
op.Debugf("Creating VMOMI session with thumbprint %s", s.Thumbprint)
|
||||
|
||||
soapURL, err := soap.ParseURL(s.Service)
|
||||
if soapURL == nil || err != nil {
|
||||
return nil, SDKURLError{
|
||||
Service: s.Service,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Update the service URL with expanded defaults
|
||||
s.Service = soapURL.String()
|
||||
|
||||
// VCH components do not include credentials within the target URL
|
||||
if s.User != nil {
|
||||
soapURL.User = s.User
|
||||
}
|
||||
|
||||
soapClient := soap.NewClient(soapURL, s.Insecure)
|
||||
|
||||
soapClient.Version = "6.0" // Pin to 6.0 until we need 6.5+ specific API
|
||||
|
||||
var login func(context.Context) error
|
||||
|
||||
login = func(ctx context.Context) error {
|
||||
return s.Client.Login(ctx, soapURL.User)
|
||||
}
|
||||
|
||||
soapClient.UserAgent = s.UserAgent
|
||||
|
||||
soapClient.SetThumbprint(soapURL.Host, s.Thumbprint)
|
||||
|
||||
maxInFlight := defaultMaxInFlight
|
||||
if e := os.Getenv("VIC_MAX_IN_FLIGHT"); e != "" {
|
||||
if i, err := strconv.Atoi(e); err == nil {
|
||||
maxInFlight = i
|
||||
}
|
||||
}
|
||||
// Limit the concurrency of SOAP requests
|
||||
if t, ok := soapClient.Transport.(*http.Transport); ok {
|
||||
t.MaxIdleConnsPerHost = maxInFlight
|
||||
t.TLSHandshakeTimeout = tlsHandshakeTimeout
|
||||
}
|
||||
soapClient.Transport = LimitConcurrency(soapClient.Transport, maxInFlight)
|
||||
|
||||
// TODO: option to set http.Client.Transport.TLSClientConfig.RootCAs
|
||||
vimClient, err := vim25.NewClient(op, soapClient)
|
||||
if err != nil {
|
||||
return nil, SoapClientError{
|
||||
Host: soapURL.Host,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if s.Keepalive != 0 {
|
||||
vimClient.RoundTripper = session.KeepAliveHandler(soapClient, s.Keepalive,
|
||||
func(roundTripper soap.RoundTripper) error {
|
||||
cop := trace.FromOperation(op, "KeepAlive")
|
||||
|
||||
_, err := methods.GetCurrentTime(cop, roundTripper)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cop.Warnf("session keepalive error: %s", err)
|
||||
|
||||
if isNotAuthenticated(err) {
|
||||
|
||||
if err = login(cop); err != nil {
|
||||
cop.Errorf("session keepalive failed to re-authenticate: %s", err)
|
||||
} else {
|
||||
cop.Info("session keepalive re-authenticated")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: get rid of govmomi.Client usage, only provides a few helpers we don't need.
|
||||
s.Client = &govmomi.Client{
|
||||
Client: vimClient,
|
||||
SessionManager: session.NewManager(vimClient),
|
||||
}
|
||||
|
||||
if s.CloneTicket != "" {
|
||||
// clone a user session if we have a ticket
|
||||
err = s.SessionManager.CloneSession(op, s.CloneTicket)
|
||||
} else {
|
||||
// otherwise login to create a new one
|
||||
err = login(op)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, UserPassLoginError{
|
||||
Host: soapURL.Host,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
s.Finder = find.NewFinder(s.Vim25(), false)
|
||||
// log high-level environment information
|
||||
s.logEnvironmentInfo(op)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Populate resolves the set of cached resources that should be presented
|
||||
// This returns accumulated error detail if there is ambiguity, but sets all
|
||||
// unambiguous or correct resources.
|
||||
func (s *Session) Populate(ctx context.Context) (*Session, error) {
|
||||
op := trace.FromContext(ctx, "Populate")
|
||||
|
||||
// Populate s
|
||||
var errs []string
|
||||
var err error
|
||||
|
||||
finder := s.Finder
|
||||
|
||||
op.Debugf("vSphere resource cache populating...")
|
||||
s.Datacenter, err = finder.DatacenterOrDefault(op, s.DatacenterPath)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("Failure finding dc (%s): %s", s.DatacenterPath, err.Error()))
|
||||
} else {
|
||||
finder.SetDatacenter(s.Datacenter)
|
||||
op.Debugf("Cached dc: %s", s.DatacenterPath)
|
||||
}
|
||||
|
||||
finder.SetDatacenter(s.Datacenter)
|
||||
|
||||
s.Cluster, err = finder.ComputeResourceOrDefault(op, s.ClusterPath)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("Failure finding cluster (%s): %s", s.ClusterPath, err.Error()))
|
||||
} else {
|
||||
op.Debugf("Cached cluster: %s", s.ClusterPath)
|
||||
}
|
||||
|
||||
s.Datastore, err = finder.DatastoreOrDefault(op, s.DatastorePath)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("Failure finding ds (%s): %s", s.DatastorePath, err.Error()))
|
||||
} else {
|
||||
op.Debugf("Cached ds: %s", s.DatastorePath)
|
||||
}
|
||||
|
||||
s.Host, err = finder.HostSystemOrDefault(op, s.HostPath)
|
||||
if err != nil {
|
||||
if _, ok := err.(*find.DefaultMultipleFoundError); !ok || !s.IsVC() {
|
||||
errs = append(errs, fmt.Sprintf("Failure finding host (%s): %s", s.HostPath, err.Error()))
|
||||
}
|
||||
} else {
|
||||
op.Debugf("Cached host: %s", s.HostPath)
|
||||
}
|
||||
|
||||
s.Pool, err = finder.ResourcePoolOrDefault(op, s.PoolPath)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("Failure finding pool (%s): %s", s.PoolPath, err.Error()))
|
||||
} else {
|
||||
op.Debugf("Cached pool: %s", s.PoolPath)
|
||||
}
|
||||
|
||||
if s.Datacenter != nil {
|
||||
folders, err := s.Datacenter.Folders(op)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("Failure finding folders (%s): %s", s.DatacenterPath, err.Error()))
|
||||
} else {
|
||||
op.Debugf("Cached folders: %s", s.DatacenterPath)
|
||||
}
|
||||
s.VMFolder = folders.VmFolder
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
op.Debugf("Error count populating vSphere cache: (%d)", len(errs))
|
||||
return nil, errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
op.Debug("vSphere resource cache populated...")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Session) logEnvironmentInfo(op trace.Operation) {
|
||||
a := s.ServiceContent.About
|
||||
op.WithFields(logrus.Fields{
|
||||
"Name": a.Name,
|
||||
"Vendor": a.Vendor,
|
||||
"Version": a.Version,
|
||||
"Build": a.Build,
|
||||
"OS Type": a.OsType,
|
||||
"API Type": a.ApiType,
|
||||
"API Version": a.ApiVersion,
|
||||
"Product ID": a.ProductLineId,
|
||||
"UUID": a.InstanceUuid,
|
||||
}).Debug("Session Environment Info: ")
|
||||
return
|
||||
}
|
||||
|
||||
func isNotAuthenticated(err error) bool {
|
||||
if soap.IsSoapFault(err) {
|
||||
switch soap.ToSoapFault(err).VimFault().(type) {
|
||||
case types.NotAuthenticated:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
186
vendor/github.com/vmware/vic/pkg/vsphere/session/session_test.go
generated
vendored
Normal file
186
vendor/github.com/vmware/vic/pkg/vsphere/session/session_test.go
generated
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/govmomi/find"
|
||||
"github.com/vmware/govmomi/simulator"
|
||||
"github.com/vmware/vic/pkg/vsphere/test/env"
|
||||
)
|
||||
|
||||
func TestSessionDefaults(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
config := &Config{
|
||||
Service: env.URL(t),
|
||||
Insecure: true,
|
||||
}
|
||||
|
||||
session, err := NewSession(config).Create(ctx)
|
||||
if err != nil {
|
||||
eStr := err.Error()
|
||||
t.Logf("%+v", eStr)
|
||||
// FIXME: See comments below
|
||||
if strings.Contains(eStr, "resolves to multiple hosts") {
|
||||
t.SkipNow()
|
||||
}
|
||||
t.Logf("%+v", eStr)
|
||||
if _, ok := err.(*find.DefaultMultipleFoundError); !ok {
|
||||
t.Errorf(eStr)
|
||||
} else {
|
||||
t.SkipNow()
|
||||
}
|
||||
}
|
||||
if session != nil {
|
||||
defer session.Logout(ctx)
|
||||
}
|
||||
|
||||
t.Logf("%+v", session)
|
||||
}
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
config := &Config{
|
||||
Service: env.URL(t),
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
DatacenterPath: "",
|
||||
DatastorePath: "/ha-datacenter/datastore/*",
|
||||
HostPath: "/ha-datacenter/host/*/*",
|
||||
PoolPath: "/ha-datacenter/host/*/Resources",
|
||||
}
|
||||
|
||||
session, err := NewSession(config).Create(ctx)
|
||||
if err != nil {
|
||||
eStr := err.Error()
|
||||
t.Logf("%+v", eStr)
|
||||
// FIXME: session.Create incorporates Populate which loses the type of any original error from vmomi
|
||||
// In the case where the test is run on a cluster with multiple hosts, find.MultipleFoundError
|
||||
// gets rolled up into a generic error in Populate. As such, the best we can do is just grep for the string, which is lame
|
||||
// The test shouldn't fail if it's run on a cluster with multiple hosts. However, it won't test for anything either.
|
||||
if strings.Contains(eStr, "resolves to multiple hosts") {
|
||||
t.SkipNow()
|
||||
}
|
||||
if _, ok := err.(*find.MultipleFoundError); !ok {
|
||||
t.Errorf(eStr)
|
||||
} else {
|
||||
t.SkipNow()
|
||||
}
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
defer session.Logout(ctx)
|
||||
|
||||
t.Logf("Session: %+v", session)
|
||||
|
||||
t.Logf("IsVC: %t", session.IsVC())
|
||||
t.Logf("IsVSAN: %t", session.IsVSAN(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolder(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
config := &Config{
|
||||
Service: env.URL(t),
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
DatacenterPath: "",
|
||||
DatastorePath: "/ha-datacenter/datastore/*",
|
||||
HostPath: "/ha-datacenter/host/*/*",
|
||||
PoolPath: "/ha-datacenter/host/*/Resources",
|
||||
}
|
||||
|
||||
session, err := NewSession(config).Create(ctx)
|
||||
if err != nil {
|
||||
eStr := err.Error()
|
||||
t.Logf("%+v", eStr)
|
||||
// FIXME: See comments above
|
||||
if strings.Contains(eStr, "resolves to multiple hosts") {
|
||||
t.SkipNow()
|
||||
}
|
||||
if _, ok := err.(*find.MultipleFoundError); !ok {
|
||||
t.Errorf(eStr)
|
||||
} else {
|
||||
t.SkipNow()
|
||||
}
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
defer session.Logout(ctx)
|
||||
|
||||
if session.VMFolder == nil {
|
||||
t.Errorf("Get empty folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnect(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
for _, model := range []*simulator.Model{simulator.ESX(), simulator.VPX()} {
|
||||
defer model.Remove()
|
||||
err := model.Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
model.Service.TLS = new(tls.Config)
|
||||
s := model.Service.NewServer()
|
||||
defer s.Close()
|
||||
|
||||
config := &Config{
|
||||
Keepalive: time.Minute,
|
||||
Service: s.URL.String(),
|
||||
}
|
||||
|
||||
for _, thumbprint := range []string{"", s.CertificateInfo().ThumbprintSHA1} {
|
||||
u := *s.URL
|
||||
config.Service = u.String()
|
||||
config.Thumbprint = thumbprint
|
||||
|
||||
_, err = NewSession(config).Connect(ctx)
|
||||
if thumbprint == "" {
|
||||
if err == nil {
|
||||
t.Error("expected x509.UnknownAuthorityError error")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
u.User = nil
|
||||
config.Service = u.String()
|
||||
_, err = NewSession(config).Connect(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected login error")
|
||||
}
|
||||
|
||||
config.Service = ""
|
||||
_, err = NewSession(config).Connect(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected URL parse error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
vendor/github.com/vmware/vic/pkg/vsphere/sys/uuid_linux.go
generated
vendored
Normal file
49
vendor/github.com/vmware/vic/pkg/vsphere/sys/uuid_linux.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
UUIDPath = "/sys/class/dmi/id/product_serial"
|
||||
UUIDPrefix = "VMware-"
|
||||
)
|
||||
|
||||
// UUID gets the BIOS UUID via the sys interface. This UUID is known by vphsere
|
||||
func UUID() (string, error) {
|
||||
id, err := ioutil.ReadFile(UUIDPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error retrieving vm uuid: %s", err)
|
||||
}
|
||||
|
||||
uuidstr := string(id[:])
|
||||
|
||||
// check the uuid starts with "VMware-"
|
||||
if !strings.HasPrefix(uuidstr, UUIDPrefix) {
|
||||
return "", fmt.Errorf("cannot find this VM's UUID")
|
||||
}
|
||||
|
||||
// Strip the prefix, white spaces, and the trailing '\n'
|
||||
uuidstr = strings.Replace(uuidstr[len(UUIDPrefix):(len(uuidstr)-1)], " ", "", -1)
|
||||
|
||||
// need to add dashes, e.g. "564d395e-d807-e18a-cb25-b79f65eb2b9f"
|
||||
uuidstr = fmt.Sprintf("%s-%s-%s-%s", uuidstr[0:8], uuidstr[8:12], uuidstr[12:21], uuidstr[21:])
|
||||
|
||||
return uuidstr, nil
|
||||
}
|
||||
24
vendor/github.com/vmware/vic/pkg/vsphere/sys/uuid_other.go
generated
vendored
Normal file
24
vendor/github.com/vmware/vic/pkg/vsphere/sys/uuid_other.go
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !linux
|
||||
|
||||
package sys
|
||||
|
||||
import "fmt"
|
||||
|
||||
// UUID gets the BIOS UUID via the sys interface. This UUID is known by vphsere
|
||||
func UUID() (string, error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
212
vendor/github.com/vmware/vic/pkg/vsphere/tags/categories.go
generated
vendored
Normal file
212
vendor/github.com/vmware/vic/pkg/vsphere/tags/categories.go
generated
vendored
Normal file
@@ -0,0 +1,212 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tags
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
CategoryURL = "/com/vmware/cis/tagging/category"
|
||||
ErrAlreadyExists = "already_exists"
|
||||
)
|
||||
|
||||
type CategoryCreateSpec struct {
|
||||
CreateSpec CategoryCreate `json:"create_spec"`
|
||||
}
|
||||
|
||||
type CategoryUpdateSpec struct {
|
||||
UpdateSpec CategoryUpdate `json:"update_spec"`
|
||||
}
|
||||
|
||||
type CategoryCreate struct {
|
||||
AssociableTypes []string `json:"associable_types"`
|
||||
Cardinality string `json:"cardinality"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type CategoryUpdate struct {
|
||||
AssociableTypes []string `json:"associable_types"`
|
||||
Cardinality string `json:"cardinality"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Category struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
Cardinality string `json:"cardinality"`
|
||||
AssociableTypes []string `json:"associable_types"`
|
||||
UsedBy []string `json:"used_by"`
|
||||
}
|
||||
|
||||
func (c *RestClient) CreateCategoryIfNotExist(ctx context.Context, name string, description string, categoryType string, multiValue bool) (*string, error) {
|
||||
categories, err := c.GetCategoriesByName(ctx, name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to query category for %s", name)
|
||||
}
|
||||
|
||||
if categories == nil {
|
||||
var multiValueStr string
|
||||
if multiValue {
|
||||
multiValueStr = "MULTIPLE"
|
||||
} else {
|
||||
multiValueStr = "SINGLE"
|
||||
}
|
||||
categoryCreate := CategoryCreate{[]string{categoryType}, multiValueStr, description, name}
|
||||
spec := CategoryCreateSpec{categoryCreate}
|
||||
id, err := c.CreateCategory(ctx, &spec)
|
||||
if err != nil {
|
||||
// in case there are two docker daemon try to create inventory category, query the category once again
|
||||
if strings.Contains(err.Error(), "ErrAlreadyExists") {
|
||||
if categories, err = c.GetCategoriesByName(ctx, name); err != nil {
|
||||
Logger.Debugf("Failed to get inventory category for %s", errors.WithStack(err))
|
||||
return nil, errors.Wrap(err, "create inventory category failed")
|
||||
}
|
||||
} else {
|
||||
Logger.Debugf("Failed to create inventory category for %s", errors.WithStack(err))
|
||||
return nil, errors.Wrap(err, "create inventory category failed")
|
||||
}
|
||||
} else {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
if categories != nil {
|
||||
return &categories[0].ID, nil
|
||||
}
|
||||
// should not happen
|
||||
Logger.Debugf("Failed to create inventory for it's existed, but could not query back. Please check system")
|
||||
return nil, errors.Errorf("Failed to create inventory for it's existed, but could not query back. Please check system")
|
||||
}
|
||||
|
||||
func (c *RestClient) CreateCategory(ctx context.Context, spec *CategoryCreateSpec) (*string, error) {
|
||||
Logger.Debugf("Create category %v", spec)
|
||||
stream, _, status, err := c.call(ctx, "POST", CategoryURL, spec, nil)
|
||||
|
||||
Logger.Debugf("Get status code: %d", status)
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Create category failed with status code: %d, error message: %s", status, errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "Status code: %d", status)
|
||||
}
|
||||
|
||||
type RespValue struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
var pID RespValue
|
||||
if err := json.NewDecoder(stream).Decode(&pID); err != nil {
|
||||
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
|
||||
return nil, errors.Wrap(err, "create category failed")
|
||||
}
|
||||
return &(pID.Value), nil
|
||||
}
|
||||
|
||||
func (c *RestClient) GetCategory(ctx context.Context, id string) (*Category, error) {
|
||||
Logger.Debugf("Get category %s", id)
|
||||
|
||||
stream, _, status, err := c.call(ctx, "GET", fmt.Sprintf("%s/id:%s", CategoryURL, id), nil, nil)
|
||||
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Get category failed with status code: %s, error message: %s", status, errors.WithStack(err))
|
||||
return nil, errors.Errorf("Status code: %d, error: %s", status, err)
|
||||
}
|
||||
|
||||
type RespValue struct {
|
||||
Value Category
|
||||
}
|
||||
|
||||
var pCategory RespValue
|
||||
if err := json.NewDecoder(stream).Decode(&pCategory); err != nil {
|
||||
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "get category %s failed", id)
|
||||
}
|
||||
return &(pCategory.Value), nil
|
||||
}
|
||||
|
||||
func (c *RestClient) UpdateCategory(ctx context.Context, id string, spec *CategoryUpdateSpec) error {
|
||||
Logger.Debugf("Update category %v", spec)
|
||||
_, _, status, err := c.call(ctx, "PATCH", fmt.Sprintf("%s/id:%s", CategoryURL, id), spec, nil)
|
||||
|
||||
Logger.Debugf("Get status code: %d", status)
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Update category failed with status code: %d, error message: %s", status, errors.WithStack(err))
|
||||
return errors.Wrapf(err, "Status code: %d", status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RestClient) DeleteCategory(ctx context.Context, id string) error {
|
||||
Logger.Debugf("Delete category %s", id)
|
||||
|
||||
_, _, status, err := c.call(ctx, "DELETE", fmt.Sprintf("%s/id:%s", CategoryURL, id), nil, nil)
|
||||
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Delete category failed with status code: %s, error message: %s", status, errors.WithStack(err))
|
||||
return errors.Errorf("Status code: %d, error: %s", status, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RestClient) ListCategories(ctx context.Context) ([]string, error) {
|
||||
Logger.Debugf("List all categories")
|
||||
|
||||
stream, _, status, err := c.call(ctx, "GET", CategoryURL, nil, nil)
|
||||
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Get categories failed with status code: %s, error message: %s", status, errors.WithStack(err))
|
||||
return nil, errors.Errorf("Status code: %d, error: %s", status, err)
|
||||
}
|
||||
|
||||
type Categories struct {
|
||||
Value []string
|
||||
}
|
||||
|
||||
var pCategories Categories
|
||||
if err := json.NewDecoder(stream).Decode(&pCategories); err != nil {
|
||||
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
|
||||
return nil, errors.Wrap(err, "list categories failed")
|
||||
}
|
||||
return pCategories.Value, nil
|
||||
}
|
||||
|
||||
func (c *RestClient) GetCategoriesByName(ctx context.Context, name string) ([]Category, error) {
|
||||
Logger.Debugf("Get category %s", name)
|
||||
categoryIds, err := c.ListCategories(ctx)
|
||||
if err != nil {
|
||||
Logger.Debugf("Get category failed for: %s", errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "get categories by name %s failed", name)
|
||||
}
|
||||
|
||||
var categories []Category
|
||||
for _, cID := range categoryIds {
|
||||
category, err := c.GetCategory(ctx, cID)
|
||||
if err != nil {
|
||||
Logger.Debugf("Get category %s failed for %s", cID, errors.WithStack(err))
|
||||
}
|
||||
if category.Name == name {
|
||||
categories = append(categories, *category)
|
||||
}
|
||||
}
|
||||
return categories, nil
|
||||
}
|
||||
202
vendor/github.com/vmware/vic/pkg/vsphere/tags/rest_client.go
generated
vendored
Normal file
202
vendor/github.com/vmware/vic/pkg/vsphere/tags/rest_client.go
generated
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tags
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/vmware/govmomi/vim25/soap"
|
||||
)
|
||||
|
||||
const (
|
||||
RestPrefix = "/rest"
|
||||
loginURL = "/com/vmware/cis/session"
|
||||
)
|
||||
|
||||
type RestClient struct {
|
||||
mu sync.Mutex
|
||||
host string
|
||||
scheme string
|
||||
endpoint *url.URL
|
||||
user *url.Userinfo
|
||||
HTTP *http.Client
|
||||
cookies []*http.Cookie
|
||||
}
|
||||
|
||||
func NewClient(u *url.URL, insecure bool, thumbprint string) *RestClient {
|
||||
endpoint := &url.URL{}
|
||||
*endpoint = *u
|
||||
Logger.Debugf("Create rest client")
|
||||
endpoint.Path = RestPrefix
|
||||
|
||||
sc := soap.NewClient(endpoint, insecure)
|
||||
if thumbprint != "" {
|
||||
sc.SetThumbprint(endpoint.Host, thumbprint)
|
||||
}
|
||||
|
||||
user := endpoint.User
|
||||
endpoint.User = nil
|
||||
|
||||
return &RestClient{
|
||||
endpoint: endpoint,
|
||||
user: user,
|
||||
host: endpoint.Host,
|
||||
scheme: endpoint.Scheme,
|
||||
HTTP: &sc.Client,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RestClient) encodeData(data interface{}) (*bytes.Buffer, error) {
|
||||
params := bytes.NewBuffer(nil)
|
||||
if data != nil {
|
||||
if err := json.NewEncoder(params).Encode(data); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to encode json data")
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (c *RestClient) call(ctx context.Context, method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) {
|
||||
// Logger.Debugf("%s: %s, headers: %+v", method, path, headers)
|
||||
params, err := c.encodeData(data)
|
||||
if err != nil {
|
||||
return nil, nil, -1, errors.Wrap(err, "call failed")
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
if headers == nil {
|
||||
headers = make(map[string][]string)
|
||||
}
|
||||
headers["Content-Type"] = []string{"application/json"}
|
||||
}
|
||||
|
||||
body, hdr, statusCode, err := c.clientRequest(ctx, method, path, params, headers)
|
||||
if statusCode == http.StatusUnauthorized && strings.Contains(err.Error(), "This method requires authentication") {
|
||||
c.Login(ctx)
|
||||
Logger.Debugf("Rerun request after login")
|
||||
return c.clientRequest(ctx, method, path, params, headers)
|
||||
}
|
||||
|
||||
return body, hdr, statusCode, errors.Wrap(err, "call failed")
|
||||
}
|
||||
|
||||
func (c *RestClient) clientRequest(ctx context.Context, method, path string, in io.Reader, headers map[string][]string) (io.ReadCloser, http.Header, int, error) {
|
||||
expectedPayload := (method == "POST" || method == "PUT")
|
||||
if expectedPayload && in == nil {
|
||||
in = bytes.NewReader([]byte{})
|
||||
}
|
||||
|
||||
req, err := c.newRequest(method, path, in)
|
||||
if err != nil {
|
||||
return nil, nil, -1, errors.Wrap(err, "failed to create request")
|
||||
}
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
c.mu.Lock()
|
||||
if c.cookies != nil {
|
||||
req.AddCookie(c.cookies[0])
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if headers != nil {
|
||||
for k, v := range headers {
|
||||
req.Header[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if expectedPayload && req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := c.HTTP.Do(req)
|
||||
return c.handleResponse(resp, err)
|
||||
}
|
||||
|
||||
func (c *RestClient) handleResponse(resp *http.Response, err error) (io.ReadCloser, http.Header, int, error) {
|
||||
statusCode := -1
|
||||
if resp != nil {
|
||||
statusCode = resp.StatusCode
|
||||
}
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "connection refused") {
|
||||
return nil, nil, statusCode, errors.Errorf("Cannot connect to endpoint %s. Is vCloud Suite API running on this server?", c.host)
|
||||
}
|
||||
return nil, nil, statusCode, errors.Wrap(err, "error occurred trying to connect")
|
||||
}
|
||||
|
||||
if statusCode < http.StatusOK || statusCode >= http.StatusBadRequest {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, nil, statusCode, errors.Wrap(err, "error reading response")
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return nil, nil, statusCode, errors.Errorf("Error: request returned %s", http.StatusText(statusCode))
|
||||
}
|
||||
Logger.Debugf("Error response: %s", bytes.TrimSpace(body))
|
||||
return nil, nil, statusCode, errors.Errorf("Error response from vCloud Suite API: %s", bytes.TrimSpace(body))
|
||||
}
|
||||
|
||||
return resp.Body, resp.Header, statusCode, nil
|
||||
}
|
||||
|
||||
func (c *RestClient) Login(ctx context.Context) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
Logger.Debugf("Login to %s through rest API.", c.host)
|
||||
|
||||
request, err := c.newRequest("POST", loginURL, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "login failed")
|
||||
}
|
||||
if c.user != nil {
|
||||
password, _ := c.user.Password()
|
||||
request.SetBasicAuth(c.user.Username(), password)
|
||||
}
|
||||
resp, err := c.HTTP.Do(request)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "login failed")
|
||||
}
|
||||
if resp == nil {
|
||||
return errors.New("response is nil in Login")
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// #nosec: Errors unhandled.
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return errors.Errorf("Login failed: body: %s, status: %s", bytes.TrimSpace(body), resp.Status)
|
||||
}
|
||||
|
||||
c.cookies = resp.Cookies()
|
||||
|
||||
Logger.Debugf("Login succeeded")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RestClient) newRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
|
||||
return http.NewRequest(method, c.endpoint.String()+urlStr, body)
|
||||
}
|
||||
135
vendor/github.com/vmware/vic/pkg/vsphere/tags/tag_association.go
generated
vendored
Normal file
135
vendor/github.com/vmware/vic/pkg/vsphere/tags/tag_association.go
generated
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tags
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
TagAssociationURL = "/com/vmware/cis/tagging/tag-association"
|
||||
)
|
||||
|
||||
type AssociatedObject struct {
|
||||
ID *string `json:"id"`
|
||||
Type *string `json:"type"`
|
||||
}
|
||||
|
||||
type TagAssociationSpec struct {
|
||||
ObjectID *AssociatedObject `json:"object_id,omitempty"`
|
||||
TagID *string `json:"tag_id,omitempty"`
|
||||
}
|
||||
|
||||
func (c *RestClient) getAssociatedObject(objID *string, objType *string) *AssociatedObject {
|
||||
if objID == nil && objType == nil {
|
||||
return nil
|
||||
}
|
||||
object := AssociatedObject{
|
||||
ID: objID,
|
||||
Type: objType,
|
||||
}
|
||||
return &object
|
||||
}
|
||||
|
||||
func (c *RestClient) getAssociationSpec(tagID *string, objID *string, objType *string) *TagAssociationSpec {
|
||||
object := c.getAssociatedObject(objID, objType)
|
||||
spec := TagAssociationSpec{
|
||||
TagID: tagID,
|
||||
ObjectID: object,
|
||||
}
|
||||
return &spec
|
||||
}
|
||||
|
||||
func (c *RestClient) AttachTagToObject(ctx context.Context, tagID string, objID string, objType string) error {
|
||||
Logger.Debugf("Attach Tag %s to object id: %s, type: %s", tagID, objID, objType)
|
||||
|
||||
spec := c.getAssociationSpec(&tagID, &objID, &objType)
|
||||
_, _, status, err := c.call(ctx, "POST", fmt.Sprintf("%s?~action=attach", TagAssociationURL), *spec, nil)
|
||||
|
||||
Logger.Debugf("Get status code: %d", status)
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Attach tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
|
||||
return errors.Wrapf(err, "Get unexpected status code: %d", status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RestClient) DetachTagFromObject(ctx context.Context, tagID string, objID string, objType string) error {
|
||||
Logger.Debugf("Detach Tag %s to object id: %s, type: %s", tagID, objID, objType)
|
||||
|
||||
spec := c.getAssociationSpec(&tagID, &objID, &objType)
|
||||
_, _, status, err := c.call(ctx, "POST", fmt.Sprintf("%s?~action=detach", TagAssociationURL), *spec, nil)
|
||||
|
||||
Logger.Debugf("Get status code: %d", status)
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Detach tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
|
||||
return errors.Wrapf(err, "Get unexpected status code: %d", status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RestClient) ListAttachedTags(ctx context.Context, objID string, objType string) ([]string, error) {
|
||||
Logger.Debugf("List attached tags of object id: %s, type: %s", objID, objType)
|
||||
|
||||
spec := c.getAssociationSpec(nil, &objID, &objType)
|
||||
stream, _, status, err := c.call(ctx, "POST", fmt.Sprintf("%s?~action=list-attached-tags", TagAssociationURL), *spec, nil)
|
||||
|
||||
Logger.Debugf("Get status code: %d", status)
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Detach tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "Get unexpected status code: %d", status)
|
||||
}
|
||||
|
||||
type RespValue struct {
|
||||
Value []string
|
||||
}
|
||||
|
||||
var pTag RespValue
|
||||
if err := json.NewDecoder(stream).Decode(&pTag); err != nil {
|
||||
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
|
||||
return nil, errors.Wrap(err, "list attached tags failed")
|
||||
}
|
||||
return pTag.Value, nil
|
||||
}
|
||||
|
||||
func (c *RestClient) ListAttachedObjects(ctx context.Context, tagID string) ([]AssociatedObject, error) {
|
||||
Logger.Debugf("List attached objects of tag: %s", tagID)
|
||||
|
||||
spec := c.getAssociationSpec(&tagID, nil, nil)
|
||||
Logger.Debugf("List attached objects for tag %v", *spec)
|
||||
// stream, _, status, err := c.call("POST", fmt.Sprintf("%s?~action=list-attached-objects", TagAssociationURL), *spec, nil)
|
||||
stream, _, status, err := c.call(ctx, "POST", fmt.Sprintf("%s?~action=list-attached-objects", TagAssociationURL), *spec, nil)
|
||||
Logger.Debugf("Get status code: %d", status)
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("List object failed with status code: %d, error message: %s", status, errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "Get unexpected status code: %d", status)
|
||||
}
|
||||
|
||||
type RespValue struct {
|
||||
Value []AssociatedObject
|
||||
}
|
||||
|
||||
var pTag RespValue
|
||||
if err := json.NewDecoder(stream).Decode(&pTag); err != nil {
|
||||
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
|
||||
return nil, errors.Wrap(err, "list attached tags failed")
|
||||
}
|
||||
return pTag.Value, nil
|
||||
}
|
||||
255
vendor/github.com/vmware/vic/pkg/vsphere/tags/tags.go
generated
vendored
Normal file
255
vendor/github.com/vmware/vic/pkg/vsphere/tags/tags.go
generated
vendored
Normal file
@@ -0,0 +1,255 @@
|
||||
// Copyright 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tags
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
TagURL = "/com/vmware/cis/tagging/tag"
|
||||
)
|
||||
|
||||
type TagCreateSpec struct {
|
||||
CreateSpec TagCreate `json:"create_spec"`
|
||||
}
|
||||
|
||||
type TagCreate struct {
|
||||
CategoryID string `json:"category_id"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type TagUpdateSpec struct {
|
||||
UpdateSpec TagUpdate `json:"update_spec"`
|
||||
}
|
||||
|
||||
type TagUpdate struct {
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
CategoryID string `json:"category_id"`
|
||||
UsedBy []string `json:"used_by"`
|
||||
}
|
||||
|
||||
var Logger = logrus.New()
|
||||
|
||||
func (c *RestClient) CreateTagIfNotExist(ctx context.Context, name string, description string, categoryID string) (*string, error) {
|
||||
tagCreate := TagCreate{categoryID, description, name}
|
||||
spec := TagCreateSpec{tagCreate}
|
||||
id, err := c.CreateTag(ctx, &spec)
|
||||
if err == nil {
|
||||
return id, nil
|
||||
}
|
||||
Logger.Debugf("Created tag %s failed for %s", errors.WithStack(err))
|
||||
// if already exists, query back
|
||||
if strings.Contains(err.Error(), ErrAlreadyExists) {
|
||||
tagObjs, err := c.GetTagByNameForCategory(ctx, name, categoryID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to query tag %s for category %s", name, categoryID)
|
||||
}
|
||||
if tagObjs != nil {
|
||||
return &tagObjs[0].ID, nil
|
||||
}
|
||||
|
||||
// should not happen
|
||||
return nil, errors.New("Failed to create tag for it's existed, but could not query back. Please check system")
|
||||
}
|
||||
|
||||
return nil, errors.Wrap(err, "failed to create tag")
|
||||
}
|
||||
|
||||
func (c *RestClient) DeleteTagIfNoObjectAttached(ctx context.Context, id string) error {
|
||||
objs, err := c.ListAttachedObjects(ctx, id)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to delete tag")
|
||||
}
|
||||
if objs != nil && len(objs) > 0 {
|
||||
Logger.Debugf("tag %s related objects is not empty, do not delete it.", id)
|
||||
return nil
|
||||
}
|
||||
return c.DeleteTag(ctx, id)
|
||||
}
|
||||
|
||||
func (c *RestClient) CreateTag(ctx context.Context, spec *TagCreateSpec) (*string, error) {
|
||||
Logger.Debugf("Create Tag %v", spec)
|
||||
stream, _, status, err := c.call(ctx, "POST", TagURL, spec, nil)
|
||||
|
||||
Logger.Debugf("Get status code: %d", status)
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Create tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "Status code: %d", status)
|
||||
}
|
||||
|
||||
type RespValue struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
var pID RespValue
|
||||
if err := json.NewDecoder(stream).Decode(&pID); err != nil {
|
||||
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
|
||||
return nil, errors.Wrap(err, "create tag failed")
|
||||
}
|
||||
return &pID.Value, nil
|
||||
}
|
||||
|
||||
func (c *RestClient) GetTag(ctx context.Context, id string) (*Tag, error) {
|
||||
Logger.Debugf("Get tag %s", id)
|
||||
|
||||
stream, _, status, err := c.call(ctx, "GET", fmt.Sprintf("%s/id:%s", TagURL, id), nil, nil)
|
||||
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Get tag failed with status code: %s, error message: %s", status, errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "Status code: %d", status)
|
||||
}
|
||||
|
||||
type RespValue struct {
|
||||
Value Tag
|
||||
}
|
||||
|
||||
var pTag RespValue
|
||||
if err := json.NewDecoder(stream).Decode(&pTag); err != nil {
|
||||
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "failed to get tag %s", id)
|
||||
}
|
||||
return &(pTag.Value), nil
|
||||
}
|
||||
|
||||
func (c *RestClient) UpdateTag(ctx context.Context, id string, spec *TagUpdateSpec) error {
|
||||
Logger.Debugf("Update tag %v", spec)
|
||||
_, _, status, err := c.call(ctx, "PATCH", fmt.Sprintf("%s/id:%s", TagURL, id), spec, nil)
|
||||
|
||||
Logger.Debugf("Get status code: %d", status)
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Update tag failed with status code: %d, error message: %s", status, errors.WithStack(err))
|
||||
return errors.Wrapf(err, "Status code: %d", status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RestClient) DeleteTag(ctx context.Context, id string) error {
|
||||
Logger.Debugf("Delete tag %s", id)
|
||||
|
||||
_, _, status, err := c.call(ctx, "DELETE", fmt.Sprintf("%s/id:%s", TagURL, id), nil, nil)
|
||||
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Delete tag failed with status code: %s, error message: %s", status, errors.WithStack(err))
|
||||
return errors.Wrapf(err, "Status code: %d", status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *RestClient) ListTags(ctx context.Context) ([]string, error) {
|
||||
Logger.Debugf("List all tags")
|
||||
|
||||
stream, _, status, err := c.call(ctx, "GET", TagURL, nil, nil)
|
||||
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("Get tags failed with status code: %s, error message: %s", status, errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "Status code: %d", status)
|
||||
}
|
||||
|
||||
return c.handleTagIDList(stream)
|
||||
}
|
||||
|
||||
func (c *RestClient) ListTagsForCategory(ctx context.Context, id string) ([]string, error) {
|
||||
Logger.Debugf("List tags for category: %s", id)
|
||||
|
||||
type PostCategory struct {
|
||||
CId string `json:"category_id"`
|
||||
}
|
||||
spec := PostCategory{id}
|
||||
stream, _, status, err := c.call(ctx, "POST", fmt.Sprintf("%s/id:%s?~action=list-tags-for-category", TagURL, id), spec, nil)
|
||||
|
||||
if status != http.StatusOK || err != nil {
|
||||
Logger.Debugf("List tags for category failed with status code: %s, error message: %s", status, errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "Status code: %d", status)
|
||||
}
|
||||
|
||||
return c.handleTagIDList(stream)
|
||||
}
|
||||
|
||||
func (c *RestClient) handleTagIDList(stream io.ReadCloser) ([]string, error) {
|
||||
type Tags struct {
|
||||
Value []string
|
||||
}
|
||||
|
||||
var pTags Tags
|
||||
if err := json.NewDecoder(stream).Decode(&pTags); err != nil {
|
||||
Logger.Debugf("Decode response body failed for: %s", errors.WithStack(err))
|
||||
return nil, errors.Wrap(err, "failed to decode json")
|
||||
}
|
||||
return pTags.Value, nil
|
||||
}
|
||||
|
||||
// Get tag through tag name and category id
|
||||
func (c *RestClient) GetTagByNameForCategory(ctx context.Context, name string, id string) ([]Tag, error) {
|
||||
Logger.Debugf("Get tag %s for category %s", name, id)
|
||||
tagIds, err := c.ListTagsForCategory(ctx, id)
|
||||
if err != nil {
|
||||
Logger.Debugf("Get tag failed for %s", errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "get tag failed for name %s category %s", name, id)
|
||||
}
|
||||
|
||||
var tags []Tag
|
||||
for _, tID := range tagIds {
|
||||
tag, err := c.GetTag(ctx, tID)
|
||||
if err != nil {
|
||||
Logger.Debugf("Get tag %s failed for %s", tID, errors.WithStack(err))
|
||||
return nil, errors.Wrapf(err, "get tag failed for name %s category %s", name, id)
|
||||
}
|
||||
if tag.Name == name {
|
||||
tags = append(tags, *tag)
|
||||
}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// Get attached tags through tag name pattern
|
||||
func (c *RestClient) GetAttachedTagsByNamePattern(ctx context.Context, namePattern string, objID string, objType string) ([]Tag, error) {
|
||||
tagIds, err := c.ListAttachedTags(ctx, objID, objType)
|
||||
if err != nil {
|
||||
Logger.Debugf("Get attached tags failed for %s", errors.WithStack(err))
|
||||
return nil, errors.Wrap(err, "get attached tags failed")
|
||||
}
|
||||
|
||||
var validName = regexp.MustCompile(namePattern)
|
||||
var tags []Tag
|
||||
for _, tID := range tagIds {
|
||||
tag, err := c.GetTag(ctx, tID)
|
||||
if err != nil {
|
||||
Logger.Debugf("Get tag %s failed for %s", tID, errors.WithStack(err))
|
||||
}
|
||||
if validName.MatchString(tag.Name) {
|
||||
tags = append(tags, *tag)
|
||||
}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
207
vendor/github.com/vmware/vic/pkg/vsphere/tasks/waiter.go
generated
vendored
Normal file
207
vendor/github.com/vmware/vic/pkg/vsphere/tasks/waiter.go
generated
vendored
Normal file
@@ -0,0 +1,207 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package tasks wraps the operation of VC. It will invoke the operation and wait
|
||||
// until it's finished, and then return the execution result or error message.
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/govmomi/task"
|
||||
"github.com/vmware/govmomi/vim25/progress"
|
||||
"github.com/vmware/govmomi/vim25/soap"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
maxBackoffFactor = int64(16)
|
||||
)
|
||||
|
||||
//FIXME: remove this type and refactor to use object.Task from govmomi
|
||||
// this will require a lot of code being touched in a lot of places.
|
||||
type Task interface {
|
||||
Wait(ctx context.Context) error
|
||||
WaitForResult(ctx context.Context, s progress.Sinker) (*types.TaskInfo, error)
|
||||
}
|
||||
|
||||
type temporary interface {
|
||||
Temporary() bool
|
||||
}
|
||||
|
||||
// Wait wraps govmomi operations and wait the operation to complete
|
||||
// Sample usage:
|
||||
// info, err := Wait(ctx, func(ctx), (*object.Reference, *TaskInfo, error) {
|
||||
// return vm, vm.Reconfigure(ctx, config)
|
||||
// })
|
||||
func Wait(ctx context.Context, f func(context.Context) (Task, error)) error {
|
||||
_, err := WaitForResult(ctx, f)
|
||||
return err
|
||||
}
|
||||
|
||||
// WaitForResult wraps govmomi operations and wait the operation to complete.
|
||||
// Return the operation result
|
||||
// Sample usage:
|
||||
// info, err := WaitForResult(ctx, func(ctx) (*TaskInfo, error) {
|
||||
// return vm, vm.Reconfigure(ctx, config)
|
||||
// })
|
||||
func WaitForResult(ctx context.Context, f func(context.Context) (Task, error)) (*types.TaskInfo, error) {
|
||||
var err error
|
||||
var backoffFactor int64 = 1
|
||||
|
||||
op := trace.FromContext(ctx, "WaitForResult")
|
||||
|
||||
for {
|
||||
var t Task
|
||||
var info *types.TaskInfo
|
||||
|
||||
if t, err = f(op); err == nil {
|
||||
if info, err = t.WaitForResult(op, nil); err == nil {
|
||||
return info, nil
|
||||
}
|
||||
}
|
||||
|
||||
if !IsRetryError(op, err) {
|
||||
return info, err
|
||||
}
|
||||
|
||||
sleepValue := time.Duration(backoffFactor * (rand.Int63n(100) + int64(50)))
|
||||
select {
|
||||
case <-time.After(sleepValue * time.Millisecond):
|
||||
backoffFactor *= 2
|
||||
if backoffFactor > maxBackoffFactor {
|
||||
backoffFactor = maxBackoffFactor
|
||||
}
|
||||
case <-op.Done():
|
||||
return info, op.Err()
|
||||
}
|
||||
|
||||
op.Warnf("retrying task")
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
vimFault = "vim"
|
||||
soapFault = "soap"
|
||||
taskFault = "task"
|
||||
)
|
||||
|
||||
// IsRetryErrors will return true for vSphere errors, which can be fixed by retry.
|
||||
// Currently the error includes TaskInProgress, NetworkDisruptedAndConfigRolledBack and InvalidArgument
|
||||
// Retry on NetworkDisruptedAndConfigRolledBack is to workaround vSphere issue
|
||||
// Retry on InvalidArgument(invlid path) is to workaround vSAN bug: https://bugzilla.eng.vmware.com/show_bug.cgi?id=1770798. TODO: Should remove it after vSAN fixed the bug
|
||||
func IsRetryError(op trace.Operation, err error) bool {
|
||||
if soap.IsSoapFault(err) {
|
||||
switch f := soap.ToSoapFault(err).VimFault().(type) {
|
||||
case types.TaskInProgress:
|
||||
return true
|
||||
case types.NetworkDisruptedAndConfigRolledBack:
|
||||
logExpectedFault(op, soapFault, f)
|
||||
return true
|
||||
case types.InvalidArgument:
|
||||
logExpectedFault(op, soapFault, f)
|
||||
return true
|
||||
case types.VAppTaskInProgress:
|
||||
logExpectedFault(op, soapFault, f)
|
||||
return true
|
||||
case types.FailToLockFaultToleranceVMs:
|
||||
logExpectedFault(op, soapFault, f)
|
||||
return true
|
||||
case types.HostCommunication:
|
||||
logExpectedFault(op, soapFault, f)
|
||||
return true
|
||||
default:
|
||||
logSoapFault(op, f)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if soap.IsVimFault(err) {
|
||||
switch f := soap.ToVimFault(err).(type) {
|
||||
case *types.TaskInProgress:
|
||||
return true
|
||||
case *types.NetworkDisruptedAndConfigRolledBack:
|
||||
logExpectedFault(op, vimFault, f)
|
||||
return true
|
||||
case *types.InvalidArgument:
|
||||
logExpectedFault(op, vimFault, f)
|
||||
return true
|
||||
case *types.VAppTaskInProgress:
|
||||
logExpectedFault(op, soapFault, f)
|
||||
return true
|
||||
case *types.FailToLockFaultToleranceVMs:
|
||||
logExpectedFault(op, soapFault, f)
|
||||
return true
|
||||
case *types.HostCommunication:
|
||||
logExpectedFault(op, soapFault, f)
|
||||
return true
|
||||
default:
|
||||
logFault(op, f)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
switch err := err.(type) {
|
||||
case task.Error:
|
||||
switch f := err.Fault().(type) {
|
||||
case *types.TaskInProgress:
|
||||
return true
|
||||
case *types.NetworkDisruptedAndConfigRolledBack:
|
||||
logExpectedFault(op, taskFault, f)
|
||||
return true
|
||||
case *types.InvalidArgument:
|
||||
logExpectedFault(op, taskFault, f)
|
||||
return true
|
||||
case *types.HostCommunication:
|
||||
logExpectedFault(op, taskFault, f)
|
||||
return true
|
||||
default:
|
||||
logFault(op, err.Fault())
|
||||
return false
|
||||
}
|
||||
default:
|
||||
// retry the temporary errors
|
||||
t, ok := err.(temporary)
|
||||
if ok && t.Temporary() {
|
||||
logExpectedError(op, err)
|
||||
return true
|
||||
}
|
||||
logError(op, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Helper Functions
|
||||
func logFault(op trace.Operation, fault types.BaseMethodFault) {
|
||||
op.Errorf("unexpected fault on task retry: %#v", fault)
|
||||
}
|
||||
|
||||
func logSoapFault(op trace.Operation, fault types.AnyType) {
|
||||
op.Debugf("unexpected soap fault on task retry: %s", fault)
|
||||
}
|
||||
|
||||
func logError(op trace.Operation, err error) {
|
||||
op.Debugf("unexpected error on task retry: %s", err)
|
||||
}
|
||||
|
||||
func logExpectedFault(op trace.Operation, kind string, fault interface{}) {
|
||||
op.Debugf("task retry on expected %s fault: %#v", kind, fault)
|
||||
}
|
||||
|
||||
func logExpectedError(op trace.Operation, err error) {
|
||||
op.Debugf("task retry on expected error %s", err)
|
||||
}
|
||||
440
vendor/github.com/vmware/vic/pkg/vsphere/tasks/waiter_test.go
generated
vendored
Normal file
440
vendor/github.com/vmware/vic/pkg/vsphere/tasks/waiter_test.go
generated
vendored
Normal file
@@ -0,0 +1,440 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/govmomi"
|
||||
"github.com/vmware/govmomi/find"
|
||||
"github.com/vmware/govmomi/simulator"
|
||||
"github.com/vmware/govmomi/task"
|
||||
"github.com/vmware/govmomi/vim25/methods"
|
||||
"github.com/vmware/govmomi/vim25/progress"
|
||||
"github.com/vmware/govmomi/vim25/soap"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/pkg/errors"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
m.Run()
|
||||
}
|
||||
|
||||
type MyTask struct {
|
||||
success bool
|
||||
}
|
||||
|
||||
func (t *MyTask) Wait(ctx context.Context) error {
|
||||
_, err := t.WaitForResult(ctx, nil)
|
||||
return err
|
||||
}
|
||||
func (t *MyTask) WaitForResult(ctx context.Context, s progress.Sinker) (*types.TaskInfo, error) {
|
||||
if t.success {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.Errorf("Wait failed")
|
||||
}
|
||||
|
||||
func createFailedTask(context.Context) (Task, error) {
|
||||
return nil, errors.Errorf("Create VM failed")
|
||||
}
|
||||
|
||||
func createFailedResultWaiter(context.Context) (Task, error) {
|
||||
task := &MyTask{
|
||||
false,
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func createResultWaiter(context.Context) (Task, error) {
|
||||
task := &MyTask{
|
||||
true,
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func TestFailedInvokeResult(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
_, err := WaitForResult(ctx, func(ctx context.Context) (Task, error) {
|
||||
return createFailedTask(ctx)
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "Create VM failed") {
|
||||
t.Errorf("Not expected error message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailedWaitResult(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
_, err := WaitForResult(ctx, func(ctx context.Context) (Task, error) {
|
||||
return createFailedResultWaiter(ctx)
|
||||
})
|
||||
log.Debugf("got error: %s", err.Error())
|
||||
if err == nil || !strings.Contains(err.Error(), "Wait failed") {
|
||||
t.Errorf("Not expected error message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuccessWaitResult(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
_, err := WaitForResult(ctx, func(ctx context.Context) (Task, error) {
|
||||
return createResultWaiter(ctx)
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func createFailed(context.Context) (Task, error) {
|
||||
return nil, errors.Errorf("Create VM failed")
|
||||
}
|
||||
|
||||
func createFailedWaiter(context.Context) (Task, error) {
|
||||
task := &MyTask{
|
||||
false,
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func createWaiter(context.Context) (Task, error) {
|
||||
task := &MyTask{
|
||||
true,
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func TestFailedInvoke(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
err := Wait(ctx, func(ctx context.Context) (Task, error) {
|
||||
return createFailed(ctx)
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "Create VM failed") {
|
||||
t.Errorf("Not expected error message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailedWait(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
err := Wait(ctx, func(ctx context.Context) (Task, error) {
|
||||
return createFailedWaiter(ctx)
|
||||
})
|
||||
log.Debugf("got error: %s", err.Error())
|
||||
if err == nil || !strings.Contains(err.Error(), "Wait failed") {
|
||||
t.Errorf("Not expected error message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuccessWait(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
err := Wait(ctx, func(ctx context.Context) (Task, error) {
|
||||
return createWaiter(ctx)
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var taskInProgressFault = task.Error{
|
||||
LocalizedMethodFault: &types.LocalizedMethodFault{
|
||||
Fault: &types.TaskInProgress{},
|
||||
},
|
||||
}
|
||||
|
||||
type taskInProgressTask struct {
|
||||
cur, max int
|
||||
err error
|
||||
info *types.TaskInfo
|
||||
}
|
||||
|
||||
func (t *taskInProgressTask) Wait(ctx context.Context) error {
|
||||
t.cur++
|
||||
if t.cur == t.max {
|
||||
return t.err
|
||||
}
|
||||
|
||||
return taskInProgressFault
|
||||
}
|
||||
|
||||
func (t *taskInProgressTask) WaitForResult(ctx context.Context, s progress.Sinker) (*types.TaskInfo, error) {
|
||||
return t.info, t.Wait(ctx)
|
||||
}
|
||||
|
||||
func mustRunInTime(t *testing.T, d time.Duration, f func()) {
|
||||
done := make(chan bool)
|
||||
|
||||
go func() {
|
||||
f()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), d)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-done: // ran within alloted time
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("test did not run in alloted time %s", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetry(t *testing.T) {
|
||||
mustRunInTime(t, 2*time.Second, func() {
|
||||
ctx := context.Background()
|
||||
i := 0
|
||||
ti, err := WaitForResult(ctx, func(_ context.Context) (Task, error) {
|
||||
i++
|
||||
return nil, assert.AnError
|
||||
})
|
||||
|
||||
assert.Nil(t, ti)
|
||||
assert.Equal(t, i, 1)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, err, assert.AnError)
|
||||
|
||||
// error != TaskInProgress during task creation
|
||||
i = 0
|
||||
e := &task.Error{
|
||||
LocalizedMethodFault: &types.LocalizedMethodFault{
|
||||
Fault: &types.RuntimeFault{}, // random fault != TaskInProgress
|
||||
LocalizedMessage: "random fault",
|
||||
},
|
||||
}
|
||||
ti, err = WaitForResult(ctx, func(_ context.Context) (Task, error) {
|
||||
i++
|
||||
return nil, e
|
||||
})
|
||||
|
||||
assert.Nil(t, ti)
|
||||
assert.Equal(t, i, 1)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, err, e)
|
||||
|
||||
// context cancelled after two retries
|
||||
i = 0
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
ti, err = WaitForResult(ctx, func(_ context.Context) (Task, error) {
|
||||
i++
|
||||
if i == 2 {
|
||||
cancel()
|
||||
}
|
||||
return nil, taskInProgressFault
|
||||
})
|
||||
|
||||
assert.Nil(t, ti)
|
||||
assert.Equal(t, i, 2)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, err, ctx.Err())
|
||||
|
||||
// TaskInProgress from task creation for 2 iterations and
|
||||
// then nil error
|
||||
tsk := &taskInProgressTask{
|
||||
max: 1,
|
||||
info: &types.TaskInfo{
|
||||
Task: types.ManagedObjectReference{
|
||||
Type: "task",
|
||||
Value: "foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
i = 0
|
||||
ti, err = WaitForResult(context.Background(), func(_ context.Context) (Task, error) {
|
||||
i++
|
||||
if i == 2 {
|
||||
return tsk, nil
|
||||
}
|
||||
return nil, taskInProgressFault
|
||||
})
|
||||
|
||||
assert.Equal(t, tsk.info, ti)
|
||||
assert.Equal(t, i, 2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// return TaskInPregress from task.WaitForResult for 2 iterations
|
||||
// and then return assert.AnError
|
||||
tsk = &taskInProgressTask{
|
||||
max: 2,
|
||||
err: assert.AnError,
|
||||
info: &types.TaskInfo{
|
||||
Task: types.ManagedObjectReference{
|
||||
Type: "task",
|
||||
Value: "foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
ti, err = WaitForResult(context.Background(), func(_ context.Context) (Task, error) {
|
||||
return tsk, nil
|
||||
})
|
||||
|
||||
assert.Equal(t, tsk.info, ti)
|
||||
assert.Equal(t, tsk.max, tsk.cur)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, err, tsk.err)
|
||||
|
||||
// return TaskInPregress from task.WaitForResult for 2 iterations
|
||||
// and then return nil error
|
||||
tsk.cur = 0
|
||||
tsk.err = nil
|
||||
ti, err = WaitForResult(context.Background(), func(_ context.Context) (Task, error) {
|
||||
return tsk, nil
|
||||
})
|
||||
|
||||
assert.Equal(t, tsk.info, ti)
|
||||
assert.Equal(t, tsk.info, ti)
|
||||
assert.Equal(t, tsk.cur, tsk.max)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// faultyVirtualMachine wrap simulator.VirtualMachine with fault injection
|
||||
type faultyVirtualMachine struct {
|
||||
simulator.VirtualMachine
|
||||
|
||||
fault types.BaseMethodFault
|
||||
}
|
||||
|
||||
// Run implements simulator.TaskRunner and always returns vm.fault
|
||||
func (vm *faultyVirtualMachine) Run(task *simulator.Task) (types.AnyType, types.BaseMethodFault) {
|
||||
return nil, vm.fault
|
||||
}
|
||||
|
||||
// Override PowerOffVMTask to inject a fault
|
||||
func (vm *faultyVirtualMachine) PowerOffVMTask(c *types.PowerOffVM_Task) soap.HasFault {
|
||||
r := &methods.PowerOffVM_TaskBody{}
|
||||
|
||||
task := simulator.NewTask(vm)
|
||||
|
||||
r.Res = &types.PowerOffVM_TaskResponse{
|
||||
Returnval: task.Self,
|
||||
}
|
||||
|
||||
task.Run()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// MarkAsTemplate implements a non-Task method to inject vm.fault
|
||||
func (vm *faultyVirtualMachine) MarkAsTemplate(c *types.MarkAsTemplate) soap.HasFault {
|
||||
return &methods.MarkAsTemplateBody{
|
||||
Fault_: simulator.Fault("nope", vm.fault),
|
||||
}
|
||||
}
|
||||
|
||||
// TestSoapFaults covers the various soap fault checking paths
|
||||
func TestSoapFaults(t *testing.T) {
|
||||
op := trace.NewOperation(context.Background(), "TestSoapFaults")
|
||||
|
||||
// Nothing VC specific in this test, so we use the simpler ESX model
|
||||
model := simulator.ESX()
|
||||
model.Autostart = false
|
||||
defer model.Remove()
|
||||
err := model.Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server := model.Service.NewServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := govmomi.NewClient(op, server.URL, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Any VM will do
|
||||
finder := find.NewFinder(client.Client, false)
|
||||
vm, err := finder.VirtualMachine(op, "/ha-datacenter/vm/*_VM0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test the success path
|
||||
err = Wait(op, func(ctx context.Context) (Task, error) {
|
||||
return vm.PowerOn(ctx)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wrap existing vm MO with faultyVirtualMachine
|
||||
ref := simulator.Map.Get(vm.Reference())
|
||||
fvm := &faultyVirtualMachine{*ref.(*simulator.VirtualMachine), nil}
|
||||
simulator.Map.Put(fvm)
|
||||
|
||||
// Inject TaskInProgress fault
|
||||
fvm.fault = new(types.TaskInProgress)
|
||||
task, err := vm.PowerOff(op)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test the task.Error path
|
||||
res, err := task.WaitForResult(op, nil)
|
||||
if !IsRetryError(op, err) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test the soap.IsVimFault() path
|
||||
if !IsRetryError(op, soap.WrapVimFault(res.Error.Fault)) {
|
||||
t.Errorf("fault=%#v", res.Error.Fault)
|
||||
}
|
||||
|
||||
// Test the soap.IsSoapFault() path
|
||||
err = vm.MarkAsTemplate(op)
|
||||
if !IsRetryError(op, err) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test a fault other than TaskInProgress
|
||||
fvm.fault = &types.QuestionPending{
|
||||
Text: "now why would you want to do such a thing?",
|
||||
}
|
||||
|
||||
err = Wait(op, func(ctx context.Context) (Task, error) {
|
||||
return vm.PowerOff(ctx)
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
if IsRetryError(op, err) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Test with retry
|
||||
fvm.fault = new(types.TaskInProgress)
|
||||
called := 0
|
||||
|
||||
err = Wait(op, func(ctx context.Context) (Task, error) {
|
||||
called++
|
||||
if called > 1 {
|
||||
simulator.Map.Put(ref) // remove fault injection
|
||||
}
|
||||
|
||||
return vm.PowerOff(ctx)
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if called != 2 {
|
||||
t.Errorf("called=%d", called)
|
||||
}
|
||||
}
|
||||
38
vendor/github.com/vmware/vic/pkg/vsphere/test/env/env.go
generated
vendored
Normal file
38
vendor/github.com/vmware/vic/pkg/vsphere/test/env/env.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2016 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package env
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// URL checks whether or not we set VIC_ESX_TEST_URL environment variable,
|
||||
// skipping the calling test if undefined
|
||||
func URL(t *testing.T) string {
|
||||
s := os.Getenv("VIC_ESX_TEST_URL")
|
||||
if s == "" && t != nil {
|
||||
t.Skip("Skipping: No test ESX URL defined")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func DS(t *testing.T) string {
|
||||
s := os.Getenv("VIC_ESX_TEST_DATASTORE")
|
||||
if s == "" && t != nil {
|
||||
t.Skip("Skipping: No test ESX DATASTORE defined")
|
||||
}
|
||||
return s
|
||||
}
|
||||
125
vendor/github.com/vmware/vic/pkg/vsphere/test/test.go
generated
vendored
Normal file
125
vendor/github.com/vmware/vic/pkg/vsphere/test/test.go
generated
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/vic/lib/spec"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/test/env"
|
||||
|
||||
"context"
|
||||
)
|
||||
|
||||
// Session returns a session.Session struct
|
||||
func Session(ctx context.Context, t *testing.T) *session.Session {
|
||||
config := &session.Config{
|
||||
Service: env.URL(t),
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
DatacenterPath: "",
|
||||
DatastorePath: "/ha-datacenter/datastore/*",
|
||||
HostPath: "/ha-datacenter/host/*/*",
|
||||
PoolPath: "/ha-datacenter/host/*/Resources",
|
||||
}
|
||||
|
||||
s, err := session.NewSession(config).Create(ctx)
|
||||
if err != nil {
|
||||
// FIXME: See session_test.go TestSession for detail. We never get to PickRandomHost in the case of multiple hosts
|
||||
if strings.Contains(err.Error(), "resolves to multiple hosts") {
|
||||
t.SkipNow()
|
||||
} else {
|
||||
t.Errorf("ERROR: %s", err)
|
||||
t.SkipNow()
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SessionWithESX returns a general-purpose ESX session for tests.
|
||||
func SessionWithESX(ctx context.Context, service string) (*session.Session, error) {
|
||||
config := &session.Config{
|
||||
Service: service,
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
DatacenterPath: "/ha-datacenter",
|
||||
ClusterPath: "*",
|
||||
DatastorePath: "/ha-datacenter/datastore/LocalDS_0",
|
||||
PoolPath: "/ha-datacenter/host/localhost.localdomain/Resources",
|
||||
}
|
||||
|
||||
s, err := session.NewSession(config).Connect(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s, err = s.Populate(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// SessionWithVPX returns a general-purpose VPX session for tests.
|
||||
func SessionWithVPX(ctx context.Context, service string) (*session.Session, error) {
|
||||
config := &session.Config{
|
||||
Service: service,
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
DatacenterPath: "/DC0",
|
||||
ClusterPath: "/DC0/host/DC0_C0",
|
||||
DatastorePath: "/DC0/datastore/LocalDS_0",
|
||||
PoolPath: "/DC0/host/DC0_C0/Resources",
|
||||
}
|
||||
|
||||
s, err := session.NewSession(config).Connect(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s, err = s.Populate(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// SpecConfig returns a spec.VirtualMachineConfigSpecConfig struct
|
||||
func SpecConfig(session *session.Session, name string) *spec.VirtualMachineConfigSpecConfig {
|
||||
return &spec.VirtualMachineConfigSpecConfig{
|
||||
NumCPUs: 2,
|
||||
MemoryMB: 2048,
|
||||
VMForkEnabled: true,
|
||||
|
||||
ID: name,
|
||||
Name: "zombie_attack",
|
||||
BootMediaPath: session.Datastore.Path("brainz.iso"),
|
||||
VMPathName: fmt.Sprintf("[%s]", session.Datastore.Name()),
|
||||
}
|
||||
}
|
||||
|
||||
// PickRandomHost returns a random object.HostSystem from the hosts attached to the datastore and also lives in the same cluster
|
||||
func PickRandomHost(ctx context.Context, session *session.Session, t *testing.T) *object.HostSystem {
|
||||
hosts, err := session.Datastore.AttachedClusterHosts(ctx, session.Cluster)
|
||||
if err != nil {
|
||||
t.Errorf("ERROR: %s", err)
|
||||
t.SkipNow()
|
||||
}
|
||||
return hosts[rand.Intn(len(hosts))]
|
||||
}
|
||||
680
vendor/github.com/vmware/vic/pkg/vsphere/vm/vm.go
generated
vendored
Normal file
680
vendor/github.com/vmware/vic/pkg/vsphere/vm/vm.go
generated
vendored
Normal file
@@ -0,0 +1,680 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package vm
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/property"
|
||||
"github.com/vmware/govmomi/vim25/methods"
|
||||
"github.com/vmware/govmomi/vim25/mo"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/extraconfig/vmomi"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/tasks"
|
||||
)
|
||||
|
||||
const (
|
||||
DestroyTask = "Destroy_Task"
|
||||
UpdateStatus = "UpdateInProgress"
|
||||
)
|
||||
|
||||
type InvalidState struct {
|
||||
r types.ManagedObjectReference
|
||||
}
|
||||
|
||||
func (i *InvalidState) Error() string {
|
||||
return fmt.Sprintf("vm %s is invalid", i.r.String())
|
||||
}
|
||||
|
||||
// VirtualMachine struct defines the VirtualMachine which provides additional
|
||||
// VIC specific methods over object.VirtualMachine as well as keeps some state
|
||||
type VirtualMachine struct {
|
||||
// TODO: Wrap Internal VirtualMachine struct when we have it
|
||||
// *internal.VirtualMachine
|
||||
|
||||
*object.VirtualMachine
|
||||
|
||||
*session.Session
|
||||
|
||||
// fxing is 1 means this vm is fixing for it's in invalid status. 0 means not in fixing status
|
||||
fixing int32
|
||||
}
|
||||
|
||||
// NewVirtualMachine returns a NewVirtualMachine object
|
||||
func NewVirtualMachine(ctx context.Context, session *session.Session, moref types.ManagedObjectReference) *VirtualMachine {
|
||||
return NewVirtualMachineFromVM(ctx, session, object.NewVirtualMachine(session.Vim25(), moref))
|
||||
}
|
||||
|
||||
// NewVirtualMachineFromVM returns a NewVirtualMachine object
|
||||
func NewVirtualMachineFromVM(ctx context.Context, session *session.Session, vm *object.VirtualMachine) *VirtualMachine {
|
||||
return &VirtualMachine{
|
||||
VirtualMachine: vm,
|
||||
Session: session,
|
||||
}
|
||||
}
|
||||
|
||||
// VMPathNameAsURL returns the full datastore path of the VM as a url. The datastore name is in the host
|
||||
// portion, the path is in the Path field, the scheme is set to "ds"
|
||||
func (vm *VirtualMachine) VMPathNameAsURL(ctx context.Context) (url.URL, error) {
|
||||
op := trace.FromContext(ctx, "VMPathNameAsURL")
|
||||
|
||||
var mvm mo.VirtualMachine
|
||||
|
||||
if err := vm.Properties(op, vm.Reference(), []string{"config.files.vmPathName"}, &mvm); err != nil {
|
||||
op.Errorf("Unable to get managed config for VM: %s", err)
|
||||
return url.URL{}, err
|
||||
}
|
||||
|
||||
if mvm.Config == nil {
|
||||
return url.URL{}, errors.New("failed to get datastore path - config not found")
|
||||
}
|
||||
|
||||
path := path.Dir(mvm.Config.Files.VmPathName)
|
||||
val := url.URL{
|
||||
Scheme: "ds",
|
||||
}
|
||||
|
||||
// split the dsPath into the url components
|
||||
if ix := strings.Index(path, "] "); ix != -1 {
|
||||
val.Host = path[strings.Index(path, "[")+1 : ix]
|
||||
val.Path = path[ix+2:]
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// FolderName returns the name of the namespace(vsan) or directory(vmfs) that holds the VM
|
||||
// this equates to the normal directory that contains the vmx file, stripped of any parent path
|
||||
func (vm *VirtualMachine) FolderName(ctx context.Context) (string, error) {
|
||||
op := trace.FromContext(ctx, "FolderName")
|
||||
|
||||
u, err := vm.VMPathNameAsURL(op)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Base(u.Path), nil
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) getNetworkName(op trace.Operation, nic types.BaseVirtualEthernetCard) (string, error) {
|
||||
if card, ok := nic.GetVirtualEthernetCard().Backing.(*types.VirtualEthernetCardDistributedVirtualPortBackingInfo); ok {
|
||||
pg := card.Port.PortgroupKey
|
||||
pgref := object.NewDistributedVirtualPortgroup(vm.Session.Vim25(), types.ManagedObjectReference{
|
||||
Type: "DistributedVirtualPortgroup",
|
||||
Value: pg,
|
||||
})
|
||||
|
||||
var pgo mo.DistributedVirtualPortgroup
|
||||
err := pgref.Properties(op, pgref.Reference(), []string{"config"}, &pgo)
|
||||
if err != nil {
|
||||
op.Errorf("Failed to query portgroup %s for %s", pg, err)
|
||||
return "", err
|
||||
}
|
||||
return pgo.Config.Name, nil
|
||||
}
|
||||
return nic.GetVirtualEthernetCard().DeviceInfo.GetDescription().Summary, nil
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) FetchExtraConfigBaseOptions(ctx context.Context) ([]types.BaseOptionValue, error) {
|
||||
op := trace.FromContext(ctx, "FetchExtraConfigBaseOptions")
|
||||
|
||||
var err error
|
||||
|
||||
var mvm mo.VirtualMachine
|
||||
|
||||
if err = vm.Properties(op, vm.Reference(), []string{"config.extraConfig"}, &mvm); err != nil {
|
||||
op.Errorf("Unable to get vm config: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mvm.Config.ExtraConfig, nil
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) FetchExtraConfig(ctx context.Context) (map[string]string, error) {
|
||||
op := trace.FromContext(ctx, "FetchExtraConfig")
|
||||
|
||||
info := make(map[string]string)
|
||||
|
||||
v, err := vm.FetchExtraConfigBaseOptions(op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, bov := range v {
|
||||
ov := bov.GetOptionValue()
|
||||
value, _ := ov.Value.(string)
|
||||
info[ov.Key] = value
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// WaitForExtraConfig waits until key shows up with the expected value inside the ExtraConfig
|
||||
func (vm *VirtualMachine) WaitForExtraConfig(ctx context.Context, waitFunc func(pc []types.PropertyChange) bool) error {
|
||||
op := trace.FromContext(ctx, "WaitForExtraConfig")
|
||||
|
||||
// Get the default collector
|
||||
p := property.DefaultCollector(vm.Vim25())
|
||||
|
||||
// Wait on config.extraConfig
|
||||
// https://www.vmware.com/support/developer/vc-sdk/visdk2xpubs/ReferenceGuide/vim.vm.ConfigInfo.html
|
||||
return property.Wait(op, p, vm.Reference(), []string{"config.extraConfig", object.PropRuntimePowerState}, waitFunc)
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) WaitForKeyInExtraConfig(ctx context.Context, key string) (string, error) {
|
||||
op := trace.FromContext(ctx, "WaitForKeyInExtraConfig")
|
||||
|
||||
var detail string
|
||||
var poweredOff error
|
||||
|
||||
waitFunc := func(pc []types.PropertyChange) bool {
|
||||
for _, c := range pc {
|
||||
if c.Op != types.PropertyChangeOpAssign {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v := c.Val.(type) {
|
||||
case types.ArrayOfOptionValue:
|
||||
for _, value := range v.OptionValue {
|
||||
// check the status of the key and return true if it's been set to non-nil
|
||||
if key == value.GetOptionValue().Key {
|
||||
detail = value.GetOptionValue().Value.(string)
|
||||
if detail != "" && detail != "<nil>" {
|
||||
// ensure we clear any tentative error
|
||||
poweredOff = nil
|
||||
|
||||
return true
|
||||
}
|
||||
break // continue the outer loop as we may have a powerState change too
|
||||
}
|
||||
}
|
||||
case types.VirtualMachinePowerState:
|
||||
// Give up if the vm has powered off
|
||||
if v != types.VirtualMachinePowerStatePoweredOn {
|
||||
msg := "powered off"
|
||||
if v == types.VirtualMachinePowerStateSuspended {
|
||||
// Unlikely, but possible if the VM was suspended out-of-band
|
||||
msg = string(v)
|
||||
}
|
||||
poweredOff = fmt.Errorf("container VM has unexpectedly %s", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return poweredOff != nil
|
||||
}
|
||||
|
||||
err := vm.WaitForExtraConfig(op, waitFunc)
|
||||
if err == nil && poweredOff != nil {
|
||||
err = poweredOff
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) Name(ctx context.Context) (string, error) {
|
||||
op := trace.FromContext(ctx, "Name")
|
||||
|
||||
var err error
|
||||
var mvm mo.VirtualMachine
|
||||
|
||||
if err = vm.Properties(op, vm.Reference(), []string{"summary.config"}, &mvm); err != nil {
|
||||
op.Errorf("Unable to get vm summary.config property: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return mvm.Summary.Config.Name, nil
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) UUID(ctx context.Context) (string, error) {
|
||||
op := trace.FromContext(ctx, "UUID")
|
||||
|
||||
var err error
|
||||
var mvm mo.VirtualMachine
|
||||
|
||||
if err = vm.Properties(op, vm.Reference(), []string{"summary.config"}, &mvm); err != nil {
|
||||
op.Errorf("Unable to get vm summary.config property: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return mvm.Summary.Config.Uuid, nil
|
||||
}
|
||||
|
||||
// DeleteExceptDisks destroys the VM after detaching all virtual disks
|
||||
func (vm *VirtualMachine) DeleteExceptDisks(ctx context.Context) (*object.Task, error) {
|
||||
op := trace.FromContext(ctx, "DeleteExceptDisks")
|
||||
|
||||
devices, err := vm.Device(op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
disks := devices.SelectByType(&types.VirtualDisk{})
|
||||
err = vm.RemoveDevice(op, true, disks...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vm.Destroy(op)
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) VMPathName(ctx context.Context) (string, error) {
|
||||
op := trace.FromContext(ctx, "VMPathName")
|
||||
|
||||
var err error
|
||||
var mvm mo.VirtualMachine
|
||||
|
||||
if err = vm.Properties(op, vm.Reference(), []string{"config.files.vmPathName"}, &mvm); err != nil {
|
||||
op.Errorf("Unable to get vm config.files property: %s", err)
|
||||
return "", err
|
||||
}
|
||||
return mvm.Config.Files.VmPathName, nil
|
||||
}
|
||||
|
||||
// GetCurrentSnapshotTree returns current snapshot, with tree information
|
||||
func (vm *VirtualMachine) GetCurrentSnapshotTree(ctx context.Context) (*types.VirtualMachineSnapshotTree, error) {
|
||||
op := trace.FromContext(ctx, "GetCurrentSnapshotTree")
|
||||
|
||||
var err error
|
||||
var mvm mo.VirtualMachine
|
||||
|
||||
if err = vm.Properties(op, vm.Reference(), []string{"snapshot"}, &mvm); err != nil {
|
||||
op.Infof("Unable to get vm properties: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
if mvm.Snapshot == nil {
|
||||
// no snapshot at all
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
current := mvm.Snapshot.CurrentSnapshot
|
||||
q := list.New()
|
||||
for _, c := range mvm.Snapshot.RootSnapshotList {
|
||||
q.PushBack(c)
|
||||
}
|
||||
|
||||
compareID := func(node types.VirtualMachineSnapshotTree) bool {
|
||||
if node.Snapshot == *current {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return vm.bfsSnapshotTree(q, compareID), nil
|
||||
}
|
||||
|
||||
// GetCurrentSnapshotTreeByName returns current snapshot, with tree information
|
||||
func (vm *VirtualMachine) GetSnapshotTreeByName(ctx context.Context, name string) (*types.VirtualMachineSnapshotTree, error) {
|
||||
op := trace.FromContext(ctx, "GetSnapshotTreeByName")
|
||||
|
||||
var err error
|
||||
var mvm mo.VirtualMachine
|
||||
|
||||
if err = vm.Properties(op, vm.Reference(), []string{"snapshot"}, &mvm); err != nil {
|
||||
op.Infof("Unable to get vm properties: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
if mvm.Snapshot == nil {
|
||||
// no snapshot at all
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
q := list.New()
|
||||
for _, c := range mvm.Snapshot.RootSnapshotList {
|
||||
q.PushBack(c)
|
||||
}
|
||||
|
||||
compareName := func(node types.VirtualMachineSnapshotTree) bool {
|
||||
if node.Name == name {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return vm.bfsSnapshotTree(q, compareName), nil
|
||||
}
|
||||
|
||||
// Finds a snapshot tree based on comparator function 'compare' via a breadth first search of the snapshot tree attached to the VM
|
||||
func (vm *VirtualMachine) bfsSnapshotTree(q *list.List, compare func(node types.VirtualMachineSnapshotTree) bool) *types.VirtualMachineSnapshotTree {
|
||||
if q.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
e := q.Front()
|
||||
tree := q.Remove(e).(types.VirtualMachineSnapshotTree)
|
||||
if compare(tree) {
|
||||
return &tree
|
||||
}
|
||||
for _, c := range tree.ChildSnapshotList {
|
||||
q.PushBack(c)
|
||||
}
|
||||
return vm.bfsSnapshotTree(q, compare)
|
||||
}
|
||||
|
||||
// IsConfigureSnapshot is the helper func that returns true if node is a snapshot with specified name prefix
|
||||
func IsConfigureSnapshot(node *types.VirtualMachineSnapshotTree, prefix string) bool {
|
||||
return node != nil && strings.HasPrefix(node.Name, prefix)
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) registerVM(op trace.Operation, path, name string,
|
||||
vapp, pool, host *types.ManagedObjectReference, vmfolder *object.Folder) (*object.Task, error) {
|
||||
op.Debugf("Register VM %s", name)
|
||||
|
||||
if vapp == nil {
|
||||
var hostObject *object.HostSystem
|
||||
if host != nil {
|
||||
hostObject = object.NewHostSystem(vm.Vim25(), *host)
|
||||
}
|
||||
poolObject := object.NewResourcePool(vm.Vim25(), *pool)
|
||||
return vmfolder.RegisterVM(op, path, name, false, poolObject, hostObject)
|
||||
}
|
||||
|
||||
req := types.RegisterChildVM_Task{
|
||||
This: vapp.Reference(),
|
||||
Path: path,
|
||||
Host: host,
|
||||
}
|
||||
|
||||
if name != "" {
|
||||
req.Name = name
|
||||
}
|
||||
|
||||
res, err := methods.RegisterChildVM_Task(op, vm.Vim25(), &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return object.NewTask(vm.Vim25(), res.Returnval), nil
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) IsFixing() bool {
|
||||
return vm.fixing > 0
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) EnterFixingState() {
|
||||
atomic.AddInt32(&vm.fixing, 1)
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) LeaveFixingState() {
|
||||
atomic.StoreInt32(&vm.fixing, 0)
|
||||
}
|
||||
|
||||
// FixInvalidState fix vm invalid state issue through unregister & register
|
||||
func (vm *VirtualMachine) fixVM(op trace.Operation) error {
|
||||
op.Debugf("Fix invalid state VM: %s", vm.Reference())
|
||||
|
||||
properties := []string{"summary.config", "summary.runtime.host", "resourcePool", "parentVApp"}
|
||||
op.Debugf("Get vm properties %s", properties)
|
||||
var mvm mo.VirtualMachine
|
||||
if err := vm.VirtualMachine.Properties(op, vm.Reference(), properties, &mvm); err != nil {
|
||||
op.Errorf("Unable to get vm properties: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
name := mvm.Summary.Config.Name
|
||||
op.Debugf("Unregister VM %s", name)
|
||||
vm.EnterFixingState()
|
||||
if err := vm.Unregister(op); err != nil {
|
||||
op.Errorf("Unable to unregister vm %q: %s", name, err)
|
||||
|
||||
// Leave fixing state since it will not be reset in the remove event handler
|
||||
vm.LeaveFixingState()
|
||||
return err
|
||||
}
|
||||
|
||||
task, err := vm.registerVM(op, mvm.Summary.Config.VmPathName, name, mvm.ParentVApp, mvm.ResourcePool, mvm.Summary.Runtime.Host, vm.Session.VMFolder)
|
||||
if err != nil {
|
||||
op.Errorf("Unable to register VM %q back: %s", name, err)
|
||||
return err
|
||||
}
|
||||
info, err := task.WaitForResult(op, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// re-register vm will change vm reference, so reset the object reference here
|
||||
if info.Error != nil {
|
||||
return errors.New(info.Error.LocalizedMessage)
|
||||
}
|
||||
|
||||
// set new registered vm attribute back
|
||||
newRef := info.Result.(types.ManagedObjectReference)
|
||||
common := object.NewCommon(vm.Vim25(), newRef)
|
||||
common.InventoryPath = vm.InventoryPath
|
||||
vm.Common = common
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) needsFix(op trace.Operation, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if vm.IsInvalidState(op) {
|
||||
op.Debugf("vm %s is invalid", vm.Reference())
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) IsInvalidState(ctx context.Context) bool {
|
||||
op := trace.FromContext(ctx, "IsInvalidState")
|
||||
|
||||
var o mo.VirtualMachine
|
||||
if err := vm.VirtualMachine.Properties(op, vm.Reference(), []string{"summary.runtime.connectionState"}, &o); err != nil {
|
||||
op.Debugf("Failed to get vm properties: %s", err)
|
||||
return false
|
||||
}
|
||||
if o.Summary.Runtime.ConnectionState == types.VirtualMachineConnectionStateInvalid {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WaitForResult is designed to handle VM invalid state error for any VM operations.
|
||||
// It will call tasks.WaitForResult to retry if there is task in progress error.
|
||||
func (vm *VirtualMachine) WaitForResult(ctx context.Context, f func(context.Context) (tasks.Task, error)) (*types.TaskInfo, error) {
|
||||
op := trace.FromContext(ctx, "WaitForResult")
|
||||
|
||||
info, err := tasks.WaitForResult(op, f)
|
||||
if err == nil || !vm.needsFix(op, err) {
|
||||
return info, err
|
||||
}
|
||||
|
||||
op.Debugf("Try to fix task failure %s", err)
|
||||
if nerr := vm.fixVM(op); nerr != nil {
|
||||
op.Errorf("Failed to fix task failure: %s", nerr)
|
||||
return info, err
|
||||
}
|
||||
op.Debug("Fixed")
|
||||
return tasks.WaitForResult(op, f)
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) Properties(ctx context.Context, r types.ManagedObjectReference, ps []string, o *mo.VirtualMachine) error {
|
||||
// lets ensure we have an operation
|
||||
op := trace.FromContext(ctx, "VM Properties")
|
||||
defer trace.End(trace.Begin(fmt.Sprintf("VM(%s) Properties(%s)", r, ps), op))
|
||||
|
||||
contains := false
|
||||
for _, v := range ps {
|
||||
if v == "summary" || v == "summary.runtime" {
|
||||
contains = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !contains {
|
||||
ps = append(ps, "summary.runtime.connectionState")
|
||||
}
|
||||
|
||||
op.Debugf("properties: %s", ps)
|
||||
|
||||
if err := vm.VirtualMachine.Properties(op, r, ps, o); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.Summary.Runtime.ConnectionState != types.VirtualMachineConnectionStateInvalid {
|
||||
return nil
|
||||
}
|
||||
op.Infof("vm %s is in invalid state", r)
|
||||
if err := vm.fixVM(op); err != nil {
|
||||
op.Errorf("Failed to fix vm %s: %s", vm.Reference(), err)
|
||||
return &InvalidState{r: vm.Reference()}
|
||||
}
|
||||
|
||||
return vm.VirtualMachine.Properties(op, vm.Reference(), ps, o)
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) Parent(ctx context.Context) (*types.ManagedObjectReference, error) {
|
||||
op := trace.FromContext(ctx, "Parent")
|
||||
|
||||
var mvm mo.VirtualMachine
|
||||
|
||||
if err := vm.Properties(op, vm.Reference(), []string{"parentVApp", "resourcePool"}, &mvm); err != nil {
|
||||
op.Errorf("Unable to get VM parent: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
if mvm.ParentVApp != nil {
|
||||
return mvm.ParentVApp, nil
|
||||
}
|
||||
return mvm.ResourcePool, nil
|
||||
}
|
||||
|
||||
func (vm *VirtualMachine) DatastoreReference(ctx context.Context) ([]types.ManagedObjectReference, error) {
|
||||
op := trace.FromContext(ctx, "DatastoreReference")
|
||||
|
||||
var mvm mo.VirtualMachine
|
||||
|
||||
if err := vm.Properties(op, vm.Reference(), []string{"datastore"}, &mvm); err != nil {
|
||||
op.Errorf("Unable to get VM datastore: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
return mvm.Datastore, nil
|
||||
}
|
||||
|
||||
// VCHUpdateStatus tells if an upgrade/configure has already been started based on the UpdateInProgress flag in ExtraConfig
|
||||
// It returns the error if the vm operation does not succeed
|
||||
func (vm *VirtualMachine) VCHUpdateStatus(ctx context.Context) (bool, error) {
|
||||
op := trace.FromContext(ctx, "VCHUpdateStatus")
|
||||
|
||||
info, err := vm.FetchExtraConfig(op)
|
||||
if err != nil {
|
||||
op.Errorf("Unable to get vm ExtraConfig: %s", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if v, ok := info[UpdateStatus]; ok {
|
||||
status, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
// If error occurs, the bool return value does not matter for the caller.
|
||||
return false, fmt.Errorf("failed to parse %s to bool: %s", v, err)
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// If UpdateStatus is not found, it might be the case that no upgrade/configure has been done to this VCH before
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// SetVCHUpdateStatus sets the VCH update status in ExtraConfig
|
||||
func (vm *VirtualMachine) SetVCHUpdateStatus(ctx context.Context, status bool) error {
|
||||
op := trace.FromContext(ctx, "SetVCHUpdateStatus")
|
||||
|
||||
info := make(map[string]string)
|
||||
info[UpdateStatus] = strconv.FormatBool(status)
|
||||
|
||||
s := &types.VirtualMachineConfigSpec{
|
||||
ExtraConfig: vmomi.OptionValueFromMap(info, true),
|
||||
}
|
||||
|
||||
_, err := vm.WaitForResult(op, func(op context.Context) (tasks.Task, error) {
|
||||
return vm.Reconfigure(op, *s)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DisableDestroy attempts to disable the VirtualMachine.Destroy_Task method.
|
||||
// When the method is disabled, the VC UI will disable/grey out the VM "delete" action.
|
||||
// Requires the "Global.Disable" VC privilege.
|
||||
// The VirtualMachine.Destroy_Task method can still be invoked via the API.
|
||||
func (vm *VirtualMachine) DisableDestroy(ctx context.Context) {
|
||||
if !vm.IsVC() {
|
||||
return
|
||||
}
|
||||
|
||||
op := trace.FromContext(ctx, "DisableDestroy")
|
||||
|
||||
m := object.NewAuthorizationManager(vm.Vim25())
|
||||
|
||||
method := []object.DisabledMethodRequest{
|
||||
{
|
||||
Method: DestroyTask,
|
||||
Reason: "Managed by VIC Engine",
|
||||
},
|
||||
}
|
||||
|
||||
obj := []types.ManagedObjectReference{vm.Reference()}
|
||||
|
||||
err := m.DisableMethods(op, obj, method, "VIC")
|
||||
if err != nil {
|
||||
op.Warnf("Failed to disable method %s for %s: %s", method[0].Method, obj[0], err)
|
||||
}
|
||||
}
|
||||
|
||||
// EnableDestroy attempts to enable the VirtualMachine.Destroy_Task method
|
||||
// so that the VC UI can enable the VM "delete" action.
|
||||
func (vm *VirtualMachine) EnableDestroy(ctx context.Context) {
|
||||
if !vm.IsVC() {
|
||||
return
|
||||
}
|
||||
|
||||
op := trace.FromContext(ctx, "EnableDestroy")
|
||||
|
||||
m := object.NewAuthorizationManager(vm.Vim25())
|
||||
|
||||
obj := []types.ManagedObjectReference{vm.Reference()}
|
||||
|
||||
err := m.EnableMethods(op, obj, []string{DestroyTask}, "VIC")
|
||||
if err != nil {
|
||||
op.Warnf("Failed to enable Destroy_Task for %s: %s", obj[0], err)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveSnapshot removes a snapshot by reference
|
||||
func (vm *VirtualMachine) RemoveSnapshotByRef(ctx context.Context, snapshot *types.ManagedObjectReference, removeChildren bool, consolidate *bool) (*object.Task, error) {
|
||||
req := types.RemoveSnapshot_Task{
|
||||
This: snapshot.Reference(),
|
||||
RemoveChildren: removeChildren,
|
||||
Consolidate: consolidate,
|
||||
}
|
||||
|
||||
res, err := methods.RemoveSnapshot_Task(ctx, vm.Vim25(), &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return object.NewTask(vm.Vim25(), res.Returnval), nil
|
||||
}
|
||||
777
vendor/github.com/vmware/vic/pkg/vsphere/vm/vm_test.go
generated
vendored
Normal file
777
vendor/github.com/vmware/vic/pkg/vsphere/vm/vm_test.go
generated
vendored
Normal file
@@ -0,0 +1,777 @@
|
||||
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package vm
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/vmware/govmomi"
|
||||
"github.com/vmware/govmomi/find"
|
||||
"github.com/vmware/govmomi/object"
|
||||
"github.com/vmware/govmomi/simulator"
|
||||
"github.com/vmware/govmomi/vim25/mo"
|
||||
"github.com/vmware/govmomi/vim25/types"
|
||||
"github.com/vmware/vic/lib/guest"
|
||||
"github.com/vmware/vic/pkg/trace"
|
||||
"github.com/vmware/vic/pkg/vsphere/extraconfig/vmomi"
|
||||
"github.com/vmware/vic/pkg/vsphere/session"
|
||||
"github.com/vmware/vic/pkg/vsphere/sys"
|
||||
"github.com/vmware/vic/pkg/vsphere/tasks"
|
||||
"github.com/vmware/vic/pkg/vsphere/test"
|
||||
)
|
||||
|
||||
func CreateVM(ctx context.Context, session *session.Session, host *object.HostSystem, name string) (*types.ManagedObjectReference, error) {
|
||||
// Create the spec config
|
||||
specconfig := test.SpecConfig(session, name)
|
||||
|
||||
// Create a linux guest
|
||||
linux, err := guest.NewLinuxGuest(ctx, session, specconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the vm
|
||||
task, err := session.VMFolder.CreateVM(ctx, *linux.Spec().Spec(), session.Pool, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := task.WaitForResult(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
moref := info.Result.(types.ManagedObjectReference)
|
||||
// Return the moRef
|
||||
return &moref, nil
|
||||
}
|
||||
|
||||
func TestDeleteExceptDisk(t *testing.T) {
|
||||
s := os.Getenv("DRONE")
|
||||
if s != "" {
|
||||
t.Skip("Skipping: test must be run in a VM")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
session := test.Session(ctx, t)
|
||||
defer session.Logout(ctx)
|
||||
|
||||
host := test.PickRandomHost(ctx, session, t)
|
||||
|
||||
uuid, err := sys.UUID()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get UUID for guest - used for VM name: %s", err)
|
||||
}
|
||||
name := fmt.Sprintf("%s-%d", uuid, rand.Intn(math.MaxInt32))
|
||||
|
||||
moref, err := CreateVM(ctx, session, host, name)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
// Wrap the result with our version of VirtualMachine
|
||||
vm := NewVirtualMachine(ctx, session, *moref)
|
||||
|
||||
folder, err := vm.FolderName(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
|
||||
// generate the disk name
|
||||
diskName := fmt.Sprintf("%s/%s.vmdk", folder, folder)
|
||||
|
||||
// Delete the VM but not it's disk
|
||||
task, err := vm.DeleteExceptDisks(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
_, err = task.WaitForResult(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
|
||||
// check that the disk still exists
|
||||
session.Datastore.Stat(ctx, diskName)
|
||||
if err != nil {
|
||||
t.Fatalf("Disk does not exist")
|
||||
}
|
||||
|
||||
// clean up
|
||||
dm := object.NewVirtualDiskManager(session.Client.Client)
|
||||
|
||||
task, err = dm.DeleteVirtualDisk(context.TODO(), diskName, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to locate orphan vmdk: %s", err)
|
||||
}
|
||||
|
||||
if err = task.Wait(context.TODO()); err != nil {
|
||||
t.Fatalf("Unable to remove orphan vmdk: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVM(t *testing.T) {
|
||||
|
||||
s := os.Getenv("DRONE")
|
||||
if s != "" {
|
||||
t.Skip("Skipping: test must be run in a VM")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
session := test.Session(ctx, t)
|
||||
defer session.Logout(ctx)
|
||||
|
||||
host := test.PickRandomHost(ctx, session, t)
|
||||
|
||||
uuid, err := sys.UUID()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get UUID for guest - used for VM name: %s", err)
|
||||
return
|
||||
}
|
||||
name := fmt.Sprintf("%s-%d", uuid, rand.Intn(math.MaxInt32))
|
||||
|
||||
moref, err := CreateVM(ctx, session, host, name)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
// Wrap the result with our version of VirtualMachine
|
||||
vm := NewVirtualMachine(ctx, session, *moref)
|
||||
|
||||
// Check the state
|
||||
state, err := vm.PowerState(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, types.VirtualMachinePowerStatePoweredOff, state)
|
||||
|
||||
// Check VM name
|
||||
rname, err := vm.Name(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load VM name: %s", err)
|
||||
}
|
||||
assert.Equal(t, name, rname)
|
||||
|
||||
// Get VM UUID
|
||||
ruuid, err := vm.UUID(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to load VM UUID: %s", err)
|
||||
}
|
||||
t.Logf("Got UUID: %s", ruuid)
|
||||
|
||||
err = vm.fixVM(trace.FromContext(ctx, "TestVM"))
|
||||
if err != nil {
|
||||
t.Errorf("Failed to fix vm: %s", err)
|
||||
}
|
||||
newVM, err := session.Finder.VirtualMachine(ctx, name)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to find fixed vm: %s", err)
|
||||
}
|
||||
assert.Equal(t, vm.Reference(), newVM.Reference())
|
||||
|
||||
// VM properties
|
||||
var ovm mo.VirtualMachine
|
||||
if err = vm.Properties(ctx, newVM.Reference(), []string{"config"}, &ovm); err != nil {
|
||||
t.Errorf("Failed to get vm properties: %s", err)
|
||||
}
|
||||
|
||||
// Destroy the vm
|
||||
task, err := vm.Destroy(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
_, err = task.WaitForResult(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMFailureWithTimeout(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
session := test.Session(ctx, t)
|
||||
defer session.Logout(ctx)
|
||||
|
||||
host := test.PickRandomHost(ctx, session, t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Microsecond)
|
||||
defer cancel()
|
||||
|
||||
uuid, err := sys.UUID()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get UUID for guest - used for VM name: %s", err)
|
||||
return
|
||||
}
|
||||
name := fmt.Sprintf("%s-%d", uuid, rand.Intn(math.MaxInt32))
|
||||
|
||||
_, err = CreateVM(ctx, session, host, name)
|
||||
if err != nil && err != context.DeadlineExceeded {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMAttributes(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
session := test.Session(ctx, t)
|
||||
defer session.Logout(ctx)
|
||||
|
||||
host := test.PickRandomHost(ctx, session, t)
|
||||
|
||||
uuid, err := sys.UUID()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get UUID for guest - used for VM name: %s", err)
|
||||
return
|
||||
}
|
||||
ID := fmt.Sprintf("%s-%d", uuid, rand.Intn(math.MaxInt32))
|
||||
|
||||
moref, err := CreateVM(ctx, session, host, ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
// Wrap the result with our version of VirtualMachine
|
||||
vm := NewVirtualMachine(ctx, session, *moref)
|
||||
|
||||
folder, err := vm.FolderName(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
|
||||
name, err := vm.Name(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
assert.Equal(t, name, folder)
|
||||
task, err := vm.PowerOn(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
_, err = task.WaitForResult(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
|
||||
if guest, err := vm.FetchExtraConfig(ctx); err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
} else {
|
||||
assert.NotEmpty(t, guest)
|
||||
}
|
||||
defer func() {
|
||||
// Destroy the vm
|
||||
task, err := vm.PowerOff(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
_, err = task.WaitForResult(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
task, err = vm.Destroy(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
_, err = task.WaitForResult(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func TestWaitForKeyInExtraConfig(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
m := simulator.ESX()
|
||||
defer m.Remove()
|
||||
err := m.Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server := m.Service.NewServer()
|
||||
defer server.Close()
|
||||
|
||||
config := &session.Config{
|
||||
Service: server.URL.String(),
|
||||
}
|
||||
|
||||
s, err := session.NewSession(config).Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if s, err = s.Populate(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
vms, err := s.Finder.VirtualMachineList(ctx, "*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
vm := NewVirtualMachineFromVM(ctx, s, vms[0])
|
||||
|
||||
opt := &types.OptionValue{Key: "foo", Value: "bar"}
|
||||
obj := simulator.Map.Get(vm.Reference()).(*simulator.VirtualMachine)
|
||||
|
||||
val, err := vm.WaitForKeyInExtraConfig(ctx, opt.Key)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
|
||||
obj.Config.ExtraConfig = append(obj.Config.ExtraConfig, opt)
|
||||
obj.Summary.Runtime.PowerState = types.VirtualMachinePowerStatePoweredOn
|
||||
|
||||
val, err = vm.WaitForKeyInExtraConfig(ctx, opt.Key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if val != opt.Value {
|
||||
t.Errorf("%s != %s", val, opt.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func createSnapshotTree(prefix string, deep int, wide int) []types.VirtualMachineSnapshotTree {
|
||||
var result []types.VirtualMachineSnapshotTree
|
||||
if deep == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := 1; i <= wide; i++ {
|
||||
nodeID := fmt.Sprintf("%s%d", prefix, i)
|
||||
node := types.VirtualMachineSnapshotTree{
|
||||
Snapshot: types.ManagedObjectReference{
|
||||
Type: "Snapshot",
|
||||
Value: nodeID,
|
||||
},
|
||||
Name: nodeID,
|
||||
}
|
||||
node.ChildSnapshotList = createSnapshotTree(nodeID, deep-1, wide)
|
||||
result = append(result, node)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestBfsSnapshotTree(t *testing.T) {
|
||||
ref := &types.ManagedObjectReference{
|
||||
Type: "Snapshot",
|
||||
Value: "12131",
|
||||
}
|
||||
rootList := createSnapshotTree("", 5, 5)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
session := test.Session(ctx, t)
|
||||
defer session.Logout(ctx)
|
||||
vm := NewVirtualMachine(ctx, session, *ref)
|
||||
q := list.New()
|
||||
for _, c := range rootList {
|
||||
q.PushBack(c)
|
||||
}
|
||||
|
||||
compareID := func(node types.VirtualMachineSnapshotTree) bool {
|
||||
if node.Snapshot == *ref {
|
||||
t.Logf("Found match")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
current := vm.bfsSnapshotTree(q, compareID)
|
||||
if current == nil {
|
||||
t.Errorf("Should found current snapshot")
|
||||
}
|
||||
q = list.New()
|
||||
for _, c := range rootList {
|
||||
q.PushBack(c)
|
||||
}
|
||||
|
||||
ref = &types.ManagedObjectReference{
|
||||
Type: "Snapshot",
|
||||
Value: "185",
|
||||
}
|
||||
current = vm.bfsSnapshotTree(q, compareID)
|
||||
if current != nil {
|
||||
t.Errorf("Should not found snapshot")
|
||||
}
|
||||
|
||||
name := "12131"
|
||||
compareName := func(node types.VirtualMachineSnapshotTree) bool {
|
||||
if node.Name == name {
|
||||
t.Logf("Found match")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
q = list.New()
|
||||
for _, c := range rootList {
|
||||
q.PushBack(c)
|
||||
}
|
||||
found := vm.bfsSnapshotTree(q, compareName)
|
||||
if found == nil {
|
||||
t.Errorf("Should found snapshot %q", name)
|
||||
}
|
||||
q = list.New()
|
||||
for _, c := range rootList {
|
||||
q.PushBack(c)
|
||||
}
|
||||
name = "185"
|
||||
found = vm.bfsSnapshotTree(q, compareName)
|
||||
if found != nil {
|
||||
t.Errorf("Should not found snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
// TestProperties test vm.properties happy path and fix vm path
|
||||
func TestIsFixing(t *testing.T) {
|
||||
mo := types.ManagedObjectReference{Type: "vm", Value: "12"}
|
||||
v := object.NewVirtualMachine(nil, mo)
|
||||
vm := NewVirtualMachineFromVM(nil, nil, v)
|
||||
assert.False(t, vm.IsFixing(), "new vm should not in fixing status")
|
||||
vm.EnterFixingState()
|
||||
assert.True(t, vm.IsFixing(), "vm should be in fixing status")
|
||||
vm.EnterFixingState()
|
||||
assert.True(t, vm.IsFixing(), "vm should be in fixing status")
|
||||
vm.LeaveFixingState()
|
||||
assert.False(t, vm.IsFixing(), "vm should not be in fixing status")
|
||||
}
|
||||
|
||||
// TestProperties test vm.properties happy path and fix vm path
|
||||
func TestProperties(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Nothing VC specific in this test, so we use the simpler ESX model
|
||||
model := simulator.ESX()
|
||||
model.Autostart = false
|
||||
defer model.Remove()
|
||||
err := model.Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server := model.Service.NewServer()
|
||||
defer server.Close()
|
||||
client, err := govmomi.NewClient(ctx, server.URL, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Any VM will do
|
||||
finder := find.NewFinder(client.Client, false)
|
||||
vmo, err := finder.VirtualMachine(ctx, "/ha-datacenter/vm/*_VM0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
config := &session.Config{
|
||||
Service: server.URL.String(),
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
DatacenterPath: "",
|
||||
DatastorePath: "/ha-datacenter/datastore/*",
|
||||
HostPath: "/ha-datacenter/host/*/*",
|
||||
PoolPath: "/ha-datacenter/host/*/Resources",
|
||||
}
|
||||
|
||||
s, err := session.NewSession(config).Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Populate(ctx)
|
||||
vmm := NewVirtualMachine(ctx, s, vmo.Reference())
|
||||
// Test the success path
|
||||
var o mo.VirtualMachine
|
||||
err = vmm.Properties(ctx, vmo.Reference(), []string{"config", "summary", "resourcePool", "parentVApp"}, &o)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// // Inject invalid connection state to vm
|
||||
ref := simulator.Map.Get(vmo.Reference()).(*simulator.VirtualMachine)
|
||||
ref.Summary.Config.VmPathName = ref.Config.Files.VmPathName
|
||||
ref.Summary.Runtime.ConnectionState = types.VirtualMachineConnectionStateInvalid
|
||||
|
||||
err = vmm.Properties(ctx, vmo.Reference(), []string{"config", "summary"}, &o)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.True(t, o.Summary.Runtime.ConnectionState != types.VirtualMachineConnectionStateInvalid, "vm state should be fixed")
|
||||
}
|
||||
|
||||
// TestWaitForResult covers the success path and invalid vm fix path
|
||||
func TestWaitForResult(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Nothing VC specific in this test, so we use the simpler ESX model
|
||||
model := simulator.ESX()
|
||||
model.Autostart = false
|
||||
defer model.Remove()
|
||||
err := model.Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server := model.Service.NewServer()
|
||||
defer server.Close()
|
||||
|
||||
client, err := govmomi.NewClient(ctx, server.URL, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Any VM will do
|
||||
finder := find.NewFinder(client.Client, false)
|
||||
vmo, err := finder.VirtualMachine(ctx, "/ha-datacenter/vm/*_VM0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
config := &session.Config{
|
||||
Service: server.URL.String(),
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
DatacenterPath: "",
|
||||
DatastorePath: "/ha-datacenter/datastore/*",
|
||||
HostPath: "/ha-datacenter/host/*/*",
|
||||
PoolPath: "/ha-datacenter/host/*/Resources",
|
||||
}
|
||||
|
||||
s, err := session.NewSession(config).Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Populate(ctx)
|
||||
vmm := NewVirtualMachine(ctx, s, vmo.Reference())
|
||||
// Test the success path
|
||||
_, err = vmm.WaitForResult(ctx, func(ctx context.Context) (tasks.Task, error) {
|
||||
return vmm.PowerOn(ctx)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = vmm.WaitForResult(ctx, func(ctx context.Context) (tasks.Task, error) {
|
||||
return vmm.PowerOff(ctx)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ref := simulator.Map.Get(vmm.Reference()).(*simulator.VirtualMachine)
|
||||
|
||||
// Test task failed, but vm is not in invalid state
|
||||
called := 0
|
||||
_, err = vmm.WaitForResult(ctx, func(ctx context.Context) (tasks.Task, error) {
|
||||
called++
|
||||
return vmm.PowerOff(ctx)
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Should have error")
|
||||
}
|
||||
assert.True(t, called == 1, "task should not be retried")
|
||||
|
||||
// Test task failure with invalid state vm
|
||||
ref.Summary.Config.VmPathName = ref.Config.Files.VmPathName
|
||||
ref.Summary.Runtime.ConnectionState = types.VirtualMachineConnectionStateInvalid
|
||||
called = 0
|
||||
_, err = vmm.WaitForResult(ctx, func(ctx context.Context) (tasks.Task, error) {
|
||||
called++
|
||||
return vmm.PowerOff(ctx)
|
||||
})
|
||||
assert.True(t, called == 2, "task should be retried once")
|
||||
assert.True(t, !vmm.IsInvalidState(ctx), "vm state should be fixed")
|
||||
}
|
||||
|
||||
// SetUpdateStatus sets the VCH upgrade/configure status.
|
||||
func SetUpdateStatus(ctx context.Context, updateStatus string, vm *VirtualMachine) error {
|
||||
info := make(map[string]string)
|
||||
info[UpdateStatus] = updateStatus
|
||||
|
||||
s := &types.VirtualMachineConfigSpec{
|
||||
ExtraConfig: vmomi.OptionValueFromMap(info, true),
|
||||
}
|
||||
|
||||
_, err := vm.WaitForResult(ctx, func(ctx context.Context) (tasks.Task, error) {
|
||||
return vm.Reconfigure(ctx, *s)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestVCHUpdateStatus tests if VCHUpdateStatus() could obtain the correct VCH upgrade/configure status
|
||||
func TestVCHUpdateStatus(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Nothing VC specific in this test, so we use the simpler ESX model
|
||||
model := simulator.ESX()
|
||||
defer model.Remove()
|
||||
err := model.Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server := model.Service.NewServer()
|
||||
defer server.Close()
|
||||
client, err := govmomi.NewClient(ctx, server.URL, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Any VM will do
|
||||
finder := find.NewFinder(client.Client, false)
|
||||
vmo, err := finder.VirtualMachine(ctx, "/ha-datacenter/vm/*_VM0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
config := &session.Config{
|
||||
Service: server.URL.String(),
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
DatacenterPath: "",
|
||||
DatastorePath: "/ha-datacenter/datastore/*",
|
||||
HostPath: "/ha-datacenter/host/*/*",
|
||||
PoolPath: "/ha-datacenter/host/*/Resources",
|
||||
}
|
||||
|
||||
s, err := session.NewSession(config).Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Populate(ctx)
|
||||
vmm := NewVirtualMachine(ctx, s, vmo.Reference())
|
||||
|
||||
updateStatus, err := vmm.VCHUpdateStatus(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
assert.False(t, updateStatus, "updateStatus should be false if UpdateInProgress is not set in the VCH's ExtraConfig")
|
||||
|
||||
// Set UpdateInProgress to false
|
||||
SetUpdateStatus(ctx, "false", vmm)
|
||||
|
||||
updateStatus, err = vmm.VCHUpdateStatus(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
assert.False(t, updateStatus, "updateStatus should be false since UpdateInProgress is set to false")
|
||||
|
||||
// Set UpdateInProgress to true
|
||||
SetUpdateStatus(ctx, "true", vmm)
|
||||
|
||||
updateStatus, err = vmm.VCHUpdateStatus(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
assert.True(t, updateStatus, "updateStatus should be true since UpdateInProgress is set to true")
|
||||
|
||||
// Set UpdateInProgress to NonBool
|
||||
SetUpdateStatus(ctx, "NonBool", vmm)
|
||||
|
||||
updateStatus, err = vmm.VCHUpdateStatus(ctx)
|
||||
if assert.Error(t, err, "An error was expected") {
|
||||
assert.Contains(t, err.Error(), "failed to parse", "Error msg should contain 'failed to parse' since UpdateInProgress is set to NonBool")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetVCHUpdateStatus tests if SetVCHUpdateStatus() could set the VCH upgrade/configure status correctly
|
||||
func TestSetVCHUpdateStatus(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Nothing VC specific in this test, so we use the simpler ESX model
|
||||
model := simulator.ESX()
|
||||
defer model.Remove()
|
||||
err := model.Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server := model.Service.NewServer()
|
||||
defer server.Close()
|
||||
client, err := govmomi.NewClient(ctx, server.URL, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Any VM will do
|
||||
finder := find.NewFinder(client.Client, false)
|
||||
vmo, err := finder.VirtualMachine(ctx, "/ha-datacenter/vm/*_VM0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
config := &session.Config{
|
||||
Service: server.URL.String(),
|
||||
Insecure: true,
|
||||
Keepalive: time.Duration(5) * time.Minute,
|
||||
DatacenterPath: "",
|
||||
DatastorePath: "/ha-datacenter/datastore/*",
|
||||
HostPath: "/ha-datacenter/host/*/*",
|
||||
PoolPath: "/ha-datacenter/host/*/Resources",
|
||||
}
|
||||
|
||||
s, err := session.NewSession(config).Connect(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.Populate(ctx)
|
||||
vmm := NewVirtualMachine(ctx, s, vmo.Reference())
|
||||
|
||||
// Set UpdateInProgress to true and then check status
|
||||
err = vmm.SetVCHUpdateStatus(ctx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
|
||||
info, err := vmm.FetchExtraConfig(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
|
||||
v, ok := info[UpdateStatus]
|
||||
if ok {
|
||||
assert.Equal(t, "true", v, "UpdateInProgress should be true")
|
||||
} else {
|
||||
t.Fatal("ERROR: UpdateInProgress does not exist in ExtraConfig")
|
||||
}
|
||||
|
||||
// Set UpdateInProgress to false and then check status
|
||||
err = vmm.SetVCHUpdateStatus(ctx, false)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
|
||||
info, err = vmm.FetchExtraConfig(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: %s", err)
|
||||
}
|
||||
|
||||
v, ok = info[UpdateStatus]
|
||||
if ok {
|
||||
assert.Equal(t, "false", v, "UpdateInProgress should be false")
|
||||
} else {
|
||||
t.Fatal("ERROR: UpdateInProgress does not exist in ExtraConfig")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user