Skip to content

Commit 50a8950

Browse files
committed
WIP: Query PCIe ports for correct amount of devices that can be attached to the node
Signed-off-by: Niclas Schad <niclas.schad@stackit.cloud>
1 parent bddf6e4 commit 50a8950

5 files changed

Lines changed: 85 additions & 4 deletions

File tree

pkg/csi/blockstorage/controllerserver.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,11 @@ func (cs *controllerServer) ControllerPublishVolume(ctx context.Context, req *cs
370370

371371
_, err = cloud.AttachVolume(ctx, instanceID, volumeID)
372372
if err != nil {
373+
// Trigger's an immediate `NodeGetInfo` RPC call when MutableCSINodeAllocatableCount is enabled
374+
// TODO: Finish Implementation of IsTooManyDevicesError
375+
//if stackiterrors.IsTooManyDevicesError(err) {
376+
// return nil, status.Errorf(codes.ResourceExhausted, "[ControllerPublishVolume] Node can't accept any more volumes %v. All PCIe lanes are exhausted!", err)
377+
//}
373378
klog.Errorf("Failed to AttachVolume: %v", err)
374379
return nil, status.Errorf(codes.Internal, "[ControllerPublishVolume] Attach Volume failed with error %v", err)
375380
}

pkg/csi/blockstorage/nodeserver.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,20 @@ func (ns *nodeServer) NodeGetInfo(ctx context.Context, _ *csi.NodeGetInfoRequest
308308
}
309309

310310
maxVolumesPerNode := DetermineMaxVolumesByFlavor(flavor)
311-
// Subtract 1 for root disk and another for configDrive/spare
312-
maxVolumesPerNode -= 2
311+
312+
// Subtract already mounted Volumes
313+
emptyPCIeRootPorts, err := mount.GetEmptyPCIeRootPorts()
314+
if err != nil {
315+
klog.Errorf("[NodeGetInfo] unable to retrieve PCIe root ports %v", err)
316+
} else {
317+
if emptyPCIeRootPorts <= 0 {
318+
klog.Warningf("[NodeGetInfo] node does not have any free PCIe ports")
319+
maxVolumesPerNode = 0
320+
} else {
321+
maxVolumesPerNode = emptyPCIeRootPorts
322+
}
323+
}
324+
313325
klog.V(4).Infof("Determined node to support %d volumes", maxVolumesPerNode)
314326

315327
nodeInfo := &csi.NodeGetInfoResponse{

pkg/csi/blockstorage/utils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func DetermineMaxVolumesByFlavor(flavor string) int64 {
9696
// AMD 2nd Gen
9797
return 159
9898
default:
99-
// All other flavors can mount 28 volumes
99+
// All other flavors can mount 25 volumes
100100
return 25
101101
}
102102
}

pkg/csi/util/mount/mount.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"os"
2222
"path"
23+
"path/filepath"
2324
"slices"
2425
"strings"
2526
"time"
@@ -119,6 +120,57 @@ func probeVolume() error {
119120
return nil
120121
}
121122

123+
// GetEmptyPCIeRootPorts returns the number of PCIe Root ports who currently
124+
// do not have ANY downstream devices, therefore considered "empty". This
125+
// function is used dynamically how many more devices (in this case "volumes")
126+
// can be mounted to the given node.
127+
func GetEmptyPCIeRootPorts() (int64, error) {
128+
const pciPath = "/sys/bus/pci/devices"
129+
130+
// Get all PCI devices
131+
devices, err := os.ReadDir(pciPath)
132+
if err != nil {
133+
return 0, fmt.Errorf("failed to read PCI bus: %w", err)
134+
}
135+
136+
emptyCount := 0
137+
138+
for _, dev := range devices {
139+
devPath := filepath.Join(pciPath, dev.Name())
140+
141+
// 1. Identify if it's a Root Port / Bridge
142+
// We check the 'class' file. PCI Bridge class code starts with 0x0604
143+
classBuf, err := os.ReadFile(filepath.Join(devPath, "class"))
144+
if err != nil {
145+
continue
146+
}
147+
class := strings.TrimSpace(string(classBuf))
148+
149+
// Class 0x060400 is a PCI-to-PCI bridge (standard for Root Ports)
150+
if strings.HasPrefix(class, "0x0604") {
151+
152+
// 2. Check if the port has downstream devices
153+
// If the bridge has children, they appear as subdirectories
154+
// matching the PCI address format (e.g., 0000:01:00.0)
155+
hasChild := false
156+
files, _ := os.ReadDir(devPath)
157+
for _, file := range files {
158+
// PCI addresses always contain a colon
159+
if strings.Contains(file.Name(), ":") {
160+
hasChild = true
161+
break
162+
}
163+
}
164+
165+
if !hasChild {
166+
emptyCount++
167+
}
168+
}
169+
}
170+
171+
return int64(emptyCount), nil
172+
}
173+
122174
// GetDevicePath returns the path of an attached block storage volume, specified by its id.
123175
func (m *Mount) GetDevicePath(volumeID string) (string, error) {
124176
backoff := wait.Backoff{

pkg/stackit/stackiterrors/errors.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import (
44
"errors"
55
"fmt"
66
"net/http"
7+
"strings"
78

89
oapiError "github.com/stackitcloud/stackit-sdk-go/core/oapierror"
9-
wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait"
10+
"github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait"
1011
)
1112

1213
var ErrNotFound = errors.New("failed to find object")
@@ -20,6 +21,17 @@ func IsNotFound(err error) bool {
2021
return oAPIError.StatusCode == http.StatusNotFound
2122
}
2223

24+
func IsTooManyDevicesError(err error) bool {
25+
var oAPIError *oapiError.GenericOpenAPIError
26+
if ok := errors.As(err, &oAPIError); !ok {
27+
return false
28+
}
29+
30+
// TODO: This is just a placeholder. Implement this correctly
31+
return oAPIError.StatusCode == http.StatusInternalServerError &&
32+
strings.Contains(oAPIError.ErrorMessage, "devices")
33+
}
34+
2335
func IgnoreNotFound(err error) error {
2436
if IsNotFound(err) {
2537
return nil

0 commit comments

Comments
 (0)