diff --git a/packages/system/linstor/Makefile b/packages/system/linstor/Makefile index b211baec..2ad4def9 100644 --- a/packages/system/linstor/Makefile +++ b/packages/system/linstor/Makefile @@ -4,7 +4,7 @@ export NAMESPACE=cozy-$(NAME) include ../../../hack/common-envs.mk include ../../../hack/package.mk -LINSTOR_VERSION ?= 1.32.3 +LINSTOR_VERSION ?= 1.33.2 LINSTOR_CSI_VERSION ?= v1.10.5 image: image-piraeus-server image-linstor-csi diff --git a/packages/system/linstor/images/piraeus-server/patches/README.md b/packages/system/linstor/images/piraeus-server/patches/README.md index 558fdfeb..dfe75c2e 100644 --- a/packages/system/linstor/images/piraeus-server/patches/README.md +++ b/packages/system/linstor/images/piraeus-server/patches/README.md @@ -1,14 +1,17 @@ # LINSTOR Server Patches -Custom patches for piraeus-server (linstor-server) v1.32.3. +Custom patches for piraeus-server (linstor-server) v1.33.2. -- **adjust-on-resfile-change.diff** — Use actual device path in res file during toggle-disk; fix LUKS data offset - - Upstream: [#473](https://github.com/LINBIT/linstor-server/pull/473), [#472](https://github.com/LINBIT/linstor-server/pull/472) -- **allow-toggle-disk-retry.diff** — Allow retry and cancellation of failed toggle-disk operations - - Upstream: [#475](https://github.com/LINBIT/linstor-server/pull/475) -- **force-metadata-check-on-disk-add.diff** — Create metadata during toggle-disk from diskless to diskful - - Upstream: [#474](https://github.com/LINBIT/linstor-server/pull/474) -- **fix-duplicate-tcp-ports.diff** — Preserve TCP ports during toggle-disk to prevent port mismatch between controller and satellites - - Upstream: [#476](https://github.com/LINBIT/linstor-server/pull/476) (superseded by this expanded fix) -- **skip-adjust-when-device-inaccessible.diff** — Fix resources stuck in StandAlone after reboot, Unknown state race condition, and encrypted resource deletion - - Upstream: [#477](https://github.com/LINBIT/linstor-server/pull/477) +- **allow-toggle-disk-retry.diff** — Backport maintainer implementation of toggle-disk retry/abort + - Source PR/comment: [#475](https://github.com/LINBIT/linstor-server/pull/475), [maintainer note](https://github.com/LINBIT/linstor-server/pull/475#issuecomment-3949630419) + - Backported from upstream commit: [`3d97f71c9`](https://github.com/LINBIT/linstor-server/commit/3d97f71c95a493588d3d521c63eac4d846935fb3) +- **fix-duplicate-tcp-ports.diff** — Preserve DRBD TCP ports during toggle-disk and avoid redundant `ensureStackDataExists()` + - Source PR/review: [#476](https://github.com/LINBIT/linstor-server/pull/476), [review suggestion](https://github.com/LINBIT/linstor-server/pull/476#discussion_r3007725079) + - Backported from commits: [`79d6375c5`](https://github.com/kvaps/linstor-server/commit/79d6375c55d6181b35a7b7f0fe8dbdfb86e126cd), [`bcc89902f`](https://github.com/kvaps/linstor-server/commit/bcc89902f4f61ac1589dd07ebb7f5aae1935370d) +- **fix-luks-header-size.diff** — Account for LUKS2 `--offset`, metadata/keyslots sizing, and device `optimal_io_size` + - Source PR/comment: [#472](https://github.com/LINBIT/linstor-server/pull/472), [maintainer note](https://github.com/LINBIT/linstor-server/pull/472#issuecomment-3949687603) + - Backported from commits: [`ccc85fbd2`](https://github.com/LINBIT/linstor-server/commit/ccc85fbd2c65f0b97c52403fa80f1efdb886ec4e), [`71b601554`](https://github.com/LINBIT/linstor-server/commit/71b601554f41bcb50cd5bd06989c5b0d3a814acd) + - Note: upstream commit [`3d0402a0c`](https://github.com/LINBIT/linstor-server/commit/3d0402a0c25f0a4b57b380321f10e89982f26e7a) is already included in `v1.33.1` +- **retry-adjust-after-stale-bitmap.diff** — Retry `drbdadm adjust` after detaching a stale local bitmap state + - Source PR: [#491](https://github.com/LINBIT/linstor-server/pull/491) + - Backported from commit: [`51ae50a84`](https://github.com/kvaps/linstor-server/commit/51ae50a84dcb98093f543b819652c750a94d96c9) diff --git a/packages/system/linstor/images/piraeus-server/patches/adjust-on-resfile-change.diff b/packages/system/linstor/images/piraeus-server/patches/adjust-on-resfile-change.diff deleted file mode 100644 index 6788efaf..00000000 --- a/packages/system/linstor/images/piraeus-server/patches/adjust-on-resfile-change.diff +++ /dev/null @@ -1,48 +0,0 @@ -diff --git a/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/ConfFileBuilder.java b/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/ConfFileBuilder.java -index 36c52ccf8..c0bb7b967 100644 ---- a/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/ConfFileBuilder.java -+++ b/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/ConfFileBuilder.java -@@ -894,12 +894,16 @@ public class ConfFileBuilder - if (((Volume) vlmData.getVolume()).getFlags().isUnset(localAccCtx, Volume.Flags.DELETE)) - { - final String disk; -+ // Check if we're in toggle-disk operation (adding disk to diskless resource) -+ boolean isDiskAdding = vlmData.getVolume().getAbsResource().getStateFlags().isSomeSet( -+ localAccCtx, Resource.Flags.DISK_ADD_REQUESTED, Resource.Flags.DISK_ADDING); - if ((!isPeerRsc && vlmData.getDataDevice() == null) || - (isPeerRsc && - // FIXME: vlmData.getRscLayerObject().getFlags should be used here - vlmData.getVolume().getAbsResource().disklessForDrbdPeers(accCtx) - ) || -- (!isPeerRsc && -+ // For toggle-disk: if dataDevice is set and we're adding disk, use the actual device -+ (!isPeerRsc && !isDiskAdding && - // FIXME: vlmData.getRscLayerObject().getFlags should be used here - vlmData.getVolume().getAbsResource().isDrbdDiskless(accCtx) - ) -diff --git a/satellite/src/main/java/com/linbit/linstor/layer/luks/CryptSetupCommands.java b/satellite/src/main/java/com/linbit/linstor/layer/luks/CryptSetupCommands.java -index 54dd5c19f..018de58cf 100644 ---- a/satellite/src/main/java/com/linbit/linstor/layer/luks/CryptSetupCommands.java -+++ b/satellite/src/main/java/com/linbit/linstor/layer/luks/CryptSetupCommands.java -@@ -34,6 +34,9 @@ public class CryptSetupCommands implements Luks - private static final Version V2_1_0 = new Version(2, 1, 0); - private static final Version V2_0_0 = new Version(2, 0, 0); - private static final String PBDKF_MAX_MEMORY_KIB = "262144"; // 256 MiB -+ // Fixed LUKS2 data offset in 512-byte sectors (16 MiB = 32768 sectors) -+ // This ensures consistent LUKS header size across all nodes regardless of system defaults -+ private static final String LUKS2_DATA_OFFSET_SECTORS = "32768"; - - @SuppressWarnings("unused") - private final ErrorReporter errorReporter; -@@ -78,6 +81,11 @@ public class CryptSetupCommands implements Luks - command.add(CRYPTSETUP); - command.add("-q"); - command.add("luksFormat"); -+ // Always specify explicit offset to ensure consistent LUKS header size across all nodes -+ // Without this, different systems may create LUKS with different header sizes (16MiB vs 32MiB) -+ // which causes "Low.dev. smaller than requested DRBD-dev. size" errors during toggle-disk -+ command.add("--offset"); -+ command.add(LUKS2_DATA_OFFSET_SECTORS); - if (version.greaterOrEqual(V2_0_0)) - { - command.add("--pbkdf-memory"); diff --git a/packages/system/linstor/images/piraeus-server/patches/allow-toggle-disk-retry.diff b/packages/system/linstor/images/piraeus-server/patches/allow-toggle-disk-retry.diff index 264e0221..952ba16c 100644 --- a/packages/system/linstor/images/piraeus-server/patches/allow-toggle-disk-retry.diff +++ b/packages/system/linstor/images/piraeus-server/patches/allow-toggle-disk-retry.diff @@ -1,168 +1,232 @@ diff --git a/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java b/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java -index d93a18014..cc8ce4f04 100644 +index d93a18014..a944cb809 100644 --- a/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java +++ b/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java -@@ -57,7 +57,9 @@ import com.linbit.linstor.stateflags.StateFlags; - import com.linbit.linstor.storage.StorageException; - import com.linbit.linstor.storage.data.adapter.drbd.DrbdRscData; - import com.linbit.linstor.storage.interfaces.categories.resource.AbsRscLayerObject; -+import com.linbit.linstor.storage.interfaces.categories.resource.VlmProviderObject; - import com.linbit.linstor.storage.kinds.DeviceLayerKind; -+import com.linbit.linstor.storage.kinds.DeviceProviderKind; - import com.linbit.linstor.storage.utils.LayerUtils; - import com.linbit.linstor.tasks.AutoDiskfulTask; - import com.linbit.linstor.utils.layer.LayerRscUtils; -@@ -387,21 +389,84 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL +@@ -111,6 +111,14 @@ import reactor.util.function.Tuple2; + @Singleton + public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionListener + { ++ private enum ToggleDiskAction ++ { ++ NOOP, ++ NORMAL, ++ ABORT, ++ RETRY ++ } ++ + private final AccessContext apiCtx; + private final ScopeRunner scopeRunner; + private final BackgroundRunner backgroundRunner; +@@ -386,69 +394,33 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL + "Toggle Disk on %s/%s %s", nodeNameStr, rscNameStr, removeDisk ? "removing disk" : "adding disk"); Resource rsc = ctrlApiDataLoader.loadRsc(nodeName, rscName, true); ++ ResourceDefinition rscDfn = rsc.getResourceDefinition(); -+ // Allow retry of the same operation if the previous attempt failed -+ // (the requested flag remains set for retry on reconnection, but we should also allow manual retry) -+ // Also allow cancellation of a failed operation by requesting the opposite operation - if (hasDiskAddRequested(rsc)) - { +- if (hasDiskAddRequested(rsc)) +- { - throw new ApiRcException(ApiCallRcImpl.simpleEntry( - ApiConsts.FAIL_RSC_BUSY, - "Addition of disk to resource already requested", - true - )); -+ if (removeDisk) -+ { -+ // User wants to cancel the failed add-disk operation and go back to diskless -+ // Use the existing disk removal flow to properly cleanup storage on satellite -+ errorReporter.logInfo( -+ "Toggle Disk cancel on %s/%s - cancelling failed DISK_ADD_REQUESTED, reverting to diskless", -+ nodeNameStr, rscNameStr); -+ unmarkDiskAddRequested(rsc); -+ // Also clear DISK_ADDING if it was set -+ unmarkDiskAdding(rsc); -+ -+ // Set storage pool to diskless pool (overwrite the diskful pool that was set) -+ Props rscProps = ctrlPropsHelper.getProps(rsc); -+ rscProps.map().put(ApiConsts.KEY_STOR_POOL_NAME, LinStor.DISKLESS_STOR_POOL_NAME); -+ -+ // Set DISK_REMOVE_REQUESTED to use the existing disk removal flow -+ // This will: -+ // 1. updateAndAdjustDisk sets DISK_REMOVING flag -+ // 2. Satellite sees DISK_REMOVING and deletes LUKS/storage devices -+ // 3. finishOperation rebuilds layer stack as diskless -+ // We keep the existing layer data so satellite can properly cleanup -+ markDiskRemoveRequested(rsc); -+ -+ ctrlTransactionHelper.commit(); -+ -+ // Use existing disk removal flow - this will properly cleanup storage on satellite -+ return Flux -+ .just(ApiCallRcImpl.singleApiCallRc( -+ ApiConsts.MODIFIED, -+ "Cancelling disk addition, reverting to diskless" -+ )) -+ .concatWith(updateAndAdjustDisk(nodeName, rscName, true, toggleIntoTiebreakerRef, context)) -+ .concatWith(ctrlRscDfnApiCallHandler.get().updateProps(rsc.getResourceDefinition())); -+ } -+ // If adding disk and DISK_ADD_REQUESTED is already set, treat as retry -+ // Simply retry the operation with existing layer data - satellite will handle it idempotently -+ // NOTE: We don't remove/recreate layer data here because removeLayerData() only deletes -+ // from controller DB without calling drbdadm down on satellite, leaving orphaned DRBD devices -+ errorReporter.logInfo( -+ "Toggle Disk retry on %s/%s - DISK_ADD_REQUESTED already set, retrying operation", -+ nodeNameStr, rscNameStr); -+ ctrlTransactionHelper.commit(); -+ return Flux -+ .just(new ApiCallRcImpl()) -+ .concatWith(updateAndAdjustDisk(nodeName, rscName, false, toggleIntoTiebreakerRef, context)) -+ .concatWith(ctrlRscDfnApiCallHandler.get().updateProps(rsc.getResourceDefinition())); - } - if (hasDiskRemoveRequested(rsc)) - { +- } +- if (hasDiskRemoveRequested(rsc)) +- { - throw new ApiRcException(ApiCallRcImpl.simpleEntry( - ApiConsts.FAIL_RSC_BUSY, - "Removal of disk from resource already requested", - true - )); -+ if (!removeDisk) -+ { -+ // User wants to cancel the failed remove-disk operation +- } ++ ToggleDiskAction action = determineToggleDiskAction(rsc, removeDisk); ++ errorReporter.logDebug("Toggle Disk action: %s", action); + +- if (!removeDisk && !ctrlVlmCrtApiHelper.isDiskless(rsc)) ++ switch (action) + { +- throw new ApiRcException(ApiCallRcImpl.simpleEntry( +- ApiConsts.WARN_RSC_ALREADY_HAS_DISK, +- "Resource already has disk", +- true +- )); +- } +- if (removeDisk && ctrlVlmCrtApiHelper.isDiskless(rsc)) +- { +- throw new ApiRcException(ApiCallRcImpl.simpleEntry( +- ApiConsts.WARN_RSC_ALREADY_DISKLESS, +- "Resource already diskless", +- true +- )); ++ case NOOP: ++ return handleNoopAction(rsc, removeDisk); ++ case RETRY: ++ return handleRetryAction(rsc, removeDisk, toggleIntoTiebreakerRef, context); ++ case ABORT: ++ clearToggleDiskFlags(rsc); + errorReporter.logInfo( -+ "Toggle Disk cancel on %s/%s - cancelling failed DISK_REMOVE_REQUESTED", -+ nodeNameStr, rscNameStr); -+ unmarkDiskRemoveRequested(rsc); -+ ctrlTransactionHelper.commit(); -+ return Flux.just( -+ ApiCallRcImpl.singleApiCallRc( -+ ApiConsts.MODIFIED, -+ "Cancelled disk removal request" -+ ) ++ "Aborting previous toggle disk transition, starting new transition to %s", ++ removeDisk ? "diskless" : "diskful" + ); -+ } -+ // If removing disk and DISK_REMOVE_REQUESTED is already set, treat as retry -+ errorReporter.logInfo( -+ "Toggle Disk retry on %s/%s - DISK_REMOVE_REQUESTED already set, continuing operation", -+ nodeNameStr, rscNameStr); -+ ctrlTransactionHelper.commit(); -+ return Flux -+ .just(new ApiCallRcImpl()) -+ .concatWith(updateAndAdjustDisk(nodeName, rscName, true, toggleIntoTiebreakerRef, context)) -+ .concatWith(ctrlRscDfnApiCallHandler.get().updateProps(rsc.getResourceDefinition())); ++ break; ++ case NORMAL: ++ break; ++ default: ++ throw new ImplementationError("Unhandled case: " + action); } - if (!removeDisk && !ctrlVlmCrtApiHelper.isDiskless(rsc)) -@@ -412,17 +477,43 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL - true - )); - } -+ ResourceDefinition rscDfn = rsc.getResourceDefinition(); -+ AccessContext peerCtx = peerAccCtx.get(); -+ - if (removeDisk && ctrlVlmCrtApiHelper.isDiskless(rsc)) - { -+ // Resource is marked as diskless - check if it has orphaned storage layers that need cleanup -+ AbsRscLayerObject layerData = getLayerData(peerCtx, rsc); -+ if (layerData != null && (LayerUtils.hasLayer(layerData, DeviceLayerKind.LUKS) || -+ hasNonDisklessStorageLayer(layerData))) -+ { -+ // Resource is marked as diskless but has orphaned storage layers - need cleanup -+ // Use the existing disk removal flow to properly cleanup storage on satellite -+ errorReporter.logInfo( -+ "Toggle Disk cleanup on %s/%s - resource is diskless but has orphaned storage layers, cleaning up", -+ nodeNameStr, rscNameStr); -+ -+ // Set DISK_REMOVE_REQUESTED to use the existing disk removal flow -+ // This will trigger proper satellite cleanup via DISK_REMOVING flag -+ markDiskRemoveRequested(rsc); -+ -+ ctrlTransactionHelper.commit(); -+ -+ // Use existing disk removal flow - this will properly cleanup storage on satellite -+ return Flux -+ .just(ApiCallRcImpl.singleApiCallRc( -+ ApiConsts.MODIFIED, -+ "Cleaning up orphaned storage layers" -+ )) -+ .concatWith(updateAndAdjustDisk(nodeName, rscName, true, toggleIntoTiebreakerRef, context)) -+ .concatWith(ctrlRscDfnApiCallHandler.get().updateProps(rsc.getResourceDefinition())); -+ } - throw new ApiRcException(ApiCallRcImpl.simpleEntry( - ApiConsts.WARN_RSC_ALREADY_DISKLESS, - "Resource already diskless", - true - )); - } -- - ResourceDefinition rscDfn = rsc.getResourceDefinition(); - AccessContext peerCtx = peerAccCtx.get(); - if (removeDisk) - { - // Prevent removal of the last disk -@@ -1446,6 +1537,30 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL +- if (removeDisk) +- { +- // Prevent removal of the last disk +- int haveDiskCount = countDisksAndIsOnline(rscDfn); +- if (haveDiskCount <= 1) +- { +- throw new ApiRcException(ApiCallRcImpl.simpleEntry( +- ApiConsts.FAIL_INSUFFICIENT_REPLICA_COUNT, +- "Cannot remove the disk from the only online resource with a disk", +- true +- )); +- } ++ validateToggleDiskPreconditions(rsc, removeDisk); + +- if (!LayerUtils.hasLayer(getLayerData(peerCtx, rsc), DeviceLayerKind.DRBD)) +- { +- throw new ApiRcException(ApiCallRcImpl.simpleEntry( +- ApiConsts.FAIL_INVLD_LAYER_STACK, +- "Toggle disk is only supported in combination with DRBD", +- true +- )); +- } +- } +- else +- { +- ensureAllPeersHavePeerSlotLeft(rscDfn); +- } ++ AccessContext peerCtx = peerAccCtx.get(); + + // Save the requested storage pool in the resource properties. + // This does not cause the storage pool to be used automatically. +@@ -628,10 +600,10 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL + + ctrlTransactionHelper.commit(); + +- String action = removeDisk ? "Removal of disk from" : "Addition of disk to"; ++ String actionStr = removeDisk ? "Removal of disk from" : "Addition of disk to"; + responses.addEntry(ApiCallRcImpl.simpleEntry( + ApiConsts.MODIFIED, +- action + " resource '" + rscDfn.getName().displayValue + "' " + ++ actionStr + " resource '" + rscDfn.getName().displayValue + "' " + + "on node '" + rsc.getNode().getName().displayValue + "' registered" + )); + +@@ -651,6 +623,40 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL + ); + } + ++ private Flux handleNoopAction(Resource rscRef, boolean removeDiskRef) ++ { ++ String state = removeDiskRef ? "diskless" : "diskful"; ++ ApiCallRcImpl responses = new ApiCallRcImpl(); ++ responses.addEntry(ApiCallRcImpl.simpleEntry( ++ ApiConsts.INFO_NOOP, ++ "Resource '" + rscRef.getResourceDefinition().getName().displayValue + "' on node '" + ++ rscRef.getNode().getName().displayValue + "' is already " + state ++ )); ++ return Flux.just(responses); ++ } ++ ++ private Flux handleRetryAction( ++ Resource rscRef, ++ boolean removeDisk, ++ boolean toggleIntoTiebreakerRef, ++ ResponseContext context ++ ) ++ { ++ String direction = removeDisk ? "diskless" : "diskful"; ++ ApiCallRcImpl responses = new ApiCallRcImpl(); ++ NodeName nodeName = rscRef.getNode().getName(); ++ ResourceName rscName = rscRef.getResourceDefinition().getName(); ++ responses.addEntry(ApiCallRcImpl.simpleEntry( ++ ApiConsts.INFO_NOOP, ++ "Retrying toggle disk to " + direction + " for resource '" + rscName.displayValue + ++ "' on node '" + nodeName.displayValue + "'" ++ )); ++ ++ return Flux ++ .just(responses) ++ .concatWith(updateAndAdjustDisk(nodeName, rscName, removeDisk, toggleIntoTiebreakerRef, context)); ++ } ++ + private long getVlmDfnSizePrivileged(VolumeDefinition vlmDfnRef) + { + try +@@ -781,6 +787,96 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL } } -+ private void unmarkDiskAddRequested(Resource rsc) ++ private ToggleDiskAction determineToggleDiskAction(Resource rsc, boolean removeDisk) ++ { ++ boolean isDiskless = ctrlVlmCrtApiHelper.isDiskless(rsc); ++ boolean diskAddRequested = hasDiskAddRequested(rsc); ++ boolean diskRemoveRequested = hasDiskRemoveRequested(rsc); ++ ++ ToggleDiskAction action; ++ if (isDiskless) ++ { ++ if (diskAddRequested) ++ { ++ action = removeDisk ? ToggleDiskAction.ABORT : ToggleDiskAction.RETRY; ++ } ++ else if (diskRemoveRequested) ++ { ++ action = removeDisk ? ToggleDiskAction.RETRY : ToggleDiskAction.ABORT; ++ } ++ else ++ { ++ action = removeDisk ? ToggleDiskAction.NOOP : ToggleDiskAction.NORMAL; ++ } ++ } ++ else ++ { ++ if (diskRemoveRequested) ++ { ++ action = removeDisk ? ToggleDiskAction.RETRY : ToggleDiskAction.ABORT; ++ } ++ else ++ { ++ action = removeDisk ? ToggleDiskAction.NORMAL : ToggleDiskAction.NOOP; ++ } ++ } ++ ++ return action; ++ } ++ ++ private void validateToggleDiskPreconditions(Resource rsc, boolean removeDisk) ++ { ++ ResourceDefinition rscDfn = rsc.getResourceDefinition(); ++ if (removeDisk) ++ { ++ validateDiskRemovalAllowed(rsc, rscDfn); ++ } ++ else ++ { ++ ensureAllPeersHavePeerSlotLeft(rscDfn); ++ } ++ } ++ ++ private void clearToggleDiskFlags(Resource rsc) + { + try + { -+ rsc.getStateFlags().disableFlags(apiCtx, Resource.Flags.DISK_ADD_REQUESTED); ++ rsc.getStateFlags().disableFlags( ++ apiCtx, ++ Resource.Flags.DISK_ADD_REQUESTED, ++ Resource.Flags.DISK_ADDING, ++ Resource.Flags.DISK_REMOVE_REQUESTED, ++ Resource.Flags.DISK_REMOVING ++ ); + } + catch (AccessDeniedException | DatabaseException exc) + { @@ -170,60 +234,28 @@ index d93a18014..cc8ce4f04 100644 + } + } + -+ private void unmarkDiskRemoveRequested(Resource rsc) ++ private void validateDiskRemovalAllowed(Resource rsc, ResourceDefinition rscDfn) + { -+ try ++ int haveDiskCount = countDisksAndIsOnline(rscDfn); ++ if (haveDiskCount <= 1) + { -+ rsc.getStateFlags().disableFlags(apiCtx, Resource.Flags.DISK_REMOVE_REQUESTED); ++ throw new ApiRcException(ApiCallRcImpl.simpleEntry( ++ ApiConsts.FAIL_INSUFFICIENT_REPLICA_COUNT, ++ "Cannot remove the disk from the only online resource with a disk", ++ true ++ )); + } -+ catch (AccessDeniedException | DatabaseException exc) ++ ++ if (!LayerUtils.hasLayer(getLayerData(peerAccCtx.get(), rsc), DeviceLayerKind.DRBD)) + { -+ throw new ImplementationError(exc); ++ throw new ApiRcException(ApiCallRcImpl.simpleEntry( ++ ApiConsts.FAIL_INVLD_LAYER_STACK, ++ "Toggle disk is only supported in combination with DRBD", ++ true ++ )); + } + } + - private void markDiskAdded(Resource rscData) - { - try -@@ -1511,6 +1626,41 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL - return layerData; - } - -+ /** -+ * Check if the layer stack has a non-diskless STORAGE layer. -+ * This is used to detect orphaned storage layers that need cleanup. -+ */ -+ private boolean hasNonDisklessStorageLayer(AbsRscLayerObject layerDataRef) -+ { -+ boolean hasNonDiskless = false; -+ if (layerDataRef != null) -+ { -+ if (layerDataRef.getLayerKind() == DeviceLayerKind.STORAGE) -+ { -+ for (VlmProviderObject vlmData : layerDataRef.getVlmLayerObjects().values()) -+ { -+ if (vlmData.getProviderKind() != DeviceProviderKind.DISKLESS) -+ { -+ hasNonDiskless = true; -+ break; -+ } -+ } -+ } -+ if (!hasNonDiskless) -+ { -+ for (AbsRscLayerObject child : layerDataRef.getChildren()) -+ { -+ if (hasNonDisklessStorageLayer(child)) -+ { -+ hasNonDiskless = true; -+ break; -+ } -+ } -+ } -+ } -+ return hasNonDiskless; -+ } -+ - private LockGuard createLockGuard() - { - return lockGuardFactory.buildDeferred(LockType.WRITE, LockObj.NODES_MAP, LockObj.RSC_DFN_MAP); + /** + * Although we need to rebuild the layerData as the layerList might have changed, if we do not + * deactivate (i.e. down) the current resource, we need to make sure that deleting DrbdRscData diff --git a/packages/system/linstor/images/piraeus-server/patches/fix-duplicate-tcp-ports.diff b/packages/system/linstor/images/piraeus-server/patches/fix-duplicate-tcp-ports.diff index b34a2500..ed106f98 100644 --- a/packages/system/linstor/images/piraeus-server/patches/fix-duplicate-tcp-ports.diff +++ b/packages/system/linstor/images/piraeus-server/patches/fix-duplicate-tcp-ports.diff @@ -1,69 +1,25 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Andrei Kvapil -Date: Fri, 28 Mar 2026 13:00:00 +0100 -Subject: [PATCH] fix(drbd): preserve TCP ports during toggle-disk operations - -Prevent TCP port mismatches after toggle-disk operations by preserving -existing TCP ports when rebuilding DrbdRscData. - -Root Cause: ------------ -During toggle-disk operations, removeLayerData() deletes DrbdRscData -(freeing its TCP ports from the number pool), then ensureStackDataExists() -creates new DrbdRscData. Since the payload has no explicit tcpPorts, -the controller allocates new ports from the pool -- which may differ from -the old ports if other resources claimed them in the meantime. - -The controller correctly avoids collisions in its own number pool, but -the satellite may miss the update (e.g. during controller restart or -network issues). When this happens, the satellite keeps the old ports -while peers receive the new ones, causing DRBD connection failures -(StandAlone/Connecting state). - -Additionally, remove the redundant ensureStackDataExists() call from -resetStoragePools() -- the caller already invokes it with the correct -payload. - -Solution: ---------- -1. Add copyDrbdTcpPortsIfExists() to save existing TCP ports into the - LayerPayload before removeLayerData() deletes them. -2. Call it from copyDrbdNodeIdIfExists() (covers both toggle-disk paths) - and from the needsDeactivate path (shared storage pool case). -3. Remove the redundant ensureStackDataExists() from resetStoragePools(). - -This ensures the same TCP ports are reused when DrbdRscData is recreated, -eliminating the window for port mismatch between controller and satellites. - -Co-Authored-By: Claude -Signed-off-by: Andrei Kvapil ---- - .../controller/CtrlRscToggleDiskApiCallHandler.java | 40 +++++++++++++++++++-- - .../linstor/layer/resource/CtrlRscLayerDataFactory.java | 2 -- - 2 files changed, 38 insertions(+), 4 deletions(-) - diff --git a/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java b/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java -index ccdb0cee5..b0554c2ec 100644 +index d93a18014..01cfbbacf 100644 --- a/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java +++ b/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java -@@ -58,6 +58,7 @@ import com.linbit.linstor.stateflags.StateFlags; - import com.linbit.linstor.storage.StorageException; - import com.linbit.linstor.storage.data.adapter.drbd.DrbdRscData; - import com.linbit.linstor.storage.interfaces.categories.resource.AbsRscLayerObject; +@@ -37,6 +37,7 @@ import com.linbit.linstor.core.objects.StorPool; + import com.linbit.linstor.core.objects.Volume; + import com.linbit.linstor.core.objects.VolumeDefinition; + import com.linbit.linstor.core.objects.utils.MixedStorPoolHelper; +import com.linbit.linstor.core.types.TcpPortNumber; - import com.linbit.linstor.storage.interfaces.categories.resource.VlmProviderObject; - import com.linbit.linstor.storage.kinds.DeviceLayerKind; - import com.linbit.linstor.storage.kinds.DeviceProviderKind; -@@ -88,6 +89,7 @@ import java.util.LinkedHashMap; - import java.util.List; + import com.linbit.linstor.dbdrivers.DatabaseException; + import com.linbit.linstor.event.EventWaiter; + import com.linbit.linstor.event.ObjectIdentifier; +@@ -85,6 +86,7 @@ import java.util.List; + import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.TreeSet; - + import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -@@ -587,8 +589,9 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL - +@@ -575,12 +577,13 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL + /* * We also have to remove the currently diskless DrbdRscData and free up the node-id as now we must - * use the shared resource's node-id @@ -73,30 +29,39 @@ index ccdb0cee5..b0554c2ec 100644 } else { -@@ -726,7 +729,7 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL +- copyDrbdNodeIdIfExists(rsc, payload); ++ copyDrbdSettings(rsc, payload); + } + /* + * rebuilds the layerdata in case we just removed it.. +@@ -782,10 +785,20 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL + } + /** - * Although we need to rebuild the layerData as the layerList might have changed, if we do not - * deactivate (i.e. down) the current resource, we need to make sure that deleting DrbdRscData +- * Although we need to rebuild the layerData as the layerList might have changed, if we do not +- * deactivate (i.e. down) the current resource, we need to make sure that deleting DrbdRscData - * and recreating a new DrbdRscData ends up with the same node-id as before. -+ * and recreating a new DrbdRscData ends up with the same node-id and TCP ports as before. ++ * Copies DRBD settings (node-id and TCP ports) from the existing DrbdRscData into the payload ++ * before removeLayerData() deletes them. This ensures that recreated DrbdRscData ends up with ++ * the same node-id and TCP ports as before. ++ * ++ * TCP ports must be preserved because if the satellite misses the update (e.g. due to controller ++ * restart or connectivity issues), it will keep the old ports while peers receive the new ones, ++ * causing DRBD connections to fail with StandAlone state. */ - private void copyDrbdNodeIdIfExists(Resource rsc, LayerPayload payload) throws ImplementationError - { -@@ -743,6 +746,37 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL - DrbdRscData drbdRscData = (DrbdRscData) drbdRscDataSet.iterator().next(); - payload.drbdRsc.nodeId = drbdRscData.getNodeId().value; - } ++ private void copyDrbdSettings(Resource rsc, LayerPayload payload) throws ImplementationError ++ { ++ copyDrbdNodeIdIfExists(rsc, payload); + copyDrbdTcpPortsIfExists(rsc, payload); + } + -+ /** -+ * Preserves existing TCP ports during toggle-disk operations. -+ * -+ * When removeLayerData() deletes DrbdRscData, the TCP ports are freed from the number pool. -+ * If ensureStackDataExists() then allocates different ports, and the satellite misses the update -+ * (e.g. due to controller restart or connectivity issues), the satellite keeps the old ports -+ * while peers get the new ones, causing DRBD connections to fail with StandAlone state. -+ */ + private void copyDrbdNodeIdIfExists(Resource rsc, LayerPayload payload) throws ImplementationError + { + Set> drbdRscDataSet = LayerRscUtils.getRscDataByLayer( +@@ -804,6 +817,28 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL + } + } + + private void copyDrbdTcpPortsIfExists(Resource rsc, LayerPayload payload) throws ImplementationError + { + Set> drbdRscDataSet = LayerRscUtils.getRscDataByLayer( @@ -117,22 +82,56 @@ index ccdb0cee5..b0554c2ec 100644 + payload.drbdRsc.tcpPorts = portInts; + } + } - } - ++ } ++ private List removeLayerData(Resource rscRef) + { + List layerList; +@@ -1058,15 +1093,15 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL + /* + * We also have to remove the possible meta-children of previous StorageRscData. + * LayerData will be recreated with ensureStackDataExists. +- * However, we still need to remember our node-id if we had / have DRBD in the list ++ * However, we still need to remember our DRBD settings if we had / have DRBD in the list + */ +- copyDrbdNodeIdIfExists(rsc, payload); ++ copyDrbdSettings(rsc, payload); + layerList = removeLayerData(rsc); + } + else + { + markDiskAdded(rsc); +- ctrlLayerStackHelper.resetStoragePools(rsc); ++ ctrlLayerStackHelper.resetStoragePools(rsc, false); + } + ctrlLayerStackHelper.ensureStackDataExists(rsc, layerList, payload); + diff --git a/controller/src/main/java/com/linbit/linstor/layer/resource/CtrlRscLayerDataFactory.java b/controller/src/main/java/com/linbit/linstor/layer/resource/CtrlRscLayerDataFactory.java -index 3538b380c..4f589145e 100644 +index 3538b380c..f9733b6f1 100644 --- a/controller/src/main/java/com/linbit/linstor/layer/resource/CtrlRscLayerDataFactory.java +++ b/controller/src/main/java/com/linbit/linstor/layer/resource/CtrlRscLayerDataFactory.java -@@ -276,8 +276,6 @@ public class CtrlRscLayerDataFactory - +@@ -263,6 +263,11 @@ public class CtrlRscLayerDataFactory + } + + public void resetStoragePools(Resource rscRef) ++ { ++ resetStoragePools(rscRef, true); ++ } ++ ++ public void resetStoragePools(Resource rscRef, boolean callEnsureStackDataExistsRef) + { + try + { +@@ -276,8 +281,10 @@ public class CtrlRscLayerDataFactory + rscDataToProcess.addAll(rscData.getChildren()); } - - ensureStackDataExists(rscRef, null, new LayerPayload()); ++ if (callEnsureStackDataExistsRef) ++ { ++ ensureStackDataExists(rscRef, null, new LayerPayload()); ++ } } catch (AccessDeniedException exc) { --- -2.39.5 (Apple Git-154) - diff --git a/packages/system/linstor/images/piraeus-server/patches/fix-luks-header-size.diff b/packages/system/linstor/images/piraeus-server/patches/fix-luks-header-size.diff new file mode 100644 index 00000000..8611825b --- /dev/null +++ b/packages/system/linstor/images/piraeus-server/patches/fix-luks-header-size.diff @@ -0,0 +1,570 @@ +diff --git a/satellite/src/main/java/com/linbit/linstor/layer/storage/AbsStorageProvider.java b/satellite/src/main/java/com/linbit/linstor/layer/storage/AbsStorageProvider.java +--- a/satellite/src/main/java/com/linbit/linstor/layer/storage/AbsStorageProvider.java ++++ b/satellite/src/main/java/com/linbit/linstor/layer/storage/AbsStorageProvider.java +@@ -2081,7 +2081,6 @@ public abstract class AbsStorageProvider< + ) + throws IOException, AccessDeniedException + { +- final StorPoolName storPoolObjName = storPoolObj.getName(); + if (storDevicePath != null) + { + errorReporter.logDebug("updateMinIoSize: Have storDevicePath \"%s\"", storDevicePath); +@@ -2094,35 +2093,18 @@ public abstract class AbsStorageProvider< + "updateMinIoSize: Block device path is \"%s\"", + blockDevicePath.toString() + ); +- final long minIoSize = BlockSizeInfo.getBlockSize(blockDevicePath); +- +- boolean updateValue = true; +- +- final String propKey = StorageConstants.NAMESPACE_INTERNAL + '/' + +- StorageConstants.BLK_DEV_MIN_IO_SIZE; +- final Props storPoolProps = storPoolObj.getProps(storDriverAccCtx); +- final @Nullable String currentPropValue = storPoolProps.getProp(propKey); +- if (currentPropValue != null) +- { +- try +- { +- final long currentPropMinIoSize = Long.parseLong(currentPropValue); +- updateValue = currentPropMinIoSize != minIoSize; +- } +- catch (NumberFormatException ignored) +- { +- } +- } +- +- if (updateValue) +- { +- final String propValue = Long.toString(minIoSize); +- errorReporter.logDebug( +- "Storage pool \"%s\": Set property \"%s\" = \"%s\"", +- storPoolObjName.displayValue, propKey, propValue +- ); +- propsChange.changeStorPoolProp(storPoolObj, propKey, propValue); +- } ++ updatePropIfNeeded( ++ propsChange, ++ storPoolObj, ++ StorageConstants.NAMESPACE_INTERNAL + '/' + StorageConstants.BLK_DEV_MIN_IO_SIZE, ++ Long.toString(BlockSizeInfo.getBlockSize(blockDevicePath)) ++ ); ++ updatePropIfNeeded( ++ propsChange, ++ storPoolObj, ++ StorageConstants.NAMESPACE_INTERNAL + '/' + StorageConstants.BLK_DEV_OPT_IO_SIZE, ++ Long.toString(BlockSizeInfo.getOptimalIoSize(blockDevicePath)) ++ ); + } + else + { +@@ -2130,6 +2112,28 @@ public abstract class AbsStorageProvider< + } + } + ++ private void updatePropIfNeeded( ++ LocalPropsChangePojo propsChangeRef, ++ StorPool storPoolObjRef, ++ String propKeyRef, ++ String propValueRef ++ ) ++ throws AccessDeniedException ++ { ++ final Props storPoolProps = storPoolObjRef.getProps(storDriverAccCtx); ++ final @Nullable String currentPropValue = storPoolProps.getProp(propKeyRef); ++ if (currentPropValue == null || !currentPropValue.equals(propValueRef)) ++ { ++ errorReporter.logDebug( ++ "Storage pool \"%s\": Set property \"%s\" = \"%s\"", ++ storPoolObjRef.getName().displayValue, ++ propKeyRef, ++ propValueRef ++ ); ++ propsChangeRef.changeStorPoolProp(storPoolObjRef, propKeyRef, propValueRef); ++ } ++ } ++ + @SuppressWarnings("unchecked") + @Override + public void updateAllocatedSize(VlmProviderObject vlmDataRef) +diff --git a/satellite/src/main/java/com/linbit/linstor/layer/storage/utils/BlockSizeInfo.java b/satellite/src/main/java/com/linbit/linstor/layer/storage/utils/BlockSizeInfo.java +--- a/satellite/src/main/java/com/linbit/linstor/layer/storage/utils/BlockSizeInfo.java ++++ b/satellite/src/main/java/com/linbit/linstor/layer/storage/utils/BlockSizeInfo.java +@@ -3,8 +3,11 @@ package com.linbit.linstor.layer.storage.utils; + import com.linbit.utils.MathUtils; + import com.linbit.utils.SymbolicLinkResolver; + ++import static com.linbit.linstor.layer.storage.BlockSizeConsts.DFLT_OPT_IO_SIZE; + import static com.linbit.linstor.layer.storage.BlockSizeConsts.DFLT_IO_SIZE; ++import static com.linbit.linstor.layer.storage.BlockSizeConsts.MAX_OPT_IO_SIZE; + import static com.linbit.linstor.layer.storage.BlockSizeConsts.MAX_IO_SIZE; ++import static com.linbit.linstor.layer.storage.BlockSizeConsts.MIN_OPT_IO_SIZE; + import static com.linbit.linstor.layer.storage.BlockSizeConsts.MIN_IO_SIZE; + + import java.io.FileInputStream; +@@ -13,6 +16,8 @@ import java.nio.file.Path; + + public class BlockSizeInfo + { ++ private static final int NUMBER_BUFFER_SIZE = 32; ++ + /** + * Determines the blocksize, aka minimum I/O size, for the specified backing storage path. + * +@@ -51,7 +56,7 @@ public class BlockSizeInfo + final Path infoSourceName = blockDevice.getFileName(); + final Path infoSource = Path.of("/sys/block", infoSourceName.toString(), "queue/physical_block_size"); + +- final byte[] data = new byte[32]; ++ final byte[] data = new byte[NUMBER_BUFFER_SIZE]; + try (final FileInputStream fileIn = new FileInputStream(infoSource.toString())) + { + final int readCount = fileIn.read(data); +@@ -75,4 +80,38 @@ public class BlockSizeInfo + } + return blockSize; + } ++ ++ public static long getOptimalIoSize(final Path storageObj) ++ { ++ long optIoSize = DFLT_OPT_IO_SIZE; ++ try ++ { ++ final Path blockDevice = SymbolicLinkResolver.resolveSymLink(storageObj); ++ final Path infoSourceName = blockDevice.getFileName(); ++ final Path infoSource = Path.of("/sys/block", infoSourceName.toString(), "queue/optimal_io_size"); ++ ++ final byte[] data = new byte[NUMBER_BUFFER_SIZE]; ++ try (final FileInputStream fileIn = new FileInputStream(infoSource.toString())) ++ { ++ final int readCount = fileIn.read(data); ++ if (readCount > 0) ++ { ++ String numberStr = new String(data, 0, readCount); ++ numberStr = numberStr.trim(); ++ try ++ { ++ final long unboundedOptIoSize = Long.parseLong(numberStr); ++ optIoSize = MathUtils.bounds(MIN_OPT_IO_SIZE, unboundedOptIoSize, MAX_OPT_IO_SIZE); ++ } ++ catch (NumberFormatException ignored) ++ { ++ } ++ } ++ } ++ } ++ catch (IOException ignored) ++ { ++ } ++ return optIoSize; ++ } + } +diff --git a/server/src/main/java/com/linbit/SizeConv.java b/server/src/main/java/com/linbit/SizeConv.java +--- a/server/src/main/java/com/linbit/SizeConv.java ++++ b/server/src/main/java/com/linbit/SizeConv.java +@@ -16,6 +16,7 @@ public class SizeConv + public enum SizeUnit + { + UNIT_B, ++ UNIT_SECTORS, + UNIT_KiB, + UNIT_MiB, + UNIT_GiB, +@@ -41,6 +42,9 @@ public class SizeConv + case UNIT_B: + factor = FACTOR_B; + break; ++ case UNIT_SECTORS: ++ factor = FACTOR_SECTORS; ++ break; + case UNIT_KiB: + factor = FACTOR_KiB; + break; +@@ -111,6 +115,9 @@ public class SizeConv + case "b": + unit = SizeUnit.UNIT_B; + break; ++ case "s": ++ unit = SizeUnit.UNIT_SECTORS; ++ break; + case "k": + // fall-through + case "kb": +@@ -217,6 +224,9 @@ public class SizeConv + // Factor 1 + public static final BigInteger FACTOR_B = BigInteger.valueOf(1L); + ++ // Factor 512 ++ public static final BigInteger FACTOR_SECTORS = BigInteger.valueOf(512L); ++ + // Factor 1,024 + // Naming convention exception: SI unit capitalization rules + @SuppressWarnings("checkstyle:constantname") +diff --git a/server/src/main/java/com/linbit/linstor/layer/luks/LuksLayerSizeCalculator.java b/server/src/main/java/com/linbit/linstor/layer/luks/LuksLayerSizeCalculator.java +--- a/server/src/main/java/com/linbit/linstor/layer/luks/LuksLayerSizeCalculator.java ++++ b/server/src/main/java/com/linbit/linstor/layer/luks/LuksLayerSizeCalculator.java +@@ -1,27 +1,62 @@ + package com.linbit.linstor.layer.luks; + ++import com.linbit.SizeConv; ++import com.linbit.SizeConv.SizeUnit; + import com.linbit.exceptions.InvalidSizeException; ++import com.linbit.linstor.PriorityProps; ++import com.linbit.linstor.annotation.Nullable; ++import com.linbit.linstor.api.ApiConsts; ++import com.linbit.linstor.core.objects.AbsResource; ++import com.linbit.linstor.core.objects.AbsVolume; ++import com.linbit.linstor.core.objects.Resource; ++import com.linbit.linstor.core.objects.ResourceDefinition; ++import com.linbit.linstor.core.objects.ResourceGroup; ++import com.linbit.linstor.core.objects.Snapshot; ++import com.linbit.linstor.core.objects.SnapshotVolume; ++import com.linbit.linstor.core.objects.StorPool; ++import com.linbit.linstor.core.objects.Volume; ++import com.linbit.linstor.core.objects.VolumeDefinition; + import com.linbit.linstor.dbdrivers.DatabaseException; + import com.linbit.linstor.layer.AbsLayerSizeCalculator; ++import com.linbit.linstor.netcom.Peer; ++import com.linbit.linstor.propscon.InvalidKeyException; ++import com.linbit.linstor.propscon.ReadOnlyProps; + import com.linbit.linstor.security.AccessDeniedException; ++import com.linbit.linstor.storage.StorageConstants; + import com.linbit.linstor.storage.data.adapter.luks.LuksVlmData; + import com.linbit.linstor.storage.interfaces.categories.resource.VlmProviderObject; + import com.linbit.linstor.storage.kinds.DeviceLayerKind; + import com.linbit.linstor.storage.kinds.ExtTools; + import com.linbit.linstor.storage.kinds.ExtToolsInfo; + import com.linbit.linstor.storage.kinds.ExtToolsInfo.Version; ++import com.linbit.linstor.utils.layer.LayerVlmUtils; ++import com.linbit.utils.ShellUtils; ++import com.linbit.utils.SignedAlign; + + import javax.inject.Inject; + import javax.inject.Singleton; + ++import java.util.Iterator; ++import java.util.LinkedList; ++import java.util.List; ++import java.util.Set; ++import java.util.regex.Matcher; ++import java.util.regex.Pattern; ++ + @Singleton + public class LuksLayerSizeCalculator extends AbsLayerSizeCalculator> + { ++ public static final String LUKS2_OPT_METADATA_SIZE = "--luks2-metadata-size"; ++ public static final String LUKS2_OPT_KEYSLOTS_SIZE = "--luks2-keyslots-size"; ++ public static final String LUKS2_OPT_OFFSET = "--offset"; ++ private static final String LUKS2_OPT_ALIGN_PAYLOAD = "--align-payload"; ++ private static final long DFLT_LUKS2_METADATA_SIZE_IN_BYTES = 16L << 10; ++ private static final long DFLT_ALIGNMENT_1MIB_IN_BYTES = 1L << 20; ++ private static final long LUKS1_HEADER_SIZE_IN_KIB = 2L << 10; ++ private static final long LUKS2_HEADER_SIZE_IN_KIB = 16L << 10; ++ private static final long LUKS2_HEADER_SIZE_IN_BYTES = LUKS2_HEADER_SIZE_IN_KIB << 10; + +- // linstor calculates in KiB +- private static final int MIB = 1024; +- private static final int LUKS1_HEADER_SIZE = 2 * MIB; +- private static final int LUKS2_HEADER_SIZE = 16 * MIB; ++ private static final Pattern PATTERN_SIZE = Pattern.compile("(\\d+)([kKmMgGtT]i?[bB]?|[sS]|)"); + + @Inject + public LuksLayerSizeCalculator(AbsLayerSizeCalculatorInit initRef) +@@ -74,30 +109,266 @@ public class LuksLayerSizeCalculator extends AbsLayerSizeCalculator vlmDataRef) +- throws AccessDeniedException ++ throws AccessDeniedException, InvalidSizeException + { +- ExtToolsInfo cryptSetupInfo = vlmDataRef.getRscLayerObject() ++ @Nullable Peer peer = vlmDataRef.getRscLayerObject() + .getAbsResource() + .getNode() +- .getPeer(sysCtx) +- .getExtToolsManager() ++ .getPeer(sysCtx); ++ if (peer == null) ++ { ++ throw new InvalidSizeException( ++ "Could not calculate size of LUKS volume, since cryptsetup's version could not be determined", ++ null ++ ); ++ } ++ ExtToolsInfo cryptSetupInfo = peer.getExtToolsManager() + .getExtToolInfo(ExtTools.CRYPT_SETUP); +- long luksHeaderSize; ++ ++ final long luksHeaderSize; + if (cryptSetupInfo != null && cryptSetupInfo.isSupported()) + { + if (cryptSetupInfo.hasVersionOrHigher(new Version(2, 1))) + { +- luksHeaderSize = LUKS2_HEADER_SIZE; ++ luksHeaderSize = calcLuks2HeaderSize(vlmDataRef); + } + else + { +- luksHeaderSize = LUKS1_HEADER_SIZE; ++ luksHeaderSize = LUKS1_HEADER_SIZE_IN_KIB; + } + } + else + { +- luksHeaderSize = -1; ++ throw new InvalidSizeException( ++ "Could not calculate size of LUKS volume, since cryptsetup's version could not be determined", ++ null ++ ); + } + return luksHeaderSize; + } ++ ++ private long calcLuks2HeaderSize(VlmProviderObject vlmDataRef) throws AccessDeniedException ++ { ++ PriorityProps prioProps = getPrioProps(vlmDataRef); ++ ++ final @Nullable String userOptProp = prioProps.getProp( ++ ApiConsts.KEY_STOR_DRIVER_LUKS_FORMAT_OPTIONS, ++ ApiConsts.NAMESPC_STORAGE_DRIVER ++ ); ++ List userOptions = userOptProp != null ? ++ ShellUtils.shellSplit(userOptProp) : ++ new LinkedList<>(); ++ ++ final long alignedHeaderSizeInBytes; ++ final long unalignedHeaderSizeInBytes; ++ @Nullable Long cryptsetupOffsetInBytes = getLongOptionValue( ++ userOptions, ++ LUKS2_OPT_OFFSET, ++ SizeUnit.UNIT_SECTORS ++ ); ++ if (cryptsetupOffsetInBytes != null) ++ { ++ alignedHeaderSizeInBytes = cryptsetupOffsetInBytes; ++ } ++ else ++ { ++ @Nullable Long cryptsetupLuks2KeyslotsSize = getLongOptionValue( ++ userOptions, ++ LUKS2_OPT_KEYSLOTS_SIZE ++ ); ++ if (cryptsetupLuks2KeyslotsSize == null) ++ { ++ unalignedHeaderSizeInBytes = LUKS2_HEADER_SIZE_IN_BYTES; ++ } ++ else ++ { ++ @Nullable Long cryptsetupLuks2MetadataSize = getLongOptionValue( ++ userOptions, ++ LUKS2_OPT_METADATA_SIZE ++ ); ++ cryptsetupLuks2MetadataSize = cryptsetupLuks2MetadataSize == null ? ++ DFLT_LUKS2_METADATA_SIZE_IN_BYTES : ++ cryptsetupLuks2MetadataSize; ++ ++ unalignedHeaderSizeInBytes = 2 * cryptsetupLuks2MetadataSize + cryptsetupLuks2KeyslotsSize; ++ } ++ ++ long alignment = getAlignment(vlmDataRef, userOptions); ++ alignedHeaderSizeInBytes = new SignedAlign(alignment).ceiling(unalignedHeaderSizeInBytes); ++ } ++ return SizeConv.convert( ++ alignedHeaderSizeInBytes, ++ SizeUnit.UNIT_B, ++ SizeUnit.UNIT_KiB ++ ); ++ } ++ ++ private long getAlignment(VlmProviderObject vlmDataRef, List userOptions) throws AccessDeniedException ++ { ++ @Nullable Long cryptsetupAlignPayloadInBytes = getLongOptionValue( ++ userOptions, ++ LUKS2_OPT_ALIGN_PAYLOAD, ++ SizeUnit.UNIT_SECTORS ++ ); ++ ++ long alignment = DFLT_ALIGNMENT_1MIB_IN_BYTES; ++ if (cryptsetupAlignPayloadInBytes != null) ++ { ++ alignment = cryptsetupAlignPayloadInBytes; ++ } ++ else ++ { ++ final long maxOptIoSize = getMaxOptIoSize(vlmDataRef); ++ alignment = Math.max(alignment, maxOptIoSize); ++ } ++ return alignment; ++ } ++ ++ private long getMaxOptIoSize(VlmProviderObject vlmDataRef) throws InvalidKeyException, AccessDeniedException ++ { ++ long ret = 0; ++ Set storPoolSet = LayerVlmUtils.getStorPoolSet(vlmDataRef, sysCtx); ++ for (StorPool sp : storPoolSet) ++ { ++ @Nullable String strValue = sp.getProps(sysCtx) ++ .getProp( ++ StorageConstants.BLK_DEV_OPT_IO_SIZE, ++ StorageConstants.NAMESPACE_INTERNAL ++ ); ++ if (strValue != null) ++ { ++ try ++ { ++ long parsed = Long.parseLong(strValue); ++ ret = Math.max(parsed, ret); ++ } ++ catch (NumberFormatException ignored) ++ { ++ errorReporter.logWarning( ++ "LuksHeaderSize: Failed to parse '%s' from prop %s. Defaulting to 0 opt_io_size " + ++ "(no recommendation/hint)", ++ strValue, ++ StorageConstants.NAMESPACE_INTERNAL + "/" + StorageConstants.BLK_DEV_OPT_IO_SIZE ++ ); ++ } ++ } ++ } ++ return ret; ++ } ++ ++ private @Nullable Long getLongOptionValue(List userOptionsRef, String optRef) ++ { ++ return getLongOptionValue(userOptionsRef, optRef, SizeUnit.UNIT_B); ++ } ++ ++ @SuppressWarnings("checkstyle:magicnumber") ++ private @Nullable Long getLongOptionValue(List userOptionsRef, String optRef, SizeUnit dfltSizeUnit) ++ { ++ @Nullable Long ret = null; ++ @Nullable String val = findLastValue(userOptionsRef, optRef); ++ if (val != null && !val.isBlank()) ++ { ++ Matcher matcher = PATTERN_SIZE.matcher(val); ++ if (matcher.matches()) ++ { ++ try ++ { ++ ret = Long.parseLong(matcher.group(1)); ++ String unit = matcher.group(2); ++ SizeUnit sizeUnit; ++ if (unit.isBlank()) ++ { ++ sizeUnit = dfltSizeUnit; ++ } ++ else ++ { ++ boolean forcePowerOfTwo = unit.length() == 1 || unit.length() == 3; ++ sizeUnit = SizeUnit.parse(unit, forcePowerOfTwo); ++ } ++ ++ ret = SizeConv.convert(ret, sizeUnit, SizeUnit.UNIT_B); ++ } ++ catch (NumberFormatException ignored) ++ { ++ errorReporter.logWarning( ++ "LuksHeaderSize: Failed to parse '%s' from option '%s %s'.", ++ matcher.group(1), ++ optRef, ++ val ++ ); ++ } ++ } ++ } ++ return ret; ++ } ++ ++ private @Nullable String findLastValue(List userOptionsRef, String optRef) ++ { ++ Iterator it = userOptionsRef.iterator(); ++ @Nullable String val = null; ++ while (it.hasNext()) ++ { ++ String opt = it.next(); ++ if (opt.equals(optRef)) ++ { ++ if (it.hasNext()) ++ { ++ val = it.next(); ++ } ++ else ++ { ++ val = null; ++ } ++ } ++ else if (opt.startsWith(optRef + "=")) ++ { ++ val = opt.substring(optRef.length() + 1); ++ if (val.isBlank()) ++ { ++ val = null; ++ } ++ } ++ } ++ return val; ++ } ++ ++ private PriorityProps getPrioProps(VlmProviderObject vlmDataRef) throws AccessDeniedException ++ { ++ final AbsVolume vlm = vlmDataRef.getVolume(); ++ final AbsResource rsc = vlm.getAbsResource(); ++ final ResourceDefinition rscDfn = vlm.getResourceDefinition(); ++ final VolumeDefinition vlmDfn = vlm.getVolumeDefinition(); ++ final ResourceGroup rscGrp = rscDfn.getResourceGroup(); ++ ++ final ReadOnlyProps vlmProps; ++ final ReadOnlyProps rscProps; ++ if (vlm instanceof Volume) ++ { ++ vlmProps = ((Volume) vlm).getProps(sysCtx); ++ rscProps = ((Resource) rsc).getProps(sysCtx); ++ } ++ else ++ { ++ vlmProps = ((SnapshotVolume) vlm).getVlmProps(sysCtx); ++ rscProps = ((Snapshot) rsc).getRscProps(sysCtx); ++ } ++ ++ final PriorityProps prioProps = new PriorityProps( ++ vlmProps, ++ rscProps ++ ); ++ for (StorPool storPool : LayerVlmUtils.getStorPoolSet(vlmDataRef, sysCtx)) ++ { ++ prioProps.addProps(storPool.getProps(sysCtx)); ++ } ++ prioProps.addProps( ++ rsc.getNode().getProps(sysCtx), ++ vlmDfn.getProps(sysCtx), ++ rscDfn.getProps(sysCtx), ++ rscGrp.getVolumeGroupProps(sysCtx, vlmDfn.getVolumeNumber()), ++ rscGrp.getProps(sysCtx), ++ stltProps ++ ); ++ return prioProps; ++ } + } +diff --git a/server/src/main/java/com/linbit/linstor/layer/storage/BlockSizeConsts.java b/server/src/main/java/com/linbit/linstor/layer/storage/BlockSizeConsts.java +--- a/server/src/main/java/com/linbit/linstor/layer/storage/BlockSizeConsts.java ++++ b/server/src/main/java/com/linbit/linstor/layer/storage/BlockSizeConsts.java +@@ -13,4 +13,9 @@ public class BlockSizeConsts + + // Default value for the minimum_io_size value of non-storage layers + public static final long DFLT_SPECIAL_IO_SIZE = (1L << 12); ++ ++ // optimal_io_size may be 0 to indicate no recommendation. ++ public static final long MIN_OPT_IO_SIZE = 0; ++ public static final long DFLT_OPT_IO_SIZE = 0; ++ public static final long MAX_OPT_IO_SIZE = Long.MAX_VALUE; + } +diff --git a/server/src/main/java/com/linbit/linstor/storage/StorageConstants.java b/server/src/main/java/com/linbit/linstor/storage/StorageConstants.java +--- a/server/src/main/java/com/linbit/linstor/storage/StorageConstants.java ++++ b/server/src/main/java/com/linbit/linstor/storage/StorageConstants.java +@@ -11,6 +11,7 @@ public class StorageConstants + public static final String NAMESPACE_NVME = ApiConsts.NAMESPC_STORAGE_DRIVER + "/NVME"; + public static final String NAMESPACE_INTERNAL = NAMESPACE_STOR_DRIVER + "/internal/"; + ++ public static final String BLK_DEV_OPT_IO_SIZE = "optIoSize"; + public static final String BLK_DEV_MIN_IO_SIZE = "minIoSize"; + public static final String BLK_DEV_MIN_IO_SIZE_AUTO = "minIoSizeAuto"; + public static final String BLK_DEV_MAX_BIO_SIZE = "maxBioSize"; diff --git a/packages/system/linstor/images/piraeus-server/patches/force-metadata-check-on-disk-add.diff b/packages/system/linstor/images/piraeus-server/patches/force-metadata-check-on-disk-add.diff deleted file mode 100644 index 0c413005..00000000 --- a/packages/system/linstor/images/piraeus-server/patches/force-metadata-check-on-disk-add.diff +++ /dev/null @@ -1,63 +0,0 @@ -diff --git a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java -index a302ee835..01967a31f 100644 ---- a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java -+++ b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java -@@ -371,10 +371,13 @@ public class DrbdLayer implements DeviceLayer - boolean isDiskless = drbdRscData.getAbsResource().isDrbdDiskless(workerCtx); - StateFlags rscFlags = drbdRscData.getAbsResource().getStateFlags(); - boolean isDiskRemoving = rscFlags.isSet(workerCtx, Resource.Flags.DISK_REMOVING); -+ // Check if we're in toggle-disk operation (adding disk to diskless resource) -+ boolean isDiskAdding = rscFlags.isSomeSet(workerCtx, Resource.Flags.DISK_ADD_REQUESTED, Resource.Flags.DISK_ADDING); - - boolean contProcess = isDiskless; - -- boolean processChildren = !isDiskless || isDiskRemoving; -+ // Process children when: has disk, removing disk, OR adding disk (toggle-disk) -+ boolean processChildren = !isDiskless || isDiskRemoving || isDiskAdding; - // do not process children when ONLY DRBD_DELETE flag is set (DELETE flag is still unset) - processChildren &= (!rscFlags.isSet(workerCtx, Resource.Flags.DRBD_DELETE) || - rscFlags.isSet(workerCtx, Resource.Flags.DELETE)); -@@ -570,7 +573,11 @@ public class DrbdLayer implements DeviceLayer - { - // hasMetaData needs to be run after child-resource processed - List> createMetaData = new ArrayList<>(); -- if (!drbdRscData.getAbsResource().isDrbdDiskless(workerCtx) && !skipDisk) -+ // Check if we're in toggle-disk operation (adding disk to diskless resource) -+ boolean isDiskAddingForMd = drbdRscData.getAbsResource().getStateFlags() -+ .isSomeSet(workerCtx, Resource.Flags.DISK_ADD_REQUESTED, Resource.Flags.DISK_ADDING); -+ // Create metadata when: has disk OR adding disk (toggle-disk), and skipDisk is disabled -+ if ((!drbdRscData.getAbsResource().isDrbdDiskless(workerCtx) || isDiskAddingForMd) && !skipDisk) - { - // do not try to create meta data while the resource is diskless or skipDisk is enabled - for (DrbdVlmData drbdVlmData : checkMetaData) -@@ -988,8 +995,10 @@ public class DrbdLayer implements DeviceLayer - { - List> checkMetaData = new ArrayList<>(); - Resource rsc = drbdRscData.getAbsResource(); -+ // Include DISK_ADD_REQUESTED/DISK_ADDING for toggle-disk scenario where we need to check/create metadata - if (!rsc.isDrbdDiskless(workerCtx) || -- rsc.getStateFlags().isSet(workerCtx, Resource.Flags.DISK_REMOVING) -+ rsc.getStateFlags().isSet(workerCtx, Resource.Flags.DISK_REMOVING) || -+ rsc.getStateFlags().isSomeSet(workerCtx, Resource.Flags.DISK_ADD_REQUESTED, Resource.Flags.DISK_ADDING) - ) - { - // using a dedicated list to prevent concurrentModificationException -@@ -1177,9 +1186,16 @@ public class DrbdLayer implements DeviceLayer - - boolean hasMetaData; - -+ // Check if we need to verify/create metadata -+ // Force metadata check when: -+ // 1. checkMetaData is enabled -+ // 2. volume doesn't have disk yet (diskless -> diskful transition) -+ // 3. DISK_ADD_REQUESTED/DISK_ADDING flag is set (retry scenario where storage exists but no metadata) -+ boolean isDiskAddingState = drbdVlmData.getRscLayerObject().getAbsResource().getStateFlags() -+ .isSomeSet(workerCtx, Resource.Flags.DISK_ADD_REQUESTED, Resource.Flags.DISK_ADDING); - if (drbdVlmData.checkMetaData() || -- // when adding a disk, DRBD believes that it is diskless but we still need to create metadata -- !drbdVlmData.hasDisk()) -+ !drbdVlmData.hasDisk() || -+ isDiskAddingState) - { - if (mdUtils.hasMetaData()) - { diff --git a/packages/system/linstor/images/piraeus-server/patches/retry-adjust-after-stale-bitmap.diff b/packages/system/linstor/images/piraeus-server/patches/retry-adjust-after-stale-bitmap.diff new file mode 100644 index 00000000..00959167 --- /dev/null +++ b/packages/system/linstor/images/piraeus-server/patches/retry-adjust-after-stale-bitmap.diff @@ -0,0 +1,141 @@ +diff --git a/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/DrbdAdm.java b/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/DrbdAdm.java +index 5627d1be8..ece191292 100644 +--- a/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/DrbdAdm.java ++++ b/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/DrbdAdm.java +@@ -42,6 +42,9 @@ import java.util.Arrays; + import java.util.List; + import java.util.concurrent.ArrayBlockingQueue; + import java.util.concurrent.TimeUnit; ++import java.nio.charset.StandardCharsets; ++import java.util.regex.Matcher; ++import java.util.regex.Pattern; + import java.util.stream.Collectors; + + @Singleton +@@ -56,6 +58,9 @@ public class DrbdAdm + + public static final int WAIT_CONNECT_RES_TIME = 10; + private static final long DOWN_WAIT_TIMEOUT_SEC = 5; ++ private static final long FORCE_DETACH_RETRY_WAIT_MS = 250; ++ private static final String BITMAP_LEAK_ERR_MSG = "already has a bitmap, this should not happen"; ++ private static final Pattern BITMAP_LEAK_MINOR_PATTERN = Pattern.compile("\\bminor\\s+(\\d+)\\b"); + + private final ExtCmdFactory extCmdFactory; + private final AccessContext sysCtx; +@@ -131,8 +136,38 @@ public class DrbdAdm + // command.add(resName); + command.add(drbdRscData.getSuffixedResourceName()); + // execute(Arrays.asList("drbdsetup", "show", drbdRscData.getSuffixedResourceName())); +- execute(command); +- // execute(Arrays.asList("drbdsetup", "show", drbdRscData.getSuffixedResourceName())); ++ String[] commandArr = command.toArray(new String[0]); ++ try ++ { ++ File nullDevice = new File(Platform.nullDevice()); ++ ExtCmd extCmd = extCmdFactory.create(); ++ if (Platform.isWindows()) ++ { ++ extCmd.setTimeout(TimeoutType.WAIT, 5 * 60 * 1000); ++ } ++ ++ OutputData outputData = extCmd.pipeExec(ProcessBuilder.Redirect.from(nullDevice), commandArr); ++ if ( ++ outputData.exitCode != 0 && ++ isBitmapLeakOnAttach(outputData) && ++ cleanupStaleBitmapAndRetry(extCmd, nullDevice, outputData) ++ ) ++ { ++ outputData = extCmd.pipeExec(ProcessBuilder.Redirect.from(nullDevice), commandArr); ++ } ++ if (outputData.exitCode != 0) ++ { ++ throw new ExtCmdFailedException(commandArr, outputData); ++ } ++ } ++ catch (ChildProcessTimeoutException timeoutExc) ++ { ++ throw new ExtCmdFailedException(commandArr, timeoutExc); ++ } ++ catch (IOException ioExc) ++ { ++ throw new ExtCmdFailedException(commandArr, ioExc); ++ } + + drbdRscData.setAdjustRequired(false); + } +@@ -805,6 +840,75 @@ public class DrbdAdm + } + } + ++ static boolean isBitmapLeakOnAttach(OutputData outputData) ++ { ++ return extractBitmapLeakMinor(outputData) != null; ++ } ++ ++ static @Nullable Integer extractBitmapLeakMinor(OutputData outputData) ++ { ++ String stderr = new String(outputData.stderrData, StandardCharsets.UTF_8); ++ if (!stderr.contains(BITMAP_LEAK_ERR_MSG)) ++ { ++ return null; ++ } ++ ++ Matcher matcher = BITMAP_LEAK_MINOR_PATTERN.matcher(stderr); ++ if (!matcher.find()) ++ { ++ return null; ++ } ++ ++ return Integer.parseInt(matcher.group(1)); ++ } ++ ++ private boolean cleanupStaleBitmapAndRetry( ++ ExtCmd extCmd, ++ File nullDevice, ++ OutputData outputData ++ ) ++ throws IOException, ChildProcessTimeoutException, ExtCmdFailedException ++ { ++ @Nullable Integer minor = extractBitmapLeakMinor(outputData); ++ if (minor == null) ++ { ++ return false; ++ } ++ ++ OutputData detachOut = extCmd.pipeExec( ++ ProcessBuilder.Redirect.from(nullDevice), ++ DRBDSETUP_UTIL, ++ "detach", ++ Integer.toString(minor) ++ ); ++ if (detachOut.exitCode == 0) ++ { ++ return true; ++ } ++ ++ OutputData forceDetachOut = extCmd.pipeExec( ++ ProcessBuilder.Redirect.from(nullDevice), ++ DRBDSETUP_UTIL, ++ "detach", ++ Integer.toString(minor), ++ "--force" ++ ); ++ if (forceDetachOut.exitCode != 0) ++ { ++ throw new ExtCmdFailedException(forceDetachOut.executedCommand, forceDetachOut); ++ } ++ ++ try ++ { ++ Thread.sleep(FORCE_DETACH_RETRY_WAIT_MS); ++ } ++ catch (InterruptedException ignored) ++ { ++ Thread.currentThread().interrupt(); ++ } ++ return true; ++ } ++ + public static class DrbdPrimary implements AutoCloseable + { + private final DrbdAdm drbdAdm; diff --git a/packages/system/linstor/images/piraeus-server/patches/skip-adjust-when-device-inaccessible.diff b/packages/system/linstor/images/piraeus-server/patches/skip-adjust-when-device-inaccessible.diff deleted file mode 100644 index a93d7811..00000000 --- a/packages/system/linstor/images/piraeus-server/patches/skip-adjust-when-device-inaccessible.diff +++ /dev/null @@ -1,155 +0,0 @@ -diff --git a/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java b/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java -index 49138a8fd..2f768ca0d 100644 ---- a/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java -+++ b/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java -@@ -83,6 +83,8 @@ import java.util.TreeMap; - import java.util.TreeSet; - import java.util.concurrent.atomic.AtomicBoolean; - import java.util.function.Function; -+import java.nio.file.Files; -+import java.nio.file.Paths; - - @Singleton - public class DeviceHandlerImpl implements DeviceHandler -@@ -1646,7 +1648,10 @@ public class DeviceHandlerImpl implements DeviceHandler - private void updateDiscGran(VlmProviderObject vlmData) throws DatabaseException, StorageException - { - String devicePath = vlmData.getDevicePath(); -- if (devicePath != null && vlmData.exists()) -+ // Check if device path physically exists before calling lsblk -+ // This is important for DRBD devices which might be temporarily unavailable during adjust -+ // (drbdadm adjust brings devices down/up, and kernel might not have created the device node yet) -+ if (devicePath != null && vlmData.exists() && Files.exists(Paths.get(devicePath))) - { - if (vlmData.getDiscGran() == VlmProviderObject.UNINITIALIZED_SIZE) - { -diff --git a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java -index 01967a31f..78b8195a4 100644 ---- a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java -+++ b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java -@@ -592,7 +592,29 @@ public class DrbdLayer implements DeviceLayer - // The .res file might not have been generated in the prepare method since it was - // missing information from the child-layers. Now that we have processed them, we - // need to make sure the .res file exists in all circumstances. -- regenerateResFile(drbdRscData); -+ // However, if the underlying devices are not accessible (e.g., LUKS device is closed -+ // during resource deletion), we skip regenerating the res file to avoid errors -+ boolean canRegenerateResFile = true; -+ if (!skipDisk && !drbdRscData.getAbsResource().isDrbdDiskless(workerCtx)) -+ { -+ AbsRscLayerObject dataChild = drbdRscData.getChildBySuffix(RscLayerSuffixes.SUFFIX_DATA); -+ if (dataChild != null) -+ { -+ for (DrbdVlmData drbdVlmData : drbdRscData.getVlmLayerObjects().values()) -+ { -+ VlmProviderObject childVlm = dataChild.getVlmProviderObject(drbdVlmData.getVlmNr()); -+ if (childVlm == null || !childVlm.exists() || childVlm.getDevicePath() == null) -+ { -+ canRegenerateResFile = false; -+ break; -+ } -+ } -+ } -+ } -+ if (canRegenerateResFile) -+ { -+ regenerateResFile(drbdRscData); -+ } - - // createMetaData needs rendered resFile - for (DrbdVlmData drbdVlmData : createMetaData) -@@ -766,19 +788,72 @@ public class DrbdLayer implements DeviceLayer - - if (drbdRscData.isAdjustRequired()) - { -- try -+ // Check if underlying devices are accessible before adjusting -+ // This is important for encrypted resources (LUKS) where the device -+ // might be closed during deletion -+ boolean canAdjust = true; -+ -+ // IMPORTANT: Check child volumes only when disk access is actually needed. -+ // For network reconnect (StandAlone -> Connected), disk access is not required. -+ boolean needsDiskAccess = false; -+ -+ // Check if there are pending operations that require disk access -+ for (DrbdVlmData drbdVlmData : drbdRscData.getVlmLayerObjects().values()) - { -- drbdUtils.adjust( -- drbdRscData, -- false, -- skipDisk, -- false -- ); -+ Volume vlm = (Volume) drbdVlmData.getVolume(); -+ StateFlags vlmFlags = vlm.getFlags(); -+ -+ // Disk access is needed if: -+ // - creating a new volume -+ // - resizing -+ // - checking/creating metadata -+ if (!drbdVlmData.exists() || -+ drbdVlmData.checkMetaData() || -+ vlmFlags.isSomeSet(workerCtx, Volume.Flags.RESIZE, Volume.Flags.DRBD_RESIZE)) -+ { -+ needsDiskAccess = true; -+ break; -+ } -+ } -+ -+ // Check child volumes only if disk access is actually needed -+ if (needsDiskAccess && !skipDisk && !drbdRscData.getAbsResource().isDrbdDiskless(workerCtx)) -+ { -+ AbsRscLayerObject dataChild = drbdRscData.getChildBySuffix(RscLayerSuffixes.SUFFIX_DATA); -+ if (dataChild != null) -+ { -+ for (DrbdVlmData drbdVlmData : drbdRscData.getVlmLayerObjects().values()) -+ { -+ VlmProviderObject childVlm = dataChild.getVlmProviderObject(drbdVlmData.getVlmNr()); -+ if (childVlm == null || !childVlm.exists() || childVlm.getDevicePath() == null) -+ { -+ canAdjust = false; -+ break; -+ } -+ } -+ } - } -- catch (ExtCmdFailedException extCmdExc) -+ -+ if (canAdjust) -+ { -+ try -+ { -+ drbdUtils.adjust( -+ drbdRscData, -+ false, -+ skipDisk, -+ false -+ ); -+ } -+ catch (ExtCmdFailedException extCmdExc) -+ { -+ restoreBackupResFile(drbdRscData); -+ throw extCmdExc; -+ } -+ } -+ else - { -- restoreBackupResFile(drbdRscData); -- throw extCmdExc; -+ drbdRscData.setAdjustRequired(false); - } - } - -diff --git a/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java b/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java -index cdca0b6d2..89c8be9da 100644 ---- a/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java -+++ b/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java -@@ -383,6 +383,7 @@ public class LuksLayer implements DeviceLayer - vlmData.setSizeState(Size.AS_EXPECTED); - - vlmData.setOpened(true); -+ vlmData.setExists(true); - vlmData.setFailed(false); - } - }