From 1b05a48c8c4b5f2fadacd9bec1b860791f6ec541 Mon Sep 17 00:00:00 2001 From: Zhongcheng Lao Date: Thu, 13 Mar 2025 12:11:38 +0800 Subject: [PATCH 1/4] Use WMI to implement iSCSI API to reduce PowerShell overhead --- integrationtests/iscsi_ps_scripts.go | 23 +- pkg/cim/iscsi.go | 368 +++++++++++++++++++++++++++ pkg/os/iscsi/api.go | 236 +++++++++++------ 3 files changed, 534 insertions(+), 93 deletions(-) create mode 100644 pkg/cim/iscsi.go diff --git a/integrationtests/iscsi_ps_scripts.go b/integrationtests/iscsi_ps_scripts.go index 89bec253..202e390b 100644 --- a/integrationtests/iscsi_ps_scripts.go +++ b/integrationtests/iscsi_ps_scripts.go @@ -42,14 +42,14 @@ $ProgressPreference = "SilentlyContinue" $targetName = "%s" # Get local IPv4 (e.g. 10.30.1.15, not 127.0.0.1) -$address = $(Get-NetIPAddress | Where-Object { $_.InterfaceAlias -eq "Ethernet" -and $_.AddressFamily -eq "IPv4" }).IPAddress +$address = $(Get-NetIPAddress | Where-Object { $_.InterfaceAlias -eq "%s" -and $_.AddressFamily -eq "IPv4" }).IPAddress # Create virtual disk in RAM -New-IscsiVirtualDisk -Path "ramdisk:scratch-${targetName}.vhdx" -Size 100MB | Out-Null +New-IscsiVirtualDisk -Path "ramdisk:scratch-${targetName}.vhdx" -Size 100MB -ComputerName $env:computername | Out-Null # Create a target that allows all initiator IQNs and map a disk to the new target -$target = New-IscsiServerTarget -TargetName $targetName -InitiatorIds @("Iqn:*") -Add-IscsiVirtualDiskTargetMapping -TargetName $targetName -DevicePath "ramdisk:scratch-${targetName}.vhdx" | Out-Null +$target = New-IscsiServerTarget -TargetName $targetName -InitiatorIds @("Iqn:*") -ComputerName $env:computername +Add-IscsiVirtualDiskTargetMapping -TargetName $targetName -DevicePath "ramdisk:scratch-${targetName}.vhdx" -ComputerName $env:computername | Out-Null $output = @{ "iqn" = "$($target.TargetIqn)" @@ -68,7 +68,7 @@ $username = "%s" $password = "%s" $securestring = ConvertTo-SecureString -String $password -AsPlainText -Force $chap = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ($username, $securestring) -Set-IscsiServerTarget -TargetName $targetName -EnableChap $true -Chap $chap +Set-IscsiServerTarget -TargetName $targetName -EnableChap $true -Chap $chap -ComputerName $env:computername ` func setChap(targetName string, username string, password string) error { @@ -92,7 +92,7 @@ $securestring = ConvertTo-SecureString -String $password -AsPlainText -Force # Windows initiator does not uses the username for mutual authentication $chap = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ($username, $securestring) -Set-IscsiServerTarget -TargetName $targetName -EnableReverseChap $true -ReverseChap $chap +Set-IscsiServerTarget -TargetName $targetName -EnableReverseChap $true -ReverseChap $chap -ComputerName $env:computername ` func setReverseChap(targetName string, password string) error { @@ -131,8 +131,8 @@ Get-IscsiTarget | Disconnect-IscsiTarget -Confirm:$false Get-IscsiTargetPortal | Remove-IscsiTargetPortal -confirm:$false # Clean target -Get-IscsiServerTarget | Remove-IscsiServerTarget -Get-IscsiVirtualDisk | Remove-IscsiVirtualDisk +Get-IscsiServerTarget -ComputerName $env:computername | Remove-IscsiServerTarget +Get-IscsiVirtualDisk -ComputerName $env:computername | Remove-IscsiVirtualDisk # Stop iSCSI initiator Get-Service "MsiSCSI" | Stop-Service @@ -173,7 +173,12 @@ func runPowershellScript(script string) (string, error) { } func setupEnv(targetName string) (*IscsiSetupConfig, error) { - script := fmt.Sprintf(IscsiEnvironmentSetupScript, targetName) + ethernetName := "Ethernet" + if val, ok := os.LookupEnv("ETHERNET_NAME"); ok { + ethernetName = val + } + + script := fmt.Sprintf(IscsiEnvironmentSetupScript, targetName, ethernetName) out, err := runPowershellScript(script) if err != nil { return nil, fmt.Errorf("failed setting up environment. err=%v", err) diff --git a/pkg/cim/iscsi.go b/pkg/cim/iscsi.go new file mode 100644 index 00000000..75234f91 --- /dev/null +++ b/pkg/cim/iscsi.go @@ -0,0 +1,368 @@ +//go:build windows +// +build windows + +package cim + +import ( + "fmt" + "strconv" + + "github.com/microsoft/wmi/pkg/base/query" + cim "github.com/microsoft/wmi/pkg/wmiinstance" + "github.com/microsoft/wmi/server2019/root/microsoft/windows/storage" +) + +// ListISCSITargetPortals retrieves a list of iSCSI target portals. +// +// The equivalent WMI query is: +// +// SELECT [selectors] FROM MSFT_IscsiTargetPortal +// +// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitargetportal +// for the WMI class definition. +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 +} + +// QueryISCSITargetPortal retrieves information about a specific iSCSI target portal +// identified by its network address and port number. +// +// The equivalent WMI query is: +// +// SELECT [selectors] FROM MSFT_IscsiTargetPortal +// WHERE TargetPortalAddress = '
' +// AND TargetPortalPortNumber = '' +// +// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitargetportal +// for the WMI class definition. +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 +} + +// NewISCSITargetPortal creates a new iSCSI target portal. +// +// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitargetportal-new +// for the WMI method definition. +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 ( + // Indexes iSCSI targets by their Object ID specified in node address + mappingISCSITargetIndexer = mappingObjectRefIndexer("iSCSITarget", "MSFT_iSCSITarget", "NodeAddress") + // Indexes iSCSI target portals by their Object ID specified in portal address + mappingISCSITargetPortalIndexer = mappingObjectRefIndexer("iSCSITargetPortal", "MSFT_iSCSITargetPortal", "TargetPortalAddress") + // Indexes iSCSI connections by their Object ID specified in connection identifier + mappingISCSIConnectionIndexer = mappingObjectRefIndexer("iSCSIConnection", "MSFT_iSCSIConnection", "ConnectionIdentifier") + // Indexes iSCSI sessions by their Object ID specified in session identifier + mappingISCSISessionIndexer = mappingObjectRefIndexer("iSCSISession", "MSFT_iSCSISession", "SessionIdentifier") + + // Indexes iSCSI targets by their node address + iscsiTargetIndexer = stringPropertyIndexer("NodeAddress") + // Indexes iSCSI targets by their target portal address + iscsiTargetPortalIndexer = stringPropertyIndexer("TargetPortalAddress") + // Indexes iSCSI connections by their connection identifier + iscsiConnectionIndexer = stringPropertyIndexer("ConnectionIdentifier") + // Indexes iSCSI sessions by their session identifier + iscsiSessionIndexer = stringPropertyIndexer("SessionIdentifier") +) + +// ListISCSITargetToISCSITargetPortalMapping builds a mapping between iSCSI target and iSCSI target portal with iSCSI target as the key. +// +// The equivalent WMI query is: +// +// SELECT [selectors] FROM MSFT_iSCSITargetToiSCSITargetPortal +// +// iSCSITarget | iSCSITargetPortal +// ----------- | ----------------- +// MSFT_iSCSITarget (NodeAddress = "iqn.1991-05.com.microsoft:win-8e2evaq9q...) | MSFT_iSCSITargetPortal (TargetPortalAdd... +// +// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitargettoiscsitargetportal +// for the WMI class definition. +func ListISCSITargetToISCSITargetPortalMapping() (map[string]string, error) { + return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSITargetToiSCSITargetPortal", nil, mappingISCSITargetIndexer, mappingISCSITargetPortalIndexer) +} + +// ListISCSIConnectionToISCSITargetMapping builds a mapping between iSCSI connection and iSCSI target with iSCSI connection as the key. +// +// The equivalent WMI query is: +// +// SELECT [selectors] FROM MSFT_iSCSITargetToiSCSIConnection +// +// iSCSIConnection | iSCSITarget +// --------------- | ----------- +// MSFT_iSCSIConnection (ConnectionIdentifier = "ffffac0cacbff010-15") | MSFT_iSCSITarget (NodeAddress = "iqn.1991-05.com... +// +// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitargettoiscsitargetportal +// for the WMI class definition. +func ListISCSIConnectionToISCSITargetMapping() (map[string]string, error) { + return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSITargetToiSCSIConnection", nil, mappingISCSIConnectionIndexer, mappingISCSITargetIndexer) +} + +// ListISCSISessionToISCSITargetMapping builds a mapping between iSCSI session and iSCSI target with iSCSI session as the key. +// +// The equivalent WMI query is: +// +// SELECT [selectors] FROM MSFT_iSCSITargetToiSCSISession +// +// iSCSISession | iSCSITarget +// ------------ | ----------- +// MSFT_iSCSISession (SessionIdentifier = "ffffac0cacbff010-4000013700000016") | MSFT_iSCSITarget (NodeAddress = "iqn.199... +// +// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitargettoiscsisession +// for the WMI class definition. +func ListISCSISessionToISCSITargetMapping() (map[string]string, error) { + return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSITargetToiSCSISession", nil, mappingISCSISessionIndexer, mappingISCSITargetIndexer) +} + +// ListDiskToISCSIConnectionMapping builds a mapping between disk and iSCSI connection with disk Object ID as the key. +// +// The equivalent WMI query is: +// +// SELECT [selectors] FROM MSFT_iSCSIConnectionToDisk +// +// Disk | iSCSIConnection +// ---- | --------------- +// MSFT_Disk (ObjectId = "{1}\\WIN-8E2EVAQ9QSB\root/Microsoft/Win...) | MSFT_iSCSIConnection (ConnectionIdentifier = "fff... +// +// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsiconnectiontodisk +// for the WMI class definition. +func ListDiskToISCSIConnectionMapping() (map[string]string, error) { + return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSIConnectionToDisk", nil, mappingObjectRefIndexer("Disk", "MSFT_Disk", "ObjectId"), mappingISCSIConnectionIndexer) +} + +// ListISCSITargetsByTargetPortalWithFilters retrieves all iSCSI targets from the specified iSCSI target portal and conditions by query filters. +// +// It lists all the iSCSI targets via the following WMI query +// +// SELECT [selectors] FROM MSFT_iSCSITarget +// +// Then find all iSCSITarget objects from MSFT_iSCSITargetToiSCSITargetPortal mapping. +// +// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitarget +// for the WMI class definition. +func ListISCSITargetsByTargetPortalWithFilters(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 +} + +// QueryISCSITarget retrieves the iSCSI target from the specified portal address, portal and node address. +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 := ListISCSITargetsByTargetPortalWithFilters(selectorList, []*storage.MSFT_iSCSITargetPortal{portal}, + query.NewWmiQueryFilter("NodeAddress", nodeAddress, query.Equals)) + if err != nil { + return nil, err + } + + return targets[0], nil +} + +// QueryISCSISessionByTarget retrieves the iSCSI session from the specified iSCSI target. +// +// It lists all the iSCSI sessions via the following WMI query +// +// SELECT [selectors] FROM MSFT_iSCSISession +// +// Then find all MSFT_iSCSISession objects from MSFT_iSCSITargetToiSCSISession mapping. +// +// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsisession +// for the WMI class definition. +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 +} + +// ListDisksByTarget lists all the disks on the specified iSCSI target. +// +// It lists all the iSCSI connections via the following WMI query +// +// SELECT [selectors] FROM MSFT_iSCSIConnection +// +// Then find all MSFT_iSCSIConnection objects from MSFT_iSCSITargetToiSCSIConnection mapping, +// locate the MSFT_Disk objects using MSFT_iSCSIConnectionToDisk mapping. +// +// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsiconnection +// for the WMI class definition. +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 +} + +// ConnectISCSITarget establishes a connection to an iSCSI target with optional CHAP authentication credential. +// +// Refer https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitarget-connect +// for the WMI method definition. +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/os/iscsi/api.go b/pkg/os/iscsi/api.go index 559ed3b5..4171c830 100644 --- a/pkg/os/iscsi/api.go +++ b/pkg/os/iscsi/api.go @@ -1,10 +1,13 @@ package iscsi import ( - "encoding/json" "fmt" + "strconv" + "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" ) // Implements the iSCSI OS API calls. All code here should be very simple @@ -18,119 +21,177 @@ 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.ListISCSITargetsByTargetPortalWithFilters(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 } -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}`) +func (APIImplementor) ConnectTarget(portal *TargetPortal, iqn string, authType string, chapUser string, chapSecret string) error { + 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 +200,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 From 018326726fe4ad82853f8aca2867b26380cc4b75 Mon Sep 17 00:00:00 2001 From: Zhongcheng Lao Date: Sat, 31 May 2025 12:55:22 +0800 Subject: [PATCH 2/4] Use associator to find iSCSI WMI instances --- pkg/cim/iscsi.go | 257 +++++++++++++------------------------------- pkg/os/iscsi/api.go | 15 ++- 2 files changed, 81 insertions(+), 191 deletions(-) diff --git a/pkg/cim/iscsi.go b/pkg/cim/iscsi.go index 75234f91..b85cef96 100644 --- a/pkg/cim/iscsi.go +++ b/pkg/cim/iscsi.go @@ -8,7 +8,6 @@ import ( "strconv" "github.com/microsoft/wmi/pkg/base/query" - cim "github.com/microsoft/wmi/pkg/wmiinstance" "github.com/microsoft/wmi/server2019/root/microsoft/windows/storage" ) @@ -103,242 +102,136 @@ func NewISCSITargetPortal(targetPortalAddress string, return QueryISCSITargetPortal(targetPortalAddress, targetPortalPortNumber, nil) } -var ( - // Indexes iSCSI targets by their Object ID specified in node address - mappingISCSITargetIndexer = mappingObjectRefIndexer("iSCSITarget", "MSFT_iSCSITarget", "NodeAddress") - // Indexes iSCSI target portals by their Object ID specified in portal address - mappingISCSITargetPortalIndexer = mappingObjectRefIndexer("iSCSITargetPortal", "MSFT_iSCSITargetPortal", "TargetPortalAddress") - // Indexes iSCSI connections by their Object ID specified in connection identifier - mappingISCSIConnectionIndexer = mappingObjectRefIndexer("iSCSIConnection", "MSFT_iSCSIConnection", "ConnectionIdentifier") - // Indexes iSCSI sessions by their Object ID specified in session identifier - mappingISCSISessionIndexer = mappingObjectRefIndexer("iSCSISession", "MSFT_iSCSISession", "SessionIdentifier") - - // Indexes iSCSI targets by their node address - iscsiTargetIndexer = stringPropertyIndexer("NodeAddress") - // Indexes iSCSI targets by their target portal address - iscsiTargetPortalIndexer = stringPropertyIndexer("TargetPortalAddress") - // Indexes iSCSI connections by their connection identifier - iscsiConnectionIndexer = stringPropertyIndexer("ConnectionIdentifier") - // Indexes iSCSI sessions by their session identifier - iscsiSessionIndexer = stringPropertyIndexer("SessionIdentifier") -) - -// ListISCSITargetToISCSITargetPortalMapping builds a mapping between iSCSI target and iSCSI target portal with iSCSI target as the key. -// -// The equivalent WMI query is: -// -// SELECT [selectors] FROM MSFT_iSCSITargetToiSCSITargetPortal -// -// iSCSITarget | iSCSITargetPortal -// ----------- | ----------------- -// MSFT_iSCSITarget (NodeAddress = "iqn.1991-05.com.microsoft:win-8e2evaq9q...) | MSFT_iSCSITargetPortal (TargetPortalAdd... -// -// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitargettoiscsitargetportal -// for the WMI class definition. -func ListISCSITargetToISCSITargetPortalMapping() (map[string]string, error) { - return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSITargetToiSCSITargetPortal", nil, mappingISCSITargetIndexer, mappingISCSITargetPortalIndexer) -} - -// ListISCSIConnectionToISCSITargetMapping builds a mapping between iSCSI connection and iSCSI target with iSCSI connection as the key. -// -// The equivalent WMI query is: -// -// SELECT [selectors] FROM MSFT_iSCSITargetToiSCSIConnection -// -// iSCSIConnection | iSCSITarget -// --------------- | ----------- -// MSFT_iSCSIConnection (ConnectionIdentifier = "ffffac0cacbff010-15") | MSFT_iSCSITarget (NodeAddress = "iqn.1991-05.com... -// -// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitargettoiscsitargetportal -// for the WMI class definition. -func ListISCSIConnectionToISCSITargetMapping() (map[string]string, error) { - return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSITargetToiSCSIConnection", nil, mappingISCSIConnectionIndexer, mappingISCSITargetIndexer) -} - -// ListISCSISessionToISCSITargetMapping builds a mapping between iSCSI session and iSCSI target with iSCSI session as the key. -// -// The equivalent WMI query is: -// -// SELECT [selectors] FROM MSFT_iSCSITargetToiSCSISession -// -// iSCSISession | iSCSITarget -// ------------ | ----------- -// MSFT_iSCSISession (SessionIdentifier = "ffffac0cacbff010-4000013700000016") | MSFT_iSCSITarget (NodeAddress = "iqn.199... -// -// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitargettoiscsisession -// for the WMI class definition. -func ListISCSISessionToISCSITargetMapping() (map[string]string, error) { - return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSITargetToiSCSISession", nil, mappingISCSISessionIndexer, mappingISCSITargetIndexer) -} - -// ListDiskToISCSIConnectionMapping builds a mapping between disk and iSCSI connection with disk Object ID as the key. -// -// The equivalent WMI query is: -// -// SELECT [selectors] FROM MSFT_iSCSIConnectionToDisk -// -// Disk | iSCSIConnection -// ---- | --------------- -// MSFT_Disk (ObjectId = "{1}\\WIN-8E2EVAQ9QSB\root/Microsoft/Win...) | MSFT_iSCSIConnection (ConnectionIdentifier = "fff... -// -// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsiconnectiontodisk -// for the WMI class definition. -func ListDiskToISCSIConnectionMapping() (map[string]string, error) { - return ListWMIInstanceMappings(WMINamespaceStorage, "MSFT_iSCSIConnectionToDisk", nil, mappingObjectRefIndexer("Disk", "MSFT_Disk", "ObjectId"), mappingISCSIConnectionIndexer) -} - -// ListISCSITargetsByTargetPortalWithFilters retrieves all iSCSI targets from the specified iSCSI target portal and conditions by query filters. -// -// It lists all the iSCSI targets via the following WMI query +// ListISCSITargetsByTargetPortal retrieves all iSCSI targets from the specified iSCSI target portal +// using MSFT_iSCSITargetToiSCSITargetPortal association. // -// SELECT [selectors] FROM MSFT_iSCSITarget +// WMI association MSFT_iSCSITargetToiSCSITargetPortal: // -// Then find all iSCSITarget objects from MSFT_iSCSITargetToiSCSITargetPortal mapping. +// iSCSITarget | iSCSITargetPortal +// ----------- | ----------------- +// MSFT_iSCSITarget (NodeAddress = "iqn.1991-05.com.microsoft:win-8e2evaq9q...) | MSFT_iSCSITargetPortal (TargetPortalAdd... // // Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitarget // for the WMI class definition. -func ListISCSITargetsByTargetPortalWithFilters(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 - } - +func ListISCSITargetsByTargetPortal(portals []*storage.MSFT_iSCSITargetPortal) ([]*storage.MSFT_iSCSITarget, error) { var targets []*storage.MSFT_iSCSITarget - for _, instance := range targetInstances { - target, err := storage.NewMSFT_iSCSITargetEx1(instance) + for _, portal := range portals { + collection, err := portal.GetAssociated("MSFT_iSCSITargetToiSCSITargetPortal", "MSFT_iSCSITarget", "iSCSITarget", "iSCSITargetPortal") if err != nil { - return nil, fmt.Errorf("failed to query iSCSI target %v. %v", instance, err) + return nil, fmt.Errorf("failed to query associated iSCSITarget for %v. error: %v", portal, err) } - targets = append(targets, target) + for _, instance := range collection { + target, err := storage.NewMSFT_iSCSITargetEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query iSCSI target %v. error: %v", instance, err) + } + + targets = append(targets, target) + } } return targets, nil } // QueryISCSITarget retrieves the iSCSI target from the specified portal address, portal and node address. -func QueryISCSITarget(address string, port uint32, nodeAddress string, selectorList []string) (*storage.MSFT_iSCSITarget, error) { +func QueryISCSITarget(address string, port uint32, nodeAddress string) (*storage.MSFT_iSCSITarget, error) { portal, err := QueryISCSITargetPortal(address, port, nil) if err != nil { return nil, err } - targets, err := ListISCSITargetsByTargetPortalWithFilters(selectorList, []*storage.MSFT_iSCSITargetPortal{portal}, - query.NewWmiQueryFilter("NodeAddress", nodeAddress, query.Equals)) + targets, err := ListISCSITargetsByTargetPortal([]*storage.MSFT_iSCSITargetPortal{portal}) if err != nil { return nil, err } - return targets[0], nil + for _, target := range targets { + targetNodeAddress, err := target.GetProperty("NodeAddress") + if err != nil { + return nil, fmt.Errorf("failed to query iSCSI target %v. error: %v", target, err) + } + + if targetNodeAddress == nodeAddress { + return target, nil + } + } + + return nil, nil } -// QueryISCSISessionByTarget retrieves the iSCSI session from the specified iSCSI target. -// -// It lists all the iSCSI sessions via the following WMI query +// QueryISCSISessionByTarget retrieves the iSCSI session from the specified iSCSI target +// using MSFT_iSCSITargetToiSCSISession association. // -// SELECT [selectors] FROM MSFT_iSCSISession +// WMI association MSFT_iSCSITargetToiSCSISession: // -// Then find all MSFT_iSCSISession objects from MSFT_iSCSITargetToiSCSISession mapping. +// iSCSISession | iSCSITarget +// ------------ | ----------- +// MSFT_iSCSISession (SessionIdentifier = "ffffac0cacbff010-4000013700000016") | MSFT_iSCSITarget (NodeAddress = "iqn.199... // // Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsisession // for the WMI class definition. -func QueryISCSISessionByTarget(target *storage.MSFT_iSCSITarget, selectorList []string) (*storage.MSFT_iSCSISession, error) { - sessionQuery := query.NewWmiQueryWithSelectList("MSFT_iSCSISession", selectorList) - sessionInstances, err := QueryInstances(WMINamespaceStorage, sessionQuery) +func QueryISCSISessionByTarget(target *storage.MSFT_iSCSITarget) (*storage.MSFT_iSCSISession, error) { + collection, err := target.GetAssociated("MSFT_iSCSITargetToiSCSISession", "MSFT_iSCSISession", "iSCSISession", "iSCSITarget") if err != nil { - return nil, err + return nil, fmt.Errorf("failed to query associated iSCSISession for %v. error: %v", target, err) } - targetToTargetSessionMapping, err := ListISCSISessionToISCSITargetMapping() - if err != nil { - return nil, err + if len(collection) == 0 { + return nil, nil } - filtered, err := FindInstancesByMapping(sessionInstances, iscsiSessionIndexer, []*cim.WmiInstance{target.WmiInstance}, iscsiTargetIndexer, targetToTargetSessionMapping) - if err != nil { - return nil, err - } - - session, err := storage.NewMSFT_iSCSISessionEx1(filtered[0]) + session, err := storage.NewMSFT_iSCSISessionEx1(collection[0]) return session, err } -// ListDisksByTarget lists all the disks on the specified iSCSI target. +// ListDisksByTarget find all disks associated with an iSCSITarget. +// It finds out the iSCSIConnections from MSFT_iSCSITargetToiSCSIConnection association, +// then locate MSFT_Disk objects from MSFT_iSCSIConnectionToDisk association. // -// It lists all the iSCSI connections via the following WMI query +// WMI association MSFT_iSCSITargetToiSCSIConnection: // -// SELECT [selectors] FROM MSFT_iSCSIConnection +// iSCSIConnection | iSCSITarget +// --------------- | ----------- +// MSFT_iSCSIConnection (ConnectionIdentifier = "ffffac0cacbff010-15") | MSFT_iSCSITarget (NodeAddress = "iqn.1991-05.com... // -// Then find all MSFT_iSCSIConnection objects from MSFT_iSCSITargetToiSCSIConnection mapping, -// locate the MSFT_Disk objects using MSFT_iSCSIConnectionToDisk mapping. +// WMI association MSFT_iSCSIConnectionToDisk: +// +// Disk | iSCSIConnection +// ---- | --------------- +// MSFT_Disk (ObjectId = "{1}\\WIN-8E2EVAQ9QSB\root/Microsoft/Win...) | MSFT_iSCSIConnection (ConnectionIdentifier = "fff... // // Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsiconnection // for the WMI class definition. -func ListDisksByTarget(target *storage.MSFT_iSCSITarget, selectorList []string) ([]*storage.MSFT_Disk, error) { +func ListDisksByTarget(target *storage.MSFT_iSCSITarget) ([]*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) + collection, err := target.GetAssociated("MSFT_iSCSITargetToiSCSIConnection", "MSFT_iSCSIConnection", "iSCSIConnection", "iSCSITarget") if err != nil { - return nil, err + return nil, fmt.Errorf("failed to query associated iSCSISession for %v. error: %v", target, err) } - var diskInstances []*cim.WmiInstance - for _, disk := range disks { - diskInstances = append(diskInstances, disk.WmiInstance) + if len(collection) == 0 { + return nil, nil } - 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) + var result []*storage.MSFT_Disk + for _, conn := range collection { + instances, err := conn.GetAssociated("MSFT_iSCSIConnectionToDisk", "MSFT_Disk", "Disk", "iSCSIConnection") if err != nil { - return nil, fmt.Errorf("failed to query disk %v. error: %v", disk, err) + return nil, fmt.Errorf("failed to query associated disk for %v. error: %v", target, err) } - filteredDisks = append(filteredDisks, disk) + for _, instance := range instances { + disk, err := storage.NewMSFT_DiskEx1(instance) + if err != nil { + return nil, fmt.Errorf("failed to query associated disk %v. error: %v", instance, err) + } + + result = append(result, disk) + } } - return filteredDisks, err + + return result, err } // ConnectISCSITarget establishes a connection to an iSCSI target with optional CHAP authentication credential. diff --git a/pkg/os/iscsi/api.go b/pkg/os/iscsi/api.go index 4171c830..45223d35 100644 --- a/pkg/os/iscsi/api.go +++ b/pkg/os/iscsi/api.go @@ -60,7 +60,7 @@ func (APIImplementor) DiscoverTargetPortal(portal *TargetPortal) ([]string, erro return nil, err } - targets, err := cim.ListISCSITargetsByTargetPortalWithFilters(nil, []*storage.MSFT_iSCSITargetPortal{instance}) + targets, err := cim.ListISCSITargetsByTargetPortal([]*storage.MSFT_iSCSITargetPortal{instance}) if err != nil { return nil, err } @@ -125,7 +125,7 @@ func (APIImplementor) RemoveTargetPortal(portal *TargetPortal) error { } func (APIImplementor) ConnectTarget(portal *TargetPortal, iqn string, authType string, chapUser string, chapSecret string) error { - target, err := cim.QueryISCSITarget(portal.Address, portal.Port, iqn, nil) + target, err := cim.QueryISCSITarget(portal.Address, portal.Port, iqn) if err != nil { return err } @@ -151,7 +151,7 @@ func (APIImplementor) ConnectTarget(portal *TargetPortal, iqn string, authType s } func (APIImplementor) DisconnectTarget(portal *TargetPortal, iqn string) error { - target, err := cim.QueryISCSITarget(portal.Address, portal.Port, iqn, nil) + target, err := cim.QueryISCSITarget(portal.Address, portal.Port, iqn) if err != nil { return err } @@ -167,7 +167,7 @@ func (APIImplementor) DisconnectTarget(portal *TargetPortal, iqn string) error { } // get session - session, err := cim.QueryISCSISessionByTarget(target, nil) + session, err := cim.QueryISCSISessionByTarget(target) if err != nil { return fmt.Errorf("error query session of target %s from target portal at (%s:%d). err: %w", iqn, portal.Address, portal.Port, err) } @@ -198,9 +198,7 @@ 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 - target, err := cim.QueryISCSITarget(portal.Address, portal.Port, iqn, nil) + target, err := cim.QueryISCSITarget(portal.Address, portal.Port, iqn) if err != nil { return nil, err } @@ -215,8 +213,7 @@ func (APIImplementor) GetTargetDisks(portal *TargetPortal, iqn string) ([]string return nil, nil } - disks, err := cim.ListDisksByTarget(target, []string{}) - + disks, err := cim.ListDisksByTarget(target) if err != nil { 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) } From 8bd4131e7bd523f19780633419b2a71e43863d5b Mon Sep 17 00:00:00 2001 From: Zhongcheng Lao Date: Fri, 20 Jun 2025 11:48:06 +0800 Subject: [PATCH 3/4] Move GetDiskNumber to cim package --- pkg/cim/disk.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/cim/disk.go b/pkg/cim/disk.go index 0b03c2ac..0298b793 100644 --- a/pkg/cim/disk.go +++ b/pkg/cim/disk.go @@ -76,3 +76,12 @@ func ListDisks(selectorList []string) ([]*storage.MSFT_Disk, error) { return disks, nil } + +// GetDiskNumber returns the number of a disk. +func GetDiskNumber(disk *storage.MSFT_Disk) (uint32, error) { + number, err := disk.GetProperty("Number") + if err != nil { + return 0, err + } + return uint32(number.(int32)), err +} From 65568084f54193917069972a4fcb423d9460f87a Mon Sep 17 00:00:00 2001 From: Zhongcheng Lao Date: Fri, 20 Jun 2025 01:09:17 +0800 Subject: [PATCH 4/4] Move iSCSI functions to cim package --- pkg/cim/iscsi.go | 98 +++++++++++++++++++++++++++++++++++++++++++-- pkg/os/iscsi/api.go | 58 ++++++++------------------- 2 files changed, 111 insertions(+), 45 deletions(-) diff --git a/pkg/cim/iscsi.go b/pkg/cim/iscsi.go index b85cef96..37b718bd 100644 --- a/pkg/cim/iscsi.go +++ b/pkg/cim/iscsi.go @@ -11,6 +11,10 @@ import ( "github.com/microsoft/wmi/server2019/root/microsoft/windows/storage" ) +var ( + ISCSITargetPortalDefaultSelectorList = []string{"TargetPortalAddress", "TargetPortalPortNumber"} +) + // ListISCSITargetPortals retrieves a list of iSCSI target portals. // // The equivalent WMI query is: @@ -102,6 +106,40 @@ func NewISCSITargetPortal(targetPortalAddress string, return QueryISCSITargetPortal(targetPortalAddress, targetPortalPortNumber, nil) } +// ParseISCSITargetPortal retrieves the portal address and port number of an iSCSI target portal. +func ParseISCSITargetPortal(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 +} + +// RemoveISCSITargetPortal removes an iSCSI target portal. +// +// Refer to https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitargetportal-remove +// for the WMI method definition. +func RemoveISCSITargetPortal(instance *storage.MSFT_iSCSITargetPortal) (int, error) { + address, port, err := ParseISCSITargetPortal(instance) + if err != nil { + return 0, fmt.Errorf("failed to parse target portal %v. error: %v", instance, err) + } + + result, err := instance.InvokeMethodWithReturn("Remove", + nil, + nil, + int(port), + address, + ) + return int(result), err +} + // ListISCSITargetsByTargetPortal retrieves all iSCSI targets from the specified iSCSI target portal // using MSFT_iSCSITargetToiSCSITargetPortal association. // @@ -147,7 +185,7 @@ func QueryISCSITarget(address string, port uint32, nodeAddress string) (*storage } for _, target := range targets { - targetNodeAddress, err := target.GetProperty("NodeAddress") + targetNodeAddress, err := GetISCSITargetNodeAddress(target) if err != nil { return nil, fmt.Errorf("failed to query iSCSI target %v. error: %v", target, err) } @@ -160,6 +198,21 @@ func QueryISCSITarget(address string, port uint32, nodeAddress string) (*storage return nil, nil } +// GetISCSITargetNodeAddress returns the node address of an iSCSI target. +func GetISCSITargetNodeAddress(target *storage.MSFT_iSCSITarget) (string, error) { + nodeAddress, err := target.GetProperty("NodeAddress") + if err != nil { + return "", err + } + + return nodeAddress.(string), err +} + +// IsISCSITargetConnected returns whether the iSCSI target is connected. +func IsISCSITargetConnected(target *storage.MSFT_iSCSITarget) (bool, error) { + return target.GetPropertyIsConnected() +} + // QueryISCSISessionByTarget retrieves the iSCSI session from the specified iSCSI target // using MSFT_iSCSITargetToiSCSISession association. // @@ -185,6 +238,34 @@ func QueryISCSISessionByTarget(target *storage.MSFT_iSCSITarget) (*storage.MSFT_ return session, err } +// UnregisterISCSISession unregisters the iSCSI session so that it is no longer persistent. +// +// Refer https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsisession-unregister +// for the WMI method definition. +func UnregisterISCSISession(session *storage.MSFT_iSCSISession) (int, error) { + result, err := session.InvokeMethodWithReturn("Unregister") + return int(result), err +} + +// SetISCSISessionChapSecret sets a CHAP secret key for use with iSCSI initiator connections. +// +// Refer https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitarget-disconnect +// for the WMI method definition. +func SetISCSISessionChapSecret(mutualChapSecret string) (int, error) { + result, _, err := InvokeCimMethod(WMINamespaceStorage, "MSFT_iSCSISession", "SetCHAPSecret", map[string]interface{}{"ChapSecret": mutualChapSecret}) + return result, err +} + +// GetISCSISessionIdentifier returns the identifier of an iSCSI session. +func GetISCSISessionIdentifier(session *storage.MSFT_iSCSISession) (string, error) { + return session.GetPropertySessionIdentifier() +} + +// IsISCSISessionPersistent returns whether an iSCSI session is persistent. +func IsISCSISessionPersistent(session *storage.MSFT_iSCSISession) (bool, error) { + return session.GetPropertyIsPersistent() +} + // ListDisksByTarget find all disks associated with an iSCSITarget. // It finds out the iSCSIConnections from MSFT_iSCSITargetToiSCSIConnection association, // then locate MSFT_Disk objects from MSFT_iSCSIConnectionToDisk association. @@ -238,7 +319,7 @@ func ListDisksByTarget(target *storage.MSFT_iSCSITarget) ([]*storage.MSFT_Disk, // // Refer https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitarget-connect // for the WMI method definition. -func ConnectISCSITarget(portalAddress string, portalPortNumber uint32, nodeAddress string, authType string, chapUsername *string, chapSecret *string) (int, map[string]interface{}, error) { +func ConnectISCSITarget(portalAddress string, portalPortNumber uint32, nodeAddress string, authType string, chapUsername *string, chapSecret *string) (int, error) { inParams := map[string]interface{}{ "NodeAddress": nodeAddress, "TargetPortalAddress": portalAddress, @@ -256,6 +337,15 @@ func ConnectISCSITarget(portalAddress string, portalPortNumber uint32, nodeAddre inParams["ChapSecret"] = *chapSecret } - result, outParams, err := InvokeCimMethod(WMINamespaceStorage, "MSFT_iSCSITarget", "Connect", inParams) - return result, outParams, err + result, _, err := InvokeCimMethod(WMINamespaceStorage, "MSFT_iSCSITarget", "Connect", inParams) + return result, err +} + +// DisconnectISCSITarget disconnects the specified session between an iSCSI initiator and an iSCSI target. +// +// Refer https://learn.microsoft.com/en-us/previous-versions/windows/desktop/iscsidisc/msft-iscsitarget-disconnect +// for the WMI method definition. +func DisconnectISCSITarget(target *storage.MSFT_iSCSITarget, sessionIdentifier string) (int, error) { + result, err := target.InvokeMethodWithReturn("Disconnect", sessionIdentifier) + return int(result), err } diff --git a/pkg/os/iscsi/api.go b/pkg/os/iscsi/api.go index 45223d35..3aba3076 100644 --- a/pkg/os/iscsi/api.go +++ b/pkg/os/iscsi/api.go @@ -21,20 +21,6 @@ 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 { existing, err := cim.QueryISCSITargetPortal(portal.Address, portal.Port, nil) if cim.IgnoreNotFound(err) != nil { @@ -67,26 +53,26 @@ func (APIImplementor) DiscoverTargetPortal(portal *TargetPortal) ([]string, erro var iqns []string for _, target := range targets { - iqn, err := target.GetProperty("NodeAddress") + iqn, err := cim.GetISCSITargetNodeAddress(target) 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)) + iqns = append(iqns, iqn) } return iqns, nil } func (APIImplementor) ListTargetPortals() ([]TargetPortal, error) { - instances, err := cim.ListISCSITargetPortals([]string{"TargetPortalAddress", "TargetPortalPortNumber"}) + instances, err := cim.ListISCSITargetPortals(cim.ISCSITargetPortalDefaultSelectorList) if err != nil { return nil, err } var portals []TargetPortal for _, instance := range instances { - address, port, err := parseTargetPortal(instance) + address, port, err := cim.ParseISCSITargetPortal(instance) if err != nil { return nil, fmt.Errorf("failed parsing target portal %v. err: %w", instance, err) } @@ -106,19 +92,9 @@ func (APIImplementor) RemoveTargetPortal(portal *TargetPortal) error { return err } - address, port, err := parseTargetPortal(instance) - if err != nil { - return fmt.Errorf("failed to parse target portal %v. error: %v", instance, err) - } - - result, err := instance.InvokeMethodWithReturn("Remove", - nil, - nil, - int(port), - address, - ) + result, err := cim.RemoveISCSITargetPortal(instance) if result != 0 || err != nil { - return fmt.Errorf("error removing target portal at (%s:%d). result: %d, err: %w", address, port, result, err) + return fmt.Errorf("error removing target portal at (%s:%d). result: %d, err: %w", portal.Address, portal.Port, result, err) } return nil @@ -130,7 +106,7 @@ func (APIImplementor) ConnectTarget(portal *TargetPortal, iqn string, authType s return err } - connected, err := target.GetPropertyIsConnected() + connected, err := cim.IsISCSITargetConnected(target) if err != nil { return err } @@ -142,7 +118,7 @@ func (APIImplementor) ConnectTarget(portal *TargetPortal, iqn string, authType s targetAuthType := strings.ToUpper(strings.ReplaceAll(authType, "_", "")) - result, _, err := cim.ConnectISCSITarget(portal.Address, portal.Port, iqn, targetAuthType, &chapUser, &chapSecret) + result, err := cim.ConnectISCSITarget(portal.Address, portal.Port, iqn, targetAuthType, &chapUser, &chapSecret) if err != nil { return fmt.Errorf("error connecting to target portal. result: %d, err: %w", result, err) } @@ -156,7 +132,7 @@ func (APIImplementor) DisconnectTarget(portal *TargetPortal, iqn string) error { return err } - connected, err := target.GetPropertyIsConnected() + connected, err := cim.IsISCSITargetConnected(target) 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) } @@ -172,24 +148,24 @@ func (APIImplementor) DisconnectTarget(portal *TargetPortal, iqn string) error { 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() + sessionIdentifier, err := cim.GetISCSISessionIdentifier(session) 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() + persistent, err := cim.IsISCSISessionPersistent(session) 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") + result, err := cim.UnregisterISCSISession(session) 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) + result, err := cim.DisconnectISCSITarget(target, 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) } @@ -203,7 +179,7 @@ func (APIImplementor) GetTargetDisks(portal *TargetPortal, iqn string) ([]string return nil, err } - connected, err := target.GetPropertyIsConnected() + connected, err := cim.IsISCSITargetConnected(target) if err != nil { return nil, fmt.Errorf("error query connected of target %s from target portal at (%s:%d). err: %w", iqn, portal.Address, portal.Port, err) } @@ -220,18 +196,18 @@ func (APIImplementor) GetTargetDisks(portal *TargetPortal, iqn string) ([]string var ids []string for _, disk := range disks { - number, err := disk.GetProperty("Number") + number, err := cim.GetDiskNumber(disk) 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)))) + ids = append(ids, strconv.Itoa(int(number))) } return ids, nil } func (APIImplementor) SetMutualChapSecret(mutualChapSecret string) error { - result, _, err := cim.InvokeCimMethod(cim.WMINamespaceStorage, "MSFT_iSCSISession", "SetCHAPSecret", map[string]interface{}{"ChapSecret": mutualChapSecret}) + result, err := cim.SetISCSISessionChapSecret(mutualChapSecret) if err != nil { return fmt.Errorf("error setting mutual chap secret. result: %d, err: %v", result, err) }