server: find suitable disk offering for volume upload (#5852)

* server: find suitable disk offering for volume upload

Fixes #5696

* fix npe check

* fixes, refactor, rename method and handle custom iops

* ui: allow offering selection

* list only disk offerings

* show name

* revert error check

* use checkaccess

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2022-02-02 16:35:47 +05:30 committed by GitHub
parent ddd311c695
commit 8adb8df2fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 121 additions and 26 deletions

View File

@ -34,4 +34,6 @@ public interface DiskOfferingDao extends GenericDao<DiskOfferingVO, Long> {
List<DiskOfferingVO> listAllBySizeAndProvisioningType(long size, Storage.ProvisioningType provisioningType);
List<DiskOfferingVO> findCustomDiskOfferings();
}

View File

@ -29,6 +29,7 @@ import javax.persistence.EntityExistsException;
import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao;
import org.springframework.stereotype.Component;
import com.cloud.offering.DiskOffering;
import com.cloud.offering.DiskOffering.Type;
import com.cloud.storage.DiskOfferingVO;
import com.cloud.storage.Storage;
@ -173,6 +174,18 @@ public class DiskOfferingDaoImpl extends GenericDaoBase<DiskOfferingVO, Long> im
}
}
@Override
public List<DiskOfferingVO> findCustomDiskOfferings() {
SearchBuilder<DiskOfferingVO> sb = createSearchBuilder();
sb.and("type", sb.entity().getType(), SearchCriteria.Op.EQ);
sb.and("customized", sb.entity().isCustomized(), SearchCriteria.Op.EQ);
sb.done();
SearchCriteria<DiskOfferingVO> sc = sb.create();
sc.setParameters("customized", true);
sc.setParameters("type", DiskOffering.Type.Disk.toString());
return listBy(sc);
}
@Override
public boolean remove(Long id) {
DiskOfferingVO diskOffering = createForUpdate();

View File

@ -19,11 +19,8 @@ package com.cloud.api.query.dao;
import java.util.List;
import java.util.Map;
import com.cloud.api.ApiDBUtils;
import com.cloud.dc.VsphereStoragePolicyVO;
import com.cloud.dc.dao.VsphereStoragePolicyDao;
import com.cloud.server.ResourceTag;
import com.cloud.user.AccountManager;
import javax.inject.Inject;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiConstants;
@ -32,16 +29,19 @@ import org.apache.cloudstack.context.CallContext;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import com.cloud.api.ApiDBUtils;
import com.cloud.api.query.vo.DiskOfferingJoinVO;
import com.cloud.dc.VsphereStoragePolicyVO;
import com.cloud.dc.dao.VsphereStoragePolicyDao;
import com.cloud.offering.DiskOffering;
import com.cloud.offering.ServiceOffering;
import com.cloud.server.ResourceTag;
import com.cloud.user.AccountManager;
import com.cloud.utils.db.Attribute;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import javax.inject.Inject;
@Component
public class DiskOfferingJoinDaoImpl extends GenericDaoBase<DiskOfferingJoinVO, Long> implements DiskOfferingJoinDao {
public static final Logger s_logger = Logger.getLogger(DiskOfferingJoinDaoImpl.class);

View File

@ -125,6 +125,7 @@ import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.hypervisor.HypervisorCapabilitiesVO;
import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao;
import com.cloud.offering.DiskOffering;
import com.cloud.org.Grouping;
import com.cloud.resource.ResourceState;
import com.cloud.serializer.GsonHelper;
@ -315,6 +316,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
private static final Set<Volume.State> STATES_VOLUME_CANNOT_BE_DESTROYED = new HashSet<>(Arrays.asList(Volume.State.Destroy, Volume.State.Expunging, Volume.State.Expunged, Volume.State.Allocated));
private static final String CUSTOM_DISK_OFFERING_UNIQUE_NAME = "Cloud.com-Custom";
protected VolumeApiServiceImpl() {
_volStateMachine = Volume.State.getStateMachine();
_gson = GsonHelper.getGsonLogger();
@ -478,7 +481,6 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
if (!diskOffering.isCustomized()) {
throw new InvalidParameterValueException("Please specify a custom sized disk offering.");
}
_configMgr.checkDiskOfferingAccess(volumeOwner, diskOffering, zone);
}
@ -489,12 +491,41 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
return UUID.randomUUID().toString();
}
private Long getDefaultCustomOfferingId(Account owner, DataCenter zone) {
DiskOfferingVO diskOfferingVO = _diskOfferingDao.findByUniqueName(CUSTOM_DISK_OFFERING_UNIQUE_NAME);
if (diskOfferingVO == null || !DiskOffering.State.Active.equals(diskOfferingVO.getState())) {
return null;
}
try {
_configMgr.checkDiskOfferingAccess(owner, diskOfferingVO, zone);
return diskOfferingVO.getId();
} catch (PermissionDeniedException ignored) {
}
return null;
}
private Long getCustomDiskOfferingIdForVolumeUpload(Account owner, DataCenter zone) {
Long offeringId = getDefaultCustomOfferingId(owner, zone);
if (offeringId != null) {
return offeringId;
}
List<DiskOfferingVO> offerings = _diskOfferingDao.findCustomDiskOfferings();
for (DiskOfferingVO offering : offerings) {
try {
_configMgr.checkDiskOfferingAccess(owner, offering, zone);
return offering.getId();
} catch (PermissionDeniedException ignored) {}
}
return null;
}
@DB
protected VolumeVO persistVolume(final Account owner, final Long zoneId, final String volumeName, final String url, final String format, final Long diskOfferingId, final Volume.State state) {
return Transaction.execute(new TransactionCallback<VolumeVO>() {
return Transaction.execute(new TransactionCallbackWithException<VolumeVO, CloudRuntimeException>() {
@Override
public VolumeVO doInTransaction(TransactionStatus status) {
VolumeVO volume = new VolumeVO(volumeName, zoneId, -1, -1, -1, new Long(-1), null, null, Storage.ProvisioningType.THIN, 0, Volume.Type.DATADISK);
DataCenter zone = _dcDao.findById(zoneId);
volume.setPoolId(null);
volume.setDataCenterId(zoneId);
volume.setPodId(null);
@ -504,23 +535,22 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
volume.setAccountId((owner == null) ? Account.ACCOUNT_ID_SYSTEM : owner.getAccountId());
volume.setDomainId((owner == null) ? Domain.ROOT_DOMAIN : owner.getDomainId());
if (diskOfferingId == null) {
DiskOfferingVO diskOfferingVO = _diskOfferingDao.findByUniqueName("Cloud.com-Custom");
if (diskOfferingVO != null) {
long defaultDiskOfferingId = diskOfferingVO.getId();
volume.setDiskOfferingId(defaultDiskOfferingId);
Long volumeDiskOfferingId = diskOfferingId;
if (volumeDiskOfferingId == null) {
volumeDiskOfferingId = getCustomDiskOfferingIdForVolumeUpload(owner, zone);
if (volumeDiskOfferingId == null) {
throw new CloudRuntimeException(String.format("Unable to find custom disk offering in zone: %s for volume upload", zone.getUuid()));
}
} else {
volume.setDiskOfferingId(diskOfferingId);
}
DiskOfferingVO diskOfferingVO = _diskOfferingDao.findById(diskOfferingId);
volume.setDiskOfferingId(volumeDiskOfferingId);
DiskOfferingVO diskOfferingVO = _diskOfferingDao.findById(volumeDiskOfferingId);
Boolean isCustomizedIops = diskOfferingVO != null && diskOfferingVO.isCustomizedIops() != null ? diskOfferingVO.isCustomizedIops() : false;
Boolean isCustomizedIops = diskOfferingVO != null && diskOfferingVO.isCustomizedIops() != null ? diskOfferingVO.isCustomizedIops() : false;
if (isCustomizedIops == null || !isCustomizedIops) {
volume.setMinIops(diskOfferingVO.getMinIops());
volume.setMaxIops(diskOfferingVO.getMaxIops());
}
if (isCustomizedIops == null || !isCustomizedIops) {
volume.setMinIops(diskOfferingVO.getMinIops());
volume.setMaxIops(diskOfferingVO.getMaxIops());
}
// volume.setSize(size);

View File

@ -69,7 +69,8 @@
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.propsData.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
}"
@change="onZoneChange" >
<a-select-option :value="zone.id" v-for="zone in zones" :key="zone.id" :label="zone.name || zone.description">
<span>
<resource-icon v-if="zone.icon" :image="zone.icon.base64image" size="1x" style="margin-right: 5px"/>
@ -79,6 +80,22 @@
</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<tooltip-label slot="label" :title="$t('label.diskofferingid')" :tooltip="apiParams.diskofferingid.description"/>
<a-select
v-decorator="['diskofferingid', {}]"
:loading="offeringLoading"
:placeholder="apiParams.diskofferingid.description"
showSearch
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.propsData.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="opt in offerings" :key="opt.id">
{{ opt.name || opt.displaytext }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<tooltip-label slot="label" :title="$t('label.format')" :tooltip="apiParams.format.description"/>
<a-select
@ -133,6 +150,8 @@ export default {
return {
fileList: [],
zones: [],
offerings: [],
offeringLoading: false,
formats: ['RAW', 'VHD', 'VHDX', 'OVA', 'QCOW2'],
zoneSelected: '',
uploadParams: null,
@ -153,11 +172,34 @@ export default {
if (json && json.listzonesresponse && json.listzonesresponse.zone) {
this.zones = json.listzonesresponse.zone
if (this.zones.length > 0) {
this.zoneSelected = this.zones[0].id
this.onZoneChange(this.zones[0].id)
}
}
})
},
onZoneChange (zoneId) {
this.zoneSelected = this.zones[0].id
this.fetchDiskOfferings(zoneId)
},
fetchDiskOfferings (zoneId) {
this.offeringLoading = true
this.offerings = [{ id: -1, name: '' }]
this.form.setFieldsValue({
diskofferingid: undefined
})
api('listDiskOfferings', {
zoneid: zoneId,
listall: true
}).then(json => {
for (var offering of json.listdiskofferingsresponse.diskoffering) {
if (offering.iscustomized) {
this.offerings.push(offering)
}
}
}).finally(() => {
this.offeringLoading = false
})
},
handleRemove (file) {
const index = this.fileList.indexOf(file)
const newFileList = this.fileList.slice()
@ -188,7 +230,7 @@ export default {
}
this.loading = true
api('getUploadParamsForVolume', params).then(json => {
this.uploadParams = (json.postuploadvolumeresponse && json.postuploadvolumeresponse.getuploadparams) ? json.postuploadvolumeresponse.getuploadparams : ''
this.uploadParams = json.postuploadvolumeresponse?.getuploadparams || ''
const { fileList } = this
if (this.fileList.length > 1) {
this.$notification.error({
@ -224,12 +266,20 @@ export default {
}).catch(e => {
this.$notification.error({
message: this.$t('message.upload.failed'),
description: `${this.$t('message.upload.iso.failed.description')} - ${e}`,
description: `${this.$t('message.upload.volume.failed')} - ${e}`,
duration: 0
})
}).finally(() => {
this.loading = false
})
}).catch(e => {
this.$notification.error({
message: this.$t('message.upload.failed'),
description: `${this.$t('message.upload.volume.failed')} - ${e?.response?.data?.postuploadvolumeresponse?.errortext || e}`,
duration: 0
})
}).finally(() => {
this.loading = false
})
})
},