package org.zstack.storage.volume; import org.springframework.beans.factory.annotation.Autowired; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.componentloader.PluginRegistry; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.SimpleQuery; import org.zstack.core.db.SimpleQuery.Op; import org.zstack.core.errorcode.ErrorFacade; import org.zstack.header.Component; import org.zstack.header.apimediator.ApiMessageInterceptionException; import org.zstack.header.apimediator.ApiMessageInterceptor; import org.zstack.header.apimediator.StopRoutingException; import org.zstack.header.errorcode.SysErrors; import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.host.HypervisorType; import org.zstack.header.image.ImageConstant.ImageMediaType; import org.zstack.header.image.ImageState; import org.zstack.header.image.ImageStatus; import org.zstack.header.image.ImageVO; import org.zstack.header.message.APIMessage; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.VmInstanceVO_; import org.zstack.header.volume.*; import static org.zstack.core.Platform.argerr; import static org.zstack.core.Platform.operr; import javax.persistence.Tuple; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Created with IntelliJ IDEA. * User: frank * Time: 10:02 PM * To change this template use File | Settings | File Templates. */ public class VolumeApiInterceptor implements ApiMessageInterceptor, Component { @Autowired private CloudBus bus; @Autowired private DatabaseFacade dbf; @Autowired private ErrorFacade errf; @Autowired private PluginRegistry pluginRgty; private Map<String, MaxDataVolumeNumberExtensionPoint> maxDataVolumeNumberExtensions = new ConcurrentHashMap<String, MaxDataVolumeNumberExtensionPoint>(); private static final int DEFAULT_MAX_DATA_VOLUME_NUMBER = 24; private void setServiceId(APIMessage msg) { if (msg instanceof VolumeMessage) { VolumeMessage vmsg = (VolumeMessage) msg; bus.makeTargetServiceIdByResourceUuid(msg, VolumeConstant.SERVICE_ID, vmsg.getVolumeUuid()); } } @Override public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionException { if (msg instanceof APIChangeVolumeStateMsg) { validate((APIChangeVolumeStateMsg) msg); } else if (msg instanceof APIDeleteDataVolumeMsg) { validate((APIDeleteDataVolumeMsg) msg); } else if (msg instanceof APIBackupDataVolumeMsg) { validate((APIBackupDataVolumeMsg) msg); } else if (msg instanceof APIAttachDataVolumeToVmMsg) { validate((APIAttachDataVolumeToVmMsg) msg); } else if (msg instanceof APIDetachDataVolumeFromVmMsg) { validate((APIDetachDataVolumeFromVmMsg) msg); } else if (msg instanceof APIGetDataVolumeAttachableVmMsg) { validate((APIGetDataVolumeAttachableVmMsg) msg); } else if (msg instanceof APICreateDataVolumeFromVolumeTemplateMsg) { validate((APICreateDataVolumeFromVolumeTemplateMsg) msg); } else if (msg instanceof APIRecoverDataVolumeMsg) { validate((APIRecoverDataVolumeMsg) msg); } setServiceId(msg); return msg; } private void validate(APIRecoverDataVolumeMsg msg) { SimpleQuery<VolumeVO> q = dbf.createQuery(VolumeVO.class); q.add(VolumeVO_.uuid, Op.EQ, msg.getVolumeUuid()); q.add(VolumeVO_.status, Op.EQ, VolumeStatus.Deleted); if (!q.isExists()) { throw new ApiMessageInterceptionException(operr("the volume[uuid:%s] is not in status of deleted. This is operation is to recover a deleted data volume", msg.getVolumeUuid())); } } private void exceptionIsVolumeIsDeleted(String volumeUuid) { SimpleQuery<VolumeVO> q = dbf.createQuery(VolumeVO.class); q.add(VolumeVO_.uuid, Op.EQ, volumeUuid); q.add(VolumeVO_.status, Op.EQ, VolumeStatus.Deleted); if (q.isExists()) { throw new ApiMessageInterceptionException(operr("the volume[uuid:%s] is in status of deleted, cannot do the operation", volumeUuid)); } } private void validate(APICreateDataVolumeFromVolumeTemplateMsg msg) { ImageVO img = dbf.findByUuid(msg.getImageUuid(), ImageVO.class); ImageMediaType type = img.getMediaType(); if (ImageMediaType.DataVolumeTemplate != type) { throw new ApiMessageInterceptionException(argerr("image[uuid:%s] is not %s, it's %s", msg.getImageUuid(), ImageMediaType.DataVolumeTemplate, type)); } if (ImageState.Enabled != img.getState()) { throw new ApiMessageInterceptionException(operr("image[uuid:%s] is not Enabled, it's %s", img.getUuid(), img.getState())); } if (ImageStatus.Ready != img.getStatus()) { throw new ApiMessageInterceptionException(operr("image[uuid:%s] is not Ready, it's %s", img.getUuid(), img.getStatus())); } } private void validate(APIGetDataVolumeAttachableVmMsg msg) { SimpleQuery<VolumeVO> q = dbf.createQuery(VolumeVO.class); q.select(VolumeVO_.vmInstanceUuid, VolumeVO_.state, VolumeVO_.status, VolumeVO_.type); q.add(VolumeVO_.uuid, Op.EQ, msg.getVolumeUuid()); Tuple t = q.findTuple(); VolumeType type = t.get(3, VolumeType.class); if (type == VolumeType.Root) { throw new ApiMessageInterceptionException(argerr("volume[uuid:%s] is Root volume, can not be attach to vm", msg.getVolumeUuid())); } // As per issue #1696, we do not report error if the volume has been attached. // Instead, an empty list will be returned later when handling this message. VolumeState state = t.get(1, VolumeState.class); if (state != VolumeState.Enabled) { throw new ApiMessageInterceptionException(argerr("volume[uuid:%s] is in state[%s], data volume can only be attached when state is %s", msg.getVolumeUuid(), state, VolumeState.Enabled)); } VolumeStatus status = t.get(2, VolumeStatus.class); if (status != VolumeStatus.Ready && status != VolumeStatus.NotInstantiated) { throw new ApiMessageInterceptionException(argerr("volume[uuid:%s] is in status[%s], data volume can only be attached when status is %s or %S", msg.getVolumeUuid(), status, VolumeStatus.Ready, VolumeStatus.NotInstantiated)); } } private void validate(APIDetachDataVolumeFromVmMsg msg) { VolumeVO vol = dbf.findByUuid(msg.getVolumeUuid(), VolumeVO.class); if (!vol.isShareable() && vol.getVmInstanceUuid() == null) { throw new ApiMessageInterceptionException(operr("data volume[uuid:%s] is not attached to any vm, can't detach", msg.getVolumeUuid())); } if (vol.isShareable() && msg.getVmUuid() == null) { throw new ApiMessageInterceptionException(operr("to detach shareable data volume[uuid:%s], vm uuid is needed.", msg.getVolumeUuid())); } if (vol.getType() == VolumeType.Root) { throw new ApiMessageInterceptionException(operr("the volume[uuid:%s, name:%s] is Root Volume, can't detach it", vol.getUuid(), vol.getName())); } } private void validate(APIAttachDataVolumeToVmMsg msg) { VolumeVO vol = dbf.findByUuid(msg.getVolumeUuid(), VolumeVO.class); if (vol.getType() == VolumeType.Root) { throw new ApiMessageInterceptionException(operr("the volume[uuid:%s, name:%s] is Root Volume, can't attach it", vol.getUuid(), vol.getName())); } if (vol.getState() == VolumeState.Disabled) { throw new ApiMessageInterceptionException(operr("data volume[uuid:%s] is Disabled, can't attach", vol.getUuid())); } if (vol.getStatus() == VolumeStatus.Deleted) { throw new ApiMessageInterceptionException(operr("the volume[uuid:%s] is in status of deleted, cannot do the operation", vol.getUuid())); } if (vol.getVmInstanceUuid() != null) { throw new ApiMessageInterceptionException(operr("data volume[%s] has been attached to vm[uuid:%s], can't attach again", vol.getUuid(), vol.getVmInstanceUuid())); } if (VolumeStatus.Ready != vol.getStatus() && VolumeStatus.NotInstantiated != vol.getStatus()) { throw new ApiMessageInterceptionException(operr("data volume can only be attached when status is [%s, %s], current is %s", VolumeStatus.Ready, VolumeStatus.NotInstantiated, vol.getStatus())); } SimpleQuery<VmInstanceVO> q = dbf.createQuery(VmInstanceVO.class); q.select(VmInstanceVO_.hypervisorType); q.add(VmInstanceVO_.uuid, Op.EQ, msg.getVmInstanceUuid()); String hvType = q.findValue(); if (vol.getFormat() != null) { HypervisorType volHvType = VolumeFormat.getMasterHypervisorTypeByVolumeFormat(vol.getFormat()); if (!hvType.equals(volHvType.toString())) { throw new ApiMessageInterceptionException(operr("data volume[uuid:%s] has format[%s] that can only be attached to hypervisor[%s], but vm[uuid:%s] has hypervisor type[%s]. Can't attach", vol.getUuid(), vol.getFormat(), volHvType, msg.getVmInstanceUuid(), hvType)); } } MaxDataVolumeNumberExtensionPoint ext = maxDataVolumeNumberExtensions.get(hvType); int maxDataVolumeNum = DEFAULT_MAX_DATA_VOLUME_NUMBER; if (ext != null) { maxDataVolumeNum = ext.getMaxDataVolumeNumber(); } SimpleQuery<VolumeVO> vq = dbf.createQuery(VolumeVO.class); vq.add(VolumeVO_.type, Op.EQ, VolumeType.Data); vq.add(VolumeVO_.vmInstanceUuid, Op.EQ, msg.getVmInstanceUuid()); long count = vq.count(); if (count + 1 > maxDataVolumeNum) { throw new ApiMessageInterceptionException(operr("hypervisor[%s] only allows max %s data volumes to be attached to a single vm; there have been %s data volumes attached to vm[uuid:%s]", hvType, maxDataVolumeNum, count, msg.getVmInstanceUuid())); } } private void validate(APIBackupDataVolumeMsg msg) { if (isRootVolume(msg.getUuid())) { throw new ApiMessageInterceptionException(operr("it's not allowed to backup root volume, uuid:%s", msg.getUuid())); } exceptionIsVolumeIsDeleted(msg.getVolumeUuid()); } private void validate(APIDeleteDataVolumeMsg msg) { if (!dbf.isExist(msg.getUuid(), VolumeVO.class)) { APIDeleteDataVolumeEvent evt = new APIDeleteDataVolumeEvent(msg.getId()); bus.publish(evt); throw new StopRoutingException(); } SimpleQuery<VolumeVO> q = dbf.createQuery(VolumeVO.class); q.select(VolumeVO_.type, VolumeVO_.status); q.add(VolumeVO_.uuid, Op.EQ, msg.getVolumeUuid()); Tuple t = q.findTuple(); VolumeType type = t.get(0, VolumeType.class); if (type == VolumeType.Root) { throw new ApiMessageInterceptionException(argerr("volume[uuid:%s] is Root volume, can't be deleted", msg.getVolumeUuid())); } VolumeStatus status = t.get(1, VolumeStatus.class); if (status == VolumeStatus.Deleted) { throw new ApiMessageInterceptionException(operr("volume[uuid:%s] is already in status of deleted", msg.getVolumeUuid())); } } private boolean isRootVolume(String uuid) { SimpleQuery<VolumeVO> q = dbf.createQuery(VolumeVO.class); q.select(VolumeVO_.type); q.add(VolumeVO_.uuid, Op.EQ, uuid); VolumeType type = q.findValue(); return type == VolumeType.Root; } private void validate(APIChangeVolumeStateMsg msg) { if (isRootVolume(msg.getUuid())) { throw new ApiMessageInterceptionException(operr("it's not allowed to change state of root volume, uuid:%s", msg.getUuid())); } exceptionIsVolumeIsDeleted(msg.getVolumeUuid()); } private void populateExtensions() { for (MaxDataVolumeNumberExtensionPoint extp : pluginRgty.getExtensionList(MaxDataVolumeNumberExtensionPoint.class)) { MaxDataVolumeNumberExtensionPoint old = maxDataVolumeNumberExtensions.get(extp.getHypervisorTypeForMaxDataVolumeNumberExtension()); if (old != null) { throw new CloudRuntimeException(String.format("duplicate MaxDataVolumeNumberExtensionPoint[%s, %s] for hypervisor type[%s]", old.getClass().getName(), extp.getClass().getName(), extp.getHypervisorTypeForMaxDataVolumeNumberExtension()) ); } maxDataVolumeNumberExtensions.put(extp.getHypervisorTypeForMaxDataVolumeNumberExtension(), extp); } } @Override public boolean start() { populateExtensions(); return true; } @Override public boolean stop() { return true; } }