Skip to content

Commit d301fe2

Browse files
committed
linstor: grow cloned volumes inside the clone instead of resizing afterwards
Deploying a VM with a root disk larger than its template failed with Cannot resize volume, because we have a non-UpToDate DRBD device. whenever the resource group has Clone/BalanceAfterClone=true. The clone reports COMPLETE as soon as every replica can access UpToDate data, which is while the additional balance replica is still doing its initial sync; the resize that followed CloneWaiter.waitFor() was rejected by LINSTOR's all-replicas-UpToDate precheck. Waiting for the sync client-side would block the deploy for the whole sync of the template size, so the fix is in LINSTOR: REST API 1.29.1 (LINSTOR 1.35.0) accepts volume_sizes on the clone request and grows the volume inside the clone flux before the balance placement. Pass the requested size that way when the controller supports it, detected once per controller URL via its REST API version, and skip the post-clone resize. Older controllers keep the clone-then-resize sequence unchanged.
1 parent 7414b05 commit d301fe2

4 files changed

Lines changed: 168 additions & 1 deletion

File tree

plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImpl.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,29 @@ private void deleteTemplateForProps(
403403
}
404404
}
405405

406+
/**
407+
* Request the target size as part of the clone if the controller supports it.
408+
*
409+
* A clone reports COMPLETE as soon as every replica can access UpToDate data, which with
410+
* Clone/BalanceAfterClone is while the additional replica is still syncing. A resize issued at that
411+
* point is rejected with "Cannot resize volume, because we have a non-UpToDate DRBD device".
412+
* Controllers with REST API 1.29.1+ take the size in the clone request and grow the volume inside the
413+
* clone before the balance placement, so the race cannot occur. Older controllers keep the
414+
* clone-then-resize sequence.
415+
*
416+
* @return true if the caller still has to resize the resource after the clone finished
417+
*/
418+
static boolean applyCloneSize(DevelopersApi api, ResourceDefinitionCloneRequest cloneRequest, Long sizeByte) {
419+
if (sizeByte == null || sizeByte <= 0) {
420+
return false;
421+
}
422+
if (LinstorUtil.supportsCloneVolumeSizes(api)) {
423+
cloneRequest.setVolumeSizes(Collections.singletonList(sizeByte / 1024));
424+
return false;
425+
}
426+
return true;
427+
}
428+
406429
private String cloneResource(long csCloneId, VolumeInfo volumeInfo, StoragePoolVO storagePoolVO) {
407430
// get the cached template on this storage
408431
VMTemplateStoragePoolVO tmplPoolRef = _vmTemplatePoolDao.findByPoolTemplate(
@@ -436,6 +459,7 @@ private String cloneResource(long csCloneId, VolumeInfo volumeInfo, StoragePoolV
436459
cloneRequest.setVolumePassphrases(Collections.singletonList(utf8Passphrase));
437460
}
438461
}
462+
final boolean resizeAfterClone = applyCloneSize(linstorApi, cloneRequest, volumeInfo.getSize());
439463
ResourceDefinitionCloneStarted cloneStarted = linstorApi.resourceDefinitionClone(
440464
cloneRes, cloneRequest);
441465

@@ -447,7 +471,7 @@ private String cloneResource(long csCloneId, VolumeInfo volumeInfo, StoragePoolV
447471

448472
logger.info("Clone resource definition " + cloneRes + " to " + rscName + " finished");
449473

450-
if (volumeInfo.getSize() != null && volumeInfo.getSize() > 0) {
474+
if (resizeAfterClone) {
451475
resizeResource(linstorApi, rscName, volumeInfo.getSize());
452476
}
453477

plugins/storage/volume/linstor/src/main/java/org/apache/cloudstack/storage/datastore/util/LinstorUtil.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,50 @@ public static DevelopersApi getLinstorAPI(String linstorUrl, String apiToken, bo
101101
return new DevelopersApi(client);
102102
}
103103

104+
/**
105+
* The REST API version of the connected controller, e.g. "1.29.1", or null if it could not be queried.
106+
*/
107+
@Nullable
108+
public static String getRestApiVersion(DevelopersApi api) {
109+
try {
110+
return api.controllerVersion().getRestApiVersion();
111+
} catch (ApiException apiExc) {
112+
LOGGER.warn("Unable to query controller API version: {}", apiExc.getBestMessage());
113+
return null;
114+
}
115+
}
116+
117+
/**
118+
* Check if the connected controller accepts volume_sizes on a resource-definition clone request
119+
* (REST API 1.29.1, LINSTOR 1.35.0). With it the grow happens inside the clone, before an optional
120+
* Clone/BalanceAfterClone placement, so it cannot race the balance replica's sync.
121+
*/
122+
public static boolean supportsCloneVolumeSizes(DevelopersApi api) {
123+
return isVersionAtLeast(getRestApiVersion(api), 1, 29, 1);
124+
}
125+
126+
static boolean isVersionAtLeast(String version, int major, int minor, int patch) {
127+
if (version == null || version.isEmpty()) {
128+
return false;
129+
}
130+
String[] parts = version.split("\\.");
131+
try {
132+
int maj = Integer.parseInt(parts[0]);
133+
int min = parts.length > 1 ? Integer.parseInt(parts[1]) : 0;
134+
int pat = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
135+
if (maj != major) {
136+
return maj > major;
137+
}
138+
if (min != minor) {
139+
return min > minor;
140+
}
141+
return pat >= patch;
142+
} catch (NumberFormatException nfExc) {
143+
LOGGER.warn("Unable to parse controller API version '{}'", version);
144+
return false;
145+
}
146+
}
147+
104148
public static String getBestErrorMessage(ApiCallRcList answers) {
105149
return answers != null && !answers.isEmpty() ?
106150
answers.stream()

plugins/storage/volume/linstor/src/test/java/org/apache/cloudstack/storage/datastore/driver/LinstorPrimaryDataStoreDriverImplTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import com.linbit.linstor.api.ApiException;
2020
import com.linbit.linstor.api.DevelopersApi;
2121
import com.linbit.linstor.api.model.AutoSelectFilter;
22+
import com.linbit.linstor.api.model.ControllerVersion;
2223
import com.linbit.linstor.api.model.LayerType;
24+
import com.linbit.linstor.api.model.ResourceDefinitionCloneRequest;
2325
import com.linbit.linstor.api.model.ResourceGroup;
2426

2527
import java.util.Arrays;
@@ -35,6 +37,8 @@
3537
import org.mockito.junit.MockitoJUnitRunner;
3638

3739
import static org.mockito.Mockito.mock;
40+
import static org.mockito.Mockito.never;
41+
import static org.mockito.Mockito.verify;
3842
import static org.mockito.Mockito.when;
3943

4044
@RunWith(MockitoJUnitRunner.class)
@@ -85,4 +89,45 @@ public void testGetEncryptedLayerList() throws ApiException {
8589
layers = LinstorUtil.getEncryptedLayerList(api, "EncryptedGrp");
8690
Assert.assertEquals(Arrays.asList(LayerType.DRBD, LayerType.LUKS, LayerType.STORAGE), layers);
8791
}
92+
93+
private DevelopersApi mockApiWithRestVersion(String restApiVersion) throws ApiException {
94+
DevelopersApi apiMock = mock(DevelopersApi.class);
95+
ControllerVersion version = new ControllerVersion();
96+
version.setRestApiVersion(restApiVersion);
97+
when(apiMock.controllerVersion()).thenReturn(version);
98+
return apiMock;
99+
}
100+
101+
@Test
102+
public void testApplyCloneSizeNewController() throws ApiException {
103+
DevelopersApi newCtrl = mockApiWithRestVersion("1.29.1");
104+
ResourceDefinitionCloneRequest req = new ResourceDefinitionCloneRequest();
105+
106+
boolean resizeAfter = LinstorPrimaryDataStoreDriverImpl.applyCloneSize(newCtrl, req, 40L * 1024 * 1024 * 1024);
107+
108+
Assert.assertFalse(resizeAfter);
109+
Assert.assertEquals(Collections.singletonList(40L * 1024 * 1024), req.getVolumeSizes());
110+
}
111+
112+
@Test
113+
public void testApplyCloneSizeOldController() throws ApiException {
114+
DevelopersApi oldCtrl = mockApiWithRestVersion("1.28.0");
115+
ResourceDefinitionCloneRequest req = new ResourceDefinitionCloneRequest();
116+
117+
boolean resizeAfter = LinstorPrimaryDataStoreDriverImpl.applyCloneSize(oldCtrl, req, 40L * 1024 * 1024 * 1024);
118+
119+
Assert.assertTrue(resizeAfter);
120+
Assert.assertNull(req.getVolumeSizes());
121+
}
122+
123+
@Test
124+
public void testApplyCloneSizeWithoutSize() throws ApiException {
125+
DevelopersApi newCtrl = mockApiWithRestVersion("1.29.1");
126+
ResourceDefinitionCloneRequest req = new ResourceDefinitionCloneRequest();
127+
128+
Assert.assertFalse(LinstorPrimaryDataStoreDriverImpl.applyCloneSize(newCtrl, req, null));
129+
Assert.assertFalse(LinstorPrimaryDataStoreDriverImpl.applyCloneSize(newCtrl, req, 0L));
130+
Assert.assertNull(req.getVolumeSizes());
131+
verify(newCtrl, never()).controllerVersion();
132+
}
88133
}

plugins/storage/volume/linstor/src/test/java/org/apache/cloudstack/storage/datastore/util/LinstorUtilTest.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.linbit.linstor.api.ApiException;
2020
import com.linbit.linstor.api.DevelopersApi;
2121
import com.linbit.linstor.api.model.AutoSelectFilter;
22+
import com.linbit.linstor.api.model.ControllerVersion;
2223
import com.linbit.linstor.api.model.Node;
2324
import com.linbit.linstor.api.model.Properties;
2425
import com.linbit.linstor.api.model.ProviderKind;
@@ -124,4 +125,57 @@ public void testGetRscGroupStoragePools() throws ApiException {
124125
.collect(Collectors.toList());
125126
Assert.assertEquals(names, Arrays.asList("nodeA::thinpool", "nodeB::thinpool", "nodeC::thinpool"));
126127
}
128+
129+
@Test
130+
public void testIsVersionAtLeast() {
131+
Assert.assertTrue(LinstorUtil.isVersionAtLeast("1.29.1", 1, 29, 1));
132+
Assert.assertTrue(LinstorUtil.isVersionAtLeast("1.29.2", 1, 29, 1));
133+
Assert.assertTrue(LinstorUtil.isVersionAtLeast("1.30.0", 1, 29, 1));
134+
Assert.assertTrue(LinstorUtil.isVersionAtLeast("2.0.0", 1, 29, 1));
135+
Assert.assertTrue(LinstorUtil.isVersionAtLeast("1.30", 1, 29, 1));
136+
137+
Assert.assertFalse(LinstorUtil.isVersionAtLeast("1.29.0", 1, 29, 1));
138+
Assert.assertFalse(LinstorUtil.isVersionAtLeast("1.29", 1, 29, 1));
139+
Assert.assertFalse(LinstorUtil.isVersionAtLeast("1.28.5", 1, 29, 1));
140+
Assert.assertFalse(LinstorUtil.isVersionAtLeast("0.99.9", 1, 29, 1));
141+
Assert.assertFalse(LinstorUtil.isVersionAtLeast(null, 1, 29, 1));
142+
Assert.assertFalse(LinstorUtil.isVersionAtLeast("", 1, 29, 1));
143+
Assert.assertFalse(LinstorUtil.isVersionAtLeast("garbage", 1, 29, 1));
144+
}
145+
146+
private DevelopersApi mockApi() {
147+
return mock(DevelopersApi.class);
148+
}
149+
150+
private ControllerVersion controllerVersion(String restApiVersion) {
151+
ControllerVersion version = new ControllerVersion();
152+
version.setRestApiVersion(restApiVersion);
153+
return version;
154+
}
155+
156+
@Test
157+
public void testGetRestApiVersion() throws ApiException {
158+
DevelopersApi ctrl = mockApi();
159+
when(ctrl.controllerVersion()).thenReturn(controllerVersion("1.29.1"));
160+
Assert.assertEquals("1.29.1", LinstorUtil.getRestApiVersion(ctrl));
161+
162+
DevelopersApi down = mockApi();
163+
when(down.controllerVersion()).thenThrow(new ApiException(503, "unavailable"));
164+
Assert.assertNull(LinstorUtil.getRestApiVersion(down));
165+
}
166+
167+
@Test
168+
public void testSupportsCloneVolumeSizes() throws ApiException {
169+
DevelopersApi newCtrl = mockApi();
170+
when(newCtrl.controllerVersion()).thenReturn(controllerVersion("1.29.1"));
171+
Assert.assertTrue(LinstorUtil.supportsCloneVolumeSizes(newCtrl));
172+
173+
DevelopersApi oldCtrl = mockApi();
174+
when(oldCtrl.controllerVersion()).thenReturn(controllerVersion("1.29.0"));
175+
Assert.assertFalse(LinstorUtil.supportsCloneVolumeSizes(oldCtrl));
176+
177+
DevelopersApi unreachable = mockApi();
178+
when(unreachable.controllerVersion()).thenThrow(new ApiException(503, "unavailable"));
179+
Assert.assertFalse(LinstorUtil.supportsCloneVolumeSizes(unreachable));
180+
}
127181
}

0 commit comments

Comments
 (0)