diff --git a/compute/src/main/java/org/zstack/compute/allocator/AvoidHostAllocatorFlow.java b/compute/src/main/java/org/zstack/compute/allocator/AvoidHostAllocatorFlow.java index 34ad9ca063d..4c6c490161b 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/AvoidHostAllocatorFlow.java +++ b/compute/src/main/java/org/zstack/compute/allocator/AvoidHostAllocatorFlow.java @@ -4,10 +4,14 @@ import org.springframework.beans.factory.annotation.Configurable; import org.zstack.core.Platform; import org.zstack.header.allocator.AbstractHostAllocatorFlow; +import org.zstack.header.candidate.CandidateCategories; +import org.zstack.header.candidate.CandidateReasonCodes; +import org.zstack.header.candidate.CandidateRejectReason; import org.zstack.header.host.HostVO; import org.zstack.utils.CollectionUtils; import org.zstack.utils.function.Function; +import java.util.ArrayList; import java.util.List; import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; @@ -17,15 +21,33 @@ public class AvoidHostAllocatorFlow extends AbstractHostAllocatorFlow { public void allocate() { throwExceptionIfIAmTheFirstFlow(); - List ret = CollectionUtils.transformToList(candidates, new Function() { - @Override - public HostVO call(HostVO arg) { - if (!spec.getAvoidHostUuids().contains(arg.getUuid())) { - return arg; + List ret; + if (isCandidateDecisionEnabled()) { + ret = new ArrayList<>(candidates.size()); + for (HostVO candidate : candidates) { + if (spec.getAvoidHostUuids().contains(candidate.getUuid())) { + reject(candidate, CandidateRejectReason.of( + CandidateReasonCodes.HOST_IN_AVOID_LIST, + CandidateCategories.POLICY, + "host is in avoid list" + ).detail("stage", "policy") + .detail("avoidHostUuids", spec.getAvoidHostUuids())); + continue; } - return null; + pass(candidate); + ret.add(candidate); } - }); + } else { + ret = CollectionUtils.transformToList(candidates, new Function() { + @Override + public HostVO call(HostVO arg) { + if (!spec.getAvoidHostUuids().contains(arg.getUuid())) { + return arg; + } + return null; + } + }); + } if (ret.isEmpty()) { fail(Platform.operr(ORG_ZSTACK_COMPUTE_ALLOCATOR_10026, "after rule out avoided host%s, there is no host left in candidates", spec.getAvoidHostUuids())); diff --git a/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java b/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java index f6d721ebb8c..18fb01f2552 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java +++ b/compute/src/main/java/org/zstack/compute/allocator/DesignatedHostAllocatorFlow.java @@ -96,12 +96,18 @@ public void allocate() { } } + if (isCandidateDecisionEnabled()) { + hypervisorType = null; + } + if (zoneUuid == null && CollectionUtils.isEmpty(clusterUuids) && hostUuid == null && hypervisorType == null) { next(candidates); return; } - if (amITheFirstFlow()) { + if (isCandidateDecisionEnabled()) { + candidates = allocate(candidates, zoneUuid, clusterUuids, hostUuid, null); + } else if (amITheFirstFlow()) { candidates = allocate(zoneUuid, clusterUuids, hostUuid, hypervisorType); } else { candidates = allocate(candidates, zoneUuid, clusterUuids, hostUuid, hypervisorType); diff --git a/compute/src/main/java/org/zstack/compute/allocator/FilterFlow.java b/compute/src/main/java/org/zstack/compute/allocator/FilterFlow.java index 18c450bd3b5..eac90bb0e18 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/FilterFlow.java +++ b/compute/src/main/java/org/zstack/compute/allocator/FilterFlow.java @@ -5,9 +5,19 @@ import org.zstack.core.componentloader.PluginRegistry; import org.zstack.header.allocator.AbstractHostAllocatorFlow; import org.zstack.header.allocator.HostAllocatorFilterExtensionPoint; +import org.zstack.header.candidate.CandidateCategories; +import org.zstack.header.candidate.CandidateReasonCodes; +import org.zstack.header.candidate.CandidateRejectReason; import org.zstack.header.errorcode.OperationFailureException; +import org.zstack.header.host.HostVO; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; /** @@ -25,12 +35,26 @@ public void allocate() { for (HostAllocatorFilterExtensionPoint filter : pluginRgty.getExtensionList(HostAllocatorFilterExtensionPoint.class)) { logger.debug(String.format("before being filtered by HostAllocatorFilterExtensionPoint[%s], candidates num: %s", filter.getClass(), candidates.size())); + List before = isCandidateDecisionEnabled() ? new ArrayList<>(candidates) : null; try { candidates = filter.filterHostCandidates(candidates, spec); } catch (OperationFailureException e) { fail(e.getErrorCode()); return; } + if (isCandidateDecisionEnabled()) { + Set after = candidates.stream().map(HostVO::getUuid).collect(Collectors.toSet()); + for (HostVO host : before) { + if (!after.contains(host.getUuid())) { + reject(host, CandidateRejectReason.of( + CandidateReasonCodes.HOST_REJECTED_BY_EXTENSION, + CandidateCategories.POLICY, + filter.filterErrorReason() + ).detail("stage", "extension") + .detail("extension", filter.getClass().getName())); + } + } + } logger.debug(String.format("after being filtered by HostAllocatorFilterExtensionPoint[%s], candidates num: %s", filter.getClass(), candidates.size())); if (candidates.isEmpty()) { diff --git a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorChain.java b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorChain.java index 371d1dfd355..db76106905f 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorChain.java +++ b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorChain.java @@ -9,6 +9,7 @@ import org.zstack.core.db.Q; import org.zstack.core.errorcode.ErrorFacade; import org.zstack.header.allocator.*; +import org.zstack.header.candidate.*; import org.zstack.header.core.ReturnValueCompletion; import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.errorcode.OperationFailureException; @@ -49,10 +50,12 @@ public class HostAllocatorChain implements HostAllocatorTrigger, HostAllocatorSt private int skipCounter = 0; private AbstractHostAllocatorFlow lastFlow; + private List lastFlowInput; private HostAllocationPaginationInfo paginationInfo; private Set seriesErrorWhenPagination = new HashSet<>(); + private CandidateDecisionRecorder decisionRecorder; @Autowired private ErrorFacade errf; @@ -60,6 +63,8 @@ public class HostAllocatorChain implements HostAllocatorTrigger, HostAllocatorSt private PluginRegistry pluginRgty; @Autowired private HostCapacityOverProvisioningManager ratioMgr; + @Autowired + private HostCandidateUniverseBuilder hostCandidateUniverseBuilder; public HostAllocatorSpec getAllocationSpec() { return allocationSpec; @@ -86,6 +91,7 @@ public void setFlows(List flows) { } private void done() { + finishCandidateDecision(); if (result == null) { if (isDryRun) { if (HostAllocatorError.NO_AVAILABLE_HOST.toString().equals(errorCode.getCode())) { @@ -118,6 +124,112 @@ private void done() { } } + private boolean initCandidateDecision() { + if (!allocationSpec.isCandidateDecisionEnabled()) { + return true; + } + + decisionRecorder = new CandidateDecisionRecorder<>(allocationSpec.getCandidateDecisionContext()); + List universe = hostCandidateUniverseBuilder.build(allocationSpec, allocationSpec.getCandidateDecisionContext()); + decisionRecorder.registerHostUniverse(universe); + result = universe; + + if (!universe.isEmpty()) { + return true; + } + + allocationSpec.setCandidateDecisionResult(decisionRecorder.build()); + ErrorCode errCode = err(ORG_ZSTACK_COMPUTE_ALLOCATOR_10018, HostAllocatorError.NO_AVAILABLE_HOST, + "no visible host in candidate universe"); + errCode.withOpaque("candidateDecisionResult", allocationSpec.getCandidateDecisionResult()); + if (isDryRun) { + dryRunCompletion.success(new ArrayList()); + } else { + completion.fail(errCode); + } + return false; + } + + private boolean isDecisionRecorderEnabled() { + return decisionRecorder != null && decisionRecorder.isEnabled(); + } + + private void finishCandidateDecision() { + if (!isDecisionRecorderEnabled()) { + return; + } + + if (result != null) { + decisionRecorder.markFinalCandidates(result.stream().map(HostVO::getUuid).collect(Collectors.toList())); + } + + CandidateDecisionResult candidateDecisionResult = decisionRecorder.build(); + allocationSpec.setCandidateDecisionResult(candidateDecisionResult); + if (errorCode != null) { + errorCode.withOpaque("candidateDecisionResult", candidateDecisionResult); + } + } + + private void recordFlowDiff(List before, List after, AbstractHostAllocatorFlow flow) { + if (!isDecisionRecorderEnabled() || before == null) { + return; + } + + Set afterUuids = after == null ? Collections.emptySet() : + after.stream().map(HostVO::getUuid).collect(Collectors.toSet()); + for (HostVO host : before) { + if (!afterUuids.contains(host.getUuid())) { + decisionRecorder.rejectIfNotRejected(host.getUuid(), fallbackReasonForFlow(flow)); + } + } + } + + private void recordFlowFailure(ErrorCode errCode) { + if (!isDecisionRecorderEnabled()) { + return; + } + + List active = result != null ? result : lastFlowInput; + if (active == null) { + return; + } + + CandidateRejectReason reason = fallbackReasonForFlow(lastFlow); + if (errCode != null && errCode.getDetails() != null) { + reason.setMessage(errCode.getDetails()); + } + for (HostVO host : active) { + decisionRecorder.rejectIfNotRejected(host.getUuid(), reason); + } + } + + private CandidateRejectReason fallbackReasonForFlow(AbstractHostAllocatorFlow flow) { + String code = CandidateReasonCodes.HOST_REJECTED_BY_ALLOCATOR_FLOW; + String category = CandidateCategories.UNKNOWN; + String stage = "allocator"; + String message = "host was rejected by allocator flow"; + + if (flow instanceof HostStateAndHypervisorAllocatorFlow) { + category = CandidateCategories.STATUS; + stage = "status"; + } else if (flow instanceof HostCapacityAllocatorFlow) { + category = CandidateCategories.CAPACITY; + stage = "capacity"; + } else if (flow instanceof HostPrimaryStorageAllocatorFlow) { + code = CandidateReasonCodes.HOST_PRIMARY_STORAGE_REQUIREMENT_NOT_SATISFIED; + category = CandidateCategories.STORAGE; + stage = "storage"; + message = "host does not satisfy primary storage requirement"; + } else if (flow instanceof AvoidHostAllocatorFlow || flow instanceof FilterFlow) { + category = CandidateCategories.POLICY; + stage = "policy"; + } + + return CandidateRejectReason.of(code, category, message) + .detail("stage", stage) + .detail("checker", flow == null ? null : flow.getClass().getSimpleName()); + } + private void startOver() { it = flows.iterator(); result = null; @@ -128,6 +240,7 @@ private void startOver() { private void runFlow(AbstractHostAllocatorFlow flow) { try { lastFlow = flow; + lastFlowInput = result == null ? null : new ArrayList<>(result); flow.setCandidates(result); flow.setSpec(allocationSpec); flow.setTrigger(this); @@ -163,8 +276,13 @@ private void start() { } if (HostAllocatorGlobalConfig.USE_PAGINATION.value(Boolean.class)) { - paginationInfo = new HostAllocationPaginationInfo(); - paginationInfo.setLimit(HostAllocatorGlobalConfig.PAGINATION_LIMIT.value(Integer.class)); + if (!allocationSpec.isCandidateDecisionEnabled()) { + paginationInfo = new HostAllocationPaginationInfo(); + paginationInfo.setLimit(HostAllocatorGlobalConfig.PAGINATION_LIMIT.value(Integer.class)); + } + } + if (!initCandidateDecision()) { + return; } it = flows.iterator(); DebugUtils.Assert(it.hasNext(), "can not run an empty host allocation chain"); @@ -187,6 +305,7 @@ private void dryRun(ReturnValueCompletion> completion) { public void next(List candidates) { DebugUtils.Assert(candidates != null, "cannot pass null to next() method"); DebugUtils.Assert(!candidates.isEmpty(), "cannot pass empty candidates to next() method"); + recordFlowDiff(lastFlowInput, candidates, lastFlow); result = candidates; VmInstanceInventory vm = allocationSpec.getVmInstance(); logger.debug(String.format("[Host Allocation]: flow[%s] successfully found %s candidate hosts for vm[uuid:%s, name:%s]", @@ -230,7 +349,6 @@ public boolean isFirstFlow(AbstractHostAllocatorFlow flow) { @Override public void fail(ErrorCode errorCode) { - result = null; if (seriesErrorWhenPagination.isEmpty()) { logger.debug(String.format("[Host Allocation] flow[%s] failed to allocate host; %s", lastFlow.getClass().getName(), errorCode.getDetails())); @@ -242,9 +360,37 @@ public void fail(ErrorCode errorCode) { logger.debug(this.errorCode.getDetails()); } + recordFlowFailure(this.errorCode); + result = null; done(); } + @Override + public boolean isCandidateDecisionEnabled() { + return isDecisionRecorderEnabled(); + } + + @Override + public void pass(String hostUuid) { + if (isDecisionRecorderEnabled()) { + decisionRecorder.pass(hostUuid); + } + } + + @Override + public void reject(String hostUuid, CandidateRejectReason reason) { + if (isDecisionRecorderEnabled()) { + decisionRecorder.reject(hostUuid, reason); + } + } + + @Override + public void rejectIfNotRejected(String hostUuid, CandidateRejectReason reason) { + if (isDecisionRecorderEnabled()) { + decisionRecorder.rejectIfNotRejected(hostUuid, reason); + } + } + @Override public void allocate(HostAllocatorSpec spec, ReturnValueCompletion> completion) { this.allocationSpec = spec; diff --git a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java index 2f59c6fb49f..2ca7e2ca56c 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java @@ -17,6 +17,9 @@ import org.zstack.header.AbstractService; import org.zstack.header.allocator.*; import org.zstack.header.allocator.datatypes.CpuMemoryCapacityData; +import org.zstack.header.candidate.CandidateDecisionResult; +import org.zstack.header.candidate.CandidateRecord; +import org.zstack.header.candidate.CandidateTypes; import org.zstack.header.cluster.ClusterVO; import org.zstack.header.cluster.ClusterVO_; import org.zstack.header.cluster.ReportHostCapacityMessage; @@ -465,6 +468,7 @@ private void doHandleAllocateHost(final AllocateHostMsg msg, Completion completi public void success(List hosts) { if (hosts.isEmpty()){ reply.setHosts(new ArrayList<>()); + fillCandidateDecisionResult(reply, spec, hosts); bus.reply(msg, reply); completion.success(); return; @@ -474,12 +478,14 @@ public void success(List hosts) { @Override public void success(List returnValue) { reply.setHosts(returnValue); + fillCandidateDecisionResult(reply, spec, returnValue); bus.reply(msg, reply); completion.success(); } @Override public void fail(ErrorCode errorCode) { + attachCandidateDecisionToError(errorCode, spec); reply.setError(errorCode); bus.reply(msg, reply); completion.fail(errorCode); @@ -489,6 +495,7 @@ public void fail(ErrorCode errorCode) { @Override public void fail(ErrorCode errorCode) { + attachCandidateDecisionToError(errorCode, spec); reply.setError(errorCode); bus.reply(msg, reply); completion.fail(errorCode); @@ -514,6 +521,7 @@ public void success(List returnValue) { @Override public void fail(ErrorCode errorCode) { + attachCandidateDecisionToError(errorCode, spec); trigger.fail(errorCode); } }); @@ -537,6 +545,7 @@ public void success(HostInventory returnValue) { @Override public void fail(ErrorCode errorCode) { + attachCandidateDecisionToError(errorCode, spec); trigger.fail(errorCode); } }); @@ -571,6 +580,7 @@ public void handle(Map data) { }).error(new FlowErrorHandler(completion, msg) { @Override public void handle(ErrorCode errCode, Map data) { + attachCandidateDecisionToError(errCode, spec); reply.setError(errCode); bus.reply(msg, reply); completion.fail(errCode); @@ -579,6 +589,54 @@ public void handle(ErrorCode errCode, Map data) { } } + private void fillCandidateDecisionResult(AllocateHostDryRunReply reply, HostAllocatorSpec spec, List sortedHosts) { + if (!spec.isCandidateDecisionEnabled()) { + return; + } + + reply.setCandidateDecisionResult(reorderSelectedCandidates(spec.getCandidateDecisionResult(), sortedHosts)); + } + + private void attachCandidateDecisionToError(ErrorCode errorCode, HostAllocatorSpec spec) { + if (errorCode == null || !spec.isCandidateDecisionEnabled() || spec.getCandidateDecisionResult() == null) { + return; + } + + errorCode.withOpaque("candidateDecisionResult", spec.getCandidateDecisionResult()); + } + + private CandidateDecisionResult reorderSelectedCandidates(CandidateDecisionResult result, List sortedHosts) { + if (result == null || sortedHosts == null || sortedHosts.isEmpty()) { + return result; + } + + Map> selected = new LinkedHashMap<>(); + List> rest = new ArrayList<>(); + for (CandidateRecord record : result.getCandidates()) { + if (CandidateTypes.SELECTED.equals(record.getDecision())) { + selected.put(record.getCandidateUuid(), record); + } else { + rest.add(record); + } + } + + if (selected.isEmpty()) { + return result; + } + + List> reordered = new ArrayList<>(); + for (HostInventory host : sortedHosts) { + CandidateRecord record = selected.remove(host.getUuid()); + if (record != null) { + reordered.add(record); + } + } + reordered.addAll(selected.values()); + reordered.addAll(rest); + result.setCandidates(reordered); + return result; + } + private void handleApiMessage(APIMessage msg) { if (msg instanceof APIGetCpuMemoryCapacityMsg) { handle((APIGetCpuMemoryCapacityMsg) msg); diff --git a/compute/src/main/java/org/zstack/compute/allocator/HostCandidateUniverseBuilder.java b/compute/src/main/java/org/zstack/compute/allocator/HostCandidateUniverseBuilder.java new file mode 100644 index 00000000000..c0d550d339a --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/allocator/HostCandidateUniverseBuilder.java @@ -0,0 +1,66 @@ +package org.zstack.compute.allocator; + +import org.apache.commons.collections.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.SimpleQuery; +import org.zstack.header.allocator.HostAllocatorConstant; +import org.zstack.header.allocator.HostAllocatorSpec; +import org.zstack.header.candidate.CandidateDecisionContext; +import org.zstack.header.host.HostVO; +import org.zstack.header.host.HostVO_; +import org.zstack.header.identity.AccountConstant; +import org.zstack.identity.AccountManager; + +import java.util.Comparator; +import java.util.Collections; +import java.util.List; + +import static org.zstack.core.db.SimpleQuery.Op.EQ; +import static org.zstack.core.db.SimpleQuery.Op.IN; + +public class HostCandidateUniverseBuilder { + @Autowired + private DatabaseFacade dbf; + @Autowired + private AccountManager acntMgr; + + @SuppressWarnings("unchecked") + public List build(HostAllocatorSpec spec, CandidateDecisionContext ctx) { + if (ctx == null || ctx.getAccountUuid() == null) { + return Collections.emptyList(); + } + + SimpleQuery q = dbf.createQuery(HostVO.class); + + String zoneUuid = (String) spec.getExtraData().get(HostAllocatorConstant.LocationSelector.zone); + List clusterUuids = (List) spec.getExtraData().get(HostAllocatorConstant.LocationSelector.cluster); + String hostUuid = (String) spec.getExtraData().get(HostAllocatorConstant.LocationSelector.host); + + if (zoneUuid != null) { + q.add(HostVO_.zoneUuid, EQ, zoneUuid); + } + if (CollectionUtils.isNotEmpty(clusterUuids)) { + q.add(HostVO_.clusterUuid, IN, clusterUuids); + } + if (hostUuid != null) { + q.add(HostVO_.uuid, EQ, hostUuid); + } + + if (!AccountConstant.isAdminPermission(ctx.getAccountUuid())) { + List visibleHostUuids = acntMgr.getResourceUuidsCanAccessByAccount(ctx.getAccountUuid(), HostVO.class); + if (CollectionUtils.isEmpty(visibleHostUuids)) { + return Collections.emptyList(); + } + q.add(HostVO_.uuid, IN, visibleHostUuids); + } + + List hosts = q.list(); + hosts.sort(Comparator + .comparing(HostVO::getZoneUuid, Comparator.nullsFirst(String::compareTo)) + .thenComparing(HostVO::getClusterUuid, Comparator.nullsFirst(String::compareTo)) + .thenComparing(HostVO::getName, Comparator.nullsFirst(String::compareTo)) + .thenComparing(HostVO::getUuid, Comparator.nullsFirst(String::compareTo))); + return hosts; + } +} diff --git a/compute/src/main/java/org/zstack/compute/allocator/HostCapacityAllocatorFlow.java b/compute/src/main/java/org/zstack/compute/allocator/HostCapacityAllocatorFlow.java index dce335e7efd..12499c9fa4e 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/HostCapacityAllocatorFlow.java +++ b/compute/src/main/java/org/zstack/compute/allocator/HostCapacityAllocatorFlow.java @@ -8,11 +8,17 @@ import org.zstack.header.allocator.AbstractHostAllocatorFlow; import org.zstack.header.allocator.HostCapacityOverProvisioningManager; import org.zstack.header.allocator.HostCpuOverProvisioningManager; +import org.zstack.header.candidate.CandidateCategories; +import org.zstack.header.candidate.CandidateReasonCodes; +import org.zstack.header.candidate.CandidateRejectReason; import org.zstack.header.host.HostVO; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; @@ -46,6 +52,54 @@ private List allocate(List vos, long cpu, long memory, long oldM && (memory == 0 || memoryCheck(memory, oldMemory, hvo))).collect(Collectors.toList()); } + private List allocateWithDecision(List vos, long cpu, long memory, long oldMemory) { + List ret = new ArrayList<>(vos.size()); + for (HostVO host : vos) { + if (cpu != 0 && host.getCapacity().getAvailableCpu() < cpu) { + reject(host, CandidateRejectReason.of( + CandidateReasonCodes.HOST_CPU_NOT_ENOUGH, + CandidateCategories.CAPACITY, + "available cpu is not enough" + ).detail("stage", "capacity") + .detail("requiredCpu", cpu) + .detail("availableCpu", host.getCapacity().getAvailableCpu())); + continue; + } + + long availableMemory = ratioMgr.calculateHostAvailableMemoryByRatio(host.getUuid(), host.getCapacity().getAvailableMemory()); + if (memory != 0 && !memoryCheck(memory, oldMemory, host)) { + reject(host, CandidateRejectReason.of( + CandidateReasonCodes.HOST_MEMORY_NOT_ENOUGH, + CandidateCategories.CAPACITY, + "available memory is not enough" + ).detail("stage", "capacity") + .detail("requiredMemory", memory) + .detail("availableMemory", availableMemory) + .detail("totalPhysicalMemory", host.getCapacity().getTotalPhysicalMemory())); + continue; + } + + pass(host); + ret.add(host); + } + + List afterReservedCapacity = reserveMgr.filterOutHostsByReservedCapacity(ret, cpu, memory); + Set passed = new HashSet<>(); + for (HostVO host : afterReservedCapacity) { + passed.add(host.getUuid()); + } + for (HostVO host : ret) { + if (!passed.contains(host.getUuid())) { + reject(host, CandidateRejectReason.of( + CandidateReasonCodes.HOST_RESERVED_CAPACITY_NOT_ENOUGH, + CandidateCategories.CAPACITY, + "host capacity is reserved by global configuration" + ).detail("stage", "capacity")); + } + } + return afterReservedCapacity; + } + private boolean isNoCpu(int cpu) { return !candidates.stream().anyMatch(vo -> vo.getCapacity().getCpuNum() >= cpu); } @@ -58,9 +112,13 @@ private boolean isNoMemory(long mem) { public void allocate() { throwExceptionIfIAmTheFirstFlow(); - List ret = - allocate(candidates, spec.getCpuCapacity(), spec.getMemoryCapacity(), spec.getOldMemoryCapacity()); - ret = reserveMgr.filterOutHostsByReservedCapacity(ret, spec.getCpuCapacity(), spec.getMemoryCapacity()); + List ret; + if (isCandidateDecisionEnabled()) { + ret = allocateWithDecision(candidates, spec.getCpuCapacity(), spec.getMemoryCapacity(), spec.getOldMemoryCapacity()); + } else { + ret = allocate(candidates, spec.getCpuCapacity(), spec.getMemoryCapacity(), spec.getOldMemoryCapacity()); + ret = reserveMgr.filterOutHostsByReservedCapacity(ret, spec.getCpuCapacity(), spec.getMemoryCapacity()); + } if (ret.isEmpty()) { fail(Platform.operr(ORG_ZSTACK_COMPUTE_ALLOCATOR_10021, "no host having cpu[%s], memory[%s bytes] found", diff --git a/compute/src/main/java/org/zstack/compute/allocator/HostStateAndHypervisorAllocatorFlow.java b/compute/src/main/java/org/zstack/compute/allocator/HostStateAndHypervisorAllocatorFlow.java index 7d3e680a5c9..e58e00cecc5 100755 --- a/compute/src/main/java/org/zstack/compute/allocator/HostStateAndHypervisorAllocatorFlow.java +++ b/compute/src/main/java/org/zstack/compute/allocator/HostStateAndHypervisorAllocatorFlow.java @@ -8,6 +8,9 @@ import org.zstack.core.db.SimpleQuery; import org.zstack.core.db.SimpleQuery.Op; import org.zstack.header.allocator.AbstractHostAllocatorFlow; +import org.zstack.header.candidate.CandidateCategories; +import org.zstack.header.candidate.CandidateReasonCodes; +import org.zstack.header.candidate.CandidateRejectReason; import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.host.HostState; import org.zstack.header.host.HostStatus; @@ -59,6 +62,48 @@ private List allocate(List vos, String hypervisorType) { return lst; } + private List allocateWithDecision(List vos, String hypervisorType) { + List lst = new ArrayList(vos.size()); + for (HostVO vo : vos) { + if (vo.getState() != HostState.Enabled) { + reject(vo, CandidateRejectReason.of( + CandidateReasonCodes.HOST_STATE_NOT_ENABLED, + CandidateCategories.STATUS, + "host state is not Enabled" + ).detail("stage", "status") + .detail("expected", HostState.Enabled.toString()) + .detail("actual", vo.getState() == null ? null : vo.getState().toString())); + continue; + } + + if (vo.getStatus() != HostStatus.Connected) { + reject(vo, CandidateRejectReason.of( + CandidateReasonCodes.HOST_STATUS_NOT_CONNECTED, + CandidateCategories.STATUS, + "host status is not Connected" + ).detail("stage", "status") + .detail("expected", HostStatus.Connected.toString()) + .detail("actual", vo.getStatus() == null ? null : vo.getStatus().toString())); + continue; + } + + if (hypervisorType != null && !hypervisorType.equals(vo.getHypervisorType())) { + reject(vo, CandidateRejectReason.of( + CandidateReasonCodes.HOST_HYPERVISOR_TYPE_NOT_MATCHED, + CandidateCategories.STATUS, + "host hypervisor type does not match" + ).detail("stage", "status") + .detail("expected", hypervisorType) + .detail("actual", vo.getHypervisorType())); + continue; + } + + pass(vo); + lst.add(vo); + } + return lst; + } + private boolean isNoEnabledHost() { return !candidates.stream().anyMatch(vo -> HostState.Enabled == vo.getState()); } @@ -74,7 +119,9 @@ private boolean isNoHypervisor(String hyType) { @Override public void allocate() { List ret; - if (amITheFirstFlow()) { + if (isCandidateDecisionEnabled()) { + ret = allocateWithDecision(candidates, spec.getHypervisorType()); + } else if (amITheFirstFlow()) { ret = allocate(spec.getHypervisorType()); } else { ret = allocate(candidates, spec.getHypervisorType()); diff --git a/compute/src/main/java/org/zstack/compute/vm/AbstractVmInstance.java b/compute/src/main/java/org/zstack/compute/vm/AbstractVmInstance.java index ae8202993c0..72780f8b036 100755 --- a/compute/src/main/java/org/zstack/compute/vm/AbstractVmInstance.java +++ b/compute/src/main/java/org/zstack/compute/vm/AbstractVmInstance.java @@ -59,6 +59,7 @@ public abstract class AbstractVmInstance implements VmInstance { GetVmMigrationTargetHostMsg.class.getName(), APIChangeInstanceOfferingMsg.class.getName(), APIGetVmMigrationCandidateHostsMsg.class.getName(), + APIGetVmMigrationCandidatesMsg.class.getName(), APIDetachL3NetworkFromVmMsg.class.getName(), APIChangeVmNicStateMsg.class.getName(), DetachNicFromVmMsg.class.getName(), @@ -107,6 +108,7 @@ public abstract class AbstractVmInstance implements VmInstance { StartVmInstanceMsg.class.getName(), HaStartVmInstanceMsg.class.getName(), APIGetVmStartingCandidateClustersHostsMsg.class.getName(), + APIGetVmStartingCandidatesMsg.class.getName(), GetVmStartingCandidateClustersHostsMsg.class.getName(), DeleteVmCdRomMsg.class.getName(), CreateVmCdRomMsg.class.getName(), @@ -185,6 +187,7 @@ public abstract class AbstractVmInstance implements VmInstance { GetVmMigrationTargetHostMsg.class.getName(), APIChangeInstanceOfferingMsg.class.getName(), APIGetVmMigrationCandidateHostsMsg.class.getName(), + APIGetVmMigrationCandidatesMsg.class.getName(), APIDetachL3NetworkFromVmMsg.class.getName(), APIChangeVmNicStateMsg.class.getName(), DetachNicFromVmMsg.class.getName(), diff --git a/compute/src/main/java/org/zstack/compute/vm/InstantiateVmFromNewCreatedStruct.java b/compute/src/main/java/org/zstack/compute/vm/InstantiateVmFromNewCreatedStruct.java index 4a0d2ba9627..ac10878f695 100644 --- a/compute/src/main/java/org/zstack/compute/vm/InstantiateVmFromNewCreatedStruct.java +++ b/compute/src/main/java/org/zstack/compute/vm/InstantiateVmFromNewCreatedStruct.java @@ -1,5 +1,6 @@ package org.zstack.compute.vm; +import org.zstack.header.candidate.CandidateDecisionContext; import org.zstack.header.vm.APICreateVmInstanceMsg; import org.zstack.header.vm.CreateVmInstanceMsg; import org.zstack.header.vm.InstantiateNewCreatedVmInstanceMsg; @@ -29,9 +30,18 @@ public class InstantiateVmFromNewCreatedStruct { private Map> dataVolumeSystemTagsOnIndex; private List disableL3Networks; private List sshKeyPairUuids; + private CandidateDecisionContext candidateDecisionContext; private final List candidatePrimaryStorageUuidsForRootVolume = new ArrayList<>(); private final List candidatePrimaryStorageUuidsForDataVolume = new ArrayList<>(); + public CandidateDecisionContext getCandidateDecisionContext() { + return candidateDecisionContext; + } + + public void setCandidateDecisionContext(CandidateDecisionContext candidateDecisionContext) { + this.candidateDecisionContext = candidateDecisionContext; + } + public List getCandidatePrimaryStorageUuidsForRootVolume() { return candidatePrimaryStorageUuidsForRootVolume; } @@ -142,6 +152,7 @@ public static InstantiateVmFromNewCreatedStruct fromMessage(InstantiateNewCreate struct.setDataVolumeSystemTagsOnIndex(msg.getDataVolumeSystemTagsOnIndex()); struct.setDisableL3Networks(msg.getDisableL3Networks()); struct.setDiskAOs(msg.getDiskAOs()); + struct.setCandidateDecisionContext(msg.getCandidateDecisionContext()); return struct; } @@ -161,6 +172,7 @@ public static InstantiateVmFromNewCreatedStruct fromMessage(CreateVmInstanceMsg struct.setDataVolumeSystemTagsOnIndex(msg.getDataVolumeSystemTagsOnIndex()); struct.setDisableL3Networks(msg.getDisableL3Networks()); struct.setDiskAOs(msg.getDiskAOs()); + struct.setCandidateDecisionContext(msg.getCandidateDecisionContext()); return struct; } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostFlow.java index 3059e9dfc19..859116a66c6 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostFlow.java @@ -130,6 +130,10 @@ public String call(L3NetworkInventory arg) { } msg.setServiceId(bus.makeLocalServiceId(HostAllocatorConstant.SERVICE_ID)); msg.setVmInstance(spec.getVmInventory()); + msg.setCandidateDecisionContext(spec.getCandidateDecisionContext()); + if (spec.getCandidateDecisionContext() != null) { + msg.setAccountUuid(spec.getCandidateDecisionContext().getAccountUuid()); + } if (spec.getImageSpec() != null && spec.getImageSpec().getSelectedBackupStorage() != null) { msg.setRequiredBackupStorageUuid(spec.getImageSpec().getSelectedBackupStorage().getBackupStorageUuid()); diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostForMigrateVmFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostForMigrateVmFlow.java index 8984957008e..080bb52d016 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostForMigrateVmFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostForMigrateVmFlow.java @@ -63,6 +63,10 @@ public void run(final FlowTrigger chain, final Map data) { msg.setZoneUuid(spec.getVmInventory().getZoneUuid()); msg.setVmInstance(spec.getVmInventory()); msg.setServiceId(bus.makeLocalServiceId(HostAllocatorConstant.SERVICE_ID)); + msg.setCandidateDecisionContext(spec.getCandidateDecisionContext()); + if (spec.getCandidateDecisionContext() != null) { + msg.setAccountUuid(spec.getCandidateDecisionContext().getAccountUuid()); + } msg.setAllocatorStrategy(HostAllocatorConstant.MIGRATE_VM_ALLOCATOR_TYPE); msg.setVmOperation(spec.getCurrentVmOperation().toString()); msg.setRequiredPrimaryStorageUuids(spec.getVmInventory().getAllVolumes().stream() diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostForStoppedVmFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostForStoppedVmFlow.java index 98f39b01372..6cc7fa11a9c 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostForStoppedVmFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostForStoppedVmFlow.java @@ -90,6 +90,10 @@ public String call(L3NetworkInventory arg) { msg.setSoftAvoidHostUuids(spec.getSoftAvoidHostUuids()); msg.setAllocationScene(spec.getAllocationScene()); msg.setAvoidHostUuids(spec.getAvoidHostUuids()); + msg.setCandidateDecisionContext(spec.getCandidateDecisionContext()); + if (spec.getCandidateDecisionContext() != null) { + msg.setAccountUuid(spec.getCandidateDecisionContext().getAccountUuid()); + } amsg = msg; bus.send(amsg, new CloudBusCallBack(chain) { diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java index 0e42d53cc71..ea8ea3f103e 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java @@ -222,6 +222,8 @@ else if (msg instanceof APIAttachVmNicToVmMsg) { validate((APIUpdateVmNicDriverMsg) msg); } else if (msg instanceof APIGetCandidateZonesClustersHostsForCreatingVmMsg) { validate((APIGetCandidateZonesClustersHostsForCreatingVmMsg) msg); + } else if (msg instanceof APIGetVmCreationCandidatesMsg) { + validate((APIGetVmCreationCandidatesMsg) msg); } else if (msg instanceof APIFstrimVmMsg) { validate((APIFstrimVmMsg) msg); } else if (msg instanceof APITakeVmConsoleScreenshotMsg) { @@ -445,6 +447,26 @@ private void validate(APIGetCandidateZonesClustersHostsForCreatingVmMsg msg) { } } + private void validate(APIGetVmCreationCandidatesMsg msg) { + final String instanceOfferingUuid = msg.getInstanceOfferingUuid(); + + if (instanceOfferingUuid == null) { + if (msg.getCpuNum() == null || msg.getMemorySize() == null) { + throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10111, "Missing CPU/memory settings")); + } + } + + ImageVO image = dbf.findByUuid(msg.getImageUuid(), ImageVO.class); + if (image != null && image.getMediaType() == ImageMediaType.ISO) { + if (msg.getRootDiskOfferingUuid() == null) { + if (msg.getRootDiskSize() == null || msg.getRootDiskSize() <= 0) { + throw new OperationFailureException(argerr(ORG_ZSTACK_COMPUTE_VM_10112, "the image[name:%s, uuid:%s] is an ISO, rootDiskSize must be set", + image.getName(), image.getUuid())); + } + } + } + } + private void validate(final APICreateVmCdRomMsg msg) { VmInstanceVO vo = dbf.findByUuid(msg.getVmInstanceUuid(), VmInstanceVO.class); if (!vo.getState().equals(VmInstanceState.Stopped)) { diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java index 332314056b9..f95e48e8e65 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -26,6 +26,9 @@ import org.zstack.core.workflow.ShareFlow; import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.allocator.*; +import org.zstack.header.candidate.CandidateDecisionContext; +import org.zstack.header.candidate.CandidateDecisionResult; +import org.zstack.header.candidate.CandidateTypes; import org.zstack.header.apimediator.ApiMessageInterceptionException; import org.zstack.header.cluster.ClusterInventory; import org.zstack.header.cluster.ClusterState; @@ -866,6 +869,37 @@ public void run(MessageReply re) { }); } + private void handle(final APIGetVmStartingCandidatesMsg msg) { + APIGetVmStartingCandidatesReply reply = new APIGetVmStartingCandidatesReply(); + final GetVmStartingCandidateClustersHostsMsg gmsg = new GetVmStartingCandidateClustersHostsMsg(); + gmsg.setUuid(msg.getUuid()); + CandidateDecisionContext ctx = CandidateDecisionContext.fromApiMessage(msg, CandidateTypes.HOST); + ctx.getRequestScope().put("vmInstanceUuid", msg.getUuid()); + gmsg.setCandidateDecisionContext(ctx); + bus.makeLocalServiceId(gmsg, VmInstanceConstant.SERVICE_ID); + bus.send(gmsg, new CloudBusCallBack(msg) { + @Override + public void run(MessageReply re) { + if (!re.isSuccess()) { + reply.setSuccess(false); + reply.setError(re.getError()); + } else { + GetVmStartingCandidateClustersHostsReply greply = (GetVmStartingCandidateClustersHostsReply) re; + CandidateDecisionResult result = greply.getCandidateDecisionResult(); + if (result == null) { + reply.setError(candidateDecisionMissingError(msg)); + } else { + reply.setClusters(greply.getClusterInventories()); + reply.setCandidates(result.getCandidates()); + reply.setSummary(result.getSummary()); + reply.setTruncated(result.getTruncated()); + } + } + bus.reply(msg, reply); + } + }); + } + private void handle(final GetVmStartingCandidateClustersHostsMsg msg) { thdf.chainSubmit(new ChainTask(msg) { @Override @@ -893,6 +927,7 @@ public void success(AllocateHostDryRunReply returnValue) { reply.setHostInventories(hosts); reply.setClusterInventories(new ArrayList<>()); } + reply.setCandidateDecisionResult(returnValue.getCandidateDecisionResult()); bus.reply(msg, reply); chain.next(); } @@ -930,7 +965,12 @@ private void getStartingCandidateHosts(final NeedReplyMessage msg, final ReturnV if (msg instanceof GetVmStartingCandidateClustersHostsMsg) { // propagate allocation purpose so downstream filters know this is a // candidate-listing call rather than a real allocation - amsg.setPurpose(((GetVmStartingCandidateClustersHostsMsg) msg).getPurpose()); + GetVmStartingCandidateClustersHostsMsg gmsg = (GetVmStartingCandidateClustersHostsMsg) msg; + amsg.setPurpose(gmsg.getPurpose()); + amsg.setCandidateDecisionContext(gmsg.getCandidateDecisionContext()); + if (gmsg.getCandidateDecisionContext() != null) { + amsg.setAccountUuid(gmsg.getCandidateDecisionContext().getAccountUuid()); + } } amsg.setCpuCapacity(self.getCpuNum()); amsg.setMemoryCapacity(self.getMemorySize()); @@ -2054,6 +2094,20 @@ private void changeMetaData(ChangeVmMetaDataMsg msg) { } private void getVmMigrationTargetHost(Message msg, final ReturnValueCompletion> completion) { + getVmMigrationTargetHostDryRun(msg, new ReturnValueCompletion(completion) { + @Override + public void success(AllocateHostDryRunReply returnValue) { + completion.success(returnValue.getHosts()); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + private void getVmMigrationTargetHostDryRun(Message msg, final ReturnValueCompletion completion) { refreshVO(); ErrorCode allowed = validateOperationByState(msg, self.getState(), VmErrors.MIGRATE_ERROR); if (allowed != null) { @@ -2070,6 +2124,10 @@ private void getVmMigrationTargetHost(Message msg, final ReturnValueCompletion()); + AllocateHostDryRunReply reply = new AllocateHostDryRunReply(); + reply.setHosts(new ArrayList<>()); + completion.success(reply); } else { completion.fail(re.getError()); } } else { - completion.success(((AllocateHostDryRunReply) re).getHosts()); + completion.success((AllocateHostDryRunReply) re); } } }); @@ -2111,10 +2178,11 @@ public String getSyncSignature() { @Override public void run(final SyncTaskChain chain) { final GetVmMigrationTargetHostReply reply = new GetVmMigrationTargetHostReply(); - getVmMigrationTargetHost(msg, new ReturnValueCompletion>(msg, chain) { + getVmMigrationTargetHostDryRun(msg, new ReturnValueCompletion(msg, chain) { @Override - public void success(List returnValue) { - reply.setHosts(returnValue); + public void success(AllocateHostDryRunReply returnValue) { + reply.setHosts(returnValue.getHosts()); + reply.setCandidateDecisionResult(returnValue.getCandidateDecisionResult()); bus.reply(msg, reply); chain.next(); } @@ -2135,6 +2203,80 @@ public String getName() { }); } + private void handle(final APIGetVmMigrationCandidatesMsg msg) { + final APIGetVmMigrationCandidatesReply reply = new APIGetVmMigrationCandidatesReply(); + getVmMigrationTargetHostDryRun(msg, new ReturnValueCompletion(msg) { + @Override + public void success(AllocateHostDryRunReply returnValue) { + CandidateDecisionResult result = returnValue.getCandidateDecisionResult(); + if (result == null) { + reply.setError(candidateDecisionMissingError(msg)); + } else { + reply.setCandidates(result.getCandidates()); + reply.setSummary(result.getSummary()); + reply.setTruncated(result.getTruncated()); + } + bus.reply(msg, reply); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + + private ErrorCode candidateDecisionMissingError(APIMessage msg) { + return inerr(ORG_ZSTACK_COMPUTE_VM_10264, + "candidate decision result is missing for diagnostic API[%s], requestUuid[%s]", + msg.getClass().getSimpleName(), msg.getId()); + } + + private CandidateDecisionContext newHostCandidateDecisionContext(APIStartVmInstanceMsg msg) { + CandidateDecisionContext ctx = CandidateDecisionContext.fromApiMessage(msg, CandidateTypes.HOST); + ctx.getRequestScope().put("vmInstanceUuid", msg.getUuid()); + ctx.getRequestScope().put("clusterUuid", msg.getClusterUuid()); + ctx.getRequestScope().put("hostUuid", msg.getHostUuid()); + return ctx; + } + + private CandidateDecisionContext newHostCandidateDecisionContext(APIMigrateVmMsg msg) { + CandidateDecisionContext ctx = CandidateDecisionContext.fromApiMessage(msg, CandidateTypes.HOST); + ctx.getRequestScope().put("vmInstanceUuid", msg.getVmInstanceUuid()); + ctx.getRequestScope().put("hostUuid", msg.getHostUuid()); + return ctx; + } + + private CandidateDecisionContext candidateDecisionContextFromMigrateMessage(MigrateVmMessage msg) { + if (msg instanceof APIMigrateVmMsg) { + return newHostCandidateDecisionContext((APIMigrateVmMsg) msg); + } + if (msg instanceof MigrateVmMsg) { + return ((MigrateVmMsg) msg).getCandidateDecisionContext(); + } + if (msg instanceof MigrateVmInnerMsg) { + return ((MigrateVmInnerMsg) msg).getCandidateDecisionContext(); + } + return null; + } + + private ErrorCode withCandidateDecisionOpaque(ErrorCode outer, ErrorCode source) { + Object result = findCandidateDecisionResult(source); + if (result != null) { + outer.withOpaque("candidateDecisionResult", result); + } + return outer; + } + + private Object findCandidateDecisionResult(ErrorCode errorCode) { + if (errorCode == null) { + return null; + } + Object result = errorCode.getFromOpaque("candidateDecisionResult"); + return result == null ? findCandidateDecisionResult(errorCode.getCause()) : result; + } + private void handle(final AttachDataVolumeToVmMsg msg) { thdf.chainSubmit(new ChainTask(msg) { @Override @@ -3413,6 +3555,8 @@ protected void handleApiMessage(APIMessage msg) { handle((APIChangeVmNicNetworkMsg) msg); } else if (msg instanceof APIGetVmMigrationCandidateHostsMsg) { handle((APIGetVmMigrationCandidateHostsMsg) msg); + } else if (msg instanceof APIGetVmMigrationCandidatesMsg) { + handle((APIGetVmMigrationCandidatesMsg) msg); } else if (msg instanceof APIGetVmAttachableDataVolumeMsg) { handle((APIGetVmAttachableDataVolumeMsg) msg); } else if (msg instanceof APIUpdateVmInstanceMsg) { @@ -3475,6 +3619,8 @@ protected void handleApiMessage(APIMessage msg) { handle((APIGetVmHostnameMsg) msg); } else if (msg instanceof APIGetVmStartingCandidateClustersHostsMsg) { handle((APIGetVmStartingCandidateClustersHostsMsg) msg); + } else if (msg instanceof APIGetVmStartingCandidatesMsg) { + handle((APIGetVmStartingCandidatesMsg) msg); } else if (msg instanceof APIGetVmCapabilitiesMsg) { handle((APIGetVmCapabilitiesMsg) msg); } else if (msg instanceof APISetVmSshKeyMsg) { @@ -7378,6 +7524,7 @@ private void doMigrateVm(final MigrateVmMessage msg, final Completion completion changeVmStateInDb(VmInstanceStateEvent.migrating); spec.setMessage((Message) msg); spec.setAllocationScene(msg.getAllocationScene()); + spec.setCandidateDecisionContext(candidateDecisionContextFromMigrateMessage(msg)); FlowChain chain = getMigrateVmWorkFlowChain(inv); setFlowMarshaller(chain); @@ -7423,7 +7570,7 @@ public void done() { }).error(new FlowErrorHandler(completion) { @Override public void handle(final ErrorCode errCode, Map data) { - String destHostUuid = spec.getDestHost().getUuid().equals(lastHostUuid) ? null : spec.getDestHost().getUuid(); + String destHostUuid = spec.getDestHost() == null || spec.getDestHost().getUuid().equals(lastHostUuid) ? null : spec.getDestHost().getUuid(); extEmitter.failedToMigrateVm(VmInstanceInventory.valueOf(self), destHostUuid, errCode, new NoErrorCompletion(completion) { @Override public void done() { @@ -7527,6 +7674,11 @@ protected void startVm(final Message msg, final Completion completion) { } else { struct.setStrategy(VmCreationStrategy.InstantStart); } + if (msg instanceof APIStartVmInstanceMsg) { + struct.setCandidateDecisionContext(newHostCandidateDecisionContext((APIStartVmInstanceMsg) msg)); + } else if (msg instanceof StartVmInstanceMsg) { + struct.setCandidateDecisionContext(((StartVmInstanceMsg) msg).getCandidateDecisionContext()); + } instantiateVmFromNewCreate(struct, completion); return; @@ -7544,6 +7696,7 @@ protected void startVm(final Message msg, final Completion completion) { if (msg instanceof APIStartVmInstanceMsg) { APIStartVmInstanceMsg amsg = (APIStartVmInstanceMsg) msg; + spec.setCandidateDecisionContext(newHostCandidateDecisionContext(amsg)); spec.setRequiredClusterUuid(amsg.getClusterUuid()); spec.setRequiredHostUuid(amsg.getHostUuid()); spec.setUsbRedirect(Boolean.parseBoolean(VmSystemTags.USB_REDIRECT.getTokenByResourceUuid(self.getUuid(), VmSystemTags.USB_REDIRECT_TOKEN))); @@ -7564,6 +7717,7 @@ protected void startVm(final Message msg, final Completion completion) { } spec.setAvoidHostUuids(((StartVmInstanceMsg) msg).getAvoidHostUuids()); spec.setCreatePaused(((StartVmInstanceMsg) msg).isStartPaused()); + spec.setCandidateDecisionContext(((StartVmInstanceMsg) msg).getCandidateDecisionContext()); } else if (msg instanceof RestoreVmInstanceMsg) { spec.setMemorySnapshotUuid(((RestoreVmInstanceMsg) msg).getMemorySnapshotUuid()); } @@ -7664,6 +7818,7 @@ private VmInstanceSpec buildVmInstanceSpecFromStruct(InstantiateVmFromNewCreated spec.setDataVolumeSystemTagsOnIndex(struct.getDataVolumeSystemTagsOnIndex()); spec.setDisableL3Networks(struct.getDisableL3Networks()); spec.setStrategy(struct.getStrategy()); + spec.setCandidateDecisionContext(struct.getCandidateDecisionContext()); spec.setVmInventory(getSelfInventory()); if (struct.getL3NetworkUuids() != null && !struct.getL3NetworkUuids().isEmpty()) { @@ -7933,7 +8088,7 @@ public void handle(final ErrorCode errCode, Map data) { logger.warn(e.getMessage()); } - completion.fail(operr(ORG_ZSTACK_COMPUTE_VM_10289, errCode, errCode.getDetails())); + completion.fail(withCandidateDecisionOpaque(operr(ORG_ZSTACK_COMPUTE_VM_10289, errCode, errCode.getDetails()), errCode)); } }).start(); } @@ -7952,7 +8107,7 @@ public void success() { @Override public void fail(ErrorCode errorCode) { StartVmInstanceReply reply = new StartVmInstanceReply(); - reply.setError(err(ORG_ZSTACK_COMPUTE_VM_10290, VmErrors.START_ERROR, errorCode, errorCode.getDetails())); + reply.setError(withCandidateDecisionOpaque(err(ORG_ZSTACK_COMPUTE_VM_10290, VmErrors.START_ERROR, errorCode, errorCode.getDetails()), errorCode)); bus.reply(msg, reply); taskChain.next(); } @@ -7976,7 +8131,7 @@ public void success() { @Override public void fail(ErrorCode errorCode) { APIStartVmInstanceEvent evt = new APIStartVmInstanceEvent(msg.getId()); - evt.setError(err(ORG_ZSTACK_COMPUTE_VM_10291, VmErrors.START_ERROR, errorCode, errorCode.getDetails())); + evt.setError(withCandidateDecisionOpaque(err(ORG_ZSTACK_COMPUTE_VM_10291, VmErrors.START_ERROR, errorCode, errorCode.getDetails()), errorCode)); bus.publish(evt); taskChain.next(); } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java index 321c7b3325d..452f242cc4a 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java @@ -30,6 +30,9 @@ import org.zstack.header.allocator.AllocateHostDryRunReply; import org.zstack.header.allocator.DesignatedAllocateHostMsg; import org.zstack.header.allocator.HostAllocatorConstant; +import org.zstack.header.candidate.CandidateDecisionContext; +import org.zstack.header.candidate.CandidateDecisionResult; +import org.zstack.header.candidate.CandidateTypes; import org.zstack.header.apimediator.ApiMessageInterceptionException; import org.zstack.header.apimediator.GlobalApiMessageInterceptor; import org.zstack.header.cluster.ClusterInventory; @@ -229,6 +232,8 @@ private void handleApiMessage(APIMessage msg) { handle((APIDeleteVmNicMsg) msg); } else if (msg instanceof APIGetCandidateZonesClustersHostsForCreatingVmMsg) { handle((APIGetCandidateZonesClustersHostsForCreatingVmMsg) msg); + } else if (msg instanceof APIGetVmCreationCandidatesMsg) { + handle((APIGetVmCreationCandidatesMsg) msg); } else if (msg instanceof APIGetCandidatePrimaryStoragesForCreatingVmMsg) { handle((APIGetCandidatePrimaryStoragesForCreatingVmMsg) msg); } else if (msg instanceof APIGetInterdependentL3NetworksImagesMsg) { @@ -774,6 +779,162 @@ public void run(MessageReply reply) { }); } + private void handle(APIGetVmCreationCandidatesMsg msg) { + DesignatedAllocateHostMsg amsg = new DesignatedAllocateHostMsg(); + + ImageVO image = dbf.findByUuid(msg.getImageUuid(), ImageVO.class); + amsg.setImage(ImageInventory.valueOf(image)); + amsg.setZoneUuid(msg.getZoneUuid()); + amsg.setClusterUuid(msg.getClusterUuid()); + + InstanceOfferingVO insvo = null; + if (msg.getInstanceOfferingUuid() == null) { + amsg.setCpuCapacity(msg.getCpuNum()); + amsg.setMemoryCapacity(msg.getMemorySize()); + } else { + insvo = dbf.findByUuid(msg.getInstanceOfferingUuid(), InstanceOfferingVO.class); + amsg.setCpuCapacity(insvo.getCpuNum()); + amsg.setMemoryCapacity(insvo.getMemorySize()); + } + + long diskSize = 0; + List diskOfferings = new ArrayList<>(); + if (msg.getDataDiskOfferingUuids() != null) { + SimpleQuery q = dbf.createQuery(DiskOfferingVO.class); + q.add(DiskOfferingVO_.uuid, Op.IN, msg.getDataDiskOfferingUuids()); + List dvos = q.list(); + diskOfferings.addAll(DiskOfferingInventory.valueOf(dvos)); + } + + if (image.getMediaType() == ImageMediaType.ISO) { + if (msg.getRootDiskOfferingUuid() == null) { + diskSize = msg.getRootDiskSize(); + } else { + DiskOfferingVO rootDiskOffering = dbf.findByUuid(msg.getRootDiskOfferingUuid(), DiskOfferingVO.class); + diskOfferings.add(DiskOfferingInventory.valueOf(rootDiskOffering)); + } + } else { + diskSize = image.getSize(); + } + + diskSize += diskOfferings.stream().mapToLong(DiskOfferingInventory::getDiskSize).sum(); + amsg.setDiskSize(diskSize); + amsg.setL3NetworkUuids(msg.getL3NetworkUuids()); + amsg.setVmOperation(VmOperation.NewCreate.toString()); + amsg.setDryRun(true); + amsg.setListAllHosts(true); + amsg.setAllocatorStrategy(HostAllocatorConstant.DESIGNATED_HOST_ALLOCATOR_STRATEGY_TYPE); + if (msg.getSession() != null) { + amsg.setAccountUuid(msg.getSession().getAccountUuid()); + } + + CandidateDecisionContext ctx = newHostCandidateDecisionContext(msg); + amsg.setCandidateDecisionContext(ctx); + + if (image.getBackupStorageRefs().size() == 1) { + amsg.setRequiredBackupStorageUuid(image.getBackupStorageRefs().iterator().next().getBackupStorageUuid()); + } else { + if (msg.getZoneUuid() == null) { + throw new OperationFailureException(argerr(ORG_ZSTACK_COMPUTE_VM_10237, "zoneUuid must be set because the image[name:%s, uuid:%s] is on multiple backup storage", + image.getName(), image.getUuid())); + } + + ImageBackupStorageSelector selector = new ImageBackupStorageSelector(); + selector.setZoneUuid(msg.getZoneUuid()); + selector.setImageUuid(image.getUuid()); + amsg.setRequiredBackupStorageUuid(selector.select()); + } + + VmInstanceInventory vm = new VmInstanceInventory(); + vm.setUuid(Platform.FAKE_UUID); + vm.setImageUuid(image.getUuid()); + if (insvo == null) { + vm.setCpuNum(msg.getCpuNum()); + vm.setMemorySize(msg.getMemorySize()); + } else { + vm.setInstanceOfferingUuid(insvo.getUuid()); + vm.setCpuNum(insvo.getCpuNum()); + vm.setMemorySize(insvo.getMemorySize()); + } + vm.setDefaultL3NetworkUuid(msg.getDefaultL3NetworkUuid() == null ? msg.getL3NetworkUuids().get(0) : msg.getDefaultL3NetworkUuid()); + vm.setName("for-getting-vm-creation-candidates"); + amsg.setVmInstance(vm); + if (msg.getSystemTags() != null && !msg.getSystemTags().isEmpty()) { + amsg.setSystemTags(new ArrayList(msg.getSystemTags())); + } + + APIGetVmCreationCandidatesReply areply = new APIGetVmCreationCandidatesReply(); + bus.makeLocalServiceId(amsg, HostAllocatorConstant.SERVICE_ID); + bus.send(amsg, new CloudBusCallBack(msg) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + areply.setError(reply.getError()); + } else { + AllocateHostDryRunReply re = reply.castReply(); + CandidateDecisionResult result = requireCandidateDecisionResult(re, msg); + if (result == null) { + areply.setError(candidateDecisionMissingError(msg)); + } else { + areply.setCandidates(result.getCandidates()); + areply.setSummary(result.getSummary()); + areply.setTruncated(result.getTruncated()); + fillCreationCandidateDestinations(areply, re.getHosts()); + } + } + + bus.reply(msg, areply); + } + }); + } + + private CandidateDecisionResult requireCandidateDecisionResult(AllocateHostDryRunReply reply, APIMessage msg) { + return reply == null ? null : reply.getCandidateDecisionResult(); + } + + private CandidateDecisionContext newHostCandidateDecisionContext(APICreateVmInstanceMsg msg) { + CandidateDecisionContext ctx = CandidateDecisionContext.fromApiMessage(msg, CandidateTypes.HOST); + ctx.getRequestScope().put("zoneUuid", msg.getZoneUuid()); + ctx.getRequestScope().put("clusterUuid", msg.getClusterUuid()); + ctx.getRequestScope().put("hostUuid", msg.getHostUuid()); + ctx.getRequestScope().put("imageUuid", msg.getImageUuid()); + ctx.getRequestScope().put("instanceOfferingUuid", msg.getInstanceOfferingUuid()); + ctx.getRequestScope().put("rootDiskOfferingUuid", msg.getRootDiskOfferingUuid()); + ctx.getRequestScope().put("dataDiskOfferingUuids", msg.getDataDiskOfferingUuids()); + return ctx; + } + + private CandidateDecisionContext newHostCandidateDecisionContext(APIGetVmCreationCandidatesMsg msg) { + CandidateDecisionContext ctx = CandidateDecisionContext.fromApiMessage(msg, CandidateTypes.HOST); + ctx.getRequestScope().put("zoneUuid", msg.getZoneUuid()); + ctx.getRequestScope().put("clusterUuid", msg.getClusterUuid()); + ctx.getRequestScope().put("imageUuid", msg.getImageUuid()); + ctx.getRequestScope().put("instanceOfferingUuid", msg.getInstanceOfferingUuid()); + ctx.getRequestScope().put("rootDiskOfferingUuid", msg.getRootDiskOfferingUuid()); + ctx.getRequestScope().put("dataDiskOfferingUuids", msg.getDataDiskOfferingUuids()); + return ctx; + } + + private ErrorCode candidateDecisionMissingError(APIMessage msg) { + return inerr(ORG_ZSTACK_COMPUTE_VM_10263, + "candidate decision result is missing for diagnostic API[%s], requestUuid[%s]", + msg.getClass().getSimpleName(), msg.getId()); + } + + private void fillCreationCandidateDestinations(APIGetVmCreationCandidatesReply reply, List hosts) { + if (!hosts.isEmpty()) { + Set clusterUuids = hosts.stream().map(HostInventory::getClusterUuid).collect(Collectors.toSet()); + List clusters = ClusterInventory.valueOf(dbf.listByPrimaryKeys(clusterUuids, ClusterVO.class)); + reply.setClusters(clusters); + + Set zoneUuids = clusters.stream().map(ClusterInventory::getZoneUuid).collect(Collectors.toSet()); + reply.setZones(ZoneInventory.valueOf(dbf.listByPrimaryKeys(zoneUuids, ZoneVO.class))); + } else { + reply.setClusters(new ArrayList<>()); + reply.setZones(new ArrayList<>()); + } + } + private void handle(APIGetCandidatePrimaryStoragesForCreatingVmMsg msg) { APIGetCandidatePrimaryStoragesForCreatingVmReply reply = new APIGetCandidatePrimaryStoragesForCreatingVmReply(); List msgs = new ArrayList<>(); @@ -1312,6 +1473,7 @@ public void run(FlowTrigger trigger, Map data) { smsg.setVmInstanceInventory(VmInstanceInventory.valueOf(finalVo)); smsg.setCandidatePrimaryStorageUuidsForDataVolume(msg.getCandidatePrimaryStorageUuidsForDataVolume()); smsg.setCandidatePrimaryStorageUuidsForRootVolume(msg.getCandidatePrimaryStorageUuidsForRootVolume()); + smsg.setCandidateDecisionContext(msg.getCandidateDecisionContext()); if (Objects.equals(msg.getStrategy(), VmCreationStrategy.InstantStart.toString()) && attachOtherDisk) { smsg.setStrategy(VmCreationStrategy.CreateStopped.toString()); } else { @@ -1392,6 +1554,7 @@ public void run(FlowTrigger trigger, Map data) { StartVmInstanceMsg smsg = new StartVmInstanceMsg(); smsg.setVmInstanceUuid(instantiateVm.getUuid()); smsg.setHostUuid(instantiateVm.getLastHostUuid()); + smsg.setCandidateDecisionContext(msg.getCandidateDecisionContext()); bus.makeTargetServiceIdByResourceUuid(smsg, VmInstanceConstant.SERVICE_ID, finalVo.getUuid()); bus.send(smsg, new CloudBusCallBack(trigger) { @Override @@ -1486,7 +1649,9 @@ public void fail(ErrorCode errorCode) { } private void handle(final APICreateVmInstanceMsg msg) { - doCreateVmInstance(VmInstanceUtils.fromAPICreateVmInstanceMsg(msg), msg, new ReturnValueCompletion(msg) { + CreateVmInstanceMsg cmsg = VmInstanceUtils.fromAPICreateVmInstanceMsg(msg); + cmsg.setCandidateDecisionContext(newHostCandidateDecisionContext(msg)); + doCreateVmInstance(cmsg, msg, new ReturnValueCompletion(msg) { APICreateVmInstanceEvent evt = new APICreateVmInstanceEvent(msg.getId()); @Override diff --git a/conf/serviceConfig/vmInstance.xml b/conf/serviceConfig/vmInstance.xml index e24559a05c5..c64504a769c 100755 --- a/conf/serviceConfig/vmInstance.xml +++ b/conf/serviceConfig/vmInstance.xml @@ -66,6 +66,9 @@ org.zstack.header.vm.APIGetVmMigrationCandidateHostsMsg + + org.zstack.header.vm.APIGetVmMigrationCandidatesMsg + org.zstack.header.vm.APIGetVmAttachableDataVolumeMsg @@ -150,6 +153,9 @@ org.zstack.header.vm.APIGetVmStartingCandidateClustersHostsMsg + + org.zstack.header.vm.APIGetVmStartingCandidatesMsg + org.zstack.header.vm.APIGetVmCapabilitiesMsg @@ -165,6 +171,9 @@ org.zstack.header.vm.APIGetCandidateZonesClustersHostsForCreatingVmMsg + + org.zstack.header.vm.APIGetVmCreationCandidatesMsg + org.zstack.header.vm.APIGetCandidatePrimaryStoragesForCreatingVmMsg diff --git a/conf/springConfigXml/HostAllocatorManager.xml b/conf/springConfigXml/HostAllocatorManager.xml index 6370d63ea00..3928cb95bf0 100755 --- a/conf/springConfigXml/HostAllocatorManager.xml +++ b/conf/springConfigXml/HostAllocatorManager.xml @@ -20,6 +20,8 @@ + + SimulatorPrimaryStorage diff --git a/header/src/main/java/org/zstack/header/allocator/AbstractHostAllocatorFlow.java b/header/src/main/java/org/zstack/header/allocator/AbstractHostAllocatorFlow.java index 2ed50c6131f..0c7a4431841 100755 --- a/header/src/main/java/org/zstack/header/allocator/AbstractHostAllocatorFlow.java +++ b/header/src/main/java/org/zstack/header/allocator/AbstractHostAllocatorFlow.java @@ -3,6 +3,7 @@ import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Configurable; import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.candidate.CandidateRejectReason; import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.host.HostVO; @@ -64,6 +65,28 @@ protected void skip() { trigger.skip(); } + protected boolean isCandidateDecisionEnabled() { + return trigger.isCandidateDecisionEnabled(); + } + + protected void pass(HostVO host) { + if (host != null) { + trigger.pass(host.getUuid()); + } + } + + protected void reject(HostVO host, CandidateRejectReason reason) { + if (host != null) { + trigger.reject(host.getUuid(), reason.detail("checker", getClass().getSimpleName())); + } + } + + protected void rejectIfNotRejected(HostVO host, CandidateRejectReason reason) { + if (host != null) { + trigger.rejectIfNotRejected(host.getUuid(), reason.detail("checker", getClass().getSimpleName())); + } + } + protected void fail(String reason) { if (paginationInfo != null && !trigger.isFirstFlow(this)) { // in pagination, and a middle flow fails, we can continue diff --git a/header/src/main/java/org/zstack/header/allocator/AllocateHostDryRunReply.java b/header/src/main/java/org/zstack/header/allocator/AllocateHostDryRunReply.java index 5214f3281b6..5ee955f73f5 100755 --- a/header/src/main/java/org/zstack/header/allocator/AllocateHostDryRunReply.java +++ b/header/src/main/java/org/zstack/header/allocator/AllocateHostDryRunReply.java @@ -1,5 +1,6 @@ package org.zstack.header.allocator; +import org.zstack.header.candidate.CandidateDecisionResult; import org.zstack.header.host.HostInventory; import org.zstack.header.message.MessageReply; @@ -7,6 +8,7 @@ public class AllocateHostDryRunReply extends MessageReply { private List hosts; + private CandidateDecisionResult candidateDecisionResult; public AllocateHostDryRunReply() { } @@ -18,4 +20,12 @@ public List getHosts() { public void setHosts(List hosts) { this.hosts = hosts; } + + public CandidateDecisionResult getCandidateDecisionResult() { + return candidateDecisionResult; + } + + public void setCandidateDecisionResult(CandidateDecisionResult candidateDecisionResult) { + this.candidateDecisionResult = candidateDecisionResult; + } } diff --git a/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java b/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java index cb0ddeeb081..608e04127f3 100755 --- a/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java +++ b/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java @@ -1,6 +1,7 @@ package org.zstack.header.allocator; import org.zstack.header.configuration.DiskOfferingInventory; +import org.zstack.header.candidate.CandidateDecisionContext; import org.zstack.header.image.ImageInventory; import org.zstack.header.message.NeedReplyMessage; import org.zstack.header.vm.VmInstanceInventory; @@ -32,6 +33,7 @@ public class AllocateHostMsg extends NeedReplyMessage { private AllocationScene allocationScene; private String architecture; private String accountUuid; + private CandidateDecisionContext candidateDecisionContext; /** * Allocation purpose. Defaults to ALLOCATE; LIST_CANDIDATES tells the * downstream filters this is a candidate-listing call. Callers must gate @@ -230,4 +232,12 @@ public HostAllocationPurpose getPurpose() { public void setPurpose(HostAllocationPurpose purpose) { this.purpose = purpose == null ? HostAllocationPurpose.ALLOCATE : purpose; } + + public CandidateDecisionContext getCandidateDecisionContext() { + return candidateDecisionContext; + } + + public void setCandidateDecisionContext(CandidateDecisionContext candidateDecisionContext) { + this.candidateDecisionContext = candidateDecisionContext; + } } diff --git a/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java b/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java index 8b9adfadc54..9162cc4f337 100755 --- a/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java +++ b/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java @@ -1,6 +1,8 @@ package org.zstack.header.allocator; import org.zstack.header.configuration.DiskOfferingInventory; +import org.zstack.header.candidate.CandidateDecisionContext; +import org.zstack.header.candidate.CandidateDecisionResult; import org.zstack.header.image.ImageInventory; import org.zstack.header.vm.VmInstanceInventory; @@ -38,6 +40,8 @@ public class HostAllocatorSpec { private AllocationScene allocationScene; private String architecture; private String accountUuid; + private CandidateDecisionContext candidateDecisionContext; + private CandidateDecisionResult candidateDecisionResult; /** * Allocation purpose. Defaults to ALLOCATE so existing call sites keep * their current behavior. Filters may relax some checks (e.g. PCI device @@ -246,6 +250,26 @@ public void setPurpose(HostAllocationPurpose purpose) { this.purpose = purpose == null ? HostAllocationPurpose.ALLOCATE : purpose; } + public CandidateDecisionContext getCandidateDecisionContext() { + return candidateDecisionContext; + } + + public void setCandidateDecisionContext(CandidateDecisionContext candidateDecisionContext) { + this.candidateDecisionContext = candidateDecisionContext; + } + + public CandidateDecisionResult getCandidateDecisionResult() { + return candidateDecisionResult; + } + + public void setCandidateDecisionResult(CandidateDecisionResult candidateDecisionResult) { + this.candidateDecisionResult = candidateDecisionResult; + } + + public boolean isCandidateDecisionEnabled() { + return candidateDecisionContext != null; + } + public static HostAllocatorSpec fromAllocationMsg(AllocateHostMsg msg) { HostAllocatorSpec spec = new HostAllocatorSpec(); spec.setAllocatorStrategy(msg.getAllocatorStrategy()); @@ -276,6 +300,7 @@ public static HostAllocatorSpec fromAllocationMsg(AllocateHostMsg msg) { spec.setArchitecture(msg.getArchitecture()); spec.setAccountUuid(msg.getAccountUuid()); spec.setPurpose(msg.getPurpose()); + spec.setCandidateDecisionContext(msg.getCandidateDecisionContext()); if (msg.getSystemTags() != null && !msg.getSystemTags().isEmpty()){ spec.setSystemTags(new ArrayList(msg.getSystemTags())); } diff --git a/header/src/main/java/org/zstack/header/allocator/HostAllocatorTrigger.java b/header/src/main/java/org/zstack/header/allocator/HostAllocatorTrigger.java index 6685cb66598..879fc8b1bd8 100755 --- a/header/src/main/java/org/zstack/header/allocator/HostAllocatorTrigger.java +++ b/header/src/main/java/org/zstack/header/allocator/HostAllocatorTrigger.java @@ -1,6 +1,7 @@ package org.zstack.header.allocator; import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.candidate.CandidateRejectReason; import org.zstack.header.host.HostVO; import java.util.List; @@ -15,4 +16,12 @@ public interface HostAllocatorTrigger { boolean isFirstFlow(AbstractHostAllocatorFlow flow); void fail(ErrorCode errorCode); + + boolean isCandidateDecisionEnabled(); + + void pass(String hostUuid); + + void reject(String hostUuid, CandidateRejectReason reason); + + void rejectIfNotRejected(String hostUuid, CandidateRejectReason reason); } diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateCategories.java b/header/src/main/java/org/zstack/header/candidate/CandidateCategories.java new file mode 100644 index 00000000000..3b8935e358e --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateCategories.java @@ -0,0 +1,11 @@ +package org.zstack.header.candidate; + +public interface CandidateCategories { + String STATUS = "STATUS"; + String CAPACITY = "CAPACITY"; + String STORAGE = "STORAGE"; + String NETWORK = "NETWORK"; + String DEVICE = "DEVICE"; + String POLICY = "POLICY"; + String UNKNOWN = "UNKNOWN"; +} diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateDecisionContext.java b/header/src/main/java/org/zstack/header/candidate/CandidateDecisionContext.java new file mode 100644 index 00000000000..da7b9b9402a --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateDecisionContext.java @@ -0,0 +1,84 @@ +package org.zstack.header.candidate; + +import org.zstack.header.message.APIMessage; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class CandidateDecisionContext { + private String requestUuid; + private String apiName; + private String accountUuid; + private String userUuid; + private String resourceType; + private Map requestScope = new LinkedHashMap<>(); + private Map metadata = new LinkedHashMap<>(); + + public static CandidateDecisionContext fromApiMessage(APIMessage msg, String resourceType) { + CandidateDecisionContext ctx = new CandidateDecisionContext(); + ctx.setRequestUuid(msg.getId()); + ctx.setApiName(msg.getClass().getSimpleName()); + ctx.setResourceType(resourceType); + if (msg.getSession() != null) { + ctx.setAccountUuid(msg.getSession().getAccountUuid()); + ctx.setUserUuid(msg.getSession().getUserUuid()); + } + return ctx; + } + + public String getRequestUuid() { + return requestUuid; + } + + public void setRequestUuid(String requestUuid) { + this.requestUuid = requestUuid; + } + + public String getApiName() { + return apiName; + } + + public void setApiName(String apiName) { + this.apiName = apiName; + } + + public String getAccountUuid() { + return accountUuid; + } + + public void setAccountUuid(String accountUuid) { + this.accountUuid = accountUuid; + } + + public String getUserUuid() { + return userUuid; + } + + public void setUserUuid(String userUuid) { + this.userUuid = userUuid; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public Map getRequestScope() { + return requestScope; + } + + public void setRequestScope(Map requestScope) { + this.requestScope = requestScope; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } +} diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateDecisionRecorder.java b/header/src/main/java/org/zstack/header/candidate/CandidateDecisionRecorder.java new file mode 100644 index 00000000000..7a26de584c1 --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateDecisionRecorder.java @@ -0,0 +1,148 @@ +package org.zstack.header.candidate; + +import org.zstack.header.host.HostInventory; +import org.zstack.header.host.HostVO; +import org.zstack.utils.DebugUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +public class CandidateDecisionRecorder { + private final CandidateDecisionContext context; + private final LinkedHashMap> records = new LinkedHashMap<>(); + private final Set finalCandidateUuids = new LinkedHashSet<>(); + + public CandidateDecisionRecorder(CandidateDecisionContext context) { + this.context = context; + } + + public boolean isEnabled() { + return context != null; + } + + public void registerUniverse(String candidateType, Collection inventories, Function uuidExtractor) { + if (!isEnabled() || inventories == null) { + return; + } + + for (T inventory : inventories) { + String uuid = uuidExtractor.apply(inventory); + if (uuid == null || records.containsKey(uuid)) { + continue; + } + + CandidateRecord record = new CandidateRecord<>(); + record.setCandidateType(candidateType); + record.setCandidateUuid(uuid); + record.setInventory(inventory); + records.put(uuid, record); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public void registerHostUniverse(Collection hosts) { + if (!isEnabled() || hosts == null) { + return; + } + + List inventories = HostInventory.valueOf(new ArrayList<>(hosts)); + registerUniverse(CandidateTypes.HOST, (Collection) inventories, inventory -> ((HostInventory) inventory).getUuid()); + } + + public void pass(String candidateUuid) { + } + + public void reject(String candidateUuid, CandidateRejectReason reason) { + CandidateRecord record = records.get(candidateUuid); + if (record == null) { + return; + } + + DebugUtils.Assert(record.getDecision() == null, + String.format("candidate[%s] decision is already %s", candidateUuid, record.getDecision())); + record.setDecision(CandidateTypes.REJECTED); + record.setReason(reason); + } + + public void rejectIfNotRejected(String candidateUuid, CandidateRejectReason reason) { + CandidateRecord record = records.get(candidateUuid); + if (record == null || CandidateTypes.REJECTED.equals(record.getDecision())) { + return; + } + + DebugUtils.Assert(record.getDecision() == null, + String.format("candidate[%s] decision is already %s", candidateUuid, record.getDecision())); + record.setDecision(CandidateTypes.REJECTED); + record.setReason(reason); + } + + public void skip(String candidateUuid, CandidateRejectReason reason) { + CandidateRecord record = records.get(candidateUuid); + if (record == null || CandidateTypes.REJECTED.equals(record.getDecision())) { + return; + } + + DebugUtils.Assert(record.getDecision() == null, + String.format("candidate[%s] decision is already %s", candidateUuid, record.getDecision())); + record.setDecision(CandidateTypes.SKIPPED); + record.setReason(reason); + } + + public void select(String candidateUuid) { + CandidateRecord record = records.get(candidateUuid); + if (record == null) { + return; + } + + DebugUtils.Assert(record.getDecision() == null, + String.format("candidate[%s] decision is already %s", candidateUuid, record.getDecision())); + record.setDecision(CandidateTypes.SELECTED); + } + + public void selectAll(Collection candidateUuids) { + if (candidateUuids == null) { + return; + } + + candidateUuids.forEach(this::select); + } + + public void markFinalCandidates(Collection candidateUuids) { + finalCandidateUuids.clear(); + if (candidateUuids != null) { + finalCandidateUuids.addAll(candidateUuids); + } + } + + public CandidateDecisionResult build() { + CandidateDecisionResult result = new CandidateDecisionResult(); + List> candidates = new ArrayList<>(); + for (CandidateRecord record : records.values()) { + if (record.getDecision() == null) { + if (finalCandidateUuids.contains(record.getCandidateUuid())) { + record.setDecision(CandidateTypes.SELECTED); + } else { + record.setDecision(CandidateTypes.SKIPPED); + if (record.getReason() == null) { + record.setReason(CandidateRejectReason.of( + CandidateReasonCodes.HOST_SKIPPED_BY_PREVIOUS_DECISION, + CandidateCategories.UNKNOWN, + "candidate was skipped by previous decision" + )); + } + } + } + candidates.add(record); + } + + result.setCandidates(candidates); + result.setSummary(CandidateDecisionSummary.from(candidates)); + result.setTruncated(false); + return result; + } +} diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateDecisionRequest.java b/header/src/main/java/org/zstack/header/candidate/CandidateDecisionRequest.java new file mode 100644 index 00000000000..f132f88d0fe --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateDecisionRequest.java @@ -0,0 +1,4 @@ +package org.zstack.header.candidate; + +public interface CandidateDecisionRequest { +} diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateDecisionResult.java b/header/src/main/java/org/zstack/header/candidate/CandidateDecisionResult.java new file mode 100644 index 00000000000..7cc732e482f --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateDecisionResult.java @@ -0,0 +1,34 @@ +package org.zstack.header.candidate; + +import java.util.ArrayList; +import java.util.List; + +public class CandidateDecisionResult { + private List> candidates = new ArrayList<>(); + private CandidateDecisionSummary summary; + private Boolean truncated = false; + + public List> getCandidates() { + return candidates; + } + + public void setCandidates(List> candidates) { + this.candidates = candidates; + } + + public CandidateDecisionSummary getSummary() { + return summary; + } + + public void setSummary(CandidateDecisionSummary summary) { + this.summary = summary; + } + + public Boolean getTruncated() { + return truncated; + } + + public void setTruncated(Boolean truncated) { + this.truncated = truncated; + } +} diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateDecisionSummary.java b/header/src/main/java/org/zstack/header/candidate/CandidateDecisionSummary.java new file mode 100644 index 00000000000..cb702239a96 --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateDecisionSummary.java @@ -0,0 +1,107 @@ +package org.zstack.header.candidate; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class CandidateDecisionSummary { + private int total; + private int selected; + private int rejected; + private int skipped; + private Map byCode = new LinkedHashMap<>(); + private Map byCategory = new LinkedHashMap<>(); + private Map byType = new LinkedHashMap<>(); + + public static CandidateDecisionSummary from(List> records) { + CandidateDecisionSummary summary = new CandidateDecisionSummary(); + for (CandidateRecord record : records) { + summary.total++; + if (CandidateTypes.SELECTED.equals(record.getDecision())) { + summary.selected++; + } else if (CandidateTypes.REJECTED.equals(record.getDecision())) { + summary.rejected++; + } else if (CandidateTypes.SKIPPED.equals(record.getDecision())) { + summary.skipped++; + } + + TypeBreakdown breakdown = summary.byType.get(record.getCandidateType()); + if (breakdown == null) { + breakdown = new TypeBreakdown(); + summary.byType.put(record.getCandidateType(), breakdown); + } + breakdown.count(record.getDecision()); + + CandidateRejectReason reason = record.getReason(); + if (reason != null) { + count(summary.byCode, reason.getCode()); + count(summary.byCategory, reason.getCategory()); + } + } + return summary; + } + + private static void count(Map map, String key) { + if (key == null) { + return; + } + Integer value = map.get(key); + map.put(key, value == null ? 1 : value + 1); + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public int getSelected() { + return selected; + } + + public void setSelected(int selected) { + this.selected = selected; + } + + public int getRejected() { + return rejected; + } + + public void setRejected(int rejected) { + this.rejected = rejected; + } + + public int getSkipped() { + return skipped; + } + + public void setSkipped(int skipped) { + this.skipped = skipped; + } + + public Map getByCode() { + return byCode; + } + + public void setByCode(Map byCode) { + this.byCode = byCode; + } + + public Map getByCategory() { + return byCategory; + } + + public void setByCategory(Map byCategory) { + this.byCategory = byCategory; + } + + public Map getByType() { + return byType; + } + + public void setByType(Map byType) { + this.byType = byType; + } +} diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateReasonCodes.java b/header/src/main/java/org/zstack/header/candidate/CandidateReasonCodes.java new file mode 100644 index 00000000000..2c8c6f3f55b --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateReasonCodes.java @@ -0,0 +1,25 @@ +package org.zstack.header.candidate; + +public interface CandidateReasonCodes { + String HOST_STATE_NOT_ENABLED = "HOST_STATE_NOT_ENABLED"; + String HOST_STATUS_NOT_CONNECTED = "HOST_STATUS_NOT_CONNECTED"; + String HOST_HYPERVISOR_TYPE_NOT_MATCHED = "HOST_HYPERVISOR_TYPE_NOT_MATCHED"; + String HOST_CPU_NOT_ENOUGH = "HOST_CPU_NOT_ENOUGH"; + String HOST_MEMORY_NOT_ENOUGH = "HOST_MEMORY_NOT_ENOUGH"; + String HOST_RESERVED_CAPACITY_NOT_ENOUGH = "HOST_RESERVED_CAPACITY_NOT_ENOUGH"; + String HOST_PRIMARY_STORAGE_REQUIREMENT_NOT_SATISFIED = "HOST_PRIMARY_STORAGE_REQUIREMENT_NOT_SATISFIED"; + String HOST_IN_AVOID_LIST = "HOST_IN_AVOID_LIST"; + String HOST_REJECTED_BY_EXTENSION = "HOST_REJECTED_BY_EXTENSION"; + String HOST_REJECTED_BY_ALLOCATOR_FLOW = "HOST_REJECTED_BY_ALLOCATOR_FLOW"; + String HOST_SKIPPED_BY_PREVIOUS_DECISION = "HOST_SKIPPED_BY_PREVIOUS_DECISION"; + String HOST_REJECTED_BY_ALLOCATOR = "HOST_REJECTED_BY_ALLOCATOR"; + String GPU_DEVICE_STATUS_NOT_AVAILABLE = "GPU_DEVICE_STATUS_NOT_AVAILABLE"; + String GPU_DEVICE_ALREADY_ATTACHED = "GPU_DEVICE_ALREADY_ATTACHED"; + String GPU_DEVICE_TYPE_NOT_MATCHED = "GPU_DEVICE_TYPE_NOT_MATCHED"; + String GPU_SPEC_NOT_MATCHED = "GPU_SPEC_NOT_MATCHED"; + String GPU_SPEC_CAPACITY_NOT_ENOUGH = "GPU_SPEC_CAPACITY_NOT_ENOUGH"; + String PCI_DEVICE_STATUS_NOT_AVAILABLE = "PCI_DEVICE_STATUS_NOT_AVAILABLE"; + String PCI_SPEC_NOT_MATCHED = "PCI_SPEC_NOT_MATCHED"; + String MDEV_DEVICE_STATUS_NOT_AVAILABLE = "MDEV_DEVICE_STATUS_NOT_AVAILABLE"; + String MDEV_SPEC_NOT_MATCHED = "MDEV_SPEC_NOT_MATCHED"; +} diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateReasonDetails.java b/header/src/main/java/org/zstack/header/candidate/CandidateReasonDetails.java new file mode 100644 index 00000000000..33fd24545f6 --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateReasonDetails.java @@ -0,0 +1,38 @@ +package org.zstack.header.candidate; + +import org.zstack.utils.DebugUtils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class CandidateReasonDetails { + private static final Set ALLOWED_KEYS = new HashSet<>(Arrays.asList( + "stage", + "checker", + "extension", + "requiredCpu", + "availableCpu", + "requiredMemory", + "availableMemory", + "totalPhysicalMemory", + "requiredCapacity", + "availableCapacity", + "zoneUuid", + "clusterUuid", + "hostUuid", + "primaryStorageUuid", + "l3NetworkUuid", + "avoidHostUuids", + "expected", + "actual" + )); + + public static void checkAllowed(String key, Object value) { + DebugUtils.Assert(ALLOWED_KEYS.contains(key), String.format("candidate reason detail key[%s] is not allowed", key)); + } + + public static boolean isAllowed(String key) { + return ALLOWED_KEYS.contains(key); + } +} diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateRecord.java b/header/src/main/java/org/zstack/header/candidate/CandidateRecord.java new file mode 100644 index 00000000000..c5a28ba032e --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateRecord.java @@ -0,0 +1,58 @@ +package org.zstack.header.candidate; + +public class CandidateRecord { + private String candidateType; + private String candidateUuid; + private String decision; + private T inventory; + private CandidateRejectReason reason; + private CandidateRef parent; + + public String getCandidateType() { + return candidateType; + } + + public void setCandidateType(String candidateType) { + this.candidateType = candidateType; + } + + public String getCandidateUuid() { + return candidateUuid; + } + + public void setCandidateUuid(String candidateUuid) { + this.candidateUuid = candidateUuid; + } + + public String getDecision() { + return decision; + } + + public void setDecision(String decision) { + this.decision = decision; + } + + public T getInventory() { + return inventory; + } + + public void setInventory(T inventory) { + this.inventory = inventory; + } + + public CandidateRejectReason getReason() { + return reason; + } + + public void setReason(CandidateRejectReason reason) { + this.reason = reason; + } + + public CandidateRef getParent() { + return parent; + } + + public void setParent(CandidateRef parent) { + this.parent = parent; + } +} diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateRef.java b/header/src/main/java/org/zstack/header/candidate/CandidateRef.java new file mode 100644 index 00000000000..5c36c41d0bd --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateRef.java @@ -0,0 +1,22 @@ +package org.zstack.header.candidate; + +public class CandidateRef { + private String candidateType; + private String candidateUuid; + + public String getCandidateType() { + return candidateType; + } + + public void setCandidateType(String candidateType) { + this.candidateType = candidateType; + } + + public String getCandidateUuid() { + return candidateUuid; + } + + public void setCandidateUuid(String candidateUuid) { + this.candidateUuid = candidateUuid; + } +} diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateRejectReason.java b/header/src/main/java/org/zstack/header/candidate/CandidateRejectReason.java new file mode 100644 index 00000000000..90c84e5d331 --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateRejectReason.java @@ -0,0 +1,60 @@ +package org.zstack.header.candidate; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class CandidateRejectReason { + private String code; + private String category; + private String message; + private Map details; + + public static CandidateRejectReason of(String code, String category, String message) { + CandidateRejectReason reason = new CandidateRejectReason(); + reason.setCode(code); + reason.setCategory(category); + reason.setMessage(message); + return reason; + } + + public CandidateRejectReason detail(String key, Object value) { + CandidateReasonDetails.checkAllowed(key, value); + if (details == null) { + details = new LinkedHashMap<>(); + } + details.put(key, value); + return this; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Map getDetails() { + return details; + } + + public void setDetails(Map details) { + this.details = details; + } +} diff --git a/header/src/main/java/org/zstack/header/candidate/CandidateTypes.java b/header/src/main/java/org/zstack/header/candidate/CandidateTypes.java new file mode 100644 index 00000000000..e725c7c99c7 --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/CandidateTypes.java @@ -0,0 +1,19 @@ +package org.zstack.header.candidate; + +public interface CandidateTypes { + String HOST = "Host"; + String PRIMARY_STORAGE = "PrimaryStorage"; + String GPU_DEVICE_SPEC = "GpuDeviceSpec"; + String GPU_DEVICE = "GpuDevice"; + String PCI_DEVICE_SPEC = "PciDeviceSpec"; + String PCI_DEVICE = "PciDevice"; + String MDEV_DEVICE_SPEC = "MdevDeviceSpec"; + String MDEV_DEVICE = "MdevDevice"; + String NETWORK = "Network"; + String VOLUME = "Volume"; + String ISO = "Iso"; + + String SELECTED = "SELECTED"; + String REJECTED = "REJECTED"; + String SKIPPED = "SKIPPED"; +} diff --git a/header/src/main/java/org/zstack/header/candidate/TypeBreakdown.java b/header/src/main/java/org/zstack/header/candidate/TypeBreakdown.java new file mode 100644 index 00000000000..fb68bd553a3 --- /dev/null +++ b/header/src/main/java/org/zstack/header/candidate/TypeBreakdown.java @@ -0,0 +1,51 @@ +package org.zstack.header.candidate; + +public class TypeBreakdown { + private int total; + private int selected; + private int rejected; + private int skipped; + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public int getSelected() { + return selected; + } + + public void setSelected(int selected) { + this.selected = selected; + } + + public int getRejected() { + return rejected; + } + + public void setRejected(int rejected) { + this.rejected = rejected; + } + + public int getSkipped() { + return skipped; + } + + public void setSkipped(int skipped) { + this.skipped = skipped; + } + + public void count(String decision) { + total++; + if (CandidateTypes.SELECTED.equals(decision)) { + selected++; + } else if (CandidateTypes.REJECTED.equals(decision)) { + rejected++; + } else if (CandidateTypes.SKIPPED.equals(decision)) { + skipped++; + } + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIGetCandidateZonesClustersHostsForCreatingVmMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetCandidateZonesClustersHostsForCreatingVmMsg.java index 0b35d5f7d77..9ebfb3daad8 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetCandidateZonesClustersHostsForCreatingVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetCandidateZonesClustersHostsForCreatingVmMsg.java @@ -23,6 +23,7 @@ method = HttpMethod.GET, responseClass = APIGetCandidateZonesClustersHostsForCreatingVmReply.class ) +@Deprecated public class APIGetCandidateZonesClustersHostsForCreatingVmMsg extends APISyncCallMessage { @APIParam(resourceType = InstanceOfferingVO.class, checkAccount = true, required = false) private String instanceOfferingUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmCreationCandidatesMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmCreationCandidatesMsg.java new file mode 100644 index 00000000000..cd4e0480d06 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmCreationCandidatesMsg.java @@ -0,0 +1,142 @@ +package org.zstack.header.vm; + +import org.springframework.http.HttpMethod; +import org.zstack.header.candidate.CandidateDecisionRequest; +import org.zstack.header.configuration.DiskOfferingVO; +import org.zstack.header.configuration.InstanceOfferingVO; +import org.zstack.header.identity.Action; +import org.zstack.header.image.ImageVO; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.network.l3.L3NetworkVO; +import org.zstack.header.rest.RestRequest; + +import java.util.List; + +import static java.util.Arrays.asList; + +@Action(category = VmInstanceConstant.ACTION_CATEGORY, names = {"read"}) +@RestRequest( + path = "/vm-instances/creation-candidates", + method = HttpMethod.GET, + responseClass = APIGetVmCreationCandidatesReply.class +) +public class APIGetVmCreationCandidatesMsg extends APISyncCallMessage implements CandidateDecisionRequest { + @APIParam(resourceType = InstanceOfferingVO.class, checkAccount = true, required = false) + private String instanceOfferingUuid; + @APIParam(resourceType = ImageVO.class, checkAccount = true) + private String imageUuid; + @APIParam(resourceType = L3NetworkVO.class, nonempty = true, checkAccount = true) + private List l3NetworkUuids; + @APIParam(required = false, resourceType = DiskOfferingVO.class, checkAccount = true) + private String rootDiskOfferingUuid; + @APIParam(numberRange = {1, Long.MAX_VALUE}, numberRangeUnit = {"byte", "bytes"}, required = false) + private Long rootDiskSize; + @APIParam(required = false, nonempty = true, resourceType = DiskOfferingVO.class, checkAccount = true) + private List dataDiskOfferingUuids; + @APIParam(numberRange = {1, 1024}, required = false) + private Integer cpuNum; + @APIParam(numberRange = {1, Long.MAX_VALUE}, numberRangeUnit = {"byte", "bytes"}, required = false) + private Long memorySize; + private String zoneUuid; + private String clusterUuid; + private String defaultL3NetworkUuid; + + public Long getRootDiskSize() { + return rootDiskSize; + } + + public void setRootDiskSize(Long rootDiskSize) { + this.rootDiskSize = rootDiskSize; + } + + public String getDefaultL3NetworkUuid() { + return defaultL3NetworkUuid; + } + + public void setDefaultL3NetworkUuid(String defaultL3NetworkUuid) { + this.defaultL3NetworkUuid = defaultL3NetworkUuid; + } + + public String getZoneUuid() { + return zoneUuid; + } + + public void setZoneUuid(String zoneUuid) { + this.zoneUuid = zoneUuid; + } + + public String getClusterUuid() { + return clusterUuid; + } + + public void setClusterUuid(String clusterUuid) { + this.clusterUuid = clusterUuid; + } + + public String getInstanceOfferingUuid() { + return instanceOfferingUuid; + } + + public void setInstanceOfferingUuid(String instanceOfferingUuid) { + this.instanceOfferingUuid = instanceOfferingUuid; + } + + public String getImageUuid() { + return imageUuid; + } + + public void setImageUuid(String imageUuid) { + this.imageUuid = imageUuid; + } + + public List getL3NetworkUuids() { + return l3NetworkUuids; + } + + public void setL3NetworkUuids(List l3NetworkUuids) { + this.l3NetworkUuids = l3NetworkUuids; + } + + public String getRootDiskOfferingUuid() { + return rootDiskOfferingUuid; + } + + public void setRootDiskOfferingUuid(String rootDiskOfferingUuid) { + this.rootDiskOfferingUuid = rootDiskOfferingUuid; + } + + public List getDataDiskOfferingUuids() { + return dataDiskOfferingUuids; + } + + public void setDataDiskOfferingUuids(List dataDiskOfferingUuids) { + this.dataDiskOfferingUuids = dataDiskOfferingUuids; + } + + public Integer getCpuNum() { + return cpuNum; + } + + public void setCpuNum(Integer cpuNum) { + this.cpuNum = cpuNum; + } + + public Long getMemorySize() { + return memorySize; + } + + public void setMemorySize(Long memorySize) { + this.memorySize = memorySize; + } + + public static APIGetVmCreationCandidatesMsg __example__() { + APIGetVmCreationCandidatesMsg msg = new APIGetVmCreationCandidatesMsg(); + msg.setClusterUuid(uuid()); + msg.setDataDiskOfferingUuids(asList(uuid(), uuid())); + msg.setImageUuid(uuid()); + msg.setInstanceOfferingUuid(uuid()); + msg.setL3NetworkUuids(asList(uuid())); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmCreationCandidatesReply.java b/header/src/main/java/org/zstack/header/vm/APIGetVmCreationCandidatesReply.java new file mode 100644 index 00000000000..215d7007440 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmCreationCandidatesReply.java @@ -0,0 +1,59 @@ +package org.zstack.header.vm; + +import org.zstack.header.candidate.CandidateDecisionSummary; +import org.zstack.header.candidate.CandidateRecord; +import org.zstack.header.cluster.ClusterInventory; +import org.zstack.header.message.APIReply; +import org.zstack.header.rest.RestResponse; +import org.zstack.header.zone.ZoneInventory; + +import java.util.List; + +@RestResponse(fieldsTo = {"all"}) +public class APIGetVmCreationCandidatesReply extends APIReply { + private List zones; + private List clusters; + private List> candidates; + private CandidateDecisionSummary summary; + private Boolean truncated; + + public List getZones() { + return zones; + } + + public void setZones(List zones) { + this.zones = zones; + } + + public List getClusters() { + return clusters; + } + + public void setClusters(List clusters) { + this.clusters = clusters; + } + + public List> getCandidates() { + return candidates; + } + + public void setCandidates(List> candidates) { + this.candidates = candidates; + } + + public CandidateDecisionSummary getSummary() { + return summary; + } + + public void setSummary(CandidateDecisionSummary summary) { + this.summary = summary; + } + + public Boolean getTruncated() { + return truncated; + } + + public void setTruncated(Boolean truncated) { + this.truncated = truncated; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidateHostsMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidateHostsMsg.java index 7fc07ba53da..d22a2cdf2b5 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidateHostsMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidateHostsMsg.java @@ -14,6 +14,7 @@ method = HttpMethod.GET, responseClass = APIGetVmMigrationCandidateHostsReply.class ) +@Deprecated public class APIGetVmMigrationCandidateHostsMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class, checkAccount = true) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidatesMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidatesMsg.java new file mode 100644 index 00000000000..c7a407cd755 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidatesMsg.java @@ -0,0 +1,34 @@ +package org.zstack.header.vm; + +import org.springframework.http.HttpMethod; +import org.zstack.header.candidate.CandidateDecisionRequest; +import org.zstack.header.identity.Action; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.rest.RestRequest; + +@Action(category = VmInstanceConstant.ACTION_CATEGORY, names = {"read"}) +@RestRequest( + path = "/vm-instances/{vmInstanceUuid}/migration-candidates", + method = HttpMethod.GET, + responseClass = APIGetVmMigrationCandidatesReply.class +) +public class APIGetVmMigrationCandidatesMsg extends APISyncCallMessage implements VmInstanceMessage, CandidateDecisionRequest { + @APIParam(resourceType = VmInstanceVO.class, checkAccount = true) + private String vmInstanceUuid; + + @Override + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public static APIGetVmMigrationCandidatesMsg __example__() { + APIGetVmMigrationCandidatesMsg msg = new APIGetVmMigrationCandidatesMsg(); + msg.vmInstanceUuid = uuid(); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidatesReply.java b/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidatesReply.java new file mode 100644 index 00000000000..d2d57693f06 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmMigrationCandidatesReply.java @@ -0,0 +1,39 @@ +package org.zstack.header.vm; + +import org.zstack.header.candidate.CandidateDecisionSummary; +import org.zstack.header.candidate.CandidateRecord; +import org.zstack.header.message.APIReply; +import org.zstack.header.rest.RestResponse; + +import java.util.List; + +@RestResponse(fieldsTo = {"all"}) +public class APIGetVmMigrationCandidatesReply extends APIReply { + private List> candidates; + private CandidateDecisionSummary summary; + private Boolean truncated; + + public List> getCandidates() { + return candidates; + } + + public void setCandidates(List> candidates) { + this.candidates = candidates; + } + + public CandidateDecisionSummary getSummary() { + return summary; + } + + public void setSummary(CandidateDecisionSummary summary) { + this.summary = summary; + } + + public Boolean getTruncated() { + return truncated; + } + + public void setTruncated(Boolean truncated) { + this.truncated = truncated; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidateClustersHostsMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidateClustersHostsMsg.java index 2294d914fbe..8c3064ecd37 100755 --- a/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidateClustersHostsMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidateClustersHostsMsg.java @@ -15,6 +15,7 @@ method = HttpMethod.GET, responseClass = APIGetVmStartingCandidateClustersHostsReply.class ) +@Deprecated public class APIGetVmStartingCandidateClustersHostsMsg extends APISyncCallMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class, checkAccount = true, operationTarget = true) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidatesMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidatesMsg.java new file mode 100644 index 00000000000..c4faa7f3c66 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidatesMsg.java @@ -0,0 +1,38 @@ +package org.zstack.header.vm; + +import org.springframework.http.HttpMethod; +import org.zstack.header.candidate.CandidateDecisionRequest; +import org.zstack.header.identity.Action; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.rest.RestRequest; + +@Action(category = VmInstanceConstant.ACTION_CATEGORY, names = {"read"}) +@RestRequest( + path = "/vm-instances/{uuid}/starting-candidates", + method = HttpMethod.GET, + responseClass = APIGetVmStartingCandidatesReply.class +) +public class APIGetVmStartingCandidatesMsg extends APISyncCallMessage implements VmInstanceMessage, CandidateDecisionRequest { + @APIParam(resourceType = VmInstanceVO.class, checkAccount = true, operationTarget = true) + private String uuid; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public String getVmInstanceUuid() { + return uuid; + } + + public static APIGetVmStartingCandidatesMsg __example__() { + APIGetVmStartingCandidatesMsg msg = new APIGetVmStartingCandidatesMsg(); + msg.uuid = uuid(); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidatesReply.java b/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidatesReply.java new file mode 100644 index 00000000000..ad98ce856b7 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmStartingCandidatesReply.java @@ -0,0 +1,49 @@ +package org.zstack.header.vm; + +import org.zstack.header.candidate.CandidateDecisionSummary; +import org.zstack.header.candidate.CandidateRecord; +import org.zstack.header.cluster.ClusterInventory; +import org.zstack.header.message.APIReply; +import org.zstack.header.rest.RestResponse; + +import java.util.List; + +@RestResponse(fieldsTo = {"all"}) +public class APIGetVmStartingCandidatesReply extends APIReply { + private List clusters; + private List> candidates; + private CandidateDecisionSummary summary; + private Boolean truncated; + + public List getClusters() { + return clusters; + } + + public void setClusters(List clusters) { + this.clusters = clusters; + } + + public List> getCandidates() { + return candidates; + } + + public void setCandidates(List> candidates) { + this.candidates = candidates; + } + + public CandidateDecisionSummary getSummary() { + return summary; + } + + public void setSummary(CandidateDecisionSummary summary) { + this.summary = summary; + } + + public Boolean getTruncated() { + return truncated; + } + + public void setTruncated(Boolean truncated) { + this.truncated = truncated; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/CreateVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/CreateVmInstanceMsg.java index dc364097a8f..f7b5247f68a 100755 --- a/header/src/main/java/org/zstack/header/vm/CreateVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/CreateVmInstanceMsg.java @@ -1,6 +1,7 @@ package org.zstack.header.vm; import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.candidate.CandidateDecisionContext; import java.util.ArrayList; import java.util.List; @@ -43,9 +44,18 @@ public class CreateVmInstanceMsg extends NeedReplyMessage implements CreateVmIns private Map> dataVolumeSystemTagsOnIndex; private List disableL3Networks; private List sshKeyPairUuids; + private CandidateDecisionContext candidateDecisionContext; private final List candidatePrimaryStorageUuidsForRootVolume = new ArrayList<>(); private final List candidatePrimaryStorageUuidsForDataVolume = new ArrayList<>(); + public CandidateDecisionContext getCandidateDecisionContext() { + return candidateDecisionContext; + } + + public void setCandidateDecisionContext(CandidateDecisionContext candidateDecisionContext) { + this.candidateDecisionContext = candidateDecisionContext; + } + public List getCandidatePrimaryStorageUuidsForRootVolume() { return candidatePrimaryStorageUuidsForRootVolume; } diff --git a/header/src/main/java/org/zstack/header/vm/GetVmMigrationTargetHostMsg.java b/header/src/main/java/org/zstack/header/vm/GetVmMigrationTargetHostMsg.java index b5ccea9d8ed..b176379483b 100755 --- a/header/src/main/java/org/zstack/header/vm/GetVmMigrationTargetHostMsg.java +++ b/header/src/main/java/org/zstack/header/vm/GetVmMigrationTargetHostMsg.java @@ -1,5 +1,6 @@ package org.zstack.header.vm; +import org.zstack.header.candidate.CandidateDecisionContext; import org.zstack.header.message.NeedReplyMessage; import java.util.List; @@ -9,6 +10,7 @@ public class GetVmMigrationTargetHostMsg extends NeedReplyMessage implements VmInstanceMessage { private String vmInstanceUuid; private List avoidHostUuids; + private CandidateDecisionContext candidateDecisionContext; public List getAvoidHostUuids() { return avoidHostUuids; @@ -25,4 +27,12 @@ public String getVmInstanceUuid() { public void setVmInstanceUuid(String vmInstanceUuid) { this.vmInstanceUuid = vmInstanceUuid; } + + public CandidateDecisionContext getCandidateDecisionContext() { + return candidateDecisionContext; + } + + public void setCandidateDecisionContext(CandidateDecisionContext candidateDecisionContext) { + this.candidateDecisionContext = candidateDecisionContext; + } } diff --git a/header/src/main/java/org/zstack/header/vm/GetVmMigrationTargetHostReply.java b/header/src/main/java/org/zstack/header/vm/GetVmMigrationTargetHostReply.java index cc83fcc637a..cf77b92a767 100755 --- a/header/src/main/java/org/zstack/header/vm/GetVmMigrationTargetHostReply.java +++ b/header/src/main/java/org/zstack/header/vm/GetVmMigrationTargetHostReply.java @@ -1,5 +1,6 @@ package org.zstack.header.vm; +import org.zstack.header.candidate.CandidateDecisionResult; import org.zstack.header.host.HostInventory; import org.zstack.header.message.MessageReply; @@ -9,6 +10,7 @@ */ public class GetVmMigrationTargetHostReply extends MessageReply { private List hosts; + private CandidateDecisionResult candidateDecisionResult; public List getHosts() { return hosts; @@ -17,4 +19,12 @@ public List getHosts() { public void setHosts(List hosts) { this.hosts = hosts; } + + public CandidateDecisionResult getCandidateDecisionResult() { + return candidateDecisionResult; + } + + public void setCandidateDecisionResult(CandidateDecisionResult candidateDecisionResult) { + this.candidateDecisionResult = candidateDecisionResult; + } } diff --git a/header/src/main/java/org/zstack/header/vm/GetVmStartingCandidateClustersHostsMsg.java b/header/src/main/java/org/zstack/header/vm/GetVmStartingCandidateClustersHostsMsg.java index 07bf7fa682e..c88ea22e56b 100644 --- a/header/src/main/java/org/zstack/header/vm/GetVmStartingCandidateClustersHostsMsg.java +++ b/header/src/main/java/org/zstack/header/vm/GetVmStartingCandidateClustersHostsMsg.java @@ -1,6 +1,7 @@ package org.zstack.header.vm; import org.zstack.header.allocator.HostAllocationPurpose; +import org.zstack.header.candidate.CandidateDecisionContext; import org.zstack.header.message.APIParam; import org.zstack.header.message.NeedReplyMessage; @@ -19,6 +20,7 @@ public class GetVmStartingCandidateClustersHostsMsg extends NeedReplyMessage imp * filters trust the value as-is. */ private HostAllocationPurpose purpose = HostAllocationPurpose.ALLOCATE; + private CandidateDecisionContext candidateDecisionContext; public String getUuid() { return uuid; @@ -40,4 +42,12 @@ public HostAllocationPurpose getPurpose() { public void setPurpose(HostAllocationPurpose purpose) { this.purpose = purpose == null ? HostAllocationPurpose.ALLOCATE : purpose; } -} \ No newline at end of file + + public CandidateDecisionContext getCandidateDecisionContext() { + return candidateDecisionContext; + } + + public void setCandidateDecisionContext(CandidateDecisionContext candidateDecisionContext) { + this.candidateDecisionContext = candidateDecisionContext; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/GetVmStartingCandidateClustersHostsReply.java b/header/src/main/java/org/zstack/header/vm/GetVmStartingCandidateClustersHostsReply.java index 4169cf9fdc8..c7104214a11 100644 --- a/header/src/main/java/org/zstack/header/vm/GetVmStartingCandidateClustersHostsReply.java +++ b/header/src/main/java/org/zstack/header/vm/GetVmStartingCandidateClustersHostsReply.java @@ -1,6 +1,7 @@ package org.zstack.header.vm; import org.zstack.header.cluster.ClusterInventory; +import org.zstack.header.candidate.CandidateDecisionResult; import org.zstack.header.host.HostInventory; import org.zstack.header.message.MessageReply; @@ -12,6 +13,7 @@ public class GetVmStartingCandidateClustersHostsReply extends MessageReply { private List hostInventories; private List clusterInventories; + private CandidateDecisionResult candidateDecisionResult; public List getHostInventories() { return hostInventories; @@ -28,4 +30,12 @@ public List getClusterInventories() { public void setClusterInventories(List clusterInventories) { this.clusterInventories = clusterInventories; } + + public CandidateDecisionResult getCandidateDecisionResult() { + return candidateDecisionResult; + } + + public void setCandidateDecisionResult(CandidateDecisionResult candidateDecisionResult) { + this.candidateDecisionResult = candidateDecisionResult; + } } diff --git a/header/src/main/java/org/zstack/header/vm/InstantiateNewCreatedVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/InstantiateNewCreatedVmInstanceMsg.java index f27bebf9802..9ce97210bd9 100755 --- a/header/src/main/java/org/zstack/header/vm/InstantiateNewCreatedVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/InstantiateNewCreatedVmInstanceMsg.java @@ -1,5 +1,6 @@ package org.zstack.header.vm; +import org.zstack.header.candidate.CandidateDecisionContext; import org.zstack.header.message.NeedReplyMessage; import java.util.ArrayList; @@ -19,11 +20,20 @@ public class InstantiateNewCreatedVmInstanceMsg extends NeedReplyMessage impleme private List dataVolumeSystemTags; private List softAvoidHostUuids; private List avoidHostUuids; + private CandidateDecisionContext candidateDecisionContext; private Map> dataVolumeSystemTagsOnIndex; private List disableL3Networks; private final List candidatePrimaryStorageUuidsForRootVolume = new ArrayList<>(); private final List candidatePrimaryStorageUuidsForDataVolume = new ArrayList<>(); + public CandidateDecisionContext getCandidateDecisionContext() { + return candidateDecisionContext; + } + + public void setCandidateDecisionContext(CandidateDecisionContext candidateDecisionContext) { + this.candidateDecisionContext = candidateDecisionContext; + } + public List getCandidatePrimaryStorageUuidsForRootVolume() { return candidatePrimaryStorageUuidsForRootVolume; } @@ -197,4 +207,3 @@ public void setDisableL3Networks(List disableL3Networks) { this.disableL3Networks = disableL3Networks; } } - diff --git a/header/src/main/java/org/zstack/header/vm/MigrateVmInnerMsg.java b/header/src/main/java/org/zstack/header/vm/MigrateVmInnerMsg.java index 8aeed12c273..b7414345330 100755 --- a/header/src/main/java/org/zstack/header/vm/MigrateVmInnerMsg.java +++ b/header/src/main/java/org/zstack/header/vm/MigrateVmInnerMsg.java @@ -1,6 +1,7 @@ package org.zstack.header.vm; import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.candidate.CandidateDecisionContext; /** * Created by camile on 3/7/2018. @@ -14,6 +15,15 @@ public class MigrateVmInnerMsg extends NeedReplyMessage implements VmInstanceMes private Boolean migrateFromDestination; private boolean allowUnknown; private Integer downTime; + private CandidateDecisionContext candidateDecisionContext; + + public CandidateDecisionContext getCandidateDecisionContext() { + return candidateDecisionContext; + } + + public void setCandidateDecisionContext(CandidateDecisionContext candidateDecisionContext) { + this.candidateDecisionContext = candidateDecisionContext; + } public void setVmInstanceUuid(String vmInstanceUuid) { this.vmInstanceUuid = vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/MigrateVmMsg.java b/header/src/main/java/org/zstack/header/vm/MigrateVmMsg.java index 7787cd53d7c..0b72cc29fce 100755 --- a/header/src/main/java/org/zstack/header/vm/MigrateVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/MigrateVmMsg.java @@ -1,6 +1,7 @@ package org.zstack.header.vm; import org.zstack.header.allocator.AllocationScene; +import org.zstack.header.candidate.CandidateDecisionContext; import org.zstack.header.message.NeedReplyMessage; import java.util.List; @@ -17,6 +18,15 @@ public class MigrateVmMsg extends NeedReplyMessage implements VmInstanceMessage, private boolean migrateFromDestination; private boolean allowUnknown; private Integer downTime; + private CandidateDecisionContext candidateDecisionContext; + + public CandidateDecisionContext getCandidateDecisionContext() { + return candidateDecisionContext; + } + + public void setCandidateDecisionContext(CandidateDecisionContext candidateDecisionContext) { + this.candidateDecisionContext = candidateDecisionContext; + } @Override public Integer getDownTime() { diff --git a/header/src/main/java/org/zstack/header/vm/StartVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/StartVmInstanceMsg.java index 8b38b567da8..804423a219b 100755 --- a/header/src/main/java/org/zstack/header/vm/StartVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/StartVmInstanceMsg.java @@ -1,6 +1,7 @@ package org.zstack.header.vm; import org.zstack.header.allocator.AllocationScene; +import org.zstack.header.candidate.CandidateDecisionContext; import org.zstack.header.message.NeedQuotaCheckMessage; import org.zstack.header.message.NeedReplyMessage; @@ -20,6 +21,15 @@ public class StartVmInstanceMsg extends NeedReplyMessage implements VmInstanceMe private List softAvoidHostUuids; private AllocationScene allocationScene; private boolean startPaused; + private CandidateDecisionContext candidateDecisionContext; + + public CandidateDecisionContext getCandidateDecisionContext() { + return candidateDecisionContext; + } + + public void setCandidateDecisionContext(CandidateDecisionContext candidateDecisionContext) { + this.candidateDecisionContext = candidateDecisionContext; + } public boolean isStartPaused() { return startPaused; diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java index 99ee2173b98..f1033d54404 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java @@ -1,6 +1,7 @@ package org.zstack.header.vm; import org.apache.commons.collections.CollectionUtils; +import org.zstack.header.candidate.CandidateDecisionContext; import org.zstack.header.allocator.AllocationScene; import org.zstack.header.configuration.DiskOfferingInventory; import org.zstack.header.host.HostInventory; @@ -363,6 +364,7 @@ public void setHostname(String hostname) { private boolean instantiateResourcesSuccess; private boolean instantiateResourcesSkipExisting; private AllocationScene allocationScene; + private CandidateDecisionContext candidateDecisionContext; private List rootVolumeSystemTags; private List dataVolumeSystemTags; @@ -430,6 +432,14 @@ public void setAllocationScene(AllocationScene allocationScene) { this.allocationScene = allocationScene; } + public CandidateDecisionContext getCandidateDecisionContext() { + return candidateDecisionContext; + } + + public void setCandidateDecisionContext(CandidateDecisionContext candidateDecisionContext) { + this.candidateDecisionContext = candidateDecisionContext; + } + public String getVDIMonitorNumber() { return VDIMonitorNumber == null ? "1" : VDIMonitorNumber; } diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/hostallocator/CandidateDecisionApiCase.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/hostallocator/CandidateDecisionApiCase.groovy new file mode 100644 index 00000000000..e8a74ba875c --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/hostallocator/CandidateDecisionApiCase.groovy @@ -0,0 +1,302 @@ +package org.zstack.test.integration.kvm.hostallocator + +import org.zstack.header.candidate.CandidateReasonCodes +import org.zstack.header.candidate.CandidateTypes +import org.zstack.header.vm.* +import org.zstack.network.securitygroup.SecurityGroupConstant +import org.zstack.sdk.HostInventory +import org.zstack.sdk.ImageInventory +import org.zstack.sdk.InstanceOfferingInventory +import org.zstack.sdk.L3NetworkInventory +import org.zstack.sdk.VmInstanceInventory +import org.zstack.test.Api +import org.zstack.test.ApiSender +import org.zstack.test.ApiSenderException +import org.zstack.test.integration.kvm.KvmTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.testlib.Test +import org.zstack.utils.data.SizeUnit + +class CandidateDecisionApiCase extends SubCase { + EnvSpec env + def session + ApiSender sender + HostInventory host1 + HostInventory host2 + VmInstanceInventory vm + ImageInventory image + InstanceOfferingInventory offering + L3NetworkInventory l3 + + @Override + void setup() { + useSpring(KvmTest.springSpec) + } + + @Override + void environment() { + env = Test.makeEnv { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(1) + cpu = 1 + } + + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "localhost" + + image { + name = "image1" + url = "http://zstack.org/download/test.qcow2" + } + } + + zone { + name = "zone" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm1" + managementIp = "127.0.0.2" + username = "root" + password = "password" + totalCpu = 8 + totalMem = SizeUnit.GIGABYTE.toByte(8) + } + + kvm { + name = "kvm2" + managementIp = "127.0.0.3" + username = "root" + password = "password" + totalCpu = 8 + totalMem = SizeUnit.GIGABYTE.toByte(8) + } + + attachPrimaryStorage("nfs") + attachL2Network("l2") + } + + nfsPrimaryStorage { + name = "nfs" + url = "/nfs_root" + } + + l2NoVlanNetwork { + name = "l2" + physicalInterface = "eth0" + + l3Network { + name = "l3" + + service { + provider = SecurityGroupConstant.SECURITY_GROUP_PROVIDER_TYPE + types = [SecurityGroupConstant.SECURITY_GROUP_NETWORK_SERVICE_TYPE] + } + + ip { + startIp = "192.168.100.10" + endIp = "192.168.100.100" + netmask = "255.255.255.0" + gateway = "192.168.100.1" + } + } + } + + attachBackupStorage("sftp") + } + + vm { + name = "vm1" + useInstanceOffering("instanceOffering") + useImage("image1") + useL3Networks("l3") + useHost("kvm1") + } + } + } + + @Override + void clean() { + env.delete() + } + + @Override + void test() { + env.create { + prepare() + testMigrationCandidatesIncludeAvoidedCurrentHost() + changeHostState { + sessionId = Test.currentEnvSpec.session.uuid + uuid = host2.uuid + stateEvent = "disable" + } + testMigrationFailureOpaqueIncludesAllRejectedHosts() + stopVmInstance { + uuid = vm.uuid + } + testCreationCandidatesIncludeDisabledHost() + testStartingCandidatesIncludeDisabledHost() + changeHostState { + sessionId = Test.currentEnvSpec.session.uuid + uuid = host1.uuid + stateEvent = "disable" + } + testCreationFailureOpaqueIncludesAllRejectedHosts() + testStartingFailureOpaqueIncludesAllRejectedHosts() + } + } + + void prepare() { + session = new Api().loginAsAdmin() + sender = new ApiSender() + sender.setTimeout(60) + host1 = env.inventoryByName("kvm1") as HostInventory + host2 = env.inventoryByName("kvm2") as HostInventory + vm = env.inventoryByName("vm1") as VmInstanceInventory + image = env.inventoryByName("image1") as ImageInventory + offering = env.inventoryByName("instanceOffering") as InstanceOfferingInventory + l3 = env.inventoryByName("l3") as L3NetworkInventory + } + + void testMigrationCandidatesIncludeAvoidedCurrentHost() { + APIGetVmMigrationCandidatesMsg msg = new APIGetVmMigrationCandidatesMsg() + msg.setSession(session) + msg.setVmInstanceUuid(vm.uuid) + + APIGetVmMigrationCandidatesReply reply = sender.call(msg, APIGetVmMigrationCandidatesReply.class) + assert reply.candidates.size() == 2 + assert reply.summary.total == 2 + assert reply.summary.selected == 1 + assert reply.summary.rejected == 1 + + def records = reply.candidates.collectEntries { [(it.candidateUuid): it] } + assert records[host1.uuid].decision == CandidateTypes.REJECTED + assert records[host1.uuid].reason.code == CandidateReasonCodes.HOST_IN_AVOID_LIST + assert records[host2.uuid].decision == CandidateTypes.SELECTED + } + + void testCreationCandidatesIncludeDisabledHost() { + APIGetVmCreationCandidatesMsg msg = new APIGetVmCreationCandidatesMsg() + msg.setSession(session) + msg.setInstanceOfferingUuid(offering.uuid) + msg.setImageUuid(image.uuid) + msg.setL3NetworkUuids([l3.uuid]) + + APIGetVmCreationCandidatesReply reply = sender.call(msg, APIGetVmCreationCandidatesReply.class) + assert reply.candidates.size() == 2 + assert reply.summary.total == 2 + assert reply.summary.selected == 1 + assert reply.summary.rejected == 1 + assert reply.zones.size() == 1 + assert reply.clusters.size() == 1 + + def records = reply.candidates.collectEntries { [(it.candidateUuid): it] } + assert records[host1.uuid].decision == CandidateTypes.SELECTED + assert records[host2.uuid].decision == CandidateTypes.REJECTED + assert records[host2.uuid].reason.code == CandidateReasonCodes.HOST_STATE_NOT_ENABLED + } + + void testStartingCandidatesIncludeDisabledHost() { + APIGetVmStartingCandidatesMsg msg = new APIGetVmStartingCandidatesMsg() + msg.setSession(session) + msg.setUuid(vm.uuid) + + APIGetVmStartingCandidatesReply reply = sender.call(msg, APIGetVmStartingCandidatesReply.class) + assert reply.candidates.size() == 2 + assert reply.summary.total == 2 + assert reply.summary.selected == 1 + assert reply.summary.rejected == 1 + assert reply.clusters.size() == 1 + + def records = reply.candidates.collectEntries { [(it.candidateUuid): it] } + assert records[host1.uuid].decision == CandidateTypes.SELECTED + assert records[host2.uuid].decision == CandidateTypes.REJECTED + assert records[host2.uuid].reason.code == CandidateReasonCodes.HOST_STATE_NOT_ENABLED + } + + void testMigrationFailureOpaqueIncludesAllRejectedHosts() { + APIMigrateVmMsg msg = new APIMigrateVmMsg() + msg.setSession(session) + msg.setVmInstanceUuid(vm.uuid) + + def result = expectCandidateDecisionFailure { + sender.send(msg, APIMigrateVmEvent.class) + } + + assert result.candidates.size() == 2 + assert result.summary.total == 2 + assert result.summary.selected == 0 + assert result.summary.rejected == 2 + + def records = result.candidates.collectEntries { [(it.candidateUuid): it] } + assert records[host1.uuid].decision == CandidateTypes.REJECTED + assert records[host1.uuid].reason.code == CandidateReasonCodes.HOST_IN_AVOID_LIST + assert records[host2.uuid].decision == CandidateTypes.REJECTED + assert records[host2.uuid].reason.code == CandidateReasonCodes.HOST_STATE_NOT_ENABLED + } + + void testCreationFailureOpaqueIncludesAllRejectedHosts() { + APICreateVmInstanceMsg msg = new APICreateVmInstanceMsg() + msg.setSession(session) + msg.setName("vm-fail") + msg.setInstanceOfferingUuid(offering.uuid) + msg.setImageUuid(image.uuid) + msg.setL3NetworkUuids([l3.uuid]) + + def result = expectCandidateDecisionFailure { + sender.send(msg, APICreateVmInstanceEvent.class) + } + + assert result.candidates.size() == 2 + assert result.summary.total == 2 + assert result.summary.selected == 0 + assert result.summary.rejected == 2 + + def records = result.candidates.collectEntries { [(it.candidateUuid): it] } + assert records[host1.uuid].decision == CandidateTypes.REJECTED + assert records[host1.uuid].reason.code == CandidateReasonCodes.HOST_STATE_NOT_ENABLED + assert records[host2.uuid].decision == CandidateTypes.REJECTED + assert records[host2.uuid].reason.code == CandidateReasonCodes.HOST_STATE_NOT_ENABLED + } + + void testStartingFailureOpaqueIncludesAllRejectedHosts() { + APIStartVmInstanceMsg msg = new APIStartVmInstanceMsg() + msg.setSession(session) + msg.setUuid(vm.uuid) + + def result = expectCandidateDecisionFailure { + sender.send(msg, APIStartVmInstanceEvent.class) + } + + assert result.candidates.size() == 2 + assert result.summary.total == 2 + assert result.summary.selected == 0 + assert result.summary.rejected == 2 + + def records = result.candidates.collectEntries { [(it.candidateUuid): it] } + assert records[host1.uuid].decision == CandidateTypes.REJECTED + assert records[host1.uuid].reason.code == CandidateReasonCodes.HOST_STATE_NOT_ENABLED + assert records[host2.uuid].decision == CandidateTypes.REJECTED + assert records[host2.uuid].reason.code == CandidateReasonCodes.HOST_STATE_NOT_ENABLED + } + + def expectCandidateDecisionFailure(Closure action) { + try { + action.call() + assert false + } catch (ApiSenderException e) { + def result = e.error.getFromOpaque("candidateDecisionResult") + assert result != null + return result + } + } +} diff --git a/test/src/test/java/org/zstack/test/candidate/TestCandidateDecisionRecorder.java b/test/src/test/java/org/zstack/test/candidate/TestCandidateDecisionRecorder.java new file mode 100644 index 00000000000..0b346ea8b8d --- /dev/null +++ b/test/src/test/java/org/zstack/test/candidate/TestCandidateDecisionRecorder.java @@ -0,0 +1,71 @@ +package org.zstack.test.candidate; + +import org.junit.Test; +import org.zstack.header.candidate.*; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.*; + +public class TestCandidateDecisionRecorder { + @Test + public void testBuildSummaryAndStates() { + CandidateDecisionContext ctx = new CandidateDecisionContext(); + CandidateDecisionRecorder recorder = new CandidateDecisionRecorder<>(ctx); + recorder.registerUniverse(CandidateTypes.HOST, Arrays.asList("host-1", "host-2", "host-3"), s -> s); + + recorder.reject("host-1", CandidateRejectReason.of( + CandidateReasonCodes.HOST_MEMORY_NOT_ENOUGH, + CandidateCategories.CAPACITY, + "available memory is not enough" + ).detail("stage", "capacity")); + recorder.markFinalCandidates(Collections.singletonList("host-2")); + + CandidateDecisionResult result = recorder.build(); + + assertEquals(3, result.getCandidates().size()); + assertFalse(result.getTruncated()); + assertEquals(3, result.getSummary().getTotal()); + assertEquals(1, result.getSummary().getSelected()); + assertEquals(1, result.getSummary().getRejected()); + assertEquals(1, result.getSummary().getSkipped()); + assertEquals(Integer.valueOf(1), result.getSummary().getByCode().get(CandidateReasonCodes.HOST_MEMORY_NOT_ENOUGH)); + assertEquals(Integer.valueOf(1), result.getSummary().getByCategory().get(CandidateCategories.CAPACITY)); + + assertEquals(CandidateTypes.REJECTED, result.getCandidates().get(0).getDecision()); + assertEquals(CandidateTypes.SELECTED, result.getCandidates().get(1).getDecision()); + assertEquals(CandidateTypes.SKIPPED, result.getCandidates().get(2).getDecision()); + } + + @Test + public void testRejectCannotBeOverwritten() { + CandidateDecisionContext ctx = new CandidateDecisionContext(); + CandidateDecisionRecorder recorder = new CandidateDecisionRecorder<>(ctx); + recorder.registerUniverse(CandidateTypes.HOST, Collections.singletonList("host-1"), s -> s); + recorder.reject("host-1", CandidateRejectReason.of( + CandidateReasonCodes.HOST_STATE_NOT_ENABLED, + CandidateCategories.STATUS, + "host state is not Enabled" + )); + + try { + recorder.select("host-1"); + fail("selecting rejected candidate should fail"); + } catch (RuntimeException expected) { + } + } + + @Test + public void testDetailsWhitelistRejectsUnknownKey() { + try { + CandidateRejectReason.of( + CandidateReasonCodes.HOST_REJECTED_BY_ALLOCATOR_FLOW, + CandidateCategories.UNKNOWN, + "host was rejected" + ).detail("notAllowed", "value"); + fail("unknown detail key should fail"); + } catch (RuntimeException expected) { + } + } +}