Skip to content

Commit ab0338a

Browse files
committed
Support for querying disks based on LUN
Adds the Go wrappers on windows device query APIs to allow finding a disk device by the provided LUN. This is mostly required inside the GCS where shim includes the LUN at which a disk is attaches in the request and then the GCS needs to be able to find that disk to use it further. Signed-off-by: Amit Barve <[email protected]>
1 parent a00144a commit ab0338a

File tree

4 files changed

+275
-1
lines changed

4 files changed

+275
-1
lines changed

internal/winapi/devices.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ package winapi
44

55
import "github.com/Microsoft/go-winio/pkg/guid"
66

7+
//sys CMGetDeviceInterfaceListSize(listlen *uint32, classGUID *g, deviceID *uint16, ulFlags uint32) (hr error) = cfgmgr32.CM_Get_Device_Interface_List_SizeW
8+
//sys CMGetDeviceInterfaceList(classGUID *g, deviceID *uint16, buffer *uint16, bufLen uint32, ulFlags uint32) (hr error) = cfgmgr32.CM_Get_Device_Interface_ListW
79
//sys CMGetDeviceIDListSize(pulLen *uint32, pszFilter *byte, uFlags uint32) (hr error) = cfgmgr32.CM_Get_Device_ID_List_SizeA
810
//sys CMGetDeviceIDList(pszFilter *byte, buffer *byte, bufferLen uint32, uFlags uint32) (hr error)= cfgmgr32.CM_Get_Device_ID_ListA
911
//sys CMLocateDevNode(pdnDevInst *uint32, pDeviceID string, uFlags uint32) (hr error) = cfgmgr32.CM_Locate_DevNodeW

internal/winapi/zsyscall_windows.go

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/windevice/devicequery.go

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@
33
package windevice
44

55
import (
6+
"context"
67
"fmt"
78
"strings"
89
"unicode/utf16"
10+
"unsafe"
911

1012
"github.com/Microsoft/go-winio/pkg/guid"
13+
"github.com/Microsoft/hcsshim/internal/log"
1114
"github.com/Microsoft/hcsshim/internal/winapi"
1215
"github.com/pkg/errors"
16+
"github.com/sirupsen/logrus"
17+
"golang.org/x/sys/windows"
1318
)
1419

