Create volume on a specified storage pool (#12966)

This commit is contained in:
Abhisar Sinha 2026-04-10 17:57:39 +05:30 committed by GitHub
parent 273699cf56
commit df7ff97271
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 123 additions and 0 deletions

View File

@ -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;
}

View File

@ -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) {

View File

@ -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",

View File

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