From 40de3671e20f97d2f9389263cb5b9531eaf41c23 Mon Sep 17 00:00:00 2001 From: Zhongcheng Lao Date: Sun, 13 Oct 2024 08:09:14 -0700 Subject: [PATCH] Use WMI instead of PowerShell for OS operations --- go.mod | 6 +- go.sum | 6 + pkg/cim/disk.go | 52 +++++++ pkg/cim/iscsi.go | 249 ++++++++++++++++++++++++++++++++ pkg/cim/volume.go | 239 ++++++++++++++++++++++++++++++ pkg/cim/wmi.go | 239 ++++++++++++++++++++++++++++++ pkg/os/disk/api.go | 254 +++++++++++++++++--------------- pkg/os/iscsi/api.go | 233 +++++++++++++++++++----------- pkg/os/smb/api.go | 53 ++++--- pkg/os/system/api.go | 99 +++++++++---- pkg/os/volume/api.go | 336 +++++++++++++++++++++++++------------------ vendor/modules.txt | 16 +++ 12 files changed, 1398 insertions(+), 384 deletions(-) create mode 100644 pkg/cim/disk.go create mode 100644 pkg/cim/iscsi.go create mode 100644 pkg/cim/volume.go create mode 100644 pkg/cim/wmi.go diff --git a/go.mod b/go.mod index b7b37f23..09bdfbea 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,16 @@ module github.com/kubernetes-csi/csi-proxy -go 1.20 +go 1.22 + +toolchain go1.22.3 require ( github.com/Microsoft/go-winio v0.6.1 + github.com/go-ole/go-ole v1.3.0 github.com/google/go-cmp v0.6.0 github.com/iancoleman/strcase v0.3.0 github.com/kubernetes-csi/csi-proxy/client v0.0.0-00010101000000-000000000000 + github.com/microsoft/wmi v0.23.0 github.com/pkg/errors v0.9.1 github.com/sergi/go-diff v1.3.1 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index 778f8c09..61312254 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -50,6 +52,8 @@ 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/mauriciopoppe/gengo v0.0.0-20210525224835-9c78f58f3486 h1:+l047vEi0SyAzdVToIaAcfoY5DwwGW+OyqTdH/P3TTg= github.com/mauriciopoppe/gengo v0.0.0-20210525224835-9c78f58f3486/go.mod h1:xXv3T4UXTLta31wMhVezwVkc26OLei4hMbLeBJbPmxc= +github.com/microsoft/wmi v0.23.0 h1:EbgjakKBOfb4QaTJNiGkfKrb2RWv7wpyicI2g3DHWkw= +github.com/microsoft/wmi v0.23.0/go.mod h1:PNc5VFG7cpB7VOb3ILZNuWMWsqFfYLPyzpoiFkA6fAQ= 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= @@ -85,10 +89,12 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/pkg/cim/disk.go b/pkg/cim/disk.go new file mode 100644 index 00000000..812df49c --- /dev/null +++ b/pkg/cim/disk.go @@ -0,0 +1,52 @@ +package cim + +import ( + "fmt" + "strconv" + + "github.com/microsoft/wmi/pkg/base/query" + "github.com/microsoft/wmi/server2019/root/microsoft/windows/storage" +) + +const ( + PartitionStyleUnknown = 0 + PartitionStyleGPT = 2 + + GPTPartitionTypeBasicData = "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}" + GPTPartitionTypeMicrosoftReserved = "{e3c9e316-0b5c-4db8-817d-f92df00215ae}" +) + +func QueryDiskByNumber(diskNumber uint32, selectorList []string) (*storage.MSFT_Disk, error) { + diskQuery := query.NewWmiQueryWithSelectList("MSFT_Disk", selectorList, "Number", strconv.Itoa(int(diskNumber))) + instances, err := QueryInstances(WMINamespaceStorage, diskQuery) + if err != nil { + return nil, err + } + + disk, err := storage.NewMSFT_DiskEx1(instances[0]) + if err != nil { + return nil, fmt.Errorf("failed to query disk %d. error: %v", diskNumber, err) + } + + return disk, nil +} + +func ListDisks(selectorList []string) ([]*storage.MSFT_Disk, error) { + diskQuery := query.NewWmiQueryWithSelectList("MSFT_Disk", selectorList) + instances, err := QueryInstances(WMINamespaceStorage, diskQuery) + if IgnoreNotFound(err) != nil { + return nil, err + } + + var disks []*storage.MSFT_Disk + for _, instance := range instances { + disk, err := storage.NewMSFT_DiskEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query disk %v. error: %v", instance, err) + } + + disks = append(disks, disk) + } + + return disks, nil +} diff --git a/pkg/cim/iscsi.go b/pkg/cim/iscsi.go new file mode 100644 index 00000000..f456a0a3 --- /dev/null +++ b/pkg/cim/iscsi.go @@ -0,0 +1,249 @@ +package cim + +import ( + "fmt" + "github.com/microsoft/wmi/pkg/base/query" + cim "github.com/microsoft/wmi/pkg/wmiinstance" + "github.com/microsoft/wmi/server2019/root/microsoft/windows/storage" + "strconv" +) + +func ListISCSITargetPortals(selectorList []string) ([]*storage.MSFT_iSCSITargetPortal, error) { + q := query.NewWmiQueryWithSelectList("MSFT_IscsiTargetPortal", selectorList) + instances, err := QueryInstances(WMINamespaceStorage, q) + if IgnoreNotFound(err) != nil { + return nil, err + } + + var targetPortals []*storage.MSFT_iSCSITargetPortal + for _, instance := range instances { + portal, err := storage.NewMSFT_iSCSITargetPortalEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query iSCSI target portal %v. error: %v", instance, err) + } + + targetPortals = append(targetPortals, portal) + } + + return targetPortals, nil +} + +func QueryISCSITargetPortal(address string, port uint32, selectorList []string) (*storage.MSFT_iSCSITargetPortal, error) { + portalQuery := query.NewWmiQueryWithSelectList( + "MSFT_iSCSITargetPortal", selectorList, + "TargetPortalAddress", address, + "TargetPortalPortNumber", strconv.Itoa(int(port))) + instances, err := QueryInstances(WMINamespaceStorage, portalQuery) + if err != nil { + return nil, err + } + + targetPortal, err := storage.NewMSFT_iSCSITargetPortalEx1(instances[0]) + if err != nil { + return nil, fmt.Errorf("failed to query iSCSI target portal at (%s:%d). error: %v", address, port, err) + } + + return targetPortal, nil +} + +func NewISCSITargetPortal(targetPortalAddress string, + targetPortalPortNumber uint32, + initiatorInstanceName *string, + initiatorPortalAddress *string, + isHeaderDigest *bool, + isDataDigest *bool) (*storage.MSFT_iSCSITargetPortal, error) { + params := map[string]interface{}{ + "TargetPortalAddress": targetPortalAddress, + "TargetPortalPortNumber": targetPortalPortNumber, + } + if initiatorInstanceName != nil { + params["InitiatorInstanceName"] = *initiatorInstanceName + } + if initiatorPortalAddress != nil { + params["InitiatorPortalAddress"] = *initiatorPortalAddress + } + if isHeaderDigest != nil { + params["IsHeaderDigest"] = *isHeaderDigest + } + if isDataDigest != nil { + params["IsDataDigest"] = *isDataDigest + } + result, _, err := InvokeCimMethod(WMINamespaceStorage, "MSFT_iSCSITargetPortal", "New", params) + if err != nil { + return nil, fmt.Errorf("failed to create iSCSI target portal with %v. result: %d, error: %v", params, result, err) + } + + return QueryISCSITargetPortal(targetPortalAddress, targetPortalPortNumber, nil) +} + +var ( + mappingISCSIiTargetIndexer = mappingObjectRefIndexer("iSCSITarget", "MSFT_iSCSITarget", "NodeAddress") + mappingISCSITargetPortalIndexer = mappingObjectRefIndexer("iSCSITargetPortal", "MSFT_iSCSITargetPortal", "TargetPortalAddress") + mappingISCSIConnectionIndexer = mappingObjectRefIndexer("iSCSIConnection", "MSFT_iSCSIConnection", "ConnectionIdentifier") + mappingISCSISessionIndexer = mappingObjectRefIndexer("iSCSISession", "MSFT_iSCSISession", "SessionIdentifier") + + iscsiTargetIndexer = stringPropertyIndexer("NodeAddress") + iscsiTargetPortalIndexer = stringPropertyIndexer("TargetPortalAddress") + iscsiConnectionIndexer = stringPropertyIndexer("ConnectionIdentifier") + iscsiSessionIndexer = stringPropertyIndexer("SessionIdentifier") +) + +func ListISCSITargetToISCSITargetPortalMapping() (map[string]string, error) { + return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSITargetToiSCSITargetPortal", nil, mappingISCSIiTargetIndexer, mappingISCSITargetPortalIndexer) +} + +func ListISCSIConnectionToISCSITargetMapping() (map[string]string, error) { + return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSITargetToiSCSIConnection", nil, mappingISCSIConnectionIndexer, mappingISCSIiTargetIndexer) +} + +func ListISCSISessionToISCSITargetMapping() (map[string]string, error) { + return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSITargetToiSCSISession", nil, mappingISCSISessionIndexer, mappingISCSIiTargetIndexer) +} + +func ListDiskToISCSIConnectionMapping() (map[string]string, error) { + return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSIConnectionToDisk", nil, mappingObjectRefIndexer("Disk", "MSFT_Disk", "ObjectId"), mappingISCSIConnectionIndexer) +} + +func ListISCSITargetByTargetPortalWithFilters(targetSelectorList []string, portals []*storage.MSFT_iSCSITargetPortal, filters ...*query.WmiQueryFilter) ([]*storage.MSFT_iSCSITarget, error) { + targetQuery := query.NewWmiQueryWithSelectList("MSFT_iSCSITarget", targetSelectorList) + targetQuery.Filters = append(targetQuery.Filters, filters...) + instances, err := QueryInstances(WMINamespaceStorage, targetQuery) + if err != nil { + return nil, err + } + + var portalInstances []*cim.WmiInstance + for _, portal := range portals { + portalInstances = append(portalInstances, portal.WmiInstance) + } + + targetToTargetPortalMapping, err := ListISCSITargetToISCSITargetPortalMapping() + if err != nil { + return nil, err + } + + targetInstances, err := FindInstancesByMapping(instances, iscsiTargetIndexer, portalInstances, iscsiTargetPortalIndexer, targetToTargetPortalMapping) + if err != nil { + return nil, err + } + + var targets []*storage.MSFT_iSCSITarget + for _, instance := range targetInstances { + target, err := storage.NewMSFT_iSCSITargetEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query iSCSI target %v. %v", instance, err) + } + + targets = append(targets, target) + } + + return targets, nil +} + +func QueryISCSITarget(address string, port uint32, nodeAddress string, selectorList []string) (*storage.MSFT_iSCSITarget, error) { + portal, err := QueryISCSITargetPortal(address, port, nil) + if err != nil { + return nil, err + } + + targets, err := ListISCSITargetByTargetPortalWithFilters(selectorList, []*storage.MSFT_iSCSITargetPortal{portal}, + query.NewWmiQueryFilter("NodeAddress", nodeAddress, query.Equals)) + if err != nil { + return nil, err + } + + return targets[0], nil +} + +func QueryISCSISessionByTarget(target *storage.MSFT_iSCSITarget, selectorList []string) (*storage.MSFT_iSCSISession, error) { + sessionQuery := query.NewWmiQueryWithSelectList("MSFT_iSCSISession", selectorList) + sessionInstances, err := QueryInstances(WMINamespaceStorage, sessionQuery) + if err != nil { + return nil, err + } + + targetToTargetSessionMapping, err := ListISCSISessionToISCSITargetMapping() + if err != nil { + return nil, err + } + + filtered, err := FindInstancesByMapping(sessionInstances, iscsiSessionIndexer, []*cim.WmiInstance{target.WmiInstance}, iscsiTargetIndexer, targetToTargetSessionMapping) + if err != nil { + return nil, err + } + + session, err := storage.NewMSFT_iSCSISessionEx1(filtered[0]) + return session, err +} + +func ListDisksByTarget(target *storage.MSFT_iSCSITarget, selectorList []string) ([]*storage.MSFT_Disk, error) { + // list connections to the given iSCSI target + connectionQuery := query.NewWmiQueryWithSelectList("MSFT_iSCSIConnection", selectorList) + connectionInstances, err := QueryInstances(WMINamespaceStorage, connectionQuery) + if err != nil { + return nil, err + } + + connectionToTargetMapping, err := ListISCSIConnectionToISCSITargetMapping() + if err != nil { + return nil, err + } + + connectionsToTarget, err := FindInstancesByMapping(connectionInstances, iscsiConnectionIndexer, []*cim.WmiInstance{target.WmiInstance}, iscsiTargetIndexer, connectionToTargetMapping) + if err != nil { + return nil, err + } + + disks, err := ListDisks(selectorList) + if err != nil { + return nil, err + } + + var diskInstances []*cim.WmiInstance + for _, disk := range disks { + diskInstances = append(diskInstances, disk.WmiInstance) + } + + diskToConnectionMapping, err := ListDiskToISCSIConnectionMapping() + if err != nil { + return nil, err + } + + filtered, err := FindInstancesByMapping(diskInstances, objectIDPropertyIndexer, connectionsToTarget, iscsiConnectionIndexer, diskToConnectionMapping) + if err != nil { + return nil, err + } + + var filteredDisks []*storage.MSFT_Disk + for _, instance := range filtered { + disk, err := storage.NewMSFT_DiskEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query disk %v. error: %v", disk, err) + } + + filteredDisks = append(filteredDisks, disk) + } + return filteredDisks, err +} + +func ConnectISCSITarget(portalAddress string, portalPortNumber uint32, nodeAddress string, authType string, chapUsername *string, chapSecret *string) (int, map[string]interface{}, error) { + inParams := map[string]interface{}{ + "NodeAddress": nodeAddress, + "TargetPortalAddress": portalAddress, + "TargetPortalPortNumber": int(portalPortNumber), + "AuthenticationType": authType, + } + // InitiatorPortalAddress + // IsDataDigest + // IsHeaderDigest + // ReportToPnP + if chapUsername != nil { + inParams["ChapUsername"] = *chapUsername + } + if chapSecret != nil { + inParams["ChapSecret"] = *chapSecret + } + + result, outParams, err := InvokeCimMethod(WMINamespaceStorage, "MSFT_iSCSITarget", "Connect", inParams) + return result, outParams, err +} diff --git a/pkg/cim/volume.go b/pkg/cim/volume.go new file mode 100644 index 00000000..bb98413f --- /dev/null +++ b/pkg/cim/volume.go @@ -0,0 +1,239 @@ +package cim + +import ( + "fmt" + "github.com/microsoft/wmi/pkg/base/query" + "github.com/microsoft/wmi/pkg/errors" + cim "github.com/microsoft/wmi/pkg/wmiinstance" + "github.com/microsoft/wmi/server2019/root/microsoft/windows/storage" + "strconv" +) + +func QueryVolumeByUniqueID(volumeID string, selectorList []string) (*storage.MSFT_Volume, error) { + var selectors []string + selectors = append(selectors, selectorList...) + selectors = append(selectors, "UniqueId") + volumeQuery := query.NewWmiQueryWithSelectList("MSFT_Volume", selectors) + instances, err := QueryInstances(WMINamespaceStorage, volumeQuery) + if err != nil { + return nil, err + } + + for _, instance := range instances { + volume, err := storage.NewMSFT_VolumeEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query volume (%s). error: %w", volumeID, err) + } + + uniqueID, err := volume.GetPropertyUniqueId() + if err != nil { + return nil, fmt.Errorf("failed to query volume unique ID (%s). error: %w", volumeID, err) + } + + if uniqueID == volumeID { + return volume, nil + } + } + + return nil, errors.NotFound +} + +func ListVolumes(selectorList []string) ([]*storage.MSFT_Volume, error) { + diskQuery := query.NewWmiQueryWithSelectList("MSFT_Volume", selectorList) + instances, err := QueryInstances(WMINamespaceStorage, diskQuery) + if IgnoreNotFound(err) != nil { + return nil, err + } + + var volumes []*storage.MSFT_Volume + for _, instance := range instances { + volume, err := storage.NewMSFT_VolumeEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query volume %v. error: %v", instance, err) + } + + volumes = append(volumes, volume) + } + + return volumes, nil +} + +func ListPartitionsOnDisk(diskNumber, partitionNumber uint32, selectorList []string) ([]*storage.MSFT_Partition, error) { + partitionQuery := query.NewWmiQueryWithSelectList("MSFT_Partition", selectorList, "DiskNumber", strconv.Itoa(int(diskNumber))) + if partitionNumber != 0 { + partitionQuery.AddFilter("PartitionNumber", strconv.Itoa(int(partitionNumber))) + } + instances, err := QueryInstances(WMINamespaceStorage, partitionQuery) + if IgnoreNotFound(err) != nil { + return nil, err + } + + var partitions []*storage.MSFT_Partition + for _, instance := range instances { + part, err := storage.NewMSFT_PartitionEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query partition %v. error: %v", instance, err) + } + + partitions = append(partitions, part) + } + + return partitions, nil +} + +func ListPartitionsWithFilters(selectorList []string, filters ...*query.WmiQueryFilter) ([]*storage.MSFT_Partition, error) { + partitionQuery := query.NewWmiQueryWithSelectList("MSFT_Partition", selectorList) + partitionQuery.Filters = append(partitionQuery.Filters, filters...) + instances, err := QueryInstances(WMINamespaceStorage, partitionQuery) + if IgnoreNotFound(err) != nil { + return nil, err + } + + var partitions []*storage.MSFT_Partition + for _, instance := range instances { + part, err := storage.NewMSFT_PartitionEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query partition %v. error: %v", instance, err) + } + + partitions = append(partitions, part) + } + + return partitions, nil +} + +func ListPartitionToVolumeMappings() (map[string]string, error) { + return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_PartitionToVolume", nil, + mappingObjectRefIndexer("Partition", "MSFT_Partition", "ObjectId"), + mappingObjectRefIndexer("Volume", "MSFT_Volume", "ObjectId"), + ) +} + +func ListVolumeToPartitionMappings() (map[string]string, error) { + return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_PartitionToVolume", nil, + mappingObjectRefIndexer("Volume", "MSFT_Volume", "ObjectId"), + mappingObjectRefIndexer("Partition", "MSFT_Partition", "ObjectId"), + ) +} + +func FindPartitionsByVolume(partitions []*storage.MSFT_Partition, volumes []*storage.MSFT_Volume) ([]*storage.MSFT_Partition, error) { + var partitionInstances []*cim.WmiInstance + for _, part := range partitions { + partitionInstances = append(partitionInstances, part.WmiInstance) + } + + var volumeInstances []*cim.WmiInstance + for _, volume := range volumes { + volumeInstances = append(volumeInstances, volume.WmiInstance) + } + + partitionToVolumeMappings, err := ListPartitionToVolumeMappings() + if err != nil { + return nil, err + } + + filtered, err := FindInstancesByObjectIDMapping(partitionInstances, volumeInstances, partitionToVolumeMappings) + if err != nil { + return nil, err + } + + var result []*storage.MSFT_Partition + for _, instance := range filtered { + part, err := storage.NewMSFT_PartitionEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query partition %v. error: %v", instance, err) + } + + result = append(result, part) + } + + return result, nil +} + +func FindVolumesByPartition(volumes []*storage.MSFT_Volume, partitions []*storage.MSFT_Partition) ([]*storage.MSFT_Volume, error) { + var volumeInstances []*cim.WmiInstance + for _, volume := range volumes { + volumeInstances = append(volumeInstances, volume.WmiInstance) + } + + var partitionInstances []*cim.WmiInstance + for _, part := range partitions { + partitionInstances = append(partitionInstances, part.WmiInstance) + } + + volumeToPartitionMappings, err := ListVolumeToPartitionMappings() + if err != nil { + return nil, err + } + + filtered, err := FindInstancesByObjectIDMapping(volumeInstances, partitionInstances, volumeToPartitionMappings) + if err != nil { + return nil, err + } + + var result []*storage.MSFT_Volume + for _, instance := range filtered { + volume, err := storage.NewMSFT_VolumeEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query volume %v. error: %v", instance, err) + } + + result = append(result, volume) + } + + return result, nil +} + +func GetPartitionByVolumeUniqueID(volumeID string, partitionSelectorList []string) (*storage.MSFT_Partition, error) { + volume, err := QueryVolumeByUniqueID(volumeID, []string{"ObjectId"}) + if err != nil { + return nil, err + } + + partitions, err := ListPartitionsWithFilters(partitionSelectorList) + if err != nil { + return nil, err + } + + result, err := FindPartitionsByVolume(partitions, []*storage.MSFT_Volume{volume}) + if err != nil { + return nil, err + } + + return result[0], nil +} + +func GetVolumeByDriveLetter(driveLetter string, partitionSelectorList []string) (*storage.MSFT_Volume, error) { + var selectorsForPart []string + selectorsForPart = append(selectorsForPart, partitionSelectorList...) + selectorsForPart = append(selectorsForPart, "ObjectId") + partitions, err := ListPartitionsWithFilters(selectorsForPart, query.NewWmiQueryFilter("DriveLetter", driveLetter, query.Equals)) + if err != nil { + return nil, err + } + + volumes, err := ListVolumes(partitionSelectorList) + if err != nil { + return nil, err + } + + result, err := FindVolumesByPartition(volumes, partitions) + if err != nil { + return nil, err + } + + if len(result) == 0 { + return nil, errors.NotFound + } + + return result[0], nil +} + +func GetPartitionDiskNumber(part *storage.MSFT_Partition) (uint32, error) { + diskNumber, err := part.GetProperty("DiskNumber") + if err != nil { + return 0, err + } + + return uint32(diskNumber.(int32)), nil +} diff --git a/pkg/cim/wmi.go b/pkg/cim/wmi.go new file mode 100644 index 00000000..25190d91 --- /dev/null +++ b/pkg/cim/wmi.go @@ -0,0 +1,239 @@ +package cim + +import ( + "fmt" + "strings" + + "github.com/microsoft/wmi/pkg/base/query" + "github.com/microsoft/wmi/pkg/errors" + cim "github.com/microsoft/wmi/pkg/wmiinstance" +) + +const ( + WMINamespaceRoot = "Root\\CimV2" + WMINamespaceStorage = "Root\\Microsoft\\Windows\\Storage" + WMINamespaceSmb = "Root\\Microsoft\\Windows\\Smb" +) + +type InstanceHandler func(instance *cim.WmiInstance) (bool, error) + +// An InstanceIndexer provides index key to a WMI Instance in a map +type InstanceIndexer func(instance *cim.WmiInstance) (string, error) + +func NewWMISession(namespace string) (*cim.WmiSession, error) { + if namespace == "" { + namespace = WMINamespaceRoot + } + + sessionManager := cim.NewWmiSessionManager() + defer sessionManager.Dispose() + + session, err := sessionManager.GetLocalSession(namespace) + if err != nil { + return nil, fmt.Errorf("failed to get local WMI session for namespace %s. error: %w", namespace, err) + } + + connected, err := session.Connect() + if !connected || err != nil { + return nil, fmt.Errorf("failed to connect to WMI. error: %w", err) + } + + return session, nil +} + +func QueryFromWMI(namespace string, query *query.WmiQuery, handler InstanceHandler) error { + session, err := NewWMISession(namespace) + if err != nil { + return err + } + + defer session.Close() + + instances, err := session.QueryInstances(query.String()) + if err != nil { + return fmt.Errorf("failed to query WMI class %s. error: %w", query.ClassName, err) + } + + if len(instances) == 0 { + return errors.NotFound + } + + var cont bool + for _, instance := range instances { + cont, err = handler(instance) + if err != nil { + err = fmt.Errorf("failed to query WMI class %s instance (%s). error: %w", query.ClassName, instance.String(), err) + } + if !cont { + break + } + } + + return err +} + +func QueryInstances(namespace string, query *query.WmiQuery) ([]*cim.WmiInstance, error) { + var instances []*cim.WmiInstance + err := QueryFromWMI(namespace, query, func(instance *cim.WmiInstance) (bool, error) { + instances = append(instances, instance) + return true, nil + }) + return instances, err +} + +func InvokeCimMethod(namespace, class, methodName string, inputParameters map[string]interface{}) (int, map[string]interface{}, error) { + session, err := NewWMISession(namespace) + if err != nil { + return -1, nil, err + } + + defer session.Close() + + rawResult, err := session.Session.CallMethod("Get", class) + if err != nil { + return -1, nil, err + } + + classInst, err := cim.CreateWmiInstance(rawResult, session) + if err != nil { + return -1, nil, err + } + + method, err := cim.NewWmiMethod(methodName, classInst) + if err != nil { + return -1, nil, err + } + + var inParam cim.WmiMethodParamCollection + for k, v := range inputParameters { + inParam = append(inParam, &cim.WmiMethodParam{ + Name: k, + Value: v, + }) + } + var outParam cim.WmiMethodParamCollection + result, err := method.Execute(inParam, outParam) + if err != nil { + return -1, nil, err + } + + outputParameters := make(map[string]interface{}) + for _, v := range result.OutMethodParams { + outputParameters[v.Name] = v.Value + } + + return int(result.ReturnValue), outputParameters, nil +} + +func IgnoreNotFound(err error) error { + if err == nil || errors.IsNotFound(err) { + return nil + } + return err +} + +func parseObjectRef(input, objectClass, refName string) (string, error) { + tokens := strings.Split(input, fmt.Sprintf("%s.%s=", objectClass, refName)) + if len(tokens) < 2 { + return "", fmt.Errorf("invalid object ID value: %s", input) + } + + objectID := tokens[1] + objectID = strings.ReplaceAll(objectID, "\\\"", "\"") + objectID = strings.ReplaceAll(objectID, "\\\\", "\\") + objectID = objectID[1 : len(objectID)-1] + return objectID, nil +} + +func ListWMIInstanceMappings(namespace, mappingClassName string, selectorList []string, keyIndexer InstanceIndexer, valueIndexer InstanceIndexer) (map[string]string, error) { + q := query.NewWmiQueryWithSelectList(mappingClassName, selectorList) + mappingInstances, err := QueryInstances(namespace, q) + if err != nil { + return nil, err + } + + result := make(map[string]string) + for _, mapping := range mappingInstances { + key, err := keyIndexer(mapping) + if err != nil { + return nil, err + } + + value, err := valueIndexer(mapping) + if err != nil { + return nil, err + } + + result[key] = value + } + + return result, nil +} + +func FindInstancesByMapping(instanceToFind []*cim.WmiInstance, instanceToFindIndex InstanceIndexer, associatedInstances []*cim.WmiInstance, associatedInstanceIndexer InstanceIndexer, instanceMappings map[string]string) ([]*cim.WmiInstance, error) { + associatedInstanceObjectIDMapping := map[string]*cim.WmiInstance{} + for _, inst := range associatedInstances { + key, err := associatedInstanceIndexer(inst) + if err != nil { + return nil, err + } + + associatedInstanceObjectIDMapping[key] = inst + } + + var filtered []*cim.WmiInstance + for _, inst := range instanceToFind { + key, err := instanceToFindIndex(inst) + if err != nil { + return nil, err + } + + valueObjectID, ok := instanceMappings[key] + if !ok { + continue + } + + _, ok = associatedInstanceObjectIDMapping[strings.ToUpper(valueObjectID)] + if !ok { + continue + } + filtered = append(filtered, inst) + } + + if len(filtered) == 0 { + return nil, errors.NotFound + } + + return filtered, nil +} + +func mappingObjectRefIndexer(propertyName, className, refName string) InstanceIndexer { + return func(instance *cim.WmiInstance) (string, error) { + valueVal, err := instance.GetProperty(propertyName) + if err != nil { + return "", err + } + + refValue, err := parseObjectRef(valueVal.(string), className, refName) + return strings.ToUpper(refValue), err + } +} + +func stringPropertyIndexer(propertyName string) InstanceIndexer { + return func(instance *cim.WmiInstance) (string, error) { + valueVal, err := instance.GetProperty(propertyName) + if err != nil { + return "", err + } + + return strings.ToUpper(valueVal.(string)), err + } +} + +var ( + objectIDPropertyIndexer = stringPropertyIndexer("ObjectId") +) + +func FindInstancesByObjectIDMapping(instanceToFind []*cim.WmiInstance, associatedInstances []*cim.WmiInstance, instanceMappings map[string]string) ([]*cim.WmiInstance, error) { + return FindInstancesByMapping(instanceToFind, objectIDPropertyIndexer, associatedInstances, objectIDPropertyIndexer, instanceMappings) +} diff --git a/pkg/os/disk/api.go b/pkg/os/disk/api.go index 566f597d..5c6313a7 100644 --- a/pkg/os/disk/api.go +++ b/pkg/os/disk/api.go @@ -2,16 +2,15 @@ package disk import ( "encoding/hex" - "encoding/json" "fmt" - "regexp" "strconv" "strings" "syscall" "unsafe" + "github.com/kubernetes-csi/csi-proxy/pkg/cim" shared "github.com/kubernetes-csi/csi-proxy/pkg/shared/disk" - "github.com/kubernetes-csi/csi-proxy/pkg/utils" + "github.com/microsoft/wmi/pkg/base/query" "k8s.io/klog/v2" ) @@ -66,31 +65,27 @@ func New() DiskAPI { // ListDiskLocations - constructs a map with the disk number as the key and the DiskLocation structure // as the value. The DiskLocation struct has various fields like the Adapter, Bus, Target and LUNID. -func (DiskAPI) ListDiskLocations() (map[uint32]shared.DiskLocation, error) { - // sample response - // [{ - // "number": 0, - // "location": "PCI Slot 3 : Adapter 0 : Port 0 : Target 1 : LUN 0" - // }, ...] - cmd := fmt.Sprintf("ConvertTo-Json @(Get-Disk | select Number, Location)") - out, err := utils.RunPowershellCmd(cmd) +func (imp DiskAPI) ListDiskLocations() (map[uint32]shared.DiskLocation, error) { + // "location": "PCI Slot 3 : Adapter 0 : Port 0 : Target 1 : LUN 0" + disks, err := cim.ListDisks([]string{"Number", "Location"}) if err != nil { - return nil, fmt.Errorf("failed to list disk location. cmd: %q, output: %q, err %v", cmd, string(out), err) - } - - var getDisk []map[string]interface{} - err = json.Unmarshal(out, &getDisk) - if err != nil { - return nil, err + return nil, fmt.Errorf("could not query disk locations") } m := make(map[uint32]shared.DiskLocation) - for _, v := range getDisk { - str := v["Location"].(string) - num := v["Number"].(float64) + for _, disk := range disks { + num, err := disk.GetProperty("Number") + if err != nil { + return m, fmt.Errorf("failed to query disk number: %v, %w", disk, err) + } + + location, err := disk.GetPropertyLocation() + if err != nil { + return m, fmt.Errorf("failed to query disk location: %v, %w", disk, err) + } found := false - s := strings.Split(str, ":") + s := strings.Split(location, ":") if len(s) >= 5 { var d shared.DiskLocation for _, item := range s { @@ -112,64 +107,113 @@ func (DiskAPI) ListDiskLocations() (map[uint32]shared.DiskLocation, error) { } if found { - m[uint32(num)] = d + m[uint32(num.(int32))] = d } } + return m, nil } + return m, nil } -func (DiskAPI) Rescan() error { - cmd := "Update-HostStorageCache" - out, err := utils.RunPowershellCmd(cmd) +func (imp DiskAPI) Rescan() error { + result, _, err := cim.InvokeCimMethod(cim.WMINamespaceStorage, "MSFT_StorageSetting", "UpdateHostStorageCache", nil) if err != nil { - return fmt.Errorf("error updating host storage cache output: %q, err: %v", string(out), err) + return fmt.Errorf("error updating host storage cache output. result: %d, err: %v", result, err) } return nil } -func (DiskAPI) IsDiskInitialized(diskNumber uint32) (bool, error) { - cmd := fmt.Sprintf("Get-Disk -Number %d | Where partitionstyle -eq 'raw'", diskNumber) - out, err := utils.RunPowershellCmd(cmd) +func (imp DiskAPI) IsDiskInitialized(diskNumber uint32) (bool, error) { + var partitionStyle int32 + disk, err := cim.QueryDiskByNumber(diskNumber, []string{"PartitionStyle"}) if err != nil { - return false, fmt.Errorf("error checking initialized status of disk %d: %v, %v", diskNumber, out, err) + return false, fmt.Errorf("error checking initialized status of disk %d. %v", diskNumber, err) } - if len(out) == 0 { - // disks with raw initialization not detected - return true, nil + + retValue, err := disk.GetProperty("PartitionStyle") + if err != nil { + return false, fmt.Errorf("failed to query partition style of disk %d: %w", diskNumber, err) } - return false, nil + + partitionStyle = retValue.(int32) + return partitionStyle != cim.PartitionStyleUnknown, nil } -func (DiskAPI) InitializeDisk(diskNumber uint32) error { - cmd := fmt.Sprintf("Initialize-Disk -Number %d -PartitionStyle GPT", diskNumber) - out, err := utils.RunPowershellCmd(cmd) +func (imp DiskAPI) InitializeDisk(diskNumber uint32) error { + disk, err := cim.QueryDiskByNumber(diskNumber, nil) if err != nil { - return fmt.Errorf("error initializing disk %d: %v, %v", diskNumber, string(out), err) + return fmt.Errorf("failed to initializing disk %d. error: %w", diskNumber, err) } + + result, err := disk.InvokeMethodWithReturn("Initialize", int32(cim.PartitionStyleGPT)) + if result != 0 || err != nil { + return fmt.Errorf("failed to initializing disk %d: result %d, error: %w", diskNumber, result, err) + } + return nil } -func (DiskAPI) BasicPartitionsExist(diskNumber uint32) (bool, error) { - cmd := fmt.Sprintf("Get-Partition | Where DiskNumber -eq %d | Where Type -ne Reserved", diskNumber) - out, err := utils.RunPowershellCmd(cmd) +func (imp DiskAPI) BasicPartitionsExist(diskNumber uint32) (bool, error) { + partitions, err := cim.ListPartitionsWithFilters(nil, + query.NewWmiQueryFilter("DiskNumber", strconv.Itoa(int(diskNumber)), query.Equals), + query.NewWmiQueryFilter("GptType", cim.GPTPartitionTypeMicrosoftReserved, query.NotEquals)) + if cim.IgnoreNotFound(err) != nil { + return false, fmt.Errorf("error checking presence of partitions on disk %d:, %v", diskNumber, err) + } + + return len(partitions) > 0, nil +} + +func (imp DiskAPI) CreateBasicPartition(diskNumber uint32) error { + disk, err := cim.QueryDiskByNumber(diskNumber, nil) if err != nil { - return false, fmt.Errorf("error checking presence of partitions on disk %d: %v, %v", diskNumber, out, err) + return err } - if len(out) > 0 { - // disk has partitions in it - return true, nil + + result, err := disk.InvokeMethodWithReturn( + "CreatePartition", + nil, // Size + true, // UseMaximumSize + nil, // Offset + nil, // Alignment + nil, // DriveLetter + false, // AssignDriveLetter + nil, // MbrType, + cim.GPTPartitionTypeBasicData, // GPT Type + false, // IsHidden + false, // IsActive, + ) + // 42002 is returened by driver letter failed to assign after partition + if (result != 0 && result != 42002) || err != nil { + return fmt.Errorf("error creating partition on disk %d. result: %d, err: %v", diskNumber, result, err) + } + + var status string + result, err = disk.InvokeMethodWithReturn("Refresh", &status) + if result != 0 || err != nil { + return fmt.Errorf("error rescan disk (%d). result %d, error: %v", diskNumber, result, err) } - return false, nil -} -func (DiskAPI) CreateBasicPartition(diskNumber uint32) error { - cmd := fmt.Sprintf("New-Partition -DiskNumber %d -UseMaximumSize", diskNumber) - out, err := utils.RunPowershellCmd(cmd) + partitions, err := cim.ListPartitionsWithFilters(nil, + query.NewWmiQueryFilter("DiskNumber", strconv.Itoa(int(diskNumber)), query.Equals), + query.NewWmiQueryFilter("GptType", cim.GPTPartitionTypeMicrosoftReserved, query.NotEquals)) if err != nil { - return fmt.Errorf("error creating partition on disk %d: %v, %v", diskNumber, out, err) + return fmt.Errorf("error query basic partition on disk %d:, %v", diskNumber, err) } - return nil + + if len(partitions) == 0 { + return fmt.Errorf("failed to create basic partition on disk %d:, %v", diskNumber, err) + } + + partition := partitions[0] + result, err = partition.InvokeMethodWithReturn("Online", status) + if result != 0 || err != nil { + return fmt.Errorf("error bring partition %v on disk %d online. result: %d, status %s, err: %v", partition, diskNumber, result, status, err) + } + + err = partition.Refresh() + return err } func (imp DiskAPI) GetDiskNumberByName(page83ID string) (uint32, error) { @@ -177,7 +221,7 @@ func (imp DiskAPI) GetDiskNumberByName(page83ID string) (uint32, error) { return diskNumber, err } -func (DiskAPI) GetDiskNumber(disk syscall.Handle) (uint32, error) { +func (imp DiskAPI) GetDiskNumber(disk syscall.Handle) (uint32, error) { var bytes uint32 devNum := StorageDeviceNumber{} buflen := uint32(unsafe.Sizeof(devNum.DeviceType)) + uint32(unsafe.Sizeof(devNum.DeviceNumber)) + uint32(unsafe.Sizeof(devNum.PartitionNumber)) @@ -187,7 +231,7 @@ func (DiskAPI) GetDiskNumber(disk syscall.Handle) (uint32, error) { return devNum.DeviceNumber, err } -func (DiskAPI) GetDiskPage83ID(disk syscall.Handle) (string, error) { +func (imp DiskAPI) GetDiskPage83ID(disk syscall.Handle) (string, error) { query := StoragePropertyQuery{} bufferSize := uint32(4 * 1024) @@ -230,21 +274,18 @@ func (DiskAPI) GetDiskPage83ID(disk syscall.Handle) (string, error) { } func (imp DiskAPI) GetDiskNumberWithID(page83ID string) (uint32, error) { - cmd := "ConvertTo-Json @(Get-Disk | Select Path)" - out, err := utils.RunPowershellCmd(cmd) - if err != nil { - return 0, fmt.Errorf("Could not query disk paths") - } - - outString := string(out) - disks := []Disk{} - err = json.Unmarshal([]byte(outString), &disks) + disks, err := cim.ListDisks([]string{"Path", "SerialNumber"}) if err != nil { return 0, err } - for i := range disks { - diskNumber, diskPage83ID, err := imp.GetDiskNumberAndPage83ID(disks[i].Path) + for _, disk := range disks { + path, err := disk.GetPropertyPath() + if err != nil { + return 0, fmt.Errorf("failed to query disk path: %v, %w", disk, err) + } + + diskNumber, diskPage83ID, err := imp.GetDiskNumberAndPage83ID(path) if err != nil { return 0, err } @@ -254,7 +295,7 @@ func (imp DiskAPI) GetDiskNumberWithID(page83ID string) (uint32, error) { } } - return 0, fmt.Errorf("Could not find disk with Page83 ID %s", page83ID) + return 0, fmt.Errorf("could not find disk with Page83 ID %s", page83ID) } func (imp DiskAPI) GetDiskNumberAndPage83ID(path string) (uint32, string, error) { @@ -280,89 +321,76 @@ func (imp DiskAPI) GetDiskNumberAndPage83ID(path string) (uint32, string, error) // ListDiskIDs - constructs a map with the disk number as the key and the DiskID structure // as the value. The DiskID struct has a field for the page83 ID. func (imp DiskAPI) ListDiskIDs() (map[uint32]shared.DiskIDs, error) { - // sample response - // [ - // { - // "Path": "\\\\?\\scsi#disk\u0026ven_google\u0026prod_persistentdisk#4\u002621cb0360\u00260\u0026000100#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}", - // "SerialNumber": " " - // }, - // { - // "Path": "\\\\?\\scsi#disk\u0026ven_msft\u0026prod_virtual_disk#2\u00261f4adffe\u00260\u0026000001#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}", - // "SerialNumber": null - // }, ] - cmd := "ConvertTo-Json @(Get-Disk | Select Path, SerialNumber)" - out, err := utils.RunPowershellCmd(cmd) - if err != nil { - return nil, fmt.Errorf("Could not query disk paths") - } - - outString := string(out) - disks := []Disk{} - err = json.Unmarshal([]byte(outString), &disks) + disks, err := cim.ListDisks([]string{"Path", "SerialNumber"}) if err != nil { return nil, err } m := make(map[uint32]shared.DiskIDs) + for _, disk := range disks { + path, err := disk.GetPropertyPath() + if err != nil { + return m, fmt.Errorf("failed to query disk path: %v, %w", disk, err) + } + + sn, err := disk.GetPropertySerialNumber() + if err != nil { + return m, fmt.Errorf("failed to query disk serial number: %v, %w", disk, err) + } - for i := range disks { - diskNumber, page83, err := imp.GetDiskNumberAndPage83ID(disks[i].Path) + diskNumber, page83, err := imp.GetDiskNumberAndPage83ID(path) if err != nil { - return nil, err + return m, err } m[diskNumber] = shared.DiskIDs{ Page83: page83, - SerialNumber: disks[i].SerialNumber, + SerialNumber: sn, } } - return m, nil } func (imp DiskAPI) GetDiskStats(diskNumber uint32) (int64, error) { - cmd := fmt.Sprintf("(Get-Disk -Number %d).Size", diskNumber) - out, err := utils.RunPowershellCmd(cmd) - if err != nil || len(out) == 0 { - return -1, fmt.Errorf("error getting size of disk. cmd: %s, output: %s, error: %v", cmd, string(out), err) - } - - reg, err := regexp.Compile("[^0-9]+") + // TODO: change to uint64 as it does not make sense to use int64 for size + var size int64 + disk, err := cim.QueryDiskByNumber(diskNumber, []string{"Size"}) if err != nil { - return -1, fmt.Errorf("error compiling regex. err: %v", err) + return -1, err } - diskSizeOutput := reg.ReplaceAllString(string(out), "") - - diskSize, err := strconv.ParseInt(diskSizeOutput, 10, 64) + sz, err := disk.GetProperty("Size") if err != nil { - return -1, fmt.Errorf("error parsing size of disk. cmd: %s, output: %s, error: %v", cmd, diskSizeOutput, err) + return -1, fmt.Errorf("failed to query size of disk %d. %v", diskNumber, err) } - return diskSize, nil + size, err = strconv.ParseInt(sz.(string), 10, 64) + return size, err } func (imp DiskAPI) SetDiskState(diskNumber uint32, isOnline bool) error { - cmd := fmt.Sprintf("(Get-Disk -Number %d) | Set-Disk -IsOffline $%t", diskNumber, !isOnline) - out, err := utils.RunPowershellCmd(cmd) + disk, err := cim.QueryDiskByNumber(diskNumber, []string{"IsOffline"}) + if err != nil { + return err + } + + err = disk.SetPropertyIsOffline(!isOnline) if err != nil { - return fmt.Errorf("error setting disk attach state. cmd: %s, output: %s, error: %v", cmd, string(out), err) + return fmt.Errorf("error setting disk %d attach state. error: %v", diskNumber, err) } return nil } func (imp DiskAPI) GetDiskState(diskNumber uint32) (bool, error) { - cmd := fmt.Sprintf("(Get-Disk -Number %d) | Select-Object -ExpandProperty IsOffline", diskNumber) - out, err := utils.RunPowershellCmd(cmd) + disk, err := cim.QueryDiskByNumber(diskNumber, []string{"IsOffline"}) if err != nil { - return false, fmt.Errorf("error getting disk state. cmd: %s, output: %s, error: %v", cmd, string(out), err) + return false, err } - sout := strings.TrimSpace(string(out)) - isOffline, err := strconv.ParseBool(sout) + isOffline, err := disk.GetPropertyIsOffline() if err != nil { - return false, fmt.Errorf("error parsing disk state. output: %s, error: %v", sout, err) + return false, fmt.Errorf("error parsing disk %d state. error: %v", diskNumber, err) } return !isOffline, nil diff --git a/pkg/os/iscsi/api.go b/pkg/os/iscsi/api.go index 559ed3b5..9f91138f 100644 --- a/pkg/os/iscsi/api.go +++ b/pkg/os/iscsi/api.go @@ -1,10 +1,13 @@ package iscsi import ( - "encoding/json" "fmt" + "strings" - "github.com/kubernetes-csi/csi-proxy/pkg/utils" + "github.com/kubernetes-csi/csi-proxy/pkg/cim" + "github.com/microsoft/wmi/server2019/root/microsoft/windows/storage" + "k8s.io/klog/v2" + "strconv" ) // Implements the iSCSI OS API calls. All code here should be very simple @@ -18,70 +21,104 @@ func New() APIImplementor { return APIImplementor{} } +func parseTargetPortal(instance *storage.MSFT_iSCSITargetPortal) (string, uint32, error) { + portalAddress, err := instance.GetPropertyTargetPortalAddress() + if err != nil { + return "", 0, fmt.Errorf("failed parsing target portal address %v. err: %w", instance, err) + } + + portalPort, err := instance.GetProperty("TargetPortalPortNumber") + if err != nil { + return "", 0, fmt.Errorf("failed parsing target portal port number %v. err: %w", instance, err) + } + + return portalAddress, uint32(portalPort.(int32)), nil +} + func (APIImplementor) AddTargetPortal(portal *TargetPortal) error { - cmdLine := fmt.Sprintf( - `New-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} ` + - `-TargetPortalPortNumber ${Env:iscsi_tp_port}`) - out, err := utils.RunPowershellCmd(cmdLine, fmt.Sprintf("iscsi_tp_address=%s", portal.Address), - fmt.Sprintf("iscsi_tp_port=%d", portal.Port)) + existing, err := cim.QueryISCSITargetPortal(portal.Address, portal.Port, nil) + if cim.IgnoreNotFound(err) != nil { + return err + } + + if existing != nil { + klog.V(2).Infof("target portal at (%s:%d) already exists", portal.Address, portal.Port) + return nil + } + + _, err = cim.NewISCSITargetPortal(portal.Address, portal.Port, nil, nil, nil, nil) if err != nil { - return fmt.Errorf("error adding target portal. cmd %s, output: %s, err: %v", cmdLine, string(out), err) + return fmt.Errorf("error adding target portal at (%s:%d). err: %v", portal.Address, portal.Port, err) } return nil } func (APIImplementor) DiscoverTargetPortal(portal *TargetPortal) ([]string, error) { - // ConvertTo-Json is not part of the pipeline because powershell converts an - // array with one element to a single element - cmdLine := fmt.Sprintf( - `ConvertTo-Json -InputObject @(Get-IscsiTargetPortal -TargetPortalAddress ` + - `${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port} | ` + - `Get-IscsiTarget | Select-Object -ExpandProperty NodeAddress)`) - out, err := utils.RunPowershellCmd(cmdLine, fmt.Sprintf("iscsi_tp_address=%s", portal.Address), - fmt.Sprintf("iscsi_tp_port=%d", portal.Port)) + instance, err := cim.QueryISCSITargetPortal(portal.Address, portal.Port, nil) if err != nil { - return nil, fmt.Errorf("error discovering target portal. cmd: %s, output: %s, err: %w", cmdLine, string(out), err) + return nil, err } - var iqns []string - err = json.Unmarshal(out, &iqns) + targets, err := cim.ListISCSITargetByTargetPortalWithFilters(nil, []*storage.MSFT_iSCSITargetPortal{instance}) if err != nil { - return nil, fmt.Errorf("failed parsing iqn list. cmd: %s output: %s, err: %w", cmdLine, string(out), err) + return nil, err + } + + var iqns []string + for _, target := range targets { + iqn, err := target.GetProperty("NodeAddress") + if err != nil { + return nil, fmt.Errorf("failed parsing node address of target %v to target portal at (%s:%d). err: %w", target, portal.Address, portal.Port, err) + } + + iqns = append(iqns, iqn.(string)) } return iqns, nil } func (APIImplementor) ListTargetPortals() ([]TargetPortal, error) { - cmdLine := fmt.Sprintf( - `ConvertTo-Json -InputObject @(Get-IscsiTargetPortal | ` + - `Select-Object TargetPortalAddress, TargetPortalPortNumber)`) - - out, err := utils.RunPowershellCmd(cmdLine) + instances, err := cim.ListISCSITargetPortals([]string{"TargetPortalAddress", "TargetPortalPortNumber"}) if err != nil { - return nil, fmt.Errorf("error listing target portals. cmd %s, output: %s, err: %w", cmdLine, string(out), err) + return nil, err } var portals []TargetPortal - err = json.Unmarshal(out, &portals) - if err != nil { - return nil, fmt.Errorf("failed parsing target portal list. cmd: %s output: %s, err: %w", cmdLine, string(out), err) + for _, instance := range instances { + address, port, err := parseTargetPortal(instance) + if err != nil { + return nil, fmt.Errorf("failed parsing target portal %v. err: %w", instance, err) + } + + portals = append(portals, TargetPortal{ + Address: address, + Port: port, + }) } return portals, nil } func (APIImplementor) RemoveTargetPortal(portal *TargetPortal) error { - cmdLine := fmt.Sprintf( - `Get-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} ` + - `-TargetPortalPortNumber ${Env:iscsi_tp_port} | Remove-IscsiTargetPortal ` + - `-Confirm:$false`) + instance, err := cim.QueryISCSITargetPortal(portal.Address, portal.Port, nil) + if err != nil { + return err + } - out, err := utils.RunPowershellCmd(cmdLine, fmt.Sprintf("iscsi_tp_address=%s", portal.Address), - fmt.Sprintf("iscsi_tp_port=%d", portal.Port)) + address, port, err := parseTargetPortal(instance) if err != nil { - return fmt.Errorf("error removing target portal. cmd %s, output: %s, err: %w", cmdLine, string(out), err) + return fmt.Errorf("failed to parse target portal %v. error: %v", instance, err) + } + + result, err := instance.InvokeMethodWithReturn("Remove", + nil, + nil, + int(port), + address, + ) + if result != 0 || err != nil { + return fmt.Errorf("error removing target portal at (%s:%d). result: %d, err: %w", address, port, result, err) } return nil @@ -89,48 +126,73 @@ func (APIImplementor) RemoveTargetPortal(portal *TargetPortal) error { func (APIImplementor) ConnectTarget(portal *TargetPortal, iqn string, authType string, chapUser string, chapSecret string) error { - // Not using InputObject as Connect-IscsiTarget's InputObject does not work. - // This is due to being a static WMI method together with a bug in the - // powershell version of the API. - cmdLine := fmt.Sprintf( - `Connect-IscsiTarget -TargetPortalAddress ${Env:iscsi_tp_address}` + - ` -TargetPortalPortNumber ${Env:iscsi_tp_port} -NodeAddress ${Env:iscsi_target_iqn}` + - ` -AuthenticationType ${Env:iscsi_auth_type}`) + target, err := cim.QueryISCSITarget(portal.Address, portal.Port, iqn, nil) + if err != nil { + return err + } - if chapUser != "" { - cmdLine += ` -ChapUsername ${Env:iscsi_chap_user}` + connected, err := target.GetPropertyIsConnected() + if err != nil { + return err } - if chapSecret != "" { - cmdLine += ` -ChapSecret ${Env:iscsi_chap_secret}` + if connected { + klog.V(2).Infof("target %s from target portal at (%s:%d) is connected.", iqn, portal.Address, portal.Port) + return nil } - out, err := utils.RunPowershellCmd(cmdLine, fmt.Sprintf("iscsi_tp_address=%s", portal.Address), - fmt.Sprintf("iscsi_tp_port=%d", portal.Port), - fmt.Sprintf("iscsi_target_iqn=%s", iqn), - fmt.Sprintf("iscsi_auth_type=%s", authType), - fmt.Sprintf("iscsi_chap_user=%s", chapUser), - fmt.Sprintf("iscsi_chap_secret=%s", chapSecret)) + targetAuthType := strings.ToUpper(strings.ReplaceAll(authType, "_", "")) + + result, _, err := cim.ConnectISCSITarget(portal.Address, portal.Port, iqn, targetAuthType, &chapUser, &chapSecret) if err != nil { - return fmt.Errorf("error connecting to target portal. cmd %s, output: %s, err: %w", cmdLine, string(out), err) + return fmt.Errorf("error connecting to target portal. result: %d, err: %w", result, err) } return nil } func (APIImplementor) DisconnectTarget(portal *TargetPortal, iqn string) error { - // Using InputObject instead of pipe to verify input is not empty - cmdLine := fmt.Sprintf( - `Disconnect-IscsiTarget -InputObject (Get-IscsiTargetPortal ` + - `-TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port} ` + - ` | Get-IscsiTarget | Where-Object { $_.NodeAddress -eq ${Env:iscsi_target_iqn} }) ` + - `-Confirm:$false`) + target, err := cim.QueryISCSITarget(portal.Address, portal.Port, iqn, nil) + if err != nil { + return err + } + + connected, err := target.GetPropertyIsConnected() + if err != nil { + return fmt.Errorf("error query connected of target %s from target portal at (%s:%d). err: %w", iqn, portal.Address, portal.Port, err) + } + + if !connected { + klog.V(2).Infof("target %s from target portal at (%s:%d) is not connected.", iqn, portal.Address, portal.Port) + return nil + } - out, err := utils.RunPowershellCmd(cmdLine, fmt.Sprintf("iscsi_tp_address=%s", portal.Address), - fmt.Sprintf("iscsi_tp_port=%d", portal.Port), - fmt.Sprintf("iscsi_target_iqn=%s", iqn)) + // get session + session, err := cim.QueryISCSISessionByTarget(target, nil) if err != nil { - return fmt.Errorf("error disconnecting from target portal. cmd %s, output: %s, err: %w", cmdLine, string(out), err) + return fmt.Errorf("error query session of target %s from target portal at (%s:%d). err: %w", iqn, portal.Address, portal.Port, err) + } + + sessionIdentifier, err := session.GetPropertySessionIdentifier() + if err != nil { + return fmt.Errorf("error query session identifier of target %s from target portal at (%s:%d). err: %w", iqn, portal.Address, portal.Port, err) + } + + persistent, err := session.GetPropertyIsPersistent() + if err != nil { + return fmt.Errorf("error query session persistency of target %s from target portal at (%s:%d). err: %w", iqn, portal.Address, portal.Port, err) + } + + if persistent { + result, err := session.InvokeMethodWithReturn("Unregister") + if err != nil { + return fmt.Errorf("error unregister session on target %s from target portal at (%s:%d). result: %d, err: %w", iqn, portal.Address, portal.Port, result, err) + } + } + + result, err := target.InvokeMethodWithReturn("Disconnect", sessionIdentifier) + if err != nil { + return fmt.Errorf("error disconnecting target %s from target portal at (%s:%d). result: %d, err: %w", iqn, portal.Address, portal.Port, result, err) } return nil @@ -139,36 +201,43 @@ func (APIImplementor) DisconnectTarget(portal *TargetPortal, iqn string) error { func (APIImplementor) GetTargetDisks(portal *TargetPortal, iqn string) ([]string, error) { // Converting DiskNumber to string for compatibility with disk api group // Not using pipeline in order to validate that items are non-empty - cmdLine := fmt.Sprintf( - `$ErrorActionPreference = "Stop"; ` + - `$tp = Get-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port}; ` + - `$t = $tp | Get-IscsiTarget | Where-Object { $_.NodeAddress -eq ${Env:iscsi_target_iqn} }; ` + - `$c = Get-IscsiConnection -IscsiTarget $t; ` + - `$ids = $c | Get-Disk | Select -ExpandProperty Number | Out-String -Stream; ` + - `ConvertTo-Json -InputObject @($ids)`) + target, err := cim.QueryISCSITarget(portal.Address, portal.Port, iqn, nil) + if err != nil { + return nil, err + } - out, err := utils.RunPowershellCmd(cmdLine, fmt.Sprintf("iscsi_tp_address=%s", portal.Address), - fmt.Sprintf("iscsi_tp_port=%d", portal.Port), - fmt.Sprintf("iscsi_target_iqn=%s", iqn)) + connected, err := target.GetPropertyIsConnected() if err != nil { - return nil, fmt.Errorf("error getting target disks. cmd %s, output: %s, err: %w", cmdLine, string(out), err) + return nil, fmt.Errorf("error query connected of target %s from target portal at (%s:%d). err: %w", iqn, portal.Address, portal.Port, err) } - var ids []string - err = json.Unmarshal(out, &ids) + if !connected { + klog.V(2).Infof("target %s from target portal at (%s:%d) is not connected.", iqn, portal.Address, portal.Port) + return nil, nil + } + + disks, err := cim.ListDisksByTarget(target, []string{}) + if err != nil { - return nil, fmt.Errorf("error parsing iqn target disks. cmd: %s output: %s, err: %w", cmdLine, string(out), err) + return nil, fmt.Errorf("error getting target disks on target %s from target portal at (%s:%d). err: %w", iqn, portal.Address, portal.Port, err) } + var ids []string + for _, disk := range disks { + number, err := disk.GetProperty("Number") + if err != nil { + return nil, fmt.Errorf("error getting number of disk %v on target %s from target portal at (%s:%d). err: %w", disk, iqn, portal.Address, portal.Port, err) + } + + ids = append(ids, strconv.Itoa(int(number.(int32)))) + } return ids, nil } func (APIImplementor) SetMutualChapSecret(mutualChapSecret string) error { - cmdLine := `Set-IscsiChapSecret -ChapSecret ${Env:iscsi_mutual_chap_secret}` - out, err := utils.RunPowershellCmd(cmdLine, fmt.Sprintf("iscsi_mutual_chap_secret=%s", mutualChapSecret)) + result, _, err := cim.InvokeCimMethod(cim.WMINamespaceStorage, "MSFT_iSCSISession", "SetCHAPSecret", map[string]interface{}{"ChapSecret": mutualChapSecret}) if err != nil { - return fmt.Errorf("error setting mutual chap secret. cmd %s,"+ - " output: %s, err: %v", cmdLine, string(out), err) + return fmt.Errorf("error setting mutual chap secret. result: %d, err: %v", result, err) } return nil diff --git a/pkg/os/smb/api.go b/pkg/os/smb/api.go index 910eb7e0..fc0c8085 100644 --- a/pkg/os/smb/api.go +++ b/pkg/os/smb/api.go @@ -4,7 +4,9 @@ import ( "fmt" "strings" + "github.com/kubernetes-csi/csi-proxy/pkg/cim" "github.com/kubernetes-csi/csi-proxy/pkg/utils" + "github.com/microsoft/wmi/pkg/base/query" ) type API interface { @@ -26,18 +28,18 @@ func New(requirePrivacy bool) *SmbAPI { } } +func remotePathForQuery(remotePath string) string { + return strings.ReplaceAll(remotePath, "\\", "\\\\") +} + func (*SmbAPI) IsSmbMapped(remotePath string) (bool, error) { - cmdLine := `$(Get-SmbGlobalMapping -RemotePath $Env:smbremotepath -ErrorAction Stop).Status ` - cmdEnv := fmt.Sprintf("smbremotepath=%s", remotePath) - out, err := utils.RunPowershellCmd(cmdLine, cmdEnv) - if err != nil { - return false, fmt.Errorf("error checking smb mapping. cmd %s, output: %s, err: %v", remotePath, string(out), err) + smbQuery := query.NewWmiQuery("MSFT_SmbGlobalMapping", "RemotePath", remotePathForQuery(remotePath)) + instances, err := cim.QueryInstances(cim.WMINamespaceSmb, smbQuery) + if cim.IgnoreNotFound(err) != nil { + return false, err } - if len(out) == 0 || !strings.EqualFold(strings.TrimSpace(string(out)), "OK") { - return false, nil - } - return true, nil + return len(instances) > 0, nil } // NewSmbLink - creates a directory symbolic link to the remote share. @@ -48,7 +50,6 @@ func (*SmbAPI) IsSmbMapped(remotePath string) (bool, error) { // alpha to merge the paths. // TODO (for beta release): Merge the link paths - os.Symlink and Powershell link path. func (*SmbAPI) NewSmbLink(remotePath, localPath string) error { - if !strings.HasSuffix(remotePath, "\\") { // Golang has issues resolving paths mapped to file shares if they do not end in a trailing \ // so add one if needed. @@ -67,23 +68,31 @@ func (*SmbAPI) NewSmbLink(remotePath, localPath string) error { func (api *SmbAPI) NewSmbGlobalMapping(remotePath, username, password string) error { // use PowerShell Environment Variables to store user input string to prevent command line injection // https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1 - cmdLine := fmt.Sprintf(`$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force`+ - `;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord`+ - `;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential -RequirePrivacy $%t`, api.RequirePrivacy) - - if output, err := utils.RunPowershellCmd(cmdLine, - fmt.Sprintf("smbuser=%s", username), - fmt.Sprintf("smbpassword=%s", password), - fmt.Sprintf("smbremotepath=%s", remotePath)); err != nil { - return fmt.Errorf("NewSmbGlobalMapping failed. output: %q, err: %v", string(output), err) + params := map[string]interface{}{ + "RemotePath": remotePath, + "RequirePrivacy": api.RequirePrivacy, + } + if username != "" { + params["Credential"] = fmt.Sprintf("%s:%s", username, password) + } + result, _, err := cim.InvokeCimMethod(cim.WMINamespaceSmb, "MSFT_SmbGlobalMapping", "Create", params) + if err != nil { + return fmt.Errorf("NewSmbGlobalMapping failed. result: %d, err: %v", result, err) } return nil } func (*SmbAPI) RemoveSmbGlobalMapping(remotePath string) error { - cmd := `Remove-SmbGlobalMapping -RemotePath $Env:smbremotepath -Force` - if output, err := utils.RunPowershellCmd(cmd, fmt.Sprintf("smbremotepath=%s", remotePath)); err != nil { - return fmt.Errorf("UnmountSmbShare failed. output: %q, err: %v", string(output), err) + smbQuery := query.NewWmiQuery("MSFT_SmbGlobalMapping", "RemotePath", remotePathForQuery(remotePath)) + instances, err := cim.QueryInstances(cim.WMINamespaceSmb, smbQuery) + if err != nil { + return err + } + + _, err = instances[0].InvokeMethod("Remove", true) + if err != nil { + return fmt.Errorf("error remove smb mapping '%s'. err: %v", remotePath, err) } + return nil } diff --git a/pkg/os/system/api.go b/pkg/os/system/api.go index 2a2740b9..2293a8b4 100644 --- a/pkg/os/system/api.go +++ b/pkg/os/system/api.go @@ -1,12 +1,12 @@ package system import ( - "encoding/json" "fmt" - "os/exec" - "strings" - + "github.com/kubernetes-csi/csi-proxy/pkg/cim" + "github.com/kubernetes-csi/csi-proxy/pkg/server/system/impl" "github.com/kubernetes-csi/csi-proxy/pkg/utils" + "github.com/microsoft/wmi/pkg/base/query" + "github.com/microsoft/wmi/server2019/root/cimv2" ) // Implements the System OS API calls. All code here should be very simple @@ -25,6 +25,35 @@ type ServiceInfo struct { Status uint32 `json:"Status"` } +var ( + startModeMappings = map[string]uint32{ + "Boot": impl.START_TYPE_BOOT, + "System": impl.START_TYPE_SYSTEM, + "Auto": impl.START_TYPE_AUTOMATIC, + "Manual": impl.START_TYPE_MANUAL, + "Disabled": impl.START_TYPE_DISABLED, + } + + statusMappings = map[string]uint32{ + "Unknown": impl.SERVICE_STATUS_UNKNOWN, + "Stopped": impl.SERVICE_STATUS_STOPPED, + "Start Pending": impl.SERVICE_STATUS_START_PENDING, + "Stop Pending": impl.SERVICE_STATUS_STOP_PENDING, + "Running": impl.SERVICE_STATUS_RUNNING, + "Continue Pending": impl.SERVICE_STATUS_CONTINUE_PENDING, + "Pause Pending": impl.SERVICE_STATUS_PAUSE_PENDING, + "Paused": impl.SERVICE_STATUS_PAUSED, + } +) + +func serviceStartModeToStartType(startMode string) uint32 { + return startModeMappings[startMode] +} + +func serviceState(status string) uint32 { + return statusMappings[status] +} + type APIImplementor struct{} func New() APIImplementor { @@ -32,45 +61,61 @@ func New() APIImplementor { } func (APIImplementor) GetBIOSSerialNumber() (string, error) { - // Taken from Kubernetes vSphere cloud provider - // https://github.com/kubernetes/kubernetes/blob/103e926604de6f79161b78af3e792d0ed282bc06/staging/src/k8s.io/legacy-cloud-providers/vsphere/vsphere_util_windows.go#L28 - result, err := exec.Command("wmic", "bios", "get", "serialnumber").Output() + biosQuery := query.NewWmiQueryWithSelectList("CIM_BIOSElement", []string{"SerialNumber"}) + instances, err := cim.QueryInstances("", biosQuery) if err != nil { return "", err } - lines := strings.FieldsFunc(string(result), func(r rune) bool { - switch r { - case '\n', '\r': - return true - default: - return false - } - }) - if len(lines) != 2 { - return "", fmt.Errorf("received unexpected value retrieving host uuid: %q", string(result)) + + bios, err := cimv2.NewCIM_BIOSElementEx1(instances[0]) + if err != nil { + return "", fmt.Errorf("failed to get BIOS element: %w", err) + } + + sn, err := bios.GetPropertySerialNumber() + if err != nil { + return "", fmt.Errorf("failed to get BIOS serial number property: %w", err) } - return lines[1], nil + + return sn, nil } func (APIImplementor) GetService(name string) (*ServiceInfo, error) { - script := `Get-Service -Name $env:ServiceName | Select-Object DisplayName, Status, StartType | ` + - `ConvertTo-JSON` - cmdEnv := fmt.Sprintf("ServiceName=%s", name) - out, err := utils.RunPowershellCmd(script, cmdEnv) + serviceQuery := query.NewWmiQueryWithSelectList("Win32_Service", []string{"DisplayName", "State", "StartMode"}, "Name", name) + instances, err := cim.QueryInstances("", serviceQuery) if err != nil { - return nil, fmt.Errorf("error querying service name=%s. cmd: %s, output: %s, error: %v", name, script, string(out), err) + return nil, err } - var serviceInfo ServiceInfo - err = json.Unmarshal(out, &serviceInfo) + service, err := cimv2.NewWin32_ServiceEx1(instances[0]) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get service %s: %w", name, err) + } + + displayName, err := service.GetPropertyDisplayName() + if err != nil { + return nil, fmt.Errorf("failed to get displayName property of service %s: %w", name, err) + } + + state, err := service.GetPropertyState() + if err != nil { + return nil, fmt.Errorf("failed to get state property of service %s: %w", name, err) + } + + startMode, err := service.GetPropertyStartMode() + if err != nil { + return nil, fmt.Errorf("failed to get startMode property of service %s: %w", name, err) } - return &serviceInfo, nil + return &ServiceInfo{ + DisplayName: displayName, + StartType: serviceStartModeToStartType(startMode), + Status: serviceState(state), + }, nil } func (APIImplementor) StartService(name string) error { + // Note: both StartService and StopService are not implemented by WMI script := `Start-Service -Name $env:ServiceName` cmdEnv := fmt.Sprintf("ServiceName=%s", name) out, err := utils.RunPowershellCmd(script, cmdEnv) diff --git a/pkg/os/volume/api.go b/pkg/os/volume/api.go index a3b7a550..2bf7cf87 100644 --- a/pkg/os/volume/api.go +++ b/pkg/os/volume/api.go @@ -1,16 +1,19 @@ package volume import ( - "encoding/json" "fmt" + "github.com/go-ole/go-ole" + "github.com/kubernetes-csi/csi-proxy/pkg/cim" + "github.com/kubernetes-csi/csi-proxy/pkg/utils" + "github.com/microsoft/wmi/pkg/base/query" + "github.com/microsoft/wmi/pkg/errors" + "github.com/microsoft/wmi/server2019/root/microsoft/windows/storage" + "k8s.io/klog/v2" "os" "path/filepath" "regexp" "strconv" "strings" - - "github.com/kubernetes-csi/csi-proxy/pkg/utils" - "k8s.io/klog/v2" ) // API exposes the internal volume operations available in the server @@ -35,7 +38,7 @@ type API interface { GetVolumeIDFromTargetPath(targetPath string) (string, error) // WriteVolumeCache writes the volume `volumeID`'s cache to disk. WriteVolumeCache(volumeID string) error - // GetVolumeIDFromTargetPath returns the volume id of a given target path. + // GetClosestVolumeIDFromTargetPath returns the volume id of a given target path. GetClosestVolumeIDFromTargetPath(targetPath string) (string, error) } @@ -61,49 +64,56 @@ func New() VolumeAPI { return VolumeAPI{} } -func getVolumeSize(volumeID string) (int64, error) { - cmd := `(Get-Volume -UniqueId "$Env:volumeID" | Get-partition).Size` - cmdEnv := fmt.Sprintf("volumeID=%s", volumeID) - out, err := utils.RunPowershellCmd(cmd, cmdEnv) - - if err != nil || len(out) == 0 { - return -1, fmt.Errorf("error getting size of the partition from mount. cmd %s, output: %s, error: %v", cmd, string(out), err) +// ListVolumesOnDisk - returns back list of volumes(volumeIDs) in a disk and a partition. +func (VolumeAPI) ListVolumesOnDisk(diskNumber uint32, partitionNumber uint32) (volumeIDs []string, err error) { + partitions, err := cim.ListPartitionsOnDisk(diskNumber, partitionNumber, []string{"ObjectId"}) + if err != nil { + return nil, errors.Wrapf(err, "failed to list partition on disk %d", diskNumber) } - outString := strings.TrimSpace(string(out)) - volumeSize, err := strconv.ParseInt(outString, 10, 64) + volumes, err := cim.ListVolumes([]string{"ObjectId", "UniqueId"}) if err != nil { - return -1, fmt.Errorf("error parsing size of volume %s received %v trimmed to %v err %v", volumeID, out, outString, err) + return nil, errors.Wrapf(err, "failed to list volumes") } - return volumeSize, nil -} - -// ListVolumesOnDisk - returns back list of volumes(volumeIDs) in a disk and a partition. -func (VolumeAPI) ListVolumesOnDisk(diskNumber uint32, partitionNumber uint32) (volumeIDs []string, err error) { - var cmd string - if partitionNumber == 0 { - // 0 means that the partitionNumber wasn't set so we list all the partitions - cmd = fmt.Sprintf("(Get-Disk -Number %d | Get-Partition | Get-Volume).UniqueId", diskNumber) - } else { - cmd = fmt.Sprintf("(Get-Disk -Number %d | Get-Partition -PartitionNumber %d | Get-Volume).UniqueId", diskNumber, partitionNumber) + filtered, err := cim.FindVolumesByPartition(volumes, partitions) + if cim.IgnoreNotFound(err) != nil { + return nil, errors.Wrapf(err, "failed to list volumes on disk %d", diskNumber) } - out, err := utils.RunPowershellCmd(cmd) - if err != nil { - return []string{}, fmt.Errorf("error list volumes on disk. cmd: %s, output: %s, error: %v", cmd, string(out), err) + + for _, volume := range filtered { + uniqueID, err := volume.GetPropertyUniqueId() + if err != nil { + return nil, errors.Wrapf(err, "failed to list volumes") + } + volumeIDs = append(volumeIDs, uniqueID) } - volumeIds := strings.Split(strings.TrimSpace(string(out)), "\r\n") - return volumeIds, nil + return volumeIDs, nil } // FormatVolume - Formats a volume with the NTFS format. func (VolumeAPI) FormatVolume(volumeID string) (err error) { - cmd := `Get-Volume -UniqueId "$Env:volumeID" | Format-Volume -FileSystem ntfs -Confirm:$false` - cmdEnv := fmt.Sprintf("volumeID=%s", volumeID) - out, err := utils.RunPowershellCmd(cmd, cmdEnv) + volume, err := cim.QueryVolumeByUniqueID(volumeID, nil) if err != nil { - return fmt.Errorf("error formatting volume. cmd: %s, output: %s, error: %v", cmd, string(out), err) + return fmt.Errorf("error formatting volume (%s). error: %v", volumeID, err) + } + + result, err := volume.InvokeMethodWithReturn( + "Format", + "NTFS", // Format, + "", // FileSystemLabel, + nil, // AllocationUnitSize, + false, // Full, + true, // Force + nil, // Compress, + nil, // ShortFileNameSupport, + nil, // SetIntegrityStreams, + nil, // UseLargeFRS, + nil, // DisableHeatGathering, + ) + if result != 0 || err != nil { + return fmt.Errorf("error formatting volume (%s). result: %d, error: %v", volumeID, result, err) } // TODO: Do we need to handle anything for len(out) == 0 return nil @@ -116,30 +126,33 @@ func (VolumeAPI) WriteVolumeCache(volumeID string) (err error) { // IsVolumeFormatted - Check if the volume is formatted with the pre specified filesystem(typically ntfs). func (VolumeAPI) IsVolumeFormatted(volumeID string) (bool, error) { - cmd := `(Get-Volume -UniqueId "$Env:volumeID" -ErrorAction Stop).FileSystemType` - cmdEnv := fmt.Sprintf("volumeID=%s", volumeID) - out, err := utils.RunPowershellCmd(cmd, cmdEnv) + volume, err := cim.QueryVolumeByUniqueID(volumeID, []string{"FileSystemType"}) if err != nil { - return false, fmt.Errorf("error checking if volume is formatted. cmd: %s, output: %s, error: %v", cmd, string(out), err) + return false, fmt.Errorf("error checking if volume (%s) is formatted. error: %v", volumeID, err) } - stringOut := strings.TrimSpace(string(out)) - if len(stringOut) == 0 || strings.EqualFold(stringOut, "Unknown") { - return false, nil + + fsType, err := volume.GetProperty("FileSystemType") + if err != nil { + return false, fmt.Errorf("failed to query volume file system type (%s): %w", volumeID, err) } - return true, nil + + const FileSystemUnknown = 0 + return fsType.(int32) != FileSystemUnknown, nil } // MountVolume - mounts a volume to a path. This is done using the Add-PartitionAccessPath for presenting the volume via a path. func (VolumeAPI) MountVolume(volumeID, path string) error { - cmd := `Get-Volume -UniqueId "$Env:volumeID" | Get-Partition | Add-PartitionAccessPath -AccessPath $Env:mountpath` - cmdEnv := []string{} - cmdEnv = append(cmdEnv, fmt.Sprintf("volumeID=%s", volumeID)) - cmdEnv = append(cmdEnv, fmt.Sprintf("mountpath=%s", path)) - out, err := utils.RunPowershellCmd(cmd, cmdEnv...) - + part, err := cim.GetPartitionByVolumeUniqueID(volumeID, nil) if err != nil { - return fmt.Errorf("error mount volume to path. cmd: %s, output: %s, error: %v", cmd, string(out), err) + return err + } + + var status string + result, err := part.InvokeMethodWithReturn("AddAccessPath", path, nil, &status) + if result != 0 || err != nil { + return fmt.Errorf("error mount volume (%s) to path %s. result %d, status %s, error: %v", volumeID, path, result, status, err) } + return nil } @@ -149,150 +162,190 @@ func (VolumeAPI) UnmountVolume(volumeID, path string) error { return err } - cmd := `Get-Volume -UniqueId "$Env:volumeID" | Get-Partition | Remove-PartitionAccessPath -AccessPath $Env:mountpath` - cmdEnv := []string{} - cmdEnv = append(cmdEnv, fmt.Sprintf("volumeID=%s", volumeID)) - cmdEnv = append(cmdEnv, fmt.Sprintf("mountpath=%s", path)) - out, err := utils.RunPowershellCmd(cmd, cmdEnv...) - + part, err := cim.GetPartitionByVolumeUniqueID(volumeID, nil) if err != nil { - return fmt.Errorf("error getting driver letter to mount volume. cmd: %s, output: %s,error: %v", cmd, string(out), err) + return err + } + + result, err := part.InvokeMethodWithReturn("RemoveAccessPath", path) + if result != 0 || err != nil { + return fmt.Errorf("error umount volume (%s) from path %s. result %d, error: %v", volumeID, path, result, err) } return nil } // ResizeVolume - resizes a volume with the given size, if size == 0 then max supported size is used func (VolumeAPI) ResizeVolume(volumeID string, size int64) error { - // If size is 0 then we will resize to the maximum size possible, otherwise just resize to size - var cmd string - var out []byte var err error var finalSize int64 - var outString string - if size == 0 { - cmd = `Get-Volume -UniqueId "$Env:volumeID" | Get-partition | Get-PartitionSupportedSize | Select SizeMax | ConvertTo-Json` - cmdEnv := fmt.Sprintf("volumeID=%s", volumeID) - out, err := utils.RunPowershellCmd(cmd, cmdEnv) + part, err := cim.GetPartitionByVolumeUniqueID(volumeID, nil) + if err != nil { + return err + } - if err != nil || len(out) == 0 { - return fmt.Errorf("error getting sizemin,sizemax from mount. cmd: %s, output: %s, error: %v", cmd, string(out), err) + // If size is 0 then we will resize to the maximum size possible, otherwise just resize to size + if size == 0 { + var sizeMin, sizeMax ole.VARIANT + var status string + result, err := part.InvokeMethodWithReturn("GetSupportedSize", &sizeMin, &sizeMax, &status) + if result != 0 || err != nil { + return fmt.Errorf("error getting sizemin, sizemax from volume (%s). result: %d, error: %v", volumeID, result, err) } - var getVolumeSizing map[string]int64 - outString = string(out) - err = json.Unmarshal([]byte(outString), &getVolumeSizing) + finalSizeStr := sizeMax.ToString() + finalSize, err = strconv.ParseInt(finalSizeStr, 10, 64) if err != nil { - return fmt.Errorf("out %v outstring %v err %v", out, outString, err) + return fmt.Errorf("error parsing the sizeMax of volume (%s) with error (%v)", volumeID, err) } - - sizeMax := getVolumeSizing["SizeMax"] - - finalSize = sizeMax } else { finalSize = size } - currentSize, err := getVolumeSize(volumeID) + currentSizeVal, err := part.GetProperty("Size") if err != nil { return fmt.Errorf("error getting the current size of volume (%s) with error (%v)", volumeID, err) } + currentSize, err := strconv.ParseInt(currentSizeVal.(string), 10, 64) + if err != nil { + return fmt.Errorf("error parsing the current size of volume (%s) with error (%v)", volumeID, err) + } + //if the partition's size is already the size we want this is a noop, just return if currentSize >= finalSize { - klog.V(2).Infof("Attempted to resize volume %s to a lower size, from currentBytes=%d wantedBytes=%d", volumeID, currentSize, finalSize) + klog.V(2).Infof("Attempted to resize volume (%s) to a lower size, from currentBytes=%d wantedBytes=%d", volumeID, currentSize, finalSize) return nil } - cmd = fmt.Sprintf(`Get-Volume -UniqueId "$Env:volumeID" | Get-Partition | Resize-Partition -Size %d`, finalSize) - cmdEnv := []string{} - cmdEnv = append(cmdEnv, fmt.Sprintf("volumeID=%s", volumeID)) - out, err = utils.RunPowershellCmd(cmd, cmdEnv...) + var status string + result, err := part.InvokeMethodWithReturn("Resize", strconv.Itoa(int(finalSize)), &status) + + if result != 0 || err != nil { + return fmt.Errorf("error resizing volume (%s). size:%v, finalSize %v, error: %v", volumeID, size, finalSize, err) + } + + diskNumber, err := cim.GetPartitionDiskNumber(part) + if err != nil { + return fmt.Errorf("error parsing disk number of volume (%s). error: %v", volumeID, err) + } + + disk, err := cim.QueryDiskByNumber(diskNumber, nil) if err != nil { - return fmt.Errorf("error resizing volume. cmd: %s, output: %s size:%v, finalSize %v, error: %v", cmd, string(out), size, finalSize, err) + return fmt.Errorf("error parsing disk number of volume (%s). error: %v", volumeID, err) + } + + result, err = disk.InvokeMethodWithReturn("Refresh", &status) + if result != 0 || err != nil { + return fmt.Errorf("error rescan disk (%d). result %d, error: %v", diskNumber, result, err) } + return nil } // GetVolumeStats - retrieves the volume stats for a given volume func (VolumeAPI) GetVolumeStats(volumeID string) (int64, int64, error) { - // get the size and sizeRemaining for the volume - cmd := `(Get-Volume -UniqueId "$Env:volumeID" | Select SizeRemaining,Size) | ConvertTo-Json` - cmdEnv := fmt.Sprintf("volumeID=%s", volumeID) - out, err := utils.RunPowershellCmd(cmd, cmdEnv) + volume, err := cim.QueryVolumeByUniqueID(volumeID, []string{"UniqueId", "SizeRemaining", "Size"}) + if err != nil && !errors.IsNotFound(err) { + return -1, -1, fmt.Errorf("error getting capacity and used size of volume (%s). error: %v", volumeID, err) + } + volumeSizeVal, err := volume.GetProperty("Size") if err != nil { - return -1, -1, fmt.Errorf("error getting capacity and used size of volume. cmd: %s, output: %s, error: %v", cmd, string(out), err) + return -1, -1, fmt.Errorf("failed to query volume size (%s): %w", volumeID, err) } - var getVolume map[string]int64 - outString := string(out) - err = json.Unmarshal([]byte(outString), &getVolume) + volumeSize, err := strconv.ParseInt(volumeSizeVal.(string), 10, 64) if err != nil { - return -1, -1, fmt.Errorf("out %v outstring %v err %v", out, outString, err) + return -1, -1, fmt.Errorf("failed to parse volume size (%s): %w", volumeID, err) } - var volumeSizeRemaining int64 - var volumeSize int64 - volumeSize = getVolume["Size"] - volumeSizeRemaining = getVolume["SizeRemaining"] + volumeUsedSizeVal, err := volume.GetProperty("SizeRemaining") + if err != nil { + return -1, -1, fmt.Errorf("failed to query volume remaining size (%s): %w", volumeID, err) + } + + volumeUsedSize, err := strconv.ParseInt(volumeUsedSizeVal.(string), 10, 64) + if err != nil { + return -1, -1, fmt.Errorf("failed to parse volume remaining size (%s): %w", volumeID, err) + } - volumeUsedSize := volumeSize - volumeSizeRemaining return volumeSize, volumeUsedSize, nil } // GetDiskNumberFromVolumeID - gets the disk number where the volume is. func (VolumeAPI) GetDiskNumberFromVolumeID(volumeID string) (uint32, error) { // get the size and sizeRemaining for the volume - cmd := `(Get-Volume -UniqueId "$Env:volumeID" | Get-Partition).DiskNumber` - cmdEnv := fmt.Sprintf("volumeID=%s", volumeID) - out, err := utils.RunPowershellCmd(cmd, cmdEnv) - - if err != nil || len(out) == 0 { - return 0, fmt.Errorf("error getting disk number. cmd: %s, output: %s, error: %v", cmd, string(out), err) - } - - reg, err := regexp.Compile("[^0-9]+") + part, err := cim.GetPartitionByVolumeUniqueID(volumeID, []string{"DiskNumber"}) if err != nil { - return 0, fmt.Errorf("error compiling regex. err: %v", err) + return 0, err } - diskNumberOutput := reg.ReplaceAllString(string(out), "") - - diskNumber, err := strconv.ParseUint(diskNumberOutput, 10, 32) + diskNumber, err := part.GetProperty("DiskNumber") if err != nil { - return 0, fmt.Errorf("error parsing disk number. cmd: %s, output: %s, error: %v", cmd, diskNumberOutput, err) + return 0, fmt.Errorf("error query disk number of volume (%s). error: %v", volumeID, err) } - return uint32(diskNumber), nil + return uint32(diskNumber.(int32)), nil } // GetVolumeIDFromTargetPath - gets the volume ID given a mount point, the function is recursive until it find a volume or errors out func (VolumeAPI) GetVolumeIDFromTargetPath(mount string) (string, error) { - volumeString, err := getTarget(mount) + partitions, err := cim.ListPartitionsWithFilters(nil, + query.NewWmiQueryFilter("GptType", cim.GPTPartitionTypeMicrosoftReserved, query.NotEquals), + ) + if err != nil { + return "", nil + } + var partitionForMount *storage.MSFT_Partition + for _, part := range partitions { + var pathsVariantReturnValue ole.VARIANT + var status string + result, err := part.InvokeMethodWithReturn("GetAccessPaths", &pathsVariantReturnValue, &status) + if err != nil { + return "", fmt.Errorf("error get partition %v access path. result: %d, error: %v", part, result, err) + } + + pathsVariant := pathsVariantReturnValue.ToArray() + for _, pathVariant := range pathsVariant.ToValueArray() { + if strings.TrimSuffix(pathVariant.(string), "\\") == mount { + partitionForMount = part + break + } + } + + if partitionForMount != nil { + break + } + } + + if partitionForMount == nil { + return "", fmt.Errorf("volume from mount %s not found", mount) + } + + volumes, err := cim.ListVolumes([]string{"UniqueId", "ObjectId"}) if err != nil { - return "", fmt.Errorf("error getting the volume for the mount %s, internal error %v", mount, err) + return "", err } - return volumeString, nil -} + filtered, err := cim.FindVolumesByPartition(volumes, []*storage.MSFT_Partition{partitionForMount}) + if err != nil { + return "", err + } -func getTarget(mount string) (string, error) { - cmd := `(Get-Item -Path $Env:mountpath).Target` - cmdEnv := fmt.Sprintf("mountpath=%s", mount) - out, err := utils.RunPowershellCmd(cmd, cmdEnv) - if err != nil || len(out) == 0 { - return "", fmt.Errorf("error getting volume from mount. cmd: %s, output: %s, error: %v", cmd, string(out), err) + if len(filtered) == 0 { + return "", fmt.Errorf("volume from mount %s not found", mount) } - volumeString := strings.TrimSpace(string(out)) - if !strings.HasPrefix(volumeString, "Volume") { - return getTarget(volumeString) + + uniqueID, err := filtered[0].GetPropertyUniqueId() + if err != nil { + return "", err } - return ensureVolumePrefix(volumeString), nil + return ensureVolumePrefix(uniqueID), nil } -// GetVolumeIDFromTargetPath returns the volume id of a given target path. +// GetClosestVolumeIDFromTargetPath returns the volume id of a given target path. func (VolumeAPI) GetClosestVolumeIDFromTargetPath(targetPath string) (string, error) { volumeString, err := findClosestVolume(targetPath) @@ -384,26 +437,31 @@ func dereferenceSymlink(path string) (string, error) { // getVolumeForDriveLetter gets a volume from a drive letter (e.g. C:/). func getVolumeForDriveLetter(path string) (string, error) { if len(path) != 1 { - return "", fmt.Errorf("The path=%s is not a valid DriverLetter", path) + return "", fmt.Errorf("the path %s is not a valid drive letter", path) } - cmd := `(Get-Partition -DriveLetter $Env:drivepath | Get-Volume).UniqueId` - cmdEnv := fmt.Sprintf("drivepath=%s", path) - out, err := utils.RunPowershellCmd(cmd, cmdEnv) + volume, err := cim.GetVolumeByDriveLetter(path, []string{"UniqueId"}) if err != nil { - return "", err + return "", nil } - output := strings.TrimSpace(string(out)) - klog.V(8).Infof("Stdout: %s", output) - return output, nil + + uniqueID, err := volume.GetPropertyUniqueId() + if err != nil { + return "", fmt.Errorf("error query unique ID of volume (%v). error: %v", volume, err) + } + + return uniqueID, nil } func writeCache(volumeID string) error { - cmd := `Get-Volume -UniqueId "$Env:volumeID" | Write-Volumecache` - cmdEnv := fmt.Sprintf("volumeID=%s", volumeID) - out, err := utils.RunPowershellCmd(cmd, cmdEnv) - if err != nil { - return fmt.Errorf("error writing volume cache. cmd: %s, output: %s, error: %v", cmd, string(out), err) + volume, err := cim.QueryVolumeByUniqueID(volumeID, []string{}) + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("error writing volume (%s) cache. error: %v", volumeID, err) + } + + result, err := volume.Flush() + if result != 0 || err != nil { + return fmt.Errorf("error writing volume (%s) cache. result: %d, error: %v", volumeID, result, err) } return nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index db2c27f7..3ab36036 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -11,6 +11,10 @@ github.com/davecgh/go-spew/spew # github.com/go-logr/logr v1.2.0 ## explicit; go 1.16 github.com/go-logr/logr +# github.com/go-ole/go-ole v1.3.0 +## explicit; go 1.12 +github.com/go-ole/go-ole +github.com/go-ole/go-ole/oleutil # github.com/golang/protobuf v1.5.3 ## explicit; go 1.9 github.com/golang/protobuf/jsonpb @@ -79,6 +83,18 @@ github.com/kubernetes-csi/csi-proxy/client/groups/volume/v1beta1 github.com/kubernetes-csi/csi-proxy/client/groups/volume/v1beta2 github.com/kubernetes-csi/csi-proxy/client/groups/volume/v1beta3 github.com/kubernetes-csi/csi-proxy/client/groups/volume/v2alpha1 +# github.com/microsoft/wmi v0.23.0 +## explicit; go 1.22 +github.com/microsoft/wmi/go/wmi +github.com/microsoft/wmi/pkg/base/credential +github.com/microsoft/wmi/pkg/base/host +github.com/microsoft/wmi/pkg/base/instance +github.com/microsoft/wmi/pkg/base/query +github.com/microsoft/wmi/pkg/base/session +github.com/microsoft/wmi/pkg/errors +github.com/microsoft/wmi/pkg/wmiinstance +github.com/microsoft/wmi/server2019/root/cimv2 +github.com/microsoft/wmi/server2019/root/microsoft/windows/storage # github.com/pkg/errors v0.9.1 ## explicit github.com/pkg/errors