/* * Copyright 2013 Qubell, Inc. * * 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.qubell.jenkinsci.plugins.qubell.builders; import com.qubell.jenkinsci.plugins.qubell.Configuration; import com.qubell.jenkinsci.plugins.qubell.JsonParser; import com.qubell.jenkinsci.plugins.qubell.VariablesAction; import com.qubell.services.*; import com.qubell.services.exceptions.InvalidCredentialsException; import com.qubell.services.exceptions.NotAuthorizedException; import com.qubell.services.exceptions.QubellServiceException; import com.qubell.services.exceptions.ResourceNotFoundException; import hudson.EnvVars; import hudson.FilePath; import hudson.Util; import hudson.model.*; import hudson.slaves.SlaveComputer; import hudson.tasks.Builder; import hudson.util.VariableResolver; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.time.StopWatch; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.util.HashMap; import java.util.Map; /** * Base type for Qubell specific Jenkins builders see {@link Builder} * * @author Alex Krupnov */ public abstract class QubellBuilder extends Builder { public static final String ASYNC_INSTANCE_ID_KEY = "instanceId"; public static final String ASYNC_EXPECTED_STATUS_KEY = "expectedStatus"; public static final String ASYNC_OUTPUT_PATH_KEY = "outputFilePath"; /** * Defines which status has to be set when failure occurs */ private final Result failureReaction; /** * Prefix for log messages */ protected static String LOG_MESSAGE_PREFIX = "[QUBELL] "; /** * Key value for storing instance id */ protected static String INSTANCE_ID_KEY = "QUBELL_INSTANCE_ID"; /** * {@link InstanceStatusCode}, expected by builder for successful finish */ protected InstanceStatusCode expectedStatus; /** * Status wait timeout in seconds */ protected final int timeout; /** * Output file name for consolidated report */ protected String outputFilePath; private String outputFilePathResolved; /** * Inits builder common properties * * @param timeout string (injected from UI value of timeout) * @param expectedStatus the {@link com.qubell.services.InstanceStatusCode}, expected by builder for successful finish * @param outputFilePath path to builder output file * @param failureReaction a target build status which should be set when instnace returns failure status */ public QubellBuilder(String timeout, InstanceStatusCode expectedStatus, String outputFilePath, String failureReaction) { this.expectedStatus = expectedStatus; this.timeout = Integer.parseInt(timeout); this.outputFilePath = outputFilePath; this.failureReaction = Result.fromString(failureReaction != null ? failureReaction : Result.FAILURE.toString()); } /** * Plugin configuration holder * * @return dynamically fetched instance of Configuration */ protected Configuration getConfiguration() { return Configuration.get(); } /** * Qubell services facade for builders disposal * * @return new instance of facade */ protected QubellFacade getServiceFacade() { return new QubellFacadeImpl(getConfiguration()); } /** * Used for validating configuration object, making sure it is suitable for builder operations * * @return true of configuration valid, otherwise false */ protected boolean validateConfiguration() { Configuration configuration = getConfiguration(); return !StringUtils.isBlank(configuration.getUrl()) && !StringUtils.isBlank(configuration.getLogin()) && !StringUtils.isBlank(configuration.getPassword()); } /** * Reads a build variable, stored into build parameter * * @param build current build * @param key variable key * @param buildLog current build log * @return string value of variable or null */ protected String readBuildVariable(AbstractBuild build, String key, PrintStream buildLog) { VariablesAction variablesAction = getVariableAction(build); String value = variablesAction.getVariable(key); if (value == null) { logMessage(buildLog, "Expected a value for key: %s, no entry found", key); } return value; } /** * Saves a custom build variable into build to make it accessible for further build steps * * @param build current build * @param key key under which variable will be stored * @param value value of variable * @param buildLog current build log */ protected void saveBuildVariable(AbstractBuild build, String key, String value, PrintStream buildLog) { logMessage(buildLog, "Saving build variable %s", key); VariablesAction variablesAction = getVariableAction(build); String currentValue = variablesAction.getVariable(key); if (currentValue != null) { logMessage(buildLog, "Replacing current value: %s", value); } variablesAction.addVariable(key, value); } private VariablesAction getVariableAction(AbstractBuild build) { VariablesAction variablesAction = build.getAction(VariablesAction.class); if (variablesAction == null) { variablesAction = new VariablesAction(); build.addAction(variablesAction); } return variablesAction; } /** * Waits for instance status to be equal to {@link #expectedStatus} with given {@link #timeout} * * @param buildLog build log * @param instance instance to be queries * @return true if instance gained expected status, false if timeout exceed before * @throws InvalidCredentialsException when credentials in configuration are invalid * @throws InterruptedException when wait was interrupted */ private boolean waitForInstanceStatus(PrintStream buildLog, Instance instance) throws QubellServiceException, InterruptedException { logMessage(buildLog, "Waiting for instance status %s with timeout of %d seconds", expectedStatus, timeout); StopWatch sw = new StopWatch(); sw.start(); int attempt = 0; int similarAttemptsCount = 0; InstanceStatus previousStatus = null; while (true) { attempt++; InstanceStatus status; try { status = getServiceFacade().getStatus(instance); } catch (QubellServiceException qse) { // Lets report similar attempts (if any) when something went wrong reportSimilarAttempts(buildLog, similarAttemptsCount); throw qse; } if (status.equals(previousStatus)) { similarAttemptsCount++; } else { reportSimilarAttempts(buildLog, similarAttemptsCount); similarAttemptsCount = 0; reportInstanceStatus(buildLog, status, attempt); } InstanceStatusCode instanceStatusCode = status.getStatus(); if (instanceStatusCode == expectedStatus) { return true; } else if (instanceStatusCode == InstanceStatusCode.FAILED) { reportSimilarAttempts(buildLog, similarAttemptsCount); //In case Failed status is not actually what we expect, considering it an issue logMessage(buildLog, "Instance returned Failed status, aborting further status wait..."); return false; } previousStatus = status; Thread.sleep(getConfiguration().getStatusPollingInterval() * 1000); if (sw.getTime() >= timeout * 1000) { reportSimilarAttempts(buildLog, similarAttemptsCount); logMessage(buildLog, "Instance did not return expected status (%s) within given timeout of %s seconds", expectedStatus, timeout); return false; } } } private void reportSimilarAttempts(PrintStream buildLog, int similarAttemptsCount) { if (similarAttemptsCount > 0) { logMessage(buildLog, "%d similar attempts passed", similarAttemptsCount); } } private void reportInstanceStatus(PrintStream buildLog, InstanceStatus status, int attempt) { logMessage(buildLog, "Attempt #%d", attempt); logMessage(buildLog, "Instance status %s", status.getStatus()); Workflow currentWorkflow = status.getCurrentWorkflow(); if (currentWorkflow != null) { logMessage(buildLog, "Current workflow %s is in status %s", currentWorkflow.getName(), currentWorkflow.getStatus()); if (currentWorkflow.getSteps() != null && currentWorkflow.getSteps().size() > 0) { logMessage(buildLog, "Workflow steps"); for (WorkflowStep step : currentWorkflow.getSteps()) { logMessage(buildLog, "Step: %s, Status %s, complete: %d percent", step.getName(), step.getStatus(), step.getPercentComplete()); } } Map<String, Object> returnValues = status.getReturnValues(); if (returnValues != null && returnValues.size() > 0) { logMessage(buildLog, "Instance contains %d return values", returnValues.size()); logMessage(buildLog, "Return values dump: \n %s", JsonParser.serialize(returnValues)); } } if (!StringUtils.isBlank(status.getErrorMessage())) { logMessage(buildLog, "ERROR instance status returned error %s", status.getErrorMessage()); } } /** * Outputs a log message into buildLog * * @param buildLog current build log * @param message log message, optionally w/ formatting placeholders * @param args arguments for string format of the message */ protected void logMessage(PrintStream buildLog, String message, Object... args) { buildLog.printf(LOG_MESSAGE_PREFIX.concat(message).concat("\n"), args); } /** * Timeout value, exposed for Jelly UI * * @return value of timeout */ public int getTimeout() { return timeout; } /** * Relative path to output of command * * @return value of path */ public String getOutputFilePath() { return outputFilePath; } /** * Target build status which should be set when instance returns failure status * * @return desired build status */ public String getFailureReaction() { return failureReaction.toString(); } public String isSelectedFailureReason(String candidate) { return candidate.equals(getFailureReaction()) ? "selected" : ""; } /** * Waits for instance to be in expected status and decides whether build was successful * see {@link #expectedStatus} * build is marked is failed when status is not reached * * @param build current * @param buildLog build log * @param instance instance of * @return true if expected status reached, otherwise false */ protected boolean waitForExpectedStatus(AbstractBuild build, PrintStream buildLog, Instance instance) { try { if (!waitForInstanceStatus(buildLog, instance)) { build.setResult(failureReaction); return failureReaction != Result.FAILURE; } else { //Since return values not always getting populated instantly, adding an explicit wait here Thread.sleep(2000); saveReturnValues(build, buildLog, instance); } } catch (QubellServiceException e) { logMessage(buildLog, "Error when getting instance status: %s", e.getMessage()); build.setResult(Result.FAILURE); return false; } catch (InterruptedException e) { logMessage(buildLog, "Build interrupted"); build.setResult(Result.FAILURE); return false; } catch (IOException e) { build.setResult(Result.FAILURE); return false; } return true; } /** * Saves instance return values into container accessible by further builds, see {@link #saveBuildVariable(hudson.model.AbstractBuild, String, String, java.io.PrintStream)} * see {@link VariablesAction} * * @param build current build * @param buildLog build log * @param instance a qubell instance to be queried for status * @throws InvalidCredentialsException when configuration contains invalid credentials */ protected void saveReturnValues(AbstractBuild build, PrintStream buildLog, Instance instance) throws InvalidCredentialsException, IOException, ResourceNotFoundException, NotAuthorizedException { if (StringUtils.isEmpty(outputFilePathResolved)) { logMessage(buildLog, "Output file is not specified, ignoring variables save"); return; } logMessage(buildLog, "Saving output data to file %s", outputFilePathResolved); InstanceStatus status = getServiceFacade().getStatus(instance); Map<String, Object> returnValues = status.getReturnValues(); Map<String, Object> resultMap = new HashMap<String, Object>(); resultMap.put("instanceId", status.getInstance().getId()); resultMap.put("applicationId", status.getApplication().getId()); resultMap.put("status", status.getStatus()); if (returnValues != null && returnValues.size() > 0) { logMessage(buildLog, "Saving %d return values", returnValues.size()); resultMap.put("returnValues", returnValues); } String outputContents = JsonParser.serialize(resultMap); saveFileToWorkspace(build, buildLog, outputContents, outputFilePathResolved); } /** * Saves a text file to master and slave (if available) workspaces * * @param build current build * @param buildLog current build log * @param contents contents of file to save * @param filePath relative file path * @throws IOException when file can't be saved or folders created */ protected void saveFileToWorkspace(AbstractBuild build, PrintStream buildLog, String contents, String filePath) throws IOException { Computer currentMachine = Computer.currentComputer(); FilePath workspaceOutput = build.getWorkspace().child(filePath); try { workspaceOutput.getParent().mkdirs(); workspaceOutput.write(contents, null); if (currentMachine instanceof SlaveComputer) { FilePath masterWorkspaceOutput = getMasterWorkspaceRoot(build, buildLog).child(filePath); masterWorkspaceOutput.getParent().mkdirs(); masterWorkspaceOutput.write(contents, null); } } catch (IOException e) { logMessage(buildLog, "Unable to save file to workspace %s", e); throw e; } catch (InterruptedException e) { throw new IOException(e); } } /** * Retrieves a path to workspace root on master node * * @param build current build * @param buildLog build log * @return path to master root */ protected FilePath getMasterWorkspaceRoot(AbstractBuild build, PrintStream buildLog) { AbstractProject project = build.getProject(); FilePath targetDirectory; if (project instanceof FreeStyleProject) { FreeStyleProject freeStyleProject = (FreeStyleProject) project; if (StringUtils.isNotEmpty(freeStyleProject.getCustomWorkspace())) { targetDirectory = new FilePath(new File(freeStyleProject.getCustomWorkspace())); } else { targetDirectory = new FilePath(new File(freeStyleProject.getRootDir(), "workspace")); } } else { targetDirectory = new FilePath(new File(project.getRootDir(), "workspace")); } try { //Attempt to create path when does not exist targetDirectory.mkdirs(); } catch (Exception e) { logMessage(buildLog, "unable to create target directory"); } return targetDirectory; } /** * Replaces build/environment variables placeholders within {@code source} with their respectful values * * @param build current build * @param listener build listener * @param source current build listeners * @return string with replaced placeholders or with placeholders themselves when no variable matched for a placeholder * @throws IOException * @throws InterruptedException */ protected String resolveVariableMacros(AbstractBuild build, BuildListener listener, String source) throws IOException, InterruptedException { if (StringUtils.isBlank(source)) { return source; } VariableResolver<String> vr = build.getBuildVariableResolver(); EnvVars env = build.getEnvironment(listener); return env.expand(Util.replaceMacro(source, vr)); } protected void resolveParameterPlaceholders(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException { this.outputFilePathResolved = resolveVariableMacros(build, listener, this.outputFilePath); } }