-
Notifications
You must be signed in to change notification settings - Fork 184
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Tests for Volume Mount Point API wrappers
These are a separate commit, because in order to implement the tests, a bunch of new modules were pulled into go.mod. And the tests only run as Administrator anyway. On the plus side, the tests exercise a few APIs in the vhd package: - CreateVirtualDisk - AttachVirtualDisk - DetachVirtualDisk - GetVirtualDiskPhysicalPath And in future, these tests could also exercise the DecodeReparsePoint API too, since it should be 1:1 with GetVolumeNameForVolumeMountPoint. Signed-off-by: Paul "TBBle" Hampson <[email protected]>
- Loading branch information
Showing
3 changed files
with
285 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,44 @@ | ||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= | ||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= | ||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= | ||
github.com/hpe-storage/common-host-libs v4.2.0+incompatible h1:NTOFzSckuKF/1S4muVv/Lgk0TEeqR5Q3XWwqf+uybAA= | ||
github.com/hpe-storage/common-host-libs v4.2.0+incompatible/go.mod h1:qQxvwt4l9C79p2V8bY1P13As1+ylyznKJVp3K2P5bz8= | ||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= | ||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | ||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= | ||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= | ||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= | ||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= | ||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= | ||
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= | ||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= | ||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= | ||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= | ||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= | ||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1cHUZgO1Ebq5r2hIjfo= | ||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= | ||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= | ||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= | ||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | ||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= | ||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= | ||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | ||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
package volmount | ||
|
||
import ( | ||
"io/ioutil" | ||
"os" | ||
"path/filepath" | ||
"regexp" | ||
"strings" | ||
"syscall" | ||
"testing" | ||
|
||
"github.com/Microsoft/go-winio/vhd" | ||
"github.com/hpe-storage/common-host-libs/windows/wmi" | ||
"github.com/pkg/errors" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
// fullObjectIDFromObjectID turns the given class and objectID into an objectID suitable for calling | ||
// wmi.ExecWmiMethod on. | ||
// There should be an __PATH property on the object for this, according to wbemtest.exe, but I cannot see | ||
// how to get it from the objects exposed by wmi. | ||
func fullObjectIDFromObjectID(class, objectID string) string { | ||
return class + `.ObjectId="` + strings.ReplaceAll(strings.ReplaceAll(objectID, `\`, `\\`), `"`, `\"`) + `"` | ||
} | ||
|
||
// turnRawDiskIntoAVolume will take the given PhysicalDisk path, **wipe its partition table** and | ||
// create a single new partition, returning the Volume path needed by the mount-point APIs. | ||
func turnRawDiskIntoAVolume(physPath string) (string, error) { | ||
// This is all implemented via WMI because that turned out to be easier. | ||
const wmiNamespace = "Root\\Microsoft\\Windows\\Storage" | ||
|
||
// If go-winio ever grows support for DRIVE_LAYOUT_INFORMATION_EX and PARTITION_INFORMATION_EX, | ||
// this could be done using DeviceIoControl instead, to exercise those APIs. | ||
|
||
physicalDiskRE := regexp.MustCompile(`\\\\.\\PhysicalDrive(.*)`) | ||
matches := physicalDiskRE.FindStringSubmatch(physPath) | ||
if len(matches) != 2 { | ||
return "", errors.Errorf("Bad drive physical path %s", physPath) | ||
} | ||
driveNumber := matches[1] | ||
|
||
disks, err := wmi.GetMSFTDisk("Number = " + driveNumber) | ||
if err != nil { | ||
return "", errors.Wrapf(err, "wmi.GetMSFTDIsk(%s) failed", "Number = "+driveNumber) | ||
} | ||
|
||
if len(disks) != 1 { | ||
return "", errors.Errorf("wmi.GetMSFTDIsk returned unexpected disk list: %v", disks) | ||
} | ||
|
||
disk := disks[0] | ||
|
||
const diskClass = "MSFT_Disk" | ||
|
||
fullDiskObjectID := fullObjectIDFromObjectID(diskClass, disk.ObjectId) | ||
|
||
// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/stormgmt/initialize-msft-disk | ||
result, err := wmi.ExecWmiMethod(fullDiskObjectID, "Initialize", wmiNamespace, 1) | ||
if err != nil { | ||
return "", errors.Wrapf(err, "wmi.ExecWmiMethod(%s, Initialize) failed", fullDiskObjectID) | ||
} | ||
if result.Val != 0 { | ||
return "", errors.Errorf("Unexpected %s::Initialize Result: %d", diskClass, result.Val) | ||
} | ||
|
||
// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/stormgmt/createpartition-msft-disk | ||
// In theory the next arg is an output UTF-16 string to save me enumerating partitions below, but | ||
// I don't know how to make this work with out-params. | ||
result, err = wmi.ExecWmiMethod(fullDiskObjectID, "CreatePartition", wmiNamespace, nil, true, nil, nil, nil, false, 7, nil, false, false) | ||
if err != nil { | ||
return "", errors.Wrapf(err, "ExecWmiMethod(%v, CreatePartition) failed", fullDiskObjectID) | ||
} | ||
if result.Val != 0 { | ||
return "", errors.Errorf("Unexpected %s::CreatePartition return value: %d", diskClass, result.Val) | ||
} | ||
|
||
if err = wmi.RescanDisks(); err != nil { | ||
return "", errors.Wrap(err, "RescanDisks failed") | ||
} | ||
|
||
// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/stormgmt/msft-partition | ||
partitions, err := wmi.GetMSFTPartitionForDiskNumber(disk.Number) | ||
if err != nil { | ||
return "", errors.Wrapf(err, "GetMSFTPartitionForDiskNumber(%d) failed", disk.Number) | ||
} | ||
|
||
if len(partitions) != 1 { | ||
return "", errors.Errorf("GetMSFTPartitionForDiskNumber failed to find our new partition: %v", partitions) | ||
} | ||
|
||
partition := partitions[0] | ||
|
||
if len(partition.AccessPaths) != 1 { | ||
return "", errors.Errorf("New partition has unexpectedly access paths (expecting only one): %v", partition.AccessPaths) | ||
} | ||
|
||
volumePath := partition.AccessPaths[0] | ||
|
||
volumeRE := regexp.MustCompile(`\\\\\?\\Volume{.*}\\`) | ||
if !volumeRE.MatchString(volumePath) { | ||
return "", errors.Errorf("New partition access path not a volume path: %s", volumePath) | ||
} | ||
|
||
// Apparently, we don't need to format this volume, SetVolumeMountPoint doesn't care. | ||
return volumePath, nil | ||
} | ||
|
||
// TestVolumeMountAPIs creates and attaches a small VHD, and then exercises the | ||
// various mount-point APIs against it. | ||
func TestVolumeMountSuccess(t *testing.T) { | ||
|
||
dir, err := ioutil.TempDir("", "volmountapis") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer os.RemoveAll(dir) | ||
|
||
vhdPath := filepath.Join(dir, "small.vhdx") | ||
|
||
createParams := vhd.CreateVirtualDiskParameters{ | ||
Version: 2, | ||
Version2: vhd.CreateVersion2{ | ||
MaximumSize: 5 * 1024 * 1024, | ||
BlockSizeInBytes: 1024 * 1024, | ||
}, | ||
} | ||
|
||
vhdHandle, err := vhd.CreateVirtualDisk(vhdPath, vhd.VirtualDiskAccessNone, vhd.CreateVirtualDiskFlagNone, &createParams) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer os.Remove(vhdPath) | ||
defer syscall.CloseHandle(vhdHandle) | ||
|
||
// This needs to be done elevated, or with some specific permission anyway. | ||
attachParams := vhd.AttachVirtualDiskParameters{Version: 2} | ||
err = vhd.AttachVirtualDisk(vhdHandle, vhd.AttachVirtualDiskFlagNoDriveLetter, &attachParams) | ||
if err != nil { | ||
if errno, ok := errors.Cause(err).(syscall.Errno); ok && errno == syscall.ERROR_PRIVILEGE_NOT_HELD { | ||
t.Skip(err) | ||
} | ||
t.Fatal(err) | ||
} | ||
defer func() { | ||
detachErr := vhd.DetachVirtualDisk(vhdHandle) | ||
if detachErr != nil { | ||
// assuming if we're already failing, failing more isn't wrong | ||
t.Fatal(detachErr) | ||
} | ||
}() | ||
|
||
physPath, err := vhd.GetVirtualDiskPhysicalPath(vhdHandle) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
volumePath, err := turnRawDiskIntoAVolume(physPath) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
mountPoints, err := GetVolumePathNamesForVolumeName(volumePath) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if len(mountPoints) != 0 { | ||
t.Fatalf("Brand new volume was unexpectedly mounted at: %v", mountPoints) | ||
} | ||
|
||
mountPoint0 := filepath.Join(dir, "mount0") | ||
err = os.MkdirAll(mountPoint0, 0) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
err = SetVolumeMountPoint(mountPoint0, volumePath) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
defer func() { | ||
deleteErr := DeleteVolumeMountPoint(mountPoint0) | ||
if deleteErr != nil { | ||
// Assuming if we're already failing, failing more isn't wrong. | ||
t.Fatal(deleteErr) | ||
} | ||
}() | ||
|
||
mountPoint0VolumePath, err := GetVolumeNameForVolumeMountPoint(mountPoint0) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if mountPoint0VolumePath != volumePath { | ||
t.Fatalf("Mount 0 read-back incorrectly, expected %s; got %s", volumePath, mountPoint0VolumePath) | ||
} | ||
|
||
mountPoints, err = GetVolumePathNamesForVolumeName(volumePath) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
expectedMountPoints := []string{mountPoint0 + string(filepath.Separator)} | ||
require.ElementsMatch(t, expectedMountPoints, mountPoints, "Mount apparently failed, expected %v (order irrelevant): got %v", expectedMountPoints, mountPoints) | ||
|
||
mountPoint1 := filepath.Join(dir, "mount1") | ||
err = os.MkdirAll(mountPoint1, 0) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
err = SetVolumeMountPoint(mountPoint1, volumePath) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
defer func() { | ||
deleteErr := DeleteVolumeMountPoint(mountPoint1) | ||
if deleteErr != nil { | ||
// Assuming if we're already failing, failing more isn't wrong. | ||
t.Fatal(deleteErr) | ||
} | ||
}() | ||
|
||
mountPoint1VolumePath, err := GetVolumeNameForVolumeMountPoint(mountPoint1) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if mountPoint1VolumePath != volumePath { | ||
t.Fatalf("Mount 1 read-back incorrectly, expected %s; got %s", volumePath, mountPoint1VolumePath) | ||
} | ||
|
||
mountPoints, err = GetVolumePathNamesForVolumeName(volumePath) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
// No order guarantee on the mounts | ||
expectedMountPoints = []string{mountPoint0 + string(filepath.Separator), mountPoint1 + string(filepath.Separator)} | ||
require.ElementsMatch(t, expectedMountPoints, mountPoints, "Mount apparently failed, expected %v (order irrelevant): got %v", expectedMountPoints, mountPoints) | ||
|
||
// Note: DeleteVolumeMountPoint is tested in the defers above. | ||
} |