Java tutorial
/* * Copyright 2016 VMware, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed * under the License is distributed on an "AS IS" BASIS, without warranties or * conditions of any kind, EITHER EXPRESS OR IMPLIED. See the License for the * specific language governing permissions and limitations under the License. */ package com.vmware.photon.controller.rootscheduler.xenon.task; import com.vmware.photon.controller.cloudstore.xenon.entity.ImageToImageDatastoreMappingService; import com.vmware.photon.controller.common.clients.HostClient; import com.vmware.photon.controller.common.clients.HostClientProvider; import com.vmware.photon.controller.common.clients.exceptions.ConstraintMatchingDatastoreNotFoundException; import com.vmware.photon.controller.common.clients.exceptions.NoSuchResourceException; import com.vmware.photon.controller.common.clients.exceptions.RpcException; import com.vmware.photon.controller.common.clients.exceptions.SystemErrorException; import com.vmware.photon.controller.common.logging.LoggingUtils; import com.vmware.photon.controller.common.xenon.CloudStoreHelper; import com.vmware.photon.controller.common.xenon.ControlFlags; import com.vmware.photon.controller.common.xenon.InitializationUtils; import com.vmware.photon.controller.common.xenon.PatchUtils; import com.vmware.photon.controller.common.xenon.QueryTaskUtils; import com.vmware.photon.controller.common.xenon.ServiceUriPaths; import com.vmware.photon.controller.common.xenon.ServiceUtils; import com.vmware.photon.controller.common.xenon.TaskUtils; import com.vmware.photon.controller.common.xenon.ValidationUtils; import com.vmware.photon.controller.common.xenon.host.PhotonControllerXenonHost; import com.vmware.photon.controller.common.zookeeper.gen.ServerAddress; import com.vmware.photon.controller.host.gen.Host; import com.vmware.photon.controller.resource.gen.Disk; import com.vmware.photon.controller.resource.gen.Resource; import com.vmware.photon.controller.resource.gen.ResourceConstraint; import com.vmware.photon.controller.resource.gen.ResourceConstraintType; import com.vmware.photon.controller.resource.gen.Vm; import com.vmware.photon.controller.rootscheduler.service.ConstraintChecker; import com.vmware.photon.controller.rootscheduler.service.ScoreCalculator; import com.vmware.photon.controller.rootscheduler.xenon.SchedulerServiceGroup; import com.vmware.photon.controller.scheduler.gen.PlaceResponse; import com.vmware.photon.controller.scheduler.gen.PlaceResultCode; import com.vmware.xenon.common.FactoryService; import com.vmware.xenon.common.Operation; import com.vmware.xenon.common.ServiceDocumentQueryResult; import com.vmware.xenon.common.StatefulService; import com.vmware.xenon.common.TaskState; import com.vmware.xenon.common.Utils; import com.vmware.xenon.common.UtilsHelper; import com.vmware.xenon.services.common.QueryTask; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import org.apache.thrift.TException; import org.apache.thrift.async.AsyncMethodCallback; import java.net.URI; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; /** * The main responsibility of this class it to pick hosts for VM/disk placements. The * placement algorithm is roughly based on Sparrow scheduler (1), and it works as follows: * <p> * 1. Randomly choose n hosts (n = 4 by default) that satisfy all the resource constraints. * 2. Send place requests to the chosen hosts and wait for responses with a timeout. * 3. After receiving all the responses or reaching the timeout, return the host with * the highest placement score. See {@link ScoreCalculator} for the placement score * calculation logic. * <p> * (1) http://www.eecs.berkeley.edu/~keo/publications/sosp13-final17.pdf */ public class PlacementTaskService extends StatefulService { public static final String FACTORY_LINK = ServiceUriPaths.SCHEDULER_ROOT + "/placement"; // This completion handler provides a new constraint to be added to a set of constraints interface CalculateConstraintCompletion { public void handle(ResourceConstraint newConstraint, Exception exception); } // This completion handler is called when all hosts have scored a resource request interface ScoreResultsCompletion { public void handle(Set<PlaceResponse> okResponses, Set<PlaceResponse> allResponses); } /** * Used to generate detailed error logging messages in JSON when placement fails. */ private class PlaceSummary { public String address; public PlaceResultCode result; PlaceSummary(String address, PlaceResultCode result) { this.address = address; this.result = result; } } /** * This class implements a Xenon micro-service that provides a factory for * {@link PlacementTaskService} instances. */ public static FactoryService createFactory() { return FactoryService.createIdempotent(PlacementTaskService.class); } public PlacementTaskService() { super(PlacementTask.class); // The placement task serves select a host for VM and disk placements, each task will handle a single // request so there is no need for multiple nodes to have the same information nor a specific node to be // the leader of this operation. Persistence is not needed since on a failure, the operation will retry with // more up to date host information for the placements. super.toggleOption(ServiceOption.PERSISTENCE, false); super.toggleOption(ServiceOption.REPLICATION, false); super.toggleOption(ServiceOption.OWNER_SELECTION, false); } @Override public void handleStart(Operation start) { ServiceUtils.logInfo(this, "Starting service %s", getSelfLink()); // The request ID is in the Xenon thread context // We need to put it in the MDC for use by Thrift String requestId = UtilsHelper.getThreadContextId(); if (requestId != null) { LoggingUtils.setRequestId(requestId); } PlacementTask startState = start.getBody(PlacementTask.class); InitializationUtils.initialize(startState); validateState(startState); if (startState.taskState.stage == TaskState.TaskStage.CREATED) { startState.taskState.stage = TaskState.TaskStage.STARTED; } if (startState.documentExpirationTimeMicros <= 0) { startState.documentExpirationTimeMicros = ServiceUtils .computeExpirationTime(ServiceUtils.DEFAULT_DOC_EXPIRATION_TIME_MICROS); } try { if (ControlFlags.isOperationProcessingDisabled(startState.controlFlags)) { ServiceUtils.logInfo(this, "Skipping start operation processing (disabled)"); start.setBody(startState).complete(); } else if (startState.taskState.isDirect) { handlePlaceRequest(startState, start); } else if (TaskState.TaskStage.STARTED == startState.taskState.stage) { start.setBody(startState).complete(); TaskUtils.sendSelfPatch(this, buildPatch(startState.taskState.stage, startState.taskState.isDirect, null)); } } catch (Throwable t) { failTask(buildPatch(TaskState.TaskStage.FAILED, startState.taskState.isDirect, t), t, start); } } @Override public void handlePatch(Operation patchOperation) { ServiceUtils.logInfo(this, "Handling patch for service %s", getSelfLink()); PlacementTask currentState = getState(patchOperation); PlacementTask patchState = patchOperation.getBody(PlacementTask.class); validatePatchState(currentState, patchState); PatchUtils.patchState(currentState, patchState); validateState(currentState); patchOperation.complete(); try { if (ControlFlags.isOperationProcessingDisabled(currentState.controlFlags)) { ServiceUtils.logInfo(this, "Skipping patch operation processing (disabled)"); } else if (TaskState.TaskStage.STARTED == currentState.taskState.stage) { handlePlaceRequest(currentState, null); } } catch (Throwable t) { failTask(buildPatch(TaskState.TaskStage.FAILED, false, t), t, null); } } private void validateState(PlacementTask startState) { ValidationUtils.validateState(startState); if (!startState.taskState.isDirect) { ValidationUtils.validateTaskStage(startState.taskState); } } private void validatePatchState(PlacementTask currentState, PlacementTask patchState) { ValidationUtils.validatePatch(currentState, patchState); ValidationUtils.validateTaskStage(patchState.taskState); ValidationUtils.validateTaskStageProgression(currentState.taskState, patchState.taskState); } /** * This method gets valid host candidates based on requested resource constraints, * sends a place request to each of the hosts, and selects the best candidate from * the responses from the candidates. * * @param currentState * @param postOperation */ private void handlePlaceRequest(PlacementTask currentState, Operation postOperation) { Stopwatch placementWatch = Stopwatch.createStarted(); // Note that getPotentialCandidates is asynchronous, so we handle the response via a completion getPotentialCandidates(currentState, (candidates, ex) -> { if (ex != null) { handleGetCandidateError(currentState, postOperation, ex); return; } handleGetCandidateResult(currentState, postOperation, candidates, placementWatch); }); } /** * Helper method to handle errors from getPotentialCandidates(). */ private void handleGetCandidateError(PlacementTask currentState, Operation postOperation, Throwable ex) { PlacementTask patchState = buildPatch(TaskState.TaskStage.FAILED, currentState.taskState.isDirect, ex); patchState.error = ex.getMessage(); if (ex instanceof NoSuchResourceException) { patchState.resultCode = PlaceResultCode.NO_SUCH_RESOURCE; } else if (ex instanceof ConstraintMatchingDatastoreNotFoundException) { patchState.resultCode = PlaceResultCode.NO_CONSTRAINT_MATCHING_DATASTORE; } else { patchState.resultCode = PlaceResultCode.SYSTEM_ERROR; } failTask(patchState, ex, postOperation); } /** * Helper method to process the candidates returned by getPotentialCandidates. */ private void handleGetCandidateResult(PlacementTask currentState, Operation postOperation, Map<String, ServerAddress> candidates, Stopwatch placementWatch) { ServiceUtils.logInfo(this, "elapsed-time flat-place-get-candidates %d milliseconds", placementWatch.elapsed(TimeUnit.MILLISECONDS)); if (candidates.isEmpty()) { String msg = String.format("Place failure, constraints cannot be satisfied for request: %s", currentState.resource); PlacementTask patchState = buildPatch(TaskState.TaskStage.FAILED, currentState.taskState.isDirect, null); patchState.resultCode = PlaceResultCode.NO_SUCH_RESOURCE; patchState.error = msg; failTask(patchState, new Throwable(msg), postOperation); return; } // Send place request to the candidates to get a score for each one ServiceUtils.logInfo(this, "Sending place requests to %s with timeout %d ms", candidates, currentState.timeoutMs); Stopwatch scoreCandidatesStopwatch = Stopwatch.createStarted(); queryHostsForScores(currentState.resource, candidates, (okResponses, allResponses) -> { ServiceUtils.logInfo(this, "elapsed-time flat-place-score-candidates %d milliseconds", scoreCandidatesStopwatch.elapsed(TimeUnit.MILLISECONDS)); // Return the best response. PlacementTask patchState = selectBestResponse(okResponses, allResponses, currentState, placementWatch); if (postOperation == null) { TaskUtils.sendSelfPatch(this, patchState); } else { postOperation.setBody(patchState).complete(); } }); } /** * Retrieves potential hosts from constraint checker that satisfy the resource constraints from the current state. * * @param currentState * @param completion */ private void getPotentialCandidates(PlacementTask currentState, ConstraintChecker.GetCandidatesCompletion completion) { // Get the list of resource constraints List<ResourceConstraint> constraints; constraints = getResourceConstraints(currentState.resource); // Note: createImageSeedingConstraint is asynchronous (it queries Cloudstore), so we use a completion createImageSeedingConstraint(currentState.resource, (newConstraint, ex) -> { if (ex != null) { // createImageSeedingConstraint handled logging, we just pass on the exception completion.handle(null, ex); return; } if (newConstraint != null) { constraints.add(newConstraint); } logConstraints(constraints); applyConstraintChecker(currentState, constraints, completion); }); return; } /** * Used by getPotentialCandidates to query the constraint checker and return the results in the completion. */ private void applyConstraintChecker(PlacementTask currentState, List<ResourceConstraint> constraints, ConstraintChecker.GetCandidatesCompletion completion) { SchedulerServiceGroup scheduler = (SchedulerServiceGroup) ((PhotonControllerXenonHost) getHost()) .getScheduler(); ConstraintChecker checker = scheduler.getConstraintChecker(); try { checker.getCandidates(constraints, currentState.sampleHostCount, completion); } catch (Exception ex) { completion.handle(null, ex); } return; } /** * Asks the host candidates to score the resource request by sending a "place" request via thrift. * * This is asynchronous: the response is returned via an a completion routine. That response will * provide two things: * - The set of "okResponses", from hosts that could accept the resource. This will include the score. * - All responses. These are used when there's an error, to summarize what went wrong * * @param resource * @param candidates * @param completion */ private void queryHostsForScores(Resource resource, Map<String, ServerAddress> candidates, ScoreResultsCompletion completion) { final int numCandidates = candidates.size(); final Set<PlaceResponse> okResponses = Sets.newConcurrentHashSet(); final Set<PlaceResponse> allResponses = Sets.newConcurrentHashSet(); final AtomicInteger resultCount = new AtomicInteger(0); final String requestId = LoggingUtils.getRequestId(); for (Map.Entry<String, ServerAddress> entry : candidates.entrySet()) { ServerAddress address = entry.getValue(); try { // The thrift "place" request is a request to get a score from the host indicating how good // of a match the host is for the resource request. // Note that the place() call has an embedded timeout (currently 60 seconds): we'll get a // timeout exception when it fails. HostClient hostClient = ((HostClientProvider) getHost()).getHostClient(); hostClient.setIpAndPort(address.getHost(), address.getPort()); hostClient.place(resource, new AsyncMethodCallback<Host.AsyncClient.place_call>() { @Override public void onComplete(Host.AsyncClient.place_call call) { if (requestId != null) { // We have to do more work here than normal: the PlaceResponse // doesn't have the request ID and we're in a new thread, so we // need to set it correctly for both Xenon (ServiceUtils.log*) and regular // logging. LoggingUtils.setRequestId(requestId); UtilsHelper.setThreadContextId(requestId); } PlaceResponse response; try { response = call.getResult(); } catch (TException ex) { onError(ex); return; } ServiceUtils.logInfo(PlacementTaskService.this, "Received a place response from %s: %s", entry, response); if (response.getAddress() == null) { response.setAddress(address); } allResponses.add(response); if (response.getResult() == PlaceResultCode.OK) { okResponses.add(response); } if (resultCount.addAndGet(1) == numCandidates) { completion.handle(okResponses, allResponses); } } @Override public void onError(Exception ex) { if (requestId != null) { // See comments above in onComplete() LoggingUtils.setRequestId(requestId); UtilsHelper.setThreadContextId(requestId); } ServiceUtils.logWarning(PlacementTaskService.this, "Failed to get a placement response from %s: %s", entry, ex); PlaceResponse errorResponse = new PlaceResponse(); errorResponse.setResult(PlaceResultCode.SYSTEM_ERROR); errorResponse.setError(String.format("Failed to get a placement response from %s: %s", entry, ex.getMessage())); if (errorResponse.getAddress() == null) { errorResponse.setAddress(address); } allResponses.add(errorResponse); if (resultCount.addAndGet(1) == numCandidates) { completion.handle(okResponses, allResponses); } } }); } catch (RpcException ex) { ServiceUtils.logWarning(PlacementTaskService.this, "Failed to send placement request to %s: %s", entry, ex); PlaceResponse errorResponse = new PlaceResponse(); errorResponse.setAddress(entry.getValue()); errorResponse.setResult(PlaceResultCode.SYSTEM_ERROR); errorResponse.setError( String.format("Failed to send placement request to %s: %s", entry, ex.getMessage())); allResponses.add(errorResponse); if (resultCount.addAndGet(1) == numCandidates) { completion.handle(okResponses, allResponses); } } } } /** * Returns the best host selected host among successful responses. If there are not any hosts to place the request, * this returns a result from the host responses. * * @param okResponses * @param allResponses * @param currentState * @param watch * @return */ private PlacementTask selectBestResponse(Set<PlaceResponse> okResponses, Set<PlaceResponse> allResponses, PlacementTask currentState, Stopwatch watch) { SchedulerServiceGroup scheduler = (SchedulerServiceGroup) ((PhotonControllerXenonHost) getHost()) .getScheduler(); ScoreCalculator scoreCalculator = scheduler.getScoreCalculator(); PlaceResponse response = scoreCalculator.pickBestResponse(okResponses); watch.stop(); PlacementTask patchState; if (response == null) { PlaceResultCode errorCode; String errorMsg; Set<PlaceResultCode> returnCodes; returnCodes = allResponses.stream().map(r -> { return r.getResult(); }).collect(Collectors.toSet()); if (returnCodes.contains(PlaceResultCode.NOT_ENOUGH_CPU_RESOURCE)) { errorCode = PlaceResultCode.NOT_ENOUGH_CPU_RESOURCE; errorMsg = "Not enough cpu resources available"; } else if (returnCodes.contains(PlaceResultCode.NOT_ENOUGH_MEMORY_RESOURCE)) { errorCode = PlaceResultCode.NOT_ENOUGH_MEMORY_RESOURCE; errorMsg = "Not enough memory resources available"; } else if (returnCodes.contains((PlaceResultCode.NOT_ENOUGH_DATASTORE_CAPACITY))) { errorCode = PlaceResultCode.NOT_ENOUGH_DATASTORE_CAPACITY; errorMsg = "Not enough capacity in the datastore available"; } else if (returnCodes.contains(PlaceResultCode.NO_SUCH_RESOURCE)) { errorCode = PlaceResultCode.NO_SUCH_RESOURCE; errorMsg = "No such resource"; } else if (returnCodes.contains(PlaceResultCode.INVALID_STATE)) { errorCode = PlaceResultCode.INVALID_STATE; errorMsg = "Agent in an invalid state"; } else { errorCode = PlaceResultCode.SYSTEM_ERROR; errorMsg = String.format("Received no response in %d ms", watch.elapsed(TimeUnit.MILLISECONDS)); } patchState = buildPatch(TaskState.TaskStage.FAILED, currentState.taskState.isDirect, null); patchState.resultCode = errorCode; patchState.error = errorMsg; ServiceUtils.logWarning(this, "Placement failure reasons: %s", genJsonErrorSummary(allResponses)); } else { patchState = buildPatch(TaskState.TaskStage.FINISHED, currentState.taskState.isDirect, null); ServiceUtils.logInfo(this, "Returning bestResponse: %s in %d ms", response, watch.elapsed(TimeUnit.MILLISECONDS)); patchState.resultCode = response.getResult(); patchState.generation = response.getGeneration(); patchState.serverAddress = response.getAddress(); patchState.resource = new Resource(); patchState.resource.setPlacement_list(response.getPlacementList()); } return patchState; } /** * We generate a JSON summary of all the placement errors that occurred. Yes, we logged the individual * errors above, but this simplifies the process of combing through the logs by collating the errors. * * This summary is a compact (one-line) JSON string. It's fairly readable as-is, but can be reformatted * with your favorite JSON reformatter to make it more readable. */ private String genJsonErrorSummary(Set<PlaceResponse> allResponses) { List<PlaceSummary> summary = allResponses.stream().map(r -> { return new PlaceSummary(r.getAddress().getHost(), r.getResult()); }).collect(Collectors.toList()); return Utils.toJson(false, false, summary); } /** * This reports the error that caused the failure state of patchState before sending an update * to itself. * @param patchState the failed PlacementTask * @param t the error associated with the failed PlacementTask * @param postOperation if there is a postOperation, this is part of a direct task and will return * once this update is complete, otherwise moves to a failed state */ private void failTask(PlacementTask patchState, Throwable t, Operation postOperation) { ServiceUtils.logSevere(this, t); if (postOperation == null) { TaskUtils.sendSelfPatch(this, patchState); } else { postOperation.setBody(patchState).complete(); } } /** * Builds a new PlacementTask with the specified stage and isDirect boolean. * If Throwable t is set then the failure response is added to the task state. * @param patchStage the stage to set the created PlacementTask * @param isDirect boolean if the PlacementTask is a direct operation. * @param t the error associated with this PlacementTask, if one occurred. * @return */ @VisibleForTesting protected static PlacementTask buildPatch(TaskState.TaskStage patchStage, boolean isDirect, Throwable t) { PlacementTask state = new PlacementTask(); state.taskState = new TaskState(); state.taskState.stage = patchStage; state.taskState.isDirect = isDirect; if (null != t) { state.taskState.failure = Utils.toServiceErrorResponse(t); } return state; } /** * Extracts resource constraints from the resource. * * @param resource * the placement task resources requested * @return a list of resource constraints. */ private List<ResourceConstraint> getResourceConstraints(Resource resource) { List<ResourceConstraint> constraints = new LinkedList<>(); if (resource == null) { return constraints; } if (resource.isSetVm()) { Vm vm = resource.getVm(); if (vm.isSetResource_constraints()) { constraints.addAll(vm.getResource_constraints()); } } if (resource.isSetDisks()) { for (Disk disk : resource.getDisks()) { if (disk.isSetResource_constraints()) { constraints.addAll(disk.getResource_constraints()); } } } return constraints; } /** * New images may not be available on all the image datastores because the replication is in progress. * We look at image seeding information available in cloud-store to add placement constraints * such that only hosts with the requested image on an attached image datastore are selected in the * placement process. * * Note that this does not directly return a result, but it provides it via a completion. This is because * the implementation requires making a call to Cloudstore, and we do that asynchronously. */ public void createImageSeedingConstraint(Resource resource, CalculateConstraintCompletion completion) { if (resource == null || !resource.isSetVm() || resource.getVm() == null) { // Nothing to add, complete immediately completion.handle(null, null); return; } // It is necessary for a VM placement request to have an associated diskImage. If none are // found, we fail placement. String imageId = extractImageIdFromResource(resource); if (imageId == null) { String errorMsg = "VM resource does not have an associated diskImage"; ServiceUtils.logSevere(this, errorMsg); completion.handle(null, new SystemErrorException(errorMsg)); return; } try { // Query Cloudstore for all Image to Image Datastore mappings with the given imageId. // Note that we don't need to do a broadcast query, because these mappings use symmetric // replication and so are on all hosts. final ImmutableMap.Builder<String, String> termsBuilder = new ImmutableMap.Builder<>(); termsBuilder.put("imageId", imageId); CloudStoreHelper cloudStoreHelper = ((PhotonControllerXenonHost) getHost()).getCloudStoreHelper(); URI queryUri = cloudStoreHelper .selectLocalCloudStoreIfAvailable(ServiceUriPaths.CORE_LOCAL_QUERY_TASKS); QueryTask.QuerySpecification spec = QueryTaskUtils .buildQuerySpec(ImageToImageDatastoreMappingService.State.class, termsBuilder.build()); spec.options.add(QueryTask.QuerySpecification.QueryOption.EXPAND_CONTENT); QueryTask queryTask = QueryTask.create(spec).setDirect(true); Operation postQuery = Operation.createPost(queryUri).setBody(queryTask) .setReferer(this.getHost().getPublicUri()).setContextId(LoggingUtils.getRequestId()) .setCompletion((response, ex) -> { try { // We're in a new thread, so ensure we set the request ID // in the MDC for future use. LoggingUtils.setRequestId(response.getContextId()); handleImageDatastoreResponse(response, ex, imageId, completion); } catch (Throwable t) { String error = "Internal error in image datastore query response handling."; ServiceUtils.logSevere(this, error, t); completion.handle(null, new SystemErrorException(error)); return; } }); this.sendRequest(postQuery); } catch (Throwable t) { String error = "Internal error in image datastore query."; ServiceUtils.logSevere(this, error, t); completion.handle(null, new SystemErrorException(error)); return; } } /** * Given a resource from a placement request, extract the imageID for the associated VM. * If there is no imageId, return null. */ private String extractImageIdFromResource(Resource resource) { String imageId = null; Vm vm = resource.getVm(); if (vm != null && vm.isSetDisks()) { for (Disk disk : vm.getDisks()) { if (disk.isSetImage()) { imageId = disk.getImage().getId(); break; } } } return imageId; } /** * Helper for createImageSeedingConstraint. * Handles response from query task service: extracts all image datastores that * contain the imageId we need, and creates a ResourceConstraint for them. */ private void handleImageDatastoreResponse(Operation response, Throwable ex, String imageId, CalculateConstraintCompletion completion) { // First, check for failure if (ex != null) { String error = String.format("Failed to call cloud-store to lookup image datastores for image %d"); ServiceUtils.logSevere(this, error, ex); Exception cloudStoreEx = new SystemErrorException(error + ": " + ex.getMessage()); completion.handle(null, cloudStoreEx); return; } // Extract the image datastore IDs List<String> seededImageDatastores = new ArrayList<>(); ServiceDocumentQueryResult queryResult = response.getBody(QueryTask.class).results; queryResult.documents.values().forEach(item -> { String datastoreId = Utils.fromJson(item, ImageToImageDatastoreMappingService.State.class).imageDatastoreId; seededImageDatastores.add(datastoreId); }); // Fail if we have no image datastore IDs: the VM can't be placed unless at least one exists if (seededImageDatastores.isEmpty()) { String error = "No seeded image datastores found for the imageId: " + imageId; ServiceUtils.logWarning(this, error); completion.handle(null, new NoSuchResourceException(error)); return; } // Construct the new Resource Constraint ResourceConstraint constraint = new ResourceConstraint(); constraint.setType(ResourceConstraintType.DATASTORE); constraint.setValues(seededImageDatastores); completion.handle(constraint, null); } /** * Log the constraints. Because the ResourceConstraint class was auto-generated by thrift, * it doesn't print particularly nicely, so do that here. * * We print a JSON string. It's all on one line (to avoid interleaving in the log file) * so it's not hugely readable. But it's easy to reformat JSON to be readable. * * When we move this away from thrift, we can just convert the objects directly to JSON. */ private void logConstraints(List<ResourceConstraint> constraints) { StringBuilder output = new StringBuilder(); output.append("Resource constraints: [ "); if (constraints != null) { for (ResourceConstraint constraint : constraints) { if (constraint != null) { output.append("{ "); // Part 1: Is it a negative constraint? output.append("\"negative\": "); if (constraint.isSetNegative()) { output.append(String.valueOf(constraint.isNegative())); } else { output.append("false"); } // Part 2: Type output.append(", \"type\": "); if (!constraint.isSetType()) { output.append("\"unknown\""); } else { switch (constraint.getType()) { case DATASTORE: output.append("\"datastore\""); break; case HOST: output.append("\"host\""); break; case NETWORK: output.append("\"network\""); break; case VIRTUAL_NETWORK: output.append("\"virtual-network\""); break; case AVAILABILITY_ZONE: output.append("\"availability-zone\""); break; case DATASTORE_TAG: output.append("\"datastore-tag\""); break; case MANAGEMENT_ONLY: output.append("\"management-only\""); break; default: output.append("\"unknown\""); break; } } // Part 3: Values if (constraint.isSetValues()) { List<String> values = constraint.getValues(); output.append(", \"values\": [ "); boolean first = true; for (String value : values) { if (!first) { output.append(", "); } first = false; output.append('\"'); output.append(value); output.append('\"'); } output.append(" ]"); } output.append(" }"); } } } output.append(" ]"); ServiceUtils.logInfo(this, "%s", output.toString()); } }