Skip to content

Commit df7ff97

Browse files
authored
Create volume on a specified storage pool (#12966)
1 parent 273699c commit df7ff97

File tree

4 files changed

+123
-0
lines changed

4 files changed

+123
-0
lines changed

api/src/main/java/org/apache/cloudstack/api/command/user/volume/CreateVolumeCmd.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.cloudstack.api.response.DomainResponse;
3333
import org.apache.cloudstack.api.response.ProjectResponse;
3434
import org.apache.cloudstack.api.response.SnapshotResponse;
35+
import org.apache.cloudstack.api.response.StoragePoolResponse;
3536
import org.apache.cloudstack.api.response.UserVmResponse;
3637
import org.apache.cloudstack.api.response.VolumeResponse;
3738
import org.apache.cloudstack.api.response.ZoneResponse;
@@ -109,6 +110,13 @@ public class CreateVolumeCmd extends BaseAsyncCreateCustomIdCmd implements UserC
109110
description = "The ID of the Instance; to be used with snapshot Id, Instance to which the volume gets attached after creation")
110111
private Long virtualMachineId;
111112

113+
@Parameter(name = ApiConstants.STORAGE_ID,
114+
type = CommandType.UUID,
115+
entityType = StoragePoolResponse.class,
116+
description = "Storage pool ID to create the volume in. Cannot be used with the snapshotid parameter.",
117+
authorized = {RoleType.Admin})
118+
private Long storageId;
119+
112120
/////////////////////////////////////////////////////
113121
/////////////////// Accessors ///////////////////////
114122
/////////////////////////////////////////////////////
@@ -153,6 +161,13 @@ private Long getProjectId() {
153161
return projectId;
154162
}
155163

