1- // Copyright 2024 NetApp, Inc. All Rights Reserved.
1+ // Copyright 2025 NetApp, Inc. All Rights Reserved.
22
33package iscsi
44
@@ -7,6 +7,7 @@ package iscsi
77import (
88 "context"
99 "fmt"
10+ "path/filepath"
1011 "strconv"
1112 "strings"
1213
@@ -19,6 +20,7 @@ import (
1920
2021type IscsiReconcileUtils interface {
2122 GetISCSIHostSessionMapForTarget (context.Context , string ) map [int ]int
23+ DiscoverSCSIAddressMapForTarget (ctx context.Context , targetIQN string ) (map [string ]models.ScsiDeviceAddress , error )
2224 GetSysfsBlockDirsForLUN (int , map [int ]int ) []string
2325 GetDevicesForLUN (paths []string ) ([]string , error )
2426 ReconcileISCSIVolumeInfo (ctx context.Context , trackingInfo * models.VolumeTrackingInfo ) (bool , error )
@@ -68,6 +70,132 @@ func (h *IscsiReconcileHelper) ReconcileISCSIVolumeInfo(
6870 return false , nil
6971}
7072
73+ // DiscoverSCSIAddressMapForTarget creates a map of unique "host:channel:targetID" to ScsiDeviceAddresses that
74+ // exist for active sessions on a given target IQN. We can rely on "host:channel:targetID" for our keys safely because
75+ // we filter by target IQN. The resulting map should be used with a set of LUN IDs to initiate precise LUN scanning.
76+ func (h * IscsiReconcileHelper ) DiscoverSCSIAddressMapForTarget (
77+ ctx context.Context , targetIQN string ,
78+ ) (map [string ]models.ScsiDeviceAddress , error ) {
79+ fields := LogFields {"iSCSINodeName" : targetIQN }
80+ Logc (ctx ).WithFields (fields ).Debug (">>>> iscsi.DiscoverSCSIAddressMapForTarget" )
81+ defer Logc (ctx ).WithFields (fields ).Debug ("<<<< iscsi.DiscoverSCSIAddressMapForTarget" )
82+
83+ // deviceMap is a map of "h:c:t" -> ScsiDeviceAddress.
84+ deviceMap := make (map [string ]models.ScsiDeviceAddress )
85+
86+ // Read in everything under: '/sys/class/scsi_host/'.
87+ scsiHostPath := filepath .Join (h .chrootPathPrefix , "sys" , "class" , "scsi_host" )
88+ hostEntries , err := h .osFs .ReadDir (scsiHostPath )
89+ if err != nil {
90+ return nil , fmt .Errorf ("failed to list hosts; %w" , err )
91+ }
92+
93+ // Search through each dir under: '/sys/class/scsi_host/'.
94+ for _ , hostEntry := range hostEntries {
95+ hostName := hostEntry .Name () // example: "host10" from "/sys/class/scsi_host/host10"
96+ if ! strings .HasPrefix (hostName , "host" ) {
97+ continue
98+ }
99+
100+ // Read in all dirs under: '/sys/class/scsi_host/host#/device'.
101+ hostDevicePath := filepath .Join (scsiHostPath , hostName , "device" )
102+ hostDeviceEntries , err := h .osFs .ReadDir (hostDevicePath )
103+ if err != nil {
104+ Logc (ctx ).WithError (err ).Errorf ("Could not read host device entries at: '%s'." , hostDevicePath )
105+ continue
106+ }
107+
108+ // Look for "session#" within the device directory entries.
109+ // It's possible that multiple sessions that Trident setup can exist for a given host.
110+ // Example:
111+ // '/sys/class/scsi_host/host10/device/session1'
112+ // '/sys/class/scsi_host/host10/device/session2'
113+ for _ , hostDeviceEntry := range hostDeviceEntries {
114+ sessionName := hostDeviceEntry .Name ()
115+ if ! strings .HasPrefix (hostDeviceEntry .Name (), "session" ) {
116+ continue
117+ }
118+
119+ // Check if the iscsi session exists: '/sys/class/iscsi_host/host#/device/session#/iscsi_session/session#'.
120+ sessionPath := filepath .Join (hostDevicePath , sessionName , "iscsi_session" , sessionName )
121+ if sessionExists , err := h .osFs .Exists (sessionPath ); err != nil {
122+ Logc (ctx ).WithError (err ).Errorf ("Could not read iscsi session path at: '%s'" , sessionPath )
123+ continue
124+ } else if ! sessionExists {
125+ Logc (ctx ).Debugf ("iSCSI session path '%s' does not exist." , sessionPath )
126+ continue
127+ }
128+
129+ // Read in target IQN from: '/sys/class/iscsi_host/host#/device/session#/iscsi_session/session#/targetname'.
130+ targetNamePath := filepath .Join (sessionPath , "targetname" )
131+ contents , err := h .osFs .ReadFile (targetNamePath )
132+ if err != nil {
133+ Logc (ctx ).WithError (err ).Errorf ("Could not read target IQN at: '%s'" , targetNamePath )
134+ continue
135+ }
136+
137+ // Ignore sessions that aren't connected to the expected target IQN.
138+ targetName := strings .TrimSpace (string (contents ))
139+ if targetName != targetIQN {
140+ Logc (ctx ).Debugf ("IQN mismatch. '%s' != '%s'; ignoring session." , targetName , targetIQN )
141+ continue
142+ }
143+
144+ // At this point, we know this session is for a NetApp target.
145+ // Read in all entries under: '/sys/class/iscsi_host/host#/device/session#/iscsi_session/session#/device'
146+ sessionDevicePath := filepath .Join (sessionPath , "device" )
147+ sessionDeviceEntries , err := h .osFs .ReadDir (sessionDevicePath )
148+ if err != nil {
149+ Logc (ctx ).WithError (err ).Errorf ("Could not read session device entries at: '%s'." , sessionDevicePath )
150+ continue
151+ }
152+
153+ // Search for the 'target<H:C:T>' directory under:
154+ // `/sys/class/iscsi_host/host#/device/session#/iscsi_session/session#/device`.
155+ for _ , entry := range sessionDeviceEntries {
156+ entryName := entry .Name ()
157+ if ! strings .HasPrefix (entryName , "target" ) {
158+ continue
159+ }
160+ Logc (ctx ).WithField ("scsiTargetDevice" , entryName ).Debug ("Found SCSI target device directory." )
161+
162+ // At this point, we know we're looking at a target device directory.
163+ // '/sys/class/iscsi_host/host#/device/session#/iscsi_session/session#/device/target<H:C:T>'
164+ var hostID , channelID , targetID string
165+ hctSuffix := strings .TrimPrefix (entryName , "target" ) // "targetH:C:T" -> "H:C:T"
166+ hctElems := strings .Split (hctSuffix , ":" ) // "H:C:T" -> ["H","C","T"]
167+ if len (hctElems ) != 3 {
168+ Logc (ctx ).Errorf ("Invalid format detected with: '%s'; expected 'target<H:C:T>'" , entryName )
169+ continue
170+ }
171+ // It can be safely assumed that if these elements exist, the kernel has assigned them valid values.
172+ hostID , channelID , targetID = hctElems [0 ], hctElems [1 ], hctElems [2 ]
173+ fields := LogFields {
174+ "hostID" : hostID ,
175+ "channelID" : channelID ,
176+ "targetID" : targetID ,
177+ }
178+
179+ // Build unique key "hostID:channelID:targetID"
180+ // Filtering by targetIQN above should remove chances of tracking scsi addresses not owned by Trident.
181+ // It is technically possible for a given host to have multiple channels and multiple targetIDs, we
182+ // can probably safely assume the targetID will remain the same for a given backend.
183+ Logc (ctx ).WithFields (fields ).Debug ("Discovered host, channel and target ID." )
184+ key := fmt .Sprintf ("%s:%s:%s" , hostID , channelID , targetID )
185+ if _ , exists := deviceMap [key ]; ! exists {
186+ deviceMap [key ] = models.ScsiDeviceAddress {
187+ Host : hostID ,
188+ Channel : channelID ,
189+ Target : targetID ,
190+ }
191+ }
192+ }
193+ }
194+ }
195+
196+ return deviceMap , nil
197+ }
198+
71199// GetISCSIHostSessionMapForTarget returns a map of iSCSI host numbers to iSCSI session numbers
72200// for a given iSCSI target.
73201func (h * IscsiReconcileHelper ) GetISCSIHostSessionMapForTarget (ctx context.Context , iSCSINodeName string ) map [int ]int {
0 commit comments