Skip to content

Commit a75908a

Browse files
authored
SAN attach: wait for multipath dm device after LUN scan (iSCSI and FCP)
SAN volume attach could proceed before multipathd assembled a dm-* map for the LUN's SCSI path devices. After the initial LUN scan, iSCSI and FCP now poll sysfs until FindMultipathDeviceForDevice finds a dm device, rescanning only when no block devices are visible yet.
1 parent 3146083 commit a75908a

6 files changed

Lines changed: 497 additions & 382 deletions

File tree

frontend/csi/node_server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2971,6 +2971,7 @@ func (p *Plugin) performISCSISelfHealing(ctx context.Context) {
29712971

29722972
if err := p.iscsi.PreChecks(ctx); err != nil {
29732973
Logc(ctx).Errorf("Skipping iSCSI self-heal cycle; pre-checks failed: %v.", err)
2974+
return
29742975
}
29752976

29762977
// Reset current sessions

utils/fcp/fcp.go

Lines changed: 160 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ const (
3636
DevMapperRoot = "/dev/mapper/"
3737
devicesRemovalMaxWaitTime = 5 * time.Second
3838

39+
// multipathDevicePollInterval bounds how often we rescan and re-check sysfs while waiting for a
40+
// multipath dm device during attach.
41+
multipathDevicePollInterval = 300 * time.Millisecond
42+
// multipathDeviceWaitBudgetPercent reserves most of the caller's remaining time budget for waiting on DM readiness.
43+
multipathDeviceWaitBudgetPercent = 80
44+
// multipathDeviceWaitMinTimeout ensures each wait attempt has enough time to make meaningful progress.
45+
multipathDeviceWaitMinTimeout = 5 * time.Second
46+
// multipathDevicePostWaitBuffer keeps some caller budget for post-wait attach steps.
47+
multipathDevicePostWaitBuffer = 5 * time.Second
48+
// multipathDeviceNoDeadlineTimeout bounds wait attempts when callers do not set a context deadline.
49+
multipathDeviceNoDeadlineTimeout = 15 * time.Second
50+
3951
// REDACTED is a copy of what is in utils package.
4052
// we can reference that once we do not have any references into the utils package
4153
REDACTED = "<REDACTED>"
@@ -45,6 +57,7 @@ var (
4557
pidRegex = regexp.MustCompile(`^\d+$`)
4658
pidRunningOrIdleRegex = regexp.MustCompile(`pid \d+ (running|idle)`)
4759
tagsWithIndentationRegex = regexp.MustCompile(`(?m)^[\t ]*find_multipaths[\t ]*["|']?(?P<tagName>[\w-_]+)["|']?[\t ]*$`)
60+
fcpRportHostNumberRegex = regexp.MustCompile(`rport-(\d+):`)
4861
beforeFlushDevice = fiji.Register("fcpBeforeFlushDevice", "devices")
4962
)
5063

@@ -147,12 +160,15 @@ func (client *Client) AttachVolumeRetry(
147160
var err error
148161
var mpathSize int64
149162

150-
if err = client.PreChecks(ctx); err != nil {
151-
return mpathSize, err
163+
attachCtx, cancel := context.WithTimeout(ctx, timeout)
164+
defer cancel()
165+
166+
if err = client.PreChecks(attachCtx); err != nil {
167+
return mpathSize, backoff.Permanent(err)
152168
}
153169

154170
checkAttachFCPVolume := func() error {
155-
mpathSize, err = client.AttachVolume(ctx, publishInfo)
171+
mpathSize, err = client.AttachVolume(attachCtx, publishInfo)
156172
return err
157173
}
158174

@@ -169,7 +185,7 @@ func (client *Client) AttachVolumeRetry(
169185
attachBackoff.RandomizationFactor = 0.1
170186
attachBackoff.MaxElapsedTime = timeout
171187

172-
err = backoff.RetryNotify(checkAttachFCPVolume, attachBackoff, attachNotify)
188+
err = backoff.RetryNotify(checkAttachFCPVolume, backoff.WithContext(attachBackoff, attachCtx), attachNotify)
173189
return mpathSize, err
174190
}
175191

@@ -356,10 +372,6 @@ func (client *Client) AttachVolume(
356372
"fstype": publishInfo.FilesystemType,
357373
}).Debug("Attaching FCP SCSI volume.")
358374

359-
if err = client.PreChecks(ctx); err != nil {
360-
return mpathSize, err
361-
}
362-
363375
// Check if zoning exists for the target
364376
if isZoned, err := client.fcpUtils.CheckZoningExistsWithTarget(ctx, publishInfo.FCTargetWWNN); !isZoned {
365377
return mpathSize, err
@@ -614,59 +626,160 @@ func rescanOneLun(ctx context.Context, path string) error {
614626
return nil
615627
}
616628

617-
// waitForMultipathDeviceForLUN
618-
// for the given LUN, this function waits for the associated multipath device to be present
619-
// first find the /dev/sd* devices assocaited with the LUN
620-
// Wait for the maultipath device dm-* for the /dev/sd* devices.
621-
func (client *Client) waitForMultipathDeviceForLUN(ctx context.Context, hostSessionMap []map[string]int, lunID int,
622-
fcpNodeName string,
623-
) error {
624-
fields := LogFields{
625-
"lunID": lunID,
626-
"fcpNodeName": fcpNodeName,
627-
}
628-
Logc(ctx).WithFields(fields).Debug(">>>> fcp.waitForMultipathDeviceForLUN")
629-
defer Logc(ctx).WithFields(fields).Debug("<<<< fcp.waitForMultipathDeviceForLUN")
629+
// fcpDeviceAddressesForLUN builds SCSI scan addresses for each host/target path in the FCP host session map.
630+
func (client *Client) fcpDeviceAddressesForLUN(ctx context.Context, hostSessionMap []map[string]int, lunID int) []models.ScsiDeviceAddress {
631+
deviceAddresses := make([]models.ScsiDeviceAddress, 0)
630632

631-
paths := client.fcpUtils.GetSysfsBlockDirsForLUN(lunID, hostSessionMap)
633+
for _, hostPortMap := range hostSessionMap {
634+
for host := range hostPortMap {
635+
hostStr := fcpRportHostNumberRegex.FindStringSubmatch(host)
636+
if len(hostStr) <= 1 {
637+
Logc(ctx).WithField("host", host).Debug("Could not parse host number from host session map key.")
638+
continue
639+
}
640+
hostNum, err := strconv.Atoi(hostStr[1])
641+
if err != nil {
642+
Logc(ctx).WithField("hostNumber", hostStr[1]).Debug("Could not parse host number")
643+
continue
644+
}
632645

633-
devices, err := client.fcpUtils.GetDevicesForLUN(paths)
634-
if err != nil {
635-
return err
636-
}
646+
targetIDFile := fmt.Sprintf("/sys/class/fc_host/host%d/device/%s/fc_remote_ports/%s/scsi_target_id",
647+
hostNum, host, host)
648+
targetIDRaw, err := os.ReadFile(targetIDFile)
649+
if err != nil {
650+
Logc(ctx).WithField("targetIDFile", targetIDFile).Debug("Could not find target ID file while waiting.")
651+
continue
652+
}
653+
targetIDStr := strings.TrimSpace(string(targetIDRaw))
654+
targetID, err := strconv.Atoi(targetIDStr)
655+
if err != nil {
656+
Logc(ctx).WithField("targetID", targetIDStr).Debug("Could not parse target ID")
657+
continue
658+
}
637659

638-
_, err = client.waitForMultipathDeviceForDevices(ctx, devices)
660+
deviceAddress := models.ScsiDeviceAddress{
661+
Host: strconv.Itoa(hostNum),
662+
Channel: models.ScanAllSCSIDeviceAddress,
663+
Target: strconv.Itoa(targetID),
664+
LUN: strconv.Itoa(lunID),
665+
}
666+
if !collection.Contains(deviceAddresses, deviceAddress) {
667+
deviceAddresses = append(deviceAddresses, deviceAddress)
668+
}
669+
}
670+
}
671+
return deviceAddresses
672+
}
639673

640-
return err
674+
func fcpHostSessionMapEmpty(hostSessionMap []map[string]int) bool {
675+
if len(hostSessionMap) == 0 {
676+
return true
677+
}
678+
for _, hostPortMap := range hostSessionMap {
679+
if len(hostPortMap) > 0 {
680+
return false
681+
}
682+
}
683+
return true
641684
}
642685

643-
// waitForMultipathDeviceForDevices accepts a list of sd* device names which are associated with same LUN
644-
// and waits until a multipath device is present for at least one of those. It returns the name of the
645-
// multipath device, or an empty string if multipathd isn't running or there is only one path.
646-
func (client *Client) waitForMultipathDeviceForDevices(ctx context.Context, devices []string) (string, error) {
647-
fields := LogFields{"devices": devices}
686+
// waitUntilMultipathDevicePresent polls until a multipath dm-* appears for the LUN's SCSI path devices.
687+
// It inspects sysfs before scanning: ScanTargetLUN runs only when no block devices exist for the LUN yet.
688+
func (client *Client) waitUntilMultipathDevicePresent(ctx context.Context, hostSessionMap []map[string]int, lunID int) error {
689+
waitCtx, cancel := multipathWaitContext(ctx)
690+
defer cancel()
648691

649-
Logc(ctx).WithFields(fields).Debug(">>>> fcp.waitForMultipathDeviceForDevices")
650-
defer Logc(ctx).WithFields(fields).Debug("<<<< fcp.waitForMultipathDeviceForDevices")
692+
tick := time.NewTicker(multipathDevicePollInterval)
693+
defer tick.Stop()
651694

652-
multipathDevice := ""
695+
for {
696+
if err := waitCtx.Err(); err != nil {
697+
return fmt.Errorf("waiting for multipath device for LUN %d: %w", lunID, err)
698+
}
699+
paths := client.fcpUtils.GetSysfsBlockDirsForLUN(lunID, hostSessionMap)
700+
devices, err := client.fcpUtils.GetDevicesForLUN(paths)
701+
if err != nil {
702+
return err
703+
}
704+
if len(devices) == 0 {
705+
addrs := client.fcpDeviceAddressesForLUN(waitCtx, hostSessionMap, lunID)
706+
if len(paths) == 0 && len(addrs) == 0 {
707+
if fcpHostSessionMapEmpty(hostSessionMap) {
708+
return fmt.Errorf("multipath device not found when it is expected for LUN %d", lunID)
709+
}
710+
} else if len(addrs) > 0 {
711+
if err := client.deviceClient.ScanTargetLUN(waitCtx, addrs); err != nil {
712+
Logc(ctx).WithField("scanError", err).Debug("Could not scan for new LUN while waiting for multipath.")
713+
}
714+
}
715+
select {
716+
case <-waitCtx.Done():
717+
return fmt.Errorf("waiting for multipath device for LUN %d: %w", lunID, waitCtx.Err())
718+
case <-tick.C:
719+
}
720+
continue
721+
}
722+
for _, device := range devices {
723+
if dm := client.deviceClient.FindMultipathDeviceForDevice(waitCtx, device); dm != "" {
724+
Logc(ctx).WithFields(LogFields{"multipathDevice": dm, "lunID": lunID}).Debug(
725+
"Multipath device found for LUN.")
726+
return nil
727+
}
728+
}
729+
select {
730+
case <-waitCtx.Done():
731+
return fmt.Errorf("waiting for multipath device for LUN %d: %w", lunID, waitCtx.Err())
732+
case <-tick.C:
733+
}
734+
}
735+
}
653736

654-
for _, device := range devices {
655-
multipathDevice = client.deviceClient.FindMultipathDeviceForDevice(ctx, device)
656-
if multipathDevice != "" {
657-
break
737+
func multipathWaitContext(ctx context.Context) (context.Context, context.CancelFunc) {
738+
if deadline, ok := ctx.Deadline(); ok {
739+
remaining := time.Until(deadline)
740+
if remaining <= 0 {
741+
return context.WithTimeout(ctx, 0)
742+
}
743+
// Spend most (but not all) of the remaining request budget on the DM wait.
744+
// This avoids consuming the entire deadline in the wait loop and preserves
745+
// time for post-wait attach steps (serial/size/device readiness checks).
746+
sliced := remaining * multipathDeviceWaitBudgetPercent / 100
747+
// Enforce a minimum wait window so very short remaining deadlines don't
748+
// degrade into immediate retry churn with no meaningful polling.
749+
if sliced < multipathDeviceWaitMinTimeout {
750+
sliced = multipathDeviceWaitMinTimeout
751+
}
752+
// Always reserve a small post-wait budget for follow-on operations.
753+
maxWait := remaining - multipathDevicePostWaitBuffer
754+
if maxWait <= 0 {
755+
maxWait = remaining
658756
}
757+
// Ceiling wins: if the minimum floor exceeds the allowed max wait, clamp to maxWait.
758+
if sliced > maxWait {
759+
sliced = maxWait
760+
}
761+
return context.WithTimeout(ctx, sliced)
659762
}
660763

661-
if multipathDevice == "" {
662-
Logc(ctx).WithField("multipathDevice", multipathDevice).Warn("Multipath device not found.")
663-
return "", errors.New("multipath device not found when it is expected")
764+
// Defensive fallback for callers that do not supply a deadline.
765+
return context.WithTimeout(ctx, multipathDeviceNoDeadlineTimeout)
766+
}
664767

665-
} else {
666-
Logc(ctx).WithField("multipathDevice", multipathDevice).Debug("Multipath device found.")
768+
// waitForMultipathDeviceForLUN
769+
// for the given LUN, this function waits for the associated multipath device to be present
770+
// first find the /dev/sd* devices associated with the LUN
771+
// Wait for the multipath device dm-* for the /dev/sd* devices.
772+
func (client *Client) waitForMultipathDeviceForLUN(ctx context.Context, hostSessionMap []map[string]int, lunID int,
773+
fcpNodeName string,
774+
) error {
775+
fields := LogFields{
776+
"lunID": lunID,
777+
"fcpNodeName": fcpNodeName,
667778
}
779+
Logc(ctx).WithFields(fields).Debug(">>>> fcp.waitForMultipathDeviceForLUN")
780+
defer Logc(ctx).WithFields(fields).Debug("<<<< fcp.waitForMultipathDeviceForLUN")
668781

669-
return multipathDevice, nil
782+
return client.waitUntilMultipathDevicePresent(ctx, hostSessionMap, lunID)
670783
}
671784

672785
// waitForDeviceScan scans all paths to a specific LUN and waits until all
@@ -683,46 +796,7 @@ func (client *Client) waitForDeviceScan(ctx context.Context, hostSessionMap []ma
683796

684797
Logc(ctx).WithField("hostSessionMap", hostSessionMap).Debug("Built FCP host/session map.")
685798

686-
deviceAddresses := make([]models.ScsiDeviceAddress, 0)
687-
var hostNum, targetID int
688-
var err error
689-
690-
// Construct the host and target lists required for the scan.
691-
for _, hostNumber := range hostSessionMap {
692-
for host := range hostNumber {
693-
re := regexp.MustCompile(`rport-(\d+):`)
694-
hostStr := re.FindStringSubmatch(host)
695-
if len(hostStr) > 1 {
696-
if hostNum, err = strconv.Atoi(hostStr[1]); err != nil {
697-
Logc(ctx).WithField("port", hostNum).Error("Could not parse port number")
698-
continue
699-
}
700-
}
701-
702-
// Read the target IDs from the sysfs path.
703-
targetIDFile := fmt.Sprintf("/sys/class/fc_host/host%d/device/%s/fc_remote_ports/%s/scsi_target_id",
704-
hostNum, host, host)
705-
if targetIDRaw, err := os.ReadFile(targetIDFile); err != nil {
706-
Logc(ctx).WithField("targetIDFile", targetIDFile).Error("Could not find the target ID file")
707-
} else {
708-
targetIDStr := strings.TrimSpace(string(targetIDRaw))
709-
if targetID, err = strconv.Atoi(targetIDStr); err != nil {
710-
Logc(ctx).WithField("targetID", targetIDStr).Error("Could not parse target ID")
711-
continue
712-
}
713-
714-
deviceAddress := models.ScsiDeviceAddress{
715-
Host: strconv.Itoa(hostNum),
716-
Channel: models.ScanAllSCSIDeviceAddress,
717-
Target: strconv.Itoa(targetID),
718-
LUN: strconv.Itoa(lunID),
719-
}
720-
if !collection.Contains(deviceAddresses, deviceAddress) {
721-
deviceAddresses = append(deviceAddresses, deviceAddress)
722-
}
723-
}
724-
}
725-
}
799+
deviceAddresses := client.fcpDeviceAddressesForLUN(ctx, hostSessionMap, lunID)
726800

727801
if err := client.deviceClient.ScanTargetLUN(ctx, deviceAddresses); err != nil {
728802
Logc(ctx).WithField("scanError", err).Error("Could not scan for new LUN.")

0 commit comments

Comments
 (0)