164+
public Long getStorageId() {
165+
if (snapshotId != null && storageId != null) {
166+
throw new IllegalArgumentException("StorageId parameter cannot be specified with the SnapshotId parameter.");
167+
}
168+
return storageId;
169+
}
170+
156171
public Boolean getDisplayVolume() {
157172
return displayVolume;
158173
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,36 @@ public boolean validateVolumeSizeInBytes(long size) {
10431043
return true;
10441044
}
10451045

1046+
private VolumeVO createVolumeOnStoragePool(Long volumeId, Long storageId) throws ExecutionException, InterruptedException {
1047+
VolumeVO volume = _volsDao.findById(volumeId);
1048+
StoragePool storagePool = (StoragePool) dataStoreMgr.getDataStore(storageId, DataStoreRole.Primary);
1049+
if (storagePool == null) {
1050+
throw new InvalidParameterValueException("Failed to find the storage pool: " + storageId);
1051+
} else if (!storagePool.getStatus().equals(StoragePoolStatus.Up)) {
1052+
throw new InvalidParameterValueException(String.format("Cannot create volume %s on storage pool %s as the storage pool is not in Up state.",
1053+
volume.getUuid(), storagePool.getName()));
1054+
}
1055+
1056+
if (storagePool.getDataCenterId() != volume.getDataCenterId()) {
1057+
throw new InvalidParameterValueException(String.format("Cannot create volume %s in zone %s on storage pool %s in zone %s.",
1058+
volume.getUuid(), volume.getDataCenterId(), storagePool.getUuid(), storagePool.getDataCenterId()));
1059+
}
1060+
1061+
DiskOfferingVO diskOffering = _diskOfferingDao.findById(volume.getDiskOfferingId());
1062+
if (!doesStoragePoolSupportDiskOffering(storagePool, diskOffering)) {
1063+
throw new InvalidParameterValueException(String.format("Disk offering: %s is not compatible with the storage pool", diskOffering.getUuid()));
1064+
}
1065+
1066+
DataStore dataStore = dataStoreMgr.getDataStore(storageId, DataStoreRole.Primary);
1067+
VolumeInfo volumeInfo = volFactory.getVolume(volumeId, dataStore);
1068+
AsyncCallFuture<VolumeApiResult> createVolumeFuture = volService.createVolumeAsync(volumeInfo, dataStore);
1069+
VolumeApiResult createVolumeResult = createVolumeFuture.get();
1070+
if (createVolumeResult.isFailed()) {
1071+
throw new CloudRuntimeException("Volume creation on storage failed: " + createVolumeResult.getResult());
1072+
}
1073+
return _volsDao.findById(volumeInfo.getId());
1074+
}
1075+
10461076
@Override
10471077
@DB
10481078
@ActionEvent(eventType = EventTypes.EVENT_VOLUME_CREATE, eventDescription = "creating volume", async = true)
@@ -1074,6 +1104,8 @@ public VolumeVO createVolume(CreateVolumeCmd cmd) {
10741104
throw new CloudRuntimeException(message.toString());
10751105
}
10761106
}
1107+
} else if (cmd.getStorageId() != null) {
1108+
volume = createVolumeOnStoragePool(cmd.getEntityId(), cmd.getStorageId());
10771109
}
10781110
return volume;
10791111
} catch (Exception e) {

ui/public/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,7 @@
681681
"label.create.sharedfs": "Create Shared FileSystem",
682682
"label.create.network": "Create new Network",
683683
"label.create.nfs.secondary.staging.storage": "Create NFS secondary staging storage",
684+
"label.create.on.storage": "Create on Storage",
684685
"label.create.project": "Create Project",
685686
"label.create.project.role": "Create Project Role",
686687
"label.create.routing.policy": "Create Routing Policy",
@@ -697,6 +698,7 @@
697698
"label.create.tier.networkofferingid.description": "The Network offering for the Network Tier.",
698699
"label.create.tungsten.routing.policy": "Create Tungsten-Fabric routing policy",
699700
"label.create.user": "Create User",
701+
"label.create.volume.on.primary.storage": "Create Volume on the specified Primary Storage",
700702
"label.create.vm": "Create Instance",
701703
"label.create.vm.and.stay": "Create Instance & stay on this page",
702704
"label.create.vpn.connection": "Create VPN connection",

ui/src/views/storage/CreateVolume.vue

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,42 @@
116116
:placeholder="apiParams.maxiops.description"/>
117117
</a-form-item>
118118
</span>
119+
<a-form-item name="createOnStorage" ref="createOnStorage" v-if="showStoragePoolSelect">
120+
<template #label>
121+
<tooltip-label :title="$t('label.create.on.storage')" :tooltip="$t('label.create.volume.on.primary.storage')" />
122+
</template>
123+
<a-switch
124+
v-model:checked="form.createOnStorage"
125+
:checked="createOnStorage"
126+
@change="onChangeCreateOnStorage" />
127+
</a-form-item>
128+
<span v-if="showStoragePoolSelect && createOnStorage">
129+
<a-form-item ref="storageid" name="storageid">
130+
<template #label>
131+
<tooltip-label :title="$t('label.storageid')" />
132+
</template>
133+
<a-select
134+
v-model:value="form.storageid"
135+
:loading="loading"
136+
showSearch
137+
optionFilterProp="label"
138+
:filterOption="(input, option) => {
139+
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
140+
}" >
141+
<a-select-option
142+
v-for="(pool, index) in storagePools"
143+
:value="pool.id"
144+
:key="index"
145+
:label="pool.name">
146+
<span>
147+
<resource-icon v-if="pool.icon" :image="pool.icon.base64image" size="1x" style="margin-right: 5px"/>
148+
<hdd-outlined v-else style="margin-right: 5px"/>
149+
{{ pool.name }}
150+
</span>
151+
</a-select-option>
152+
</a-select>
153+
</a-form-item>
154+
</span>
119155
<a-form-item name="attachVolume" ref="attachVolume" v-if="!createVolumeFromVM">
120156
<template #label>
121157
<tooltip-label :title="$t('label.action.attach.to.instance')" :tooltip="$t('label.attach.vol.to.instance')" />
@@ -170,6 +206,7 @@
170206
import { ref, reactive, toRaw } from 'vue'
171207
import { getAPI, postAPI } from '@/api'
172208
import { mixinForm } from '@/utils/mixin'
209+
import { isAdmin } from '@/role'
173210
import ResourceIcon from '@/components/view/ResourceIcon'
174211
import TooltipLabel from '@/components/widgets/TooltipLabel'
175212
import OwnershipSelection from '@/views/compute/wizard/OwnershipSelection.vue'
@@ -203,11 +240,16 @@ export default {
203240
loading: false,
204241
isCustomizedDiskIOps: false,
205242
virtualmachines: [],
243+
createOnStorage: false,
244+
storagePools: [],
206245
attachVolume: false,
207246
vmidtoattach: null
208247
}
209248
},
210249
computed: {
250+
showStoragePoolSelect () {
251+
return isAdmin() && !this.createVolumeFromSnapshot
252+
},
211253
createVolumeFromVM () {
212254
return this.$route.path.startsWith('/vm/')
213255
},
@@ -299,6 +341,9 @@ export default {
299341
this.zones = json.listzonesresponse.zone || []
300342
this.form.zoneid = this.zones[0].id || ''
301343
this.fetchDiskOfferings(this.form.zoneid)
344+
if (this.createOnStorage) {
345+
this.fetchStoragePools(this.form.zoneid)
346+
}
302347
if (this.attachVolume) {
303348
this.fetchVirtualMachines(this.form.zoneid)
304349
}
@@ -355,6 +400,25 @@ export default {
355400
this.loading = false
356401
})
357402
},
403+
fetchStoragePools (zoneId) {
404+
if (!zoneId) {
405+
this.storagePools = []
406+
return
407+
}
408+
this.loading = true
409+
getAPI('listStoragePools', {
410+
zoneid: zoneId,
411+
showicon: true
412+
}).then(json => {
413+
const pools = json.liststoragepoolsresponse.storagepool || []
414+
this.storagePools = pools.filter(p => p.state === 'Up')
415+
}).catch(error => {
416+
this.$notifyError(error)
417+
this.storagePools = []
418+
}).finally(() => {
419+
this.loading = false
420+
})
421+
},
358422
fetchVirtualMachines (zoneId) {
359423
var params = {
360424
zoneid: zoneId,
@@ -394,6 +458,7 @@ export default {
394458
if (this.customDiskOffering) {
395459
values.size = values.size.trim()
396460
}
461+
delete values.createOnStorage
397462
if (this.createVolumeFromSnapshot) {
398463
values.snapshotid = this.resource.id
399464
}
@@ -467,6 +532,15 @@ export default {
467532
this.attachVolumeApiParams = this.$getApiParams('attachVolume')
468533
this.fetchVirtualMachines(this.form.zoneid)
469534
}
535+
},
536+
onChangeCreateOnStorage () {
537+
this.createOnStorage = !this.createOnStorage
538+
if (this.createOnStorage) {
539+
this.fetchStoragePools(this.form.zoneid)
540+
this.form.storageid = this.storagePools[0]?.id || undefined
541+
} else {
542+
this.form.storageid = undefined
543+
}
470544
}
471545
}
472546
}

0 commit comments

Comments
 (0)