This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch 4.22
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/4.22 by this push:
     new df7ff972719 Create volume on a specified storage pool (#12966)
df7ff972719 is described below

commit df7ff97271925151a5ca08a495280866d4af1fbd
Author: Abhisar Sinha <[email protected]>
AuthorDate: Fri Apr 10 17:57:39 2026 +0530

    Create volume on a specified storage pool (#12966)
---
 .../api/command/user/volume/CreateVolumeCmd.java   | 15 +++++
 .../com/cloud/storage/VolumeApiServiceImpl.java    | 32 ++++++++++
 ui/public/locales/en.json                          |  2 +
 ui/src/views/storage/CreateVolume.vue              | 74 ++++++++++++++++++++++
 4 files changed, 123 insertions(+)

diff --git 
a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java
 
b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java
index 27b592aa8f1..84dd4525265 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java
@@ -32,6 +32,7 @@ import 
org.apache.cloudstack.api.response.DiskOfferingResponse;
 import org.apache.cloudstack.api.response.DomainResponse;
 import org.apache.cloudstack.api.response.ProjectResponse;
 import org.apache.cloudstack.api.response.SnapshotResponse;
+import org.apache.cloudstack.api.response.StoragePoolResponse;
 import org.apache.cloudstack.api.response.UserVmResponse;
 import org.apache.cloudstack.api.response.VolumeResponse;
 import org.apache.cloudstack.api.response.ZoneResponse;
@@ -109,6 +110,13 @@ public class CreateVolumeCmd extends 
BaseAsyncCreateCustomIdCmd implements UserC
                description = "The ID of the Instance; to be used with snapshot 
Id, Instance to which the volume gets attached after creation")
     private Long virtualMachineId;
 
+    @Parameter(name = ApiConstants.STORAGE_ID,
+            type = CommandType.UUID,
+            entityType = StoragePoolResponse.class,
+            description = "Storage pool ID to create the volume in. Cannot be 
used with the snapshotid parameter.",
+            authorized = {RoleType.Admin})
+    private Long storageId;
+
     /////////////////////////////////////////////////////
     /////////////////// Accessors ///////////////////////
     /////////////////////////////////////////////////////
@@ -153,6 +161,13 @@ public class CreateVolumeCmd extends 
BaseAsyncCreateCustomIdCmd implements UserC
         return projectId;
     }
 
+    public Long getStorageId() {
+        if (snapshotId != null && storageId != null) {
+            throw new IllegalArgumentException("StorageId parameter cannot be 
specified with the SnapshotId parameter.");
+        }
+        return storageId;
+    }
+
     public Boolean getDisplayVolume() {
         return displayVolume;
     }
diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java 
b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
index ca3d31d4fad..d9f54e7de02 100644
--- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
+++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
@@ -1043,6 +1043,36 @@ public class VolumeApiServiceImpl extends ManagerBase 
implements VolumeApiServic
         return true;
     }
 
+    private VolumeVO createVolumeOnStoragePool(Long volumeId, Long storageId) 
throws ExecutionException, InterruptedException {
+        VolumeVO volume = _volsDao.findById(volumeId);
+        StoragePool storagePool = (StoragePool) 
dataStoreMgr.getDataStore(storageId, DataStoreRole.Primary);
+        if (storagePool == null) {
+            throw new InvalidParameterValueException("Failed to find the 
storage pool: " + storageId);
+        } else if (!storagePool.getStatus().equals(StoragePoolStatus.Up)) {
+            throw new InvalidParameterValueException(String.format("Cannot 
create volume %s on storage pool %s as the storage pool is not in Up state.",
+                    volume.getUuid(), storagePool.getName()));
+        }
+
+        if (storagePool.getDataCenterId() != volume.getDataCenterId()) {
+            throw new InvalidParameterValueException(String.format("Cannot 
create volume %s in zone %s on storage pool %s in zone %s.",
+                    volume.getUuid(), volume.getDataCenterId(), 
storagePool.getUuid(), storagePool.getDataCenterId()));
+        }
+
+        DiskOfferingVO diskOffering = 
_diskOfferingDao.findById(volume.getDiskOfferingId());
+        if (!doesStoragePoolSupportDiskOffering(storagePool, diskOffering)) {
+            throw new InvalidParameterValueException(String.format("Disk 
offering: %s is not compatible with the storage pool", diskOffering.getUuid()));
+        }
+
+        DataStore dataStore = dataStoreMgr.getDataStore(storageId, 
DataStoreRole.Primary);
+        VolumeInfo volumeInfo = volFactory.getVolume(volumeId, dataStore);
+        AsyncCallFuture<VolumeApiResult> createVolumeFuture = 
volService.createVolumeAsync(volumeInfo, dataStore);
+        VolumeApiResult createVolumeResult = createVolumeFuture.get();
+        if (createVolumeResult.isFailed()) {
+            throw new CloudRuntimeException("Volume creation on storage 
failed: " + createVolumeResult.getResult());
+        }
+        return _volsDao.findById(volumeInfo.getId());
+    }
+
     @Override
     @DB
     @ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription 
= "creating volume", async = true)
@@ -1074,6 +1104,8 @@ public class VolumeApiServiceImpl extends ManagerBase 
implements VolumeApiServic
                         throw new CloudRuntimeException(message.toString());
                     }
                 }
+            } else if (cmd.getStorageId() != null) {
+                volume = createVolumeOnStoragePool(cmd.getEntityId(), 
cmd.getStorageId());
             }
             return volume;
         } catch (Exception e) {
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 6494995bf22..4d8d32f87bb 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -681,6 +681,7 @@
 "label.create.sharedfs": "Create Shared FileSystem",
 "label.create.network": "Create new Network",
 "label.create.nfs.secondary.staging.storage": "Create NFS secondary staging 
storage",
+"label.create.on.storage": "Create on Storage",
 "label.create.project": "Create Project",
 "label.create.project.role": "Create Project Role",
 "label.create.routing.policy": "Create Routing Policy",
@@ -697,6 +698,7 @@
 "label.create.tier.networkofferingid.description": "The Network offering for 
the Network Tier.",
 "label.create.tungsten.routing.policy": "Create Tungsten-Fabric routing 
policy",
 "label.create.user": "Create User",
+"label.create.volume.on.primary.storage": "Create Volume on the specified 
Primary Storage",
 "label.create.vm": "Create Instance",
 "label.create.vm.and.stay": "Create Instance & stay on this page",
 "label.create.vpn.connection": "Create VPN connection",
diff --git a/ui/src/views/storage/CreateVolume.vue 
b/ui/src/views/storage/CreateVolume.vue
index e99cc749170..f5185091f31 100644
--- a/ui/src/views/storage/CreateVolume.vue
+++ b/ui/src/views/storage/CreateVolume.vue
@@ -116,6 +116,42 @@
             :placeholder="apiParams.maxiops.description"/>
         </a-form-item>
       </span>
+      <a-form-item name="createOnStorage" ref="createOnStorage" 
v-if="showStoragePoolSelect">
+        <template #label>
+          <tooltip-label :title="$t('label.create.on.storage')" 
:tooltip="$t('label.create.volume.on.primary.storage')" />
+        </template>
+        <a-switch
+          v-model:checked="form.createOnStorage"
+          :checked="createOnStorage"
+          @change="onChangeCreateOnStorage" />
+      </a-form-item>
+      <span v-if="showStoragePoolSelect && createOnStorage">
+        <a-form-item ref="storageid" name="storageid">
+          <template #label>
+            <tooltip-label :title="$t('label.storageid')" />
+          </template>
+          <a-select
+            v-model:value="form.storageid"
+            :loading="loading"
+            showSearch
+            optionFilterProp="label"
+            :filterOption="(input, option) => {
+              return option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
+            }" >
+            <a-select-option
+              v-for="(pool, index) in storagePools"
+              :value="pool.id"
+              :key="index"
+              :label="pool.name">
+              <span>
+                <resource-icon v-if="pool.icon" :image="pool.icon.base64image" 
size="1x" style="margin-right: 5px"/>
+                <hdd-outlined v-else style="margin-right: 5px"/>
+                {{ pool.name }}
+              </span>
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+      </span>
       <a-form-item name="attachVolume" ref="attachVolume" 
