Skip to content

Commit eaffcd7

Browse files
author
Pearl Dsilva
committed
Add test, and propagate the modification of URL of primary store to the hosts only for nfs and gluster
1 parent 3bae392 commit eaffcd7

4 files changed

Lines changed: 288 additions & 16 deletions

File tree

api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateStoragePoolCmd.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,12 @@ public class UpdateStoragePoolCmd extends BaseCmd {
7373
@Parameter(name = ApiConstants.URL,
7474
type = CommandType.STRING,
7575
required = false,
76-
description = "the URL of the storage pool",
76+
description = "the URL of the storage pool. Supported only for NFS and Gluster pool type." +
77+
"The pool must be in Maintenance mode before changing the URL. WARNING: use this parameter" +
78+
"with caution. It is intended for failover scenarios where the storage content is already " +
79+
"fully mirrored at the new location. Pointing to a new location without ensuring complete " +
80+
"data parity will result in data loss or corruption. After the URL is updated, the new mount" +
81+
"is applied to all connected hosts or restart the Management server",
7782
since = "4.19.0")
7883
private String url;
7984

plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,39 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri
766766
path = path.substring(0, path.length() - 1);
767767
}
768768

769+
// For NFS and Gluster pools, if the existing pool's source host or path has changed
770+
// (e.g. after a storage URL update), destroy and undefine it so it gets recreated
771+
// with the new source below. Restricted to NFS/Gluster only.
772+
if (sp != null && (type == StoragePoolType.NetworkFilesystem || type == StoragePoolType.Gluster)) {
773+
try {
774+
LibvirtStoragePoolDef existingDef = getStoragePoolDef(conn, sp);
775+
if (existingDef != null) {
776+
String existingSourceHost = existingDef.getSourceHost();
777+
String existingSourceDir = existingDef.getSourceDir();
778+
boolean hostChanged = host != null && existingSourceHost != null && !existingSourceHost.equals(host);
779+
boolean pathChanged = path != null && existingSourceDir != null && !existingSourceDir.equals(path);
780+
if (hostChanged || pathChanged) {
781+
logger.info("Storage pool {} source has changed (host: {} -> {}, path: {} -> {}); destroying and redefining.",
782+
name, existingSourceHost, host, existingSourceDir, path);
783+
try {
784+
if (sp.isPersistent() == 1) {
785+
sp.destroy();
786+
sp.undefine();
787+
} else {
788+
sp.destroy();
789+
}
790+
sp.free();
791+
sp = null;
792+
} catch (LibvirtException destroyEx) {
793+
logger.error("Failed to destroy storage pool {} with changed source; will attempt to reuse existing pool: {}", name, destroyEx.getMessage());
794+
}
795+
}
796+
}
797+
} catch (LibvirtException e) {
798+
logger.warn("Failed to check existing storage pool {} source path, will attempt to reuse it: {}", name, e.getMessage());
799+
}
800+
}
801+
769802
if (sp == null) {
770803
// see if any existing pool by another name is using our storage path.
771804
// if anyone is, undefine the pool so we can define it as requested.

plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptorTest.java

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,13 @@
4242
import com.cloud.hypervisor.kvm.resource.LibvirtConnection;
4343
import com.cloud.hypervisor.kvm.resource.LibvirtStoragePoolDef;
4444
import com.cloud.storage.Storage;
45+
import com.cloud.storage.StorageLayer;
4546
import com.cloud.utils.Pair;
4647
import com.cloud.utils.exception.CloudRuntimeException;
4748
import com.cloud.utils.script.Script;
49+
import org.libvirt.LibvirtException;
50+
51+
import org.springframework.test.util.ReflectionTestUtils;
4852

4953
@RunWith(MockitoJUnitRunner.class)
5054
public class LibvirtStorageAdaptorTest {
@@ -56,6 +60,9 @@ public class LibvirtStorageAdaptorTest {
5660
@Mock
5761
LibvirtStoragePool mockPool;
5862

63+
@Mock
64+
StorageLayer storageLayer;
65+
5966
MockedStatic<Script> mockScript;
6067

6168
@Spy
@@ -67,6 +74,7 @@ public void initMocks() {
6774
libvirtConnectionMockedStatic = Mockito.mockStatic(LibvirtConnection.class);
6875
Mockito.reset(mockPool);
6976
mockScript = Mockito.mockStatic(Script.class);
77+
ReflectionTestUtils.setField(libvirtStorageAdaptor, "_storageLayer", storageLayer);
7078
}
7179

7280
@After
@@ -176,4 +184,171 @@ public void testUpdateLocalPoolIops_NullResultFromScript() {
176184

177185
Mockito.verify(mockPool, never()).setUsedIops(anyLong());
178186
}
187+
188+
/**
189+
* Creates a {@link LibvirtException} via reflection since its only constructor is package-private.
190+
*/
191+
private static LibvirtException createLibvirtException(String message) throws Exception {
192+
org.libvirt.Error virError = Mockito.mock(org.libvirt.Error.class);
193+
Mockito.when(virError.getMessage()).thenReturn(message);
194+
java.lang.reflect.Constructor<LibvirtException> ctor =
195+
LibvirtException.class.getDeclaredConstructor(org.libvirt.Error.class);
196+
ctor.setAccessible(true);
197+
return ctor.newInstance(virError);
198+
}
199+
200+
private String buildNfsPoolXml(String uuid, String host, String dir, String targetPath) {
201+
return "<pool type='netfs'>\n" +
202+
"<name>" + uuid + "</name>\n<uuid>" + uuid + "</uuid>\n" +
203+
"<source>\n<host name='" + host + "'/>\n<dir path='" + dir + "'/>\n</source>\n" +
204+
"<target>\n<path>" + targetPath + "</path>\n</target>\n</pool>\n";
205+
}
206+
207+
/**
208+
* NFS pool exists with same host and path: should reuse it without destroying.
209+
*/
210+
@Test
211+
public void testCreateStoragePool_NFS_SameHostAndPath_ReusesPool() throws Exception {
212+
String uuid = UUID.randomUUID().toString();
213+
String host = "10.0.0.1";
214+
String path = "/export/primary";
215+
String targetPath = "/mnt/" + uuid;
216+
String poolXml = buildNfsPoolXml(uuid, host, path, targetPath);
217+
218+
Connect conn = Mockito.mock(Connect.class);
219+
StoragePool sp = Mockito.mock(StoragePool.class);
220+
Mockito.when(LibvirtConnection.getConnection()).thenReturn(conn);
221+
Mockito.when(conn.storagePoolLookupByUUIDString(uuid)).thenReturn(sp);
222+
Mockito.when(sp.isActive()).thenReturn(1);
223+
Mockito.when(sp.getXMLDesc(0)).thenReturn(poolXml);
224+
Mockito.when(Script.runSimpleBashScriptForExitValue(anyString())).thenReturn(0);
225+
Mockito.doReturn(Mockito.mock(KVMStoragePool.class)).when(libvirtStorageAdaptor).getStoragePool(uuid);
226+
227+
libvirtStorageAdaptor.createStoragePool(uuid, host, 0, path, null,
228+
Storage.StoragePoolType.NetworkFilesystem, null, true);
229+
230+
// pool was not destroyed since source didn't change
231+
Mockito.verify(sp, Mockito.never()).destroy();
232+
Mockito.verify(sp, Mockito.never()).undefine();
233+
}
234+
235+
/**
236+
* NFS pool exists but path has changed: should destroy and recreate.
237+
*/
238+
@Test
239+
public void testCreateStoragePool_NFS_PathChanged_DestroysAndRecreates() throws Exception {
240+
String uuid = UUID.randomUUID().toString();
241+
String host = "10.0.0.1";
242+
String oldPath = "/export/old";
243+
String newPath = "/export/new";
244+
String targetPath = "/mnt/" + uuid;
245+
String poolXml = buildNfsPoolXml(uuid, host, oldPath, targetPath);
246+
247+
Connect conn = Mockito.mock(Connect.class);
248+
StoragePool sp = Mockito.mock(StoragePool.class);
249+
Mockito.when(LibvirtConnection.getConnection()).thenReturn(conn);
250+
Mockito.when(conn.storagePoolLookupByUUIDString(uuid)).thenReturn(sp);
251+
Mockito.when(sp.isActive()).thenReturn(1);
252+
Mockito.when(sp.getXMLDesc(0)).thenReturn(poolXml);
253+
Mockito.when(sp.isPersistent()).thenReturn(1);
254+
Mockito.when(conn.listStoragePools()).thenReturn(new String[]{});
255+
StoragePool newSp = Mockito.mock(StoragePool.class);
256+
Mockito.when(conn.storagePoolCreateXML(anyString(), Mockito.eq(0))).thenReturn(newSp);
257+
Mockito.when(Script.runSimpleBashScriptForExitValue(anyString())).thenReturn(0);
258+
Mockito.doReturn(Mockito.mock(KVMStoragePool.class)).when(libvirtStorageAdaptor).getStoragePool(uuid);
259+
260+
libvirtStorageAdaptor.createStoragePool(uuid, host, 0, newPath, null,
261+
Storage.StoragePoolType.NetworkFilesystem, null, true);
262+
263+
Mockito.verify(sp).destroy();
264+
Mockito.verify(sp).undefine();
265+
Mockito.verify(sp).free();
266+
}
267+
268+
/**
269+
* NFS pool exists but host has changed: should destroy and recreate.
270+
*/
271+
@Test
272+
public void testCreateStoragePool_NFS_HostChanged_DestroysAndRecreates() throws Exception {
273+
String uuid = UUID.randomUUID().toString();
274+
String oldHost = "10.0.0.1";
275+
String newHost = "10.0.0.2";
276+
String path = "/export/primary";
277+
String targetPath = "/mnt/" + uuid;
278+
String poolXml = buildNfsPoolXml(uuid, oldHost, path, targetPath);
279+
280+
Connect conn = Mockito.mock(Connect.class);
281+
StoragePool sp = Mockito.mock(StoragePool.class);
282+
Mockito.when(LibvirtConnection.getConnection()).thenReturn(conn);
283+
Mockito.when(conn.storagePoolLookupByUUIDString(uuid)).thenReturn(sp);
284+
Mockito.when(sp.isActive()).thenReturn(1);
285+
Mockito.when(sp.getXMLDesc(0)).thenReturn(poolXml);
286+
Mockito.when(sp.isPersistent()).thenReturn(1);
287+
Mockito.when(conn.listStoragePools()).thenReturn(new String[]{});
288+
StoragePool newSp = Mockito.mock(StoragePool.class);
289+
Mockito.when(conn.storagePoolCreateXML(anyString(), Mockito.eq(0))).thenReturn(newSp);
290+
Mockito.when(Script.runSimpleBashScriptForExitValue(anyString())).thenReturn(0);
291+
Mockito.doReturn(Mockito.mock(KVMStoragePool.class)).when(libvirtStorageAdaptor).getStoragePool(uuid);
292+
293+
libvirtStorageAdaptor.createStoragePool(uuid, newHost, 0, path, null,
294+
Storage.StoragePoolType.NetworkFilesystem, null, true);
295+
296+
Mockito.verify(sp).destroy();
297+
Mockito.verify(sp).undefine();
298+
Mockito.verify(sp).free();
299+
}
300+
301+
/**
302+
* RBD pool exists with different source — should NOT destroy (RBD is excluded from this logic).
303+
*/
304+
@Test
305+
public void testCreateStoragePool_RBD_SourceChanged_DoesNotDestroy() throws Exception {
306+
String uuid = UUID.randomUUID().toString();
307+
String rbdPoolXml = "<pool type='rbd'>\n<name>" + uuid + "</name>\n<uuid>" + uuid + "</uuid>\n" +
308+
"<source>\n<host name='10.0.0.1'/>\n<name>oldpool</name>\n</source>\n</pool>\n";
309+
310+
Connect conn = Mockito.mock(Connect.class);
311+
StoragePool sp = Mockito.mock(StoragePool.class);
312+
Mockito.when(LibvirtConnection.getConnection()).thenReturn(conn);
313+
Mockito.when(conn.storagePoolLookupByUUIDString(uuid)).thenReturn(sp);
314+
Mockito.when(sp.isActive()).thenReturn(1);
315+
Mockito.doReturn(Mockito.mock(KVMStoragePool.class)).when(libvirtStorageAdaptor).getStoragePool(uuid);
316+
317+
libvirtStorageAdaptor.createStoragePool(uuid, "10.0.0.2", 6789, "newpool", "user:secret",
318+
Storage.StoragePoolType.RBD, null, true);
319+
320+
Mockito.verify(sp, Mockito.never()).destroy();
321+
Mockito.verify(sp, Mockito.never()).undefine();
322+
}
323+
324+
/**
325+
* NFS pool exists, destroy fails — pool should be reused (not crash), sp.free() not called.
326+
*/
327+
@Test
328+
public void testCreateStoragePool_NFS_DestroyFails_ReusesExistingPool() throws Exception {
329+
String uuid = UUID.randomUUID().toString();
330+
String host = "10.0.0.1";
331+
String oldPath = "/export/old";
332+
String newPath = "/export/new";
333+
String targetPath = "/mnt/" + uuid;
334+
String poolXml = buildNfsPoolXml(uuid, host, oldPath, targetPath);
335+
336+
Connect conn = Mockito.mock(Connect.class);
337+
StoragePool sp = Mockito.mock(StoragePool.class);
338+
Mockito.when(LibvirtConnection.getConnection()).thenReturn(conn);
339+
Mockito.when(conn.storagePoolLookupByUUIDString(uuid)).thenReturn(sp);
340+
Mockito.when(sp.isActive()).thenReturn(1);
341+
Mockito.when(sp.getXMLDesc(0)).thenReturn(poolXml);
342+
Mockito.when(sp.isPersistent()).thenReturn(1);
343+
LibvirtException libvirtException = createLibvirtException("pool is busy");
344+
Mockito.doThrow(libvirtException).when(sp).destroy();
345+
Mockito.when(Script.runSimpleBashScriptForExitValue(anyString())).thenReturn(0);
346+
Mockito.doReturn(Mockito.mock(KVMStoragePool.class)).when(libvirtStorageAdaptor).getStoragePool(uuid);
347+
348+
libvirtStorageAdaptor.createStoragePool(uuid, host, 0, newPath, null,
349+
Storage.StoragePoolType.NetworkFilesystem, null, true);
350+
351+
Mockito.verify(sp).destroy();
352+
Mockito.verify(sp, Mockito.never()).free();
353+
}
179354
}

server/src/main/java/com/cloud/storage/StorageManagerImpl.java

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,31 +1304,90 @@ public PrimaryDataStoreInfo updateStoragePool(UpdateStoragePoolCmd cmd) throws I
13041304
_storagePoolDao.updateCapacityIops(id, updatedCapacityIops);
13051305
}
13061306
if (cmd.getUrl() != null) {
1307-
if (!storagePool.isInMaintenance()) {
1308-
throw new InvalidParameterValueException("Storage pool must be in Maintenance state before its URL can be changed. " +
1309-
"Please put the pool into maintenance first.");
1310-
}
1311-
URI newUri;
1312-
try {
1313-
newUri = new URI(cmd.getUrl());
1314-
} catch (URISyntaxException e) {
1315-
throw new InvalidParameterValueException("Invalid URL format: " + cmd.getUrl());
1316-
}
1317-
storagePool.setHostAddress(newUri.getHost());
1318-
storagePool.setPath(newUri.getPath());
1319-
if (newUri.getPort() != -1) {
1320-
storagePool.setPort(newUri.getPort());
1321-
}
13221307
details.put("url", cmd.getUrl());
1308+
1309+
// Updating host/path/port and propagating the remount to agents is only
1310+
// supported for NFS and Gluster pools. For other types of storage pools, the URL is just informational and won't be used for actual connection, so we don't need to parse and propagate it.
1311+
StoragePoolType poolType = storagePool.getPoolType();
1312+
if (poolType == StoragePoolType.NetworkFilesystem || poolType == StoragePoolType.Gluster) {
1313+
if (!storagePool.isInMaintenance()) {
1314+
throw new InvalidParameterValueException("Storage pool must be in Maintenance state before its URL can be changed. " +
1315+
"Please put the pool into maintenance first.");
1316+
}
1317+
URI newUri;
1318+
try {
1319+
newUri = new URI(cmd.getUrl());
1320+
} catch (URISyntaxException e) {
1321+
throw new InvalidParameterValueException("Invalid URL format: " + cmd.getUrl());
1322+
}
1323+
storagePool.setHostAddress(newUri.getHost());
1324+
storagePool.setPath(newUri.getPath());
1325+
if (newUri.getPort() != -1) {
1326+
storagePool.setPort(newUri.getPort());
1327+
}
1328+
}
13231329
}
13241330
_storagePoolDao.update(id, storagePool);
13251331
_storagePoolDao.updateDetails(id, details);
1332+
((PrimaryDataStoreLifeCycle) dataStoreLifeCycle).updateStoragePool(storagePool, details);
1333+
StoragePoolType poolType = storagePool.getPoolType();
1334+
if (cmd.getUrl() != null &&
1335+
(poolType == StoragePoolType.NetworkFilesystem || poolType == StoragePoolType.Gluster)) {
1336+
propagateStoragePoolUrlChangeToHosts(storagePool);
1337+
}
13261338
}
13271339
}
13281340