1520
const (
16-
_CM_GETIDLIST_FILTER_BUSRELATIONS uint32 = 0x00000020
21+
_CM_GETIDLIST_FILTER_BUSRELATIONS uint32 = 0x00000020
22+
_CM_GET_DEVICE_INTERFACE_LIST_PRESENT uint32 = 0x00000000
23+
_CM_GET_DEVICE_INTERFACE_LIST_ALL_DEVICES uint32 = 0x00000001
1724

1825
_CM_LOCATE_DEVNODE_NORMAL uint32 = 0x00000000
1926

@@ -22,8 +29,82 @@ const (
2229
_DEVPROP_TYPE_STRING_LIST uint32 = (_DEVPROP_TYPE_STRING | _DEVPROP_TYPEMOD_LIST)
2330

2431
_DEVPKEY_LOCATIONPATHS_GUID = "a45c254e-df1c-4efd-8020-67d146a850e0"
32+
33+
_IOCTL_SCSI_GET_ADDRESS = 0x41018
34+
_IOCTL_STORAGE_GET_DEVICE_NUMBER = 0x2d1080
35+
)
36+
37+
var (
38+
// class GUID for devices with interface type Disk
39+
// 53f56307-b6bf-11d0-94f2-00a0c91efb8b
40+
devClassDiskGUID = guid.GUID{
41+
Data1: 0x53f56307,
42+
Data2: 0xb6bf,
43+
Data3: 0x11d0,
44+
Data4: [8]byte{0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b},
45+
}
2546
)
2647

48+
// SCSI_ADDRESS structure used with IOCTL_SCSI_GET_ADDRESS.
49+
// defined here: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddscsi/ns-ntddscsi-_scsi_address
50+
type SCSIAddress struct {
51+
Length uint32
52+
PortNumber uint8
53+
PathId uint8
54+
TargetId uint8
55+
Lun uint8
56+
}
57+
58+
// STORAGE_DEVICE_NUMBER structure used with IOCTL_STORAGE_GET_DEVICE_NUMBER
59+
// https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ns-winioctl-storage_device_number
60+
type StorageDeviceNumber struct {
61+
DeviceType uint32
62+
DeviceNumber uint32
63+
PartitionNumber uint32
64+
}
65+
66+
// getScsiAddress retrieves the SCSI address from a given disk handle
67+
func getScsiAddress(ctx context.Context, handle windows.Handle) (*SCSIAddress, error) {
68+
// Create a SCSI_ADDRESS structure to receive the address information
69+
address := &SCSIAddress{}
70+
address.Length = uint32(unsafe.Sizeof(SCSIAddress{}))
71+
72+
// Buffer for the returned data
73+
var bytesReturned uint32
74+
75+
// Call DeviceIoControl with IOCTL_SCSI_GET_ADDRESS
76+
err := windows.DeviceIoControl(
77+
handle,
78+
_IOCTL_SCSI_GET_ADDRESS,
79+
nil, 0, // no input buffer
80+
(*byte)(unsafe.Pointer(address)),
81+
address.Length, &bytesReturned, nil)
82+
if err != nil {
83+
return nil, fmt.Errorf("DeviceIoControl failed with error: %w", err)
84+
}
85+
if bytesReturned <= 0 {
86+
return nil, fmt.Errorf("DeviceIoControl returned %d bytes", bytesReturned)
87+
}
88+
return address, nil
89+
}
90+
91+
func getStorageDeviceNumber(ctx context.Context, handle windows.Handle) (*StorageDeviceNumber, error) {
92+
var bytesReturned uint32
93+
var deviceNumber StorageDeviceNumber
94+
if err := windows.DeviceIoControl(
95+
handle,
96+
_IOCTL_STORAGE_GET_DEVICE_NUMBER,
97+
nil, 0, // No input buffer
98+
(*byte)(unsafe.Pointer(&deviceNumber)),
99+
uint32(unsafe.Sizeof(deviceNumber)),
100+
&bytesReturned,
101+
nil,
102+
); err != nil {
103+
return nil, fmt.Errorf("get device number ioctl failed: %w", err)
104+
}
105+
return &deviceNumber, nil
106+
}
107+
27108
// getDevPKeyDeviceLocationPaths creates a DEVPROPKEY struct for the
28109
// DEVPKEY_Device_LocationPaths property as defined in devpkey.h
29110
func getDevPKeyDeviceLocationPaths() (*winapi.DevPropKey, error) {
@@ -93,6 +174,21 @@ func convertFirstNullTerminatedValueToString(buf []uint16) (string, error) {
93174
return converted[:zerosIndex], nil
94175
}
95176

177+
func convertNullSeparatedUint16BufToStringSlice(buf []uint16) []string {
178+
result := []string{}
179+
r := utf16.Decode(buf)
180+
converted := string(r)
181+
for {
182+
i := strings.IndexRune(converted, '\u0000')
183+
if i <= 0 {
184+
break
185+
}
186+
result = append(result, string(converted[:i]))
187+
converted = converted[i+1:]
188+
}
189+
return result
190+
}
191+
96192
func GetChildrenFromInstanceIDs(parentIDs []string) ([]string, error) {
97193
var result []string
98194
for _, id := range parentIDs {
@@ -121,3 +217,92 @@ func getDeviceIDList(pszFilter *byte, ulFlags uint32) ([]string, error) {
121217

122218
return winapi.ConvertStringSetToSlice(buf)
123219
}
220+
221+
// A device interface class represents a conceptual functionality that any device in that
222+
// class should support or represent such as a particular I/O contract. There are several
223+
// predefined interface classes and each class is identified by its own unique GUID. A
224+
// single device can implement/support multiple interface classes and there can be
225+
// multiple devices in the system that implement/support a particular interface class. A
226+
// device that implements a particular interface class is referred to as a device
227+
// interface instance. (For further details see:
228+
// https://learn.microsoft.com/en-us/windows-hardware/drivers/install/overview-of-device-interface-classes).
229+
//
230+
// getDeviceInterfaceInstancesByClass retrieves a list of device interface instances for
231+
// all the devices that are currently attached to the system filtered by the given
232+
// interface class(`interfaceClassGUID`). By default this only returns the list of devices
233+
// that are currently attached to the system. If `includeNonAttached` is true, includes
234+
// the devices that the system has seen earlier but aren't currently attached.
235+
//
236+
// For further details see: https://learn.microsoft.com/en-us/windows/win32/api/cfgmgr32/nf-cfgmgr32-cm_get_device_interface_lista
237+
func getDeviceInterfaceInstancesByClass(ctx context.Context, interfaceClassGUID *guid.GUID, includeNonAttached bool) (_ []string, err error) {
238+
log.G(ctx).WithFields(logrus.Fields{
239+
"inteface class": interfaceClassGUID,
240+
"includeNonAttached": includeNonAttached,
241+
}).Debugf("get device interface instances by class")
242+
243+
interfaceListSize := uint32(0)
244+
ulFlags := _CM_GET_DEVICE_INTERFACE_LIST_PRESENT
245+
if includeNonAttached {
246+
ulFlags = _CM_GET_DEVICE_INTERFACE_LIST_ALL_DEVICES
247+
}
248+
249+
if err := winapi.CMGetDeviceInterfaceListSize(&interfaceListSize, interfaceClassGUID, nil, ulFlags); err != nil {
250+
return nil, fmt.Errorf("failed to get size of device interface list: %w", err)
251+
}
252+
253+
log.G(ctx).WithField("interface list size", interfaceListSize).Trace("retrieved device interface list size")
254+
255+
buf := make([]uint16, interfaceListSize)
256+
if err := winapi.CMGetDeviceInterfaceList(&devClassDiskGUID, nil, &buf[0], interfaceListSize, 0); err != nil {
257+
return nil, fmt.Errorf("failed to get device interface list: %w", err)
258+
}
259+
return convertNullSeparatedUint16BufToStringSlice(buf), nil
260+
}
261+
262+
// GetDeviceNumberFromControllerLUN finds the storage device that has a matching
263+
// `controller` and `LUN` and returns a physical device number of that device. This device
264+
// number can then be used to make a path of that device and open handles to that device,
265+
// mount that disk etc.
266+
func GetDeviceNumberFromControllerLUN(ctx context.Context, controller, LUN uint8) (uint32, error) {
267+
interfacePaths, err := getDeviceInterfaceInstancesByClass(ctx, &devClassDiskGUID, false)
268+
if err != nil {
269+
return 0, fmt.Errorf("failed to get device interface instances: %w", err)
270+
}
271+
272+
log.G(ctx).Debugf("disk device interface list: %+v", interfacePaths)
273+
274+
// go over each disk device interface and find out its LUN
275+
for _, iPath := range interfacePaths {
276+
utf16Path, err := windows.UTF16PtrFromString(iPath)
277+
if err != nil {
278+
return 0, fmt.Errorf("failed to convert interface path [%s] to utf16: %w", iPath, err)
279+
}
280+
281+
handle, err := windows.CreateFile(utf16Path, windows.GENERIC_READ|windows.GENERIC_WRITE,
282+
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE,
283+
nil, windows.OPEN_EXISTING, 0, 0)
284+
if err != nil {
285+
return 0, fmt.Errorf("failed to get handle to interface path [%s]: %w", iPath, err)
286+
}
287+
defer windows.Close(handle)
288+
289+
scsiAddr, err := getScsiAddress(ctx, handle)
290+
if err != nil {
291+
return 0, fmt.Errorf("failed to get SCSI address for interface path [%s]: %w", iPath, err)
292+
}
293+
log.G(ctx).WithFields(logrus.Fields{
294+
"device interface path": iPath,
295+
"scsi address": scsiAddr,
296+
}).Trace("scsi path from device interface path")
297+
298+
//TODO(ambarve): is comparing controller with port number the correct way?
299+
if scsiAddr.Lun == LUN && scsiAddr.PortNumber == controller {
300+
deviceNumber, err := getStorageDeviceNumber(ctx, handle)
301+
if err != nil {
302+
return 0, fmt.Errorf("failed to get physical device number: %w", err)
303+
}
304+
return deviceNumber.DeviceNumber, nil
305+
}
306+
}
307+
return 0, fmt.Errorf("no device found with controller: %d & LUN:%d", controller, LUN)
308+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//go:build windows
2+
3+
package windevice
4+
5+
import (
6+
"context"
7+
"path/filepath"
8+
"testing"
9+
"time"
10+
11+
"github.com/Microsoft/go-winio/vhd"
12+
)
13+
14+
func TestDeviceInterfaceInstancesWithPnpUtil(t *testing.T) {
15+
ctx := context.Background()
16+
initialInterfacesList, err := getDeviceInterfaceInstancesByClass(ctx, &devClassDiskGUID, false)
17+
if err != nil {
18+
t.Fatalf("failed to get initial disk interfaces: %v", err)
19+
}
20+
t.Logf("initial interface list: %+v\n", initialInterfacesList)
21+
22+
// make a fake VHD and attach it.
23+
tempDir := t.TempDir()
24+
vhdxPath := filepath.Join(tempDir, "test.vhdx")
25+
if err := vhd.CreateVhdx(vhdxPath, 1, 1); err != nil {
26+
t.Fatalf("failed to create vhd: %s", err)
27+
}
28+
if err := vhd.AttachVhd(vhdxPath); err != nil {
29+
t.Fatalf("failed to attach vhd: %s", err)
30+
}
31+
t.Cleanup(func() {
32+
if err := vhd.DetachVhd(vhdxPath); err != nil {
33+
t.Logf("failed to detach VHD during cleanup: %s\n", err)
34+
}
35+
})
36+
37+
interfaceListAfterVHDAttach, err := getDeviceInterfaceInstancesByClass(ctx, &devClassDiskGUID, false)
38+
if err != nil {
39+
t.Fatalf("failed to get initial disk interfaces: %v", err)
40+
}
41+
t.Logf("interface list after attaching VHD: %+v\n", interfaceListAfterVHDAttach)
42+
43+
if len(initialInterfacesList) != (len(interfaceListAfterVHDAttach) - 1) {
44+
t.Fatalf("expected to find exactly 1 new interface in the returned interfaces list")
45+
}
46+
47+
if err := vhd.DetachVhd(vhdxPath); err != nil {
48+
t.Fatalf("failed to detach VHD: %s\n", err)
49+
}
50+
51+
// Looks like a small time gap is required before we query for device interfaces again to get the updated list.
52+
time.Sleep(2 * time.Second)
53+
54+
finalInterfaceList, err := getDeviceInterfaceInstancesByClass(ctx, &devClassDiskGUID, false)
55+
if err != nil {
56+
t.Fatalf("failed to get initial disk interfaces: %v", err)
57+
}
58+
t.Logf("interface list after detaching VHD: %+v\n", finalInterfaceList)
59+
60+
if len(initialInterfacesList) != len(finalInterfaceList) {
61+
t.Fatalf("expected interface lists to have same length")
62+
}
63+
}

0 commit comments

Comments
 (0)