VMware vSphere Integrated Containers provider (#206)

* Add Virtual Kubelet provider for VIC

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

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

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

* Cleanup and readme file

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

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

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

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

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

* Vendored packages for the VIC provider

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

File diff suppressed because one or more lines are too long