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
+ }
}
}
}