13291341
return (PrimaryDataStoreInfo)_dataStoreMgr.getDataStore(pool.getId(), DataStoreRole.Primary);
13301342
}
13311343

1344+
/**
1345+
* Propagates a storage pool URL change to all currently connected hosts by sending
1346+
* a {@link ModifyStoragePoolCommand} with the updated connection details.
1347+
* This is called after the pool URL has been persisted to the DB while the pool is
1348+
* in Maintenance state (so no VMs are actively using it).
1349+
*
1350+
* @param updatedPool the {@link StoragePoolVO} whose hostAddress/path/port have already been updated
1351+
*/
1352+
private void propagateStoragePoolUrlChangeToHosts(StoragePoolVO updatedPool) {
1353+
List<Long> poolIds = List.of(updatedPool.getId());
1354+
List<Long> connectedHostIds = _storagePoolHostDao.findHostsConnectedToPools(poolIds);
1355+
if (connectedHostIds.isEmpty()) {
1356+
logger.debug("No connected hosts found for storage pool [{}]; nothing to propagate for URL change.", updatedPool.getName());
1357+
return;
1358+
}
1359+
1360+
// Fetch fresh DataStore so that StoragePool delegates (getHostAddress, getPath, getPort)
1361+
// return the newly saved values.
1362+
StoragePool freshPool = (StoragePool) _dataStoreMgr.getDataStore(updatedPool.getId(), DataStoreRole.Primary);
1363+
1364+
Pair<Map<String, String>, Boolean> nfsMountOpts = getStoragePoolNFSMountOpts(freshPool, null);
1365+
Map<String, String> mountDetails = nfsMountOpts != null ? nfsMountOpts.first() : new java.util.HashMap<>();
1366+
1367+
logger.info("Propagating new URL for storage pool [{}] to {} connected host(s).", updatedPool.getName(), connectedHostIds.size());
1368+
1369+
for (Long hostId : connectedHostIds) {
1370+
HostVO host = _hostDao.findById(hostId);
1371+
if (host == null) {
1372+
logger.warn("Host [{}] not found; skipping URL propagation for pool [{}].", hostId, updatedPool.getName());
1373+
continue;
1374+
}
1375+
1376+
ModifyStoragePoolCommand cmd = new ModifyStoragePoolCommand(true, freshPool, mountDetails);
1377+
Answer answer = _agentMgr.easySend(hostId, cmd);
1378+
1379+
if (answer == null || !answer.getResult()) {
1380+
String detail = (answer == null) ? "no answer returned" : answer.getDetails();
1381+
logger.warn("Failed to propagate new URL for storage pool [{}] to host [{}]: {}",
1382+
updatedPool.getName(), host.getName(), detail);
1383+
} else {
1384+
logger.info("Successfully propagated new URL for storage pool [{}] to host [{}].",
1385+
updatedPool.getName(), host.getName());
1386+
updateStoragePoolHostVOAndBytes(freshPool, hostId, (ModifyStoragePoolAnswer) answer);
1387+
}
1388+
}
1389+
}
1390+
13321391
private void changeStoragePoolScopeToZone(StoragePoolVO primaryStorage) {
13331392
/*
13341393
* For cluster wide primary storage the hypervisor type might not be set.

0 commit comments

Comments
 (0)