v-if="!createVolumeFromVM">
         <template #label>
           <tooltip-label :title="$t('label.action.attach.to.instance')" 
:tooltip="$t('label.attach.vol.to.instance')" />
@@ -170,6 +206,7 @@
 import { ref, reactive, toRaw } from 'vue'
 import { getAPI, postAPI } from '@/api'
 import { mixinForm } from '@/utils/mixin'
+import { isAdmin } from '@/role'
 import ResourceIcon from '@/components/view/ResourceIcon'
 import TooltipLabel from '@/components/widgets/TooltipLabel'
 import OwnershipSelection from '@/views/compute/wizard/OwnershipSelection.vue'
@@ -203,11 +240,16 @@ export default {
       loading: false,
       isCustomizedDiskIOps: false,
       virtualmachines: [],
+      createOnStorage: false,
+      storagePools: [],
       attachVolume: false,
       vmidtoattach: null
     }
   },
   computed: {
+    showStoragePoolSelect () {
+      return isAdmin() && !this.createVolumeFromSnapshot
+    },
     createVolumeFromVM () {
       return this.$route.path.startsWith('/vm/')
     },
@@ -299,6 +341,9 @@ export default {
         this.zones = json.listzonesresponse.zone || []
         this.form.zoneid = this.zones[0].id || ''
         this.fetchDiskOfferings(this.form.zoneid)
+        if (this.createOnStorage) {
+          this.fetchStoragePools(this.form.zoneid)
+        }
         if (this.attachVolume) {
           this.fetchVirtualMachines(this.form.zoneid)
         }
@@ -355,6 +400,25 @@ export default {
         this.loading = false
       })
     },
+    fetchStoragePools (zoneId) {
+      if (!zoneId) {
+        this.storagePools = []
+        return
+      }
+      this.loading = true
+      getAPI('listStoragePools', {
+        zoneid: zoneId,
+        showicon: true
+      }).then(json => {
+        const pools = json.liststoragepoolsresponse.storagepool || []
+        this.storagePools = pools.filter(p => p.state === 'Up')
+      }).catch(error => {
+        this.$notifyError(error)
+        this.storagePools = []
+      }).finally(() => {
+        this.loading = false
+      })
+    },
     fetchVirtualMachines (zoneId) {
       var params = {
         zoneid: zoneId,
@@ -394,6 +458,7 @@ export default {
         if (this.customDiskOffering) {
           values.size = values.size.trim()
         }
+        delete values.createOnStorage
         if (this.createVolumeFromSnapshot) {
           values.snapshotid = this.resource.id
         }
@@ -467,6 +532,15 @@ export default {
         this.attachVolumeApiParams = this.$getApiParams('attachVolume')
         this.fetchVirtualMachines(this.form.zoneid)
       }
+    },
+    onChangeCreateOnStorage () {
+      this.createOnStorage = !this.createOnStorage
+      if (this.createOnStorage) {
+        this.fetchStoragePools(this.form.zoneid)
+        this.form.storageid = this.storagePools[0]?.id || undefined
+      } else {
+        this.form.storageid = undefined
+      }
     }
   }
 }

Reply via email to