diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateImageStoreCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateImageStoreCmd.java index 0e1631a46ba2..ee83b78d07fe 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateImageStoreCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateImageStoreCmd.java @@ -50,6 +50,13 @@ public class UpdateImageStoreCmd extends BaseCmd { description = "The number of bytes CloudStack can use on this image storage.\n\tNOTE: this will be overwritten by the StatsCollector as soon as there is a SSVM to query the storage.") private Long capacityBytes; + @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = false, + description = "The new URL for the image store (e.g. nfs://new-host/export). " + + "The image store must be in read-only state before its URL can be changed. " + + "After updating, destroy and recreate any Secondary Storage VMs so they remount the new path.", + since = "4.23.0") + private String url; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -70,6 +77,10 @@ public Long getCapacityBytes() { return capacityBytes; } + public String getUrl() { + return url; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateStoragePoolCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateStoragePoolCmd.java index 4b0a6ba00b28..9529da84df76 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateStoragePoolCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateStoragePoolCmd.java @@ -73,7 +73,12 @@ public class UpdateStoragePoolCmd extends BaseCmd { @Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = false, - description = "the URL of the storage pool", + description = "the URL of the storage pool. Supported only for NFS and Gluster pool type." + + "The pool must be in Maintenance mode before changing the URL. WARNING: use this parameter" + + "with caution. It is intended for failover scenarios where the storage content is already " + + "fully mirrored at the new location. Pointing to a new location without ensuring complete " + + "data parity will result in data loss or corruption. After the URL is updated, the new mount" + + "is applied to all connected hosts or restart the Management server", since = "4.19.0") private String url; diff --git a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java index 3c62738f9ed5..455ac8c33389 100644 --- a/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java +++ b/engine/components-api/src/main/java/com/cloud/storage/StorageManager.java @@ -411,7 +411,7 @@ void connectHostsToPool(DataStore primaryStore, List hostIds, Scope scope, Long getDiskIopsWriteRate(ServiceOffering offering, DiskOffering diskOffering); - ImageStore updateImageStoreStatus(Long id, String name, Boolean readonly, Long capacityBytes); + ImageStore updateImageStoreStatus(Long id, String name, Boolean readonly, Long capacityBytes, String url); void cleanupDownloadUrls(); diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java index a03daeb197bf..5e00c24e45c8 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java @@ -766,6 +766,39 @@ public KVMStoragePool createStoragePool(String name, String host, int port, Stri path = path.substring(0, path.length() - 1); } + // For NFS and Gluster pools, if the existing pool's source host or path has changed + // (e.g. after a storage URL update), destroy and undefine it so it gets recreated + // with the new source below. Restricted to NFS/Gluster only. + if (sp != null && (type == StoragePoolType.NetworkFilesystem || type == StoragePoolType.Gluster)) { + try { + LibvirtStoragePoolDef existingDef = getStoragePoolDef(conn, sp); + if (existingDef != null) { + String existingSourceHost = existingDef.getSourceHost(); + String existingSourceDir = existingDef.getSourceDir(); + boolean hostChanged = host != null && existingSourceHost != null && !existingSourceHost.equals(host); + boolean pathChanged = path != null && existingSourceDir != null && !existingSourceDir.equals(path); + if (hostChanged || pathChanged) { + logger.info("Storage pool {} source has changed (host: {} -> {}, path: {} -> {}); destroying and redefining.", + name, existingSourceHost, host, existingSourceDir, path); + try { + if (sp.isPersistent() == 1) { + sp.destroy(); + sp.undefine(); + } else { + sp.destroy(); + } + sp.free(); + sp = null; + } catch (LibvirtException destroyEx) { + logger.error("Failed to destroy storage pool {} with changed source; will attempt to reuse existing pool: {}", name, destroyEx.getMessage()); + } + } + } + } catch (LibvirtException e) { + logger.warn("Failed to check existing storage pool {} source path, will attempt to reuse it: {}", name, e.getMessage()); + } + } + if (sp == null) { // see if any existing pool by another name is using our storage path. // if anyone is, undefine the pool so we can define it as requested. diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptorTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptorTest.java index 88346abd0176..259cb603aeb8 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptorTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptorTest.java @@ -42,9 +42,13 @@ import com.cloud.hypervisor.kvm.resource.LibvirtConnection; import com.cloud.hypervisor.kvm.resource.LibvirtStoragePoolDef; import com.cloud.storage.Storage; +import com.cloud.storage.StorageLayer; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.script.Script; +import org.libvirt.LibvirtException; + +import org.springframework.test.util.ReflectionTestUtils; @RunWith(MockitoJUnitRunner.class) public class LibvirtStorageAdaptorTest { @@ -56,6 +60,9 @@ public class LibvirtStorageAdaptorTest { @Mock LibvirtStoragePool mockPool; + @Mock + StorageLayer storageLayer; + MockedStatic