/* * 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.JsonParser; import com.qubell.services.Instance; import com.qubell.services.InstanceStatus; import com.qubell.services.InstanceStatusCode; import com.qubell.services.exceptions.*; import hudson.Extension; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.Result; import hudson.util.FormValidation; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.time.StopWatch; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import javax.servlet.ServletException; import java.io.IOException; import java.io.PrintStream; import java.util.HashMap; import java.util.Map; /** * Runs a command on Qubell instance, initialized by {@link StartInstanceBuilder} * Waits for instance to reach the Running state and attempts to save return values (if any) * * @author Alex Krupnov */ public class RunCommandBuilder extends QubellBuilder { private final String commandName; private String commandNameResolved; private final String extraParameters; private String extraParametersResolved; private String instanceId; private String instanceIdResolved; private InstanceOptions instanceOptions; private AsyncExecutionOptions asyncExecutionOptions; private String jobId; private String jobIdResolved; /** * A data bound constructor, executed by Jenkins * * @param name command name * @param extraParameters extended parameters {@link #getExtraParameters()} * @param timeout execution timeout {@link #getTimeout()} * @param instanceOptions pre-defined instance options see {@link #getInstanceId()} * @param outputFilePath path to output file * @param failureReaction a target build status which should be set when instance returns failure status * @param asyncExecutionOptions optional settings for asynchronous job execution */ @DataBoundConstructor public RunCommandBuilder(String name, String extraParameters, String timeout, InstanceOptions instanceOptions, String outputFilePath, String failureReaction, AsyncExecutionOptions asyncExecutionOptions) { this(name, extraParameters, timeout, instanceOptions, outputFilePath, InstanceStatusCode.RUNNING, failureReaction, asyncExecutionOptions); } protected RunCommandBuilder(String name, String extraParameters, String timeout, InstanceOptions instanceOptions, String outputFilePath, InstanceStatusCode expectedStatus, String failureReaction, AsyncExecutionOptions asyncExecutionOptions) { super(timeout, expectedStatus, outputFilePath, failureReaction); this.commandName = name; this.extraParameters = extraParameters; this.instanceOptions = instanceOptions; this.asyncExecutionOptions = asyncExecutionOptions; if (instanceOptions != null) { this.instanceId = instanceOptions.getInstanceId(); } if (asyncExecutionOptions != null) { this.jobId = asyncExecutionOptions.getJobId(); } } /** * @return Command name, exposed for Jelly UI */ public String getName() { return commandName; } /** * Id of pre-defined instance * * @return value or null */ public String getInstanceId() { return instanceId; } /** * A custom, pre defined instance information, if supplied, saved value is ignored * * @return instance options or null */ public InstanceOptions getInstanceOptions() { return instanceOptions; } /** * Identifier for the job, spawned by {@link RunCommandBuilder}, used to pick the job result later * * @return identifier of the job */ public String getJobId() { return jobId; } /** * Optional settings for async job execution * * @return async settings or null */ public AsyncExecutionOptions getAsyncExecutionOptions() { return asyncExecutionOptions; } /** * Gets instance id from build pre-defined value or, if not pre-defined, from persistent container * * @param build current build * @param buildLog build log * @return value of instance id */ protected String retrieveInstanceId(AbstractBuild build, PrintStream buildLog) { String instanceId = !StringUtils.isBlank(instanceIdResolved) ? instanceIdResolved : readBuildVariable(build, INSTANCE_ID_KEY, buildLog); logMessage(buildLog, "retrieved instance id %s", instanceId); return instanceId; } @Override protected void resolveParameterPlaceholders(AbstractBuild build, BuildListener listener) throws IOException, InterruptedException { super.resolveParameterPlaceholders(build, listener); this.instanceIdResolved = resolveVariableMacros(build, listener, this.instanceId); this.commandNameResolved = resolveVariableMacros(build, listener, this.commandName); this.extraParametersResolved = resolveVariableMacros(build, listener, this.extraParameters); this.jobIdResolved = resolveVariableMacros(build, listener, this.jobId); } /** * @return extra parameters (json string), exposed for Jelly UI */ public String getExtraParameters() { return extraParameters; } /** * Performs a build with following steps * <ol> * <li>Attempts to get instance id, saved previously</li> * <li>Launches runs given command on instance</li> * <li>Waits for instance to turn into {@link #expectedStatus} state/li> * <li>Saves the return values of instance if any</li> * </ol> * If any of steps above failed, fails the job * * @param build current build * @param launcher build launcher * @param listener build listener * @return true of builder finished successfully, otherwise false */ @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { resolveParameterPlaceholders(build, listener); PrintStream buildLog = listener.getLogger(); if (!validateConfiguration()) { logMessage(buildLog, "Unable to proceed without configuration. Please check global settings page."); build.setResult(Result.FAILURE); return false; } logMessage(buildLog, "Getting instance id"); String instanceId = retrieveInstanceId(build, buildLog); if (StringUtils.isBlank(instanceId)) { logMessage(buildLog, "Unable to run command, instance id is unavailable, instance was not started during the build"); build.setResult(Result.FAILURE); return false; } Instance instance = new Instance(instanceId); logMessage(buildLog, "Running command %s on instance %s.", commandNameResolved, instance.getId()); int attempt = 0; StopWatch sw = new StopWatch(); sw.start(); while (true) { attempt++; try { getServiceFacade().runCommand(instance, commandNameResolved, JsonParser.parseMap(extraParametersResolved)); break; }catch (InstanceBusyException ibe){ logMessage(buildLog, "Instance not ready to accept requests. Sleep #%d", attempt); if (sw.getTime() >= timeout * 1000) { logMessage(buildLog, "Timeout exceeded"); build.setResult(Result.FAILURE); return false; } Thread.sleep(getConfiguration().getStatusPollingInterval() * 1000); } catch (QubellServiceException e) { logMessage(buildLog, "Error when running command: %s", e.getMessage()); build.setResult(Result.FAILURE); return false; } } if (StringUtils.isNotBlank(jobIdResolved)) { logMessage(buildLog, "Job configured to be ran asynchronously, saving instance id and expected status for job id %s", jobIdResolved); Map<String, Object> asyncData = new HashMap<String, Object>(); asyncData.put(ASYNC_INSTANCE_ID_KEY, instanceId); asyncData.put(ASYNC_EXPECTED_STATUS_KEY, expectedStatus); asyncData.put(ASYNC_OUTPUT_PATH_KEY, getOutputFilePath()); saveFileToWorkspace(build, buildLog, JsonParser.serialize(asyncData), jobIdResolved); return true; } return waitForExpectedStatus(build, buildLog, instance); } /** * Descriptor for {@link RunCommandBuilder}. Used as a singleton. * The class is marked as public so that it can be accessed from views. * See <tt>src/main/resources/com/qubell/jenkinsci/plugins/qubell/builders/RunCommandBuilder/*.jelly</tt> * for the actual HTML fragment for the configuration screen. */ @Extension // This indicates to Jenkins that this is an implementation of an extension point. public static final class RunCommandDescriptor extends BaseDescriptor { /** * Performs on-the-fly validation of the form field command name * * @param value This parameter receives the value that the user has typed. * @return Indicates the outcome of the validation. This is sent to the browser. */ public FormValidation doCheckName(@QueryParameter String value) throws IOException, ServletException { if (value.length() == 0) return FormValidation.error("Please set a commandName"); return FormValidation.ok(); } /** * This human readable name is used in the configuration screen. */ public String getDisplayName() { return "Qubell: Run Command"; } } }