/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.brooklyn.test.framework; import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.ON_FIRE; import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.RUNNING; import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.STARTING; import static org.apache.brooklyn.core.entity.lifecycle.Lifecycle.STOPPED; import static org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.setExpectedState; import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.checkAssertions; import static org.apache.brooklyn.test.framework.TestFrameworkAssertions.getAssertions; import static org.apache.brooklyn.util.text.Strings.isBlank; import static org.apache.brooklyn.util.text.Strings.isNonBlank; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMap; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.mgmt.TaskFactory; import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks; import org.apache.brooklyn.core.entity.lifecycle.Lifecycle; import org.apache.brooklyn.core.location.Machines; import org.apache.brooklyn.location.ssh.SshMachineLocation; import org.apache.brooklyn.test.framework.TestFrameworkAssertions.AssertionSupport; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.core.task.DynamicTasks; import org.apache.brooklyn.util.core.task.ssh.SshTasks; import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.text.Identifiers; import org.apache.brooklyn.util.text.Strings; import org.apache.brooklyn.util.time.Duration; // TODO assertions below should use TestFrameworkAssertions but that class needs to be improved to give better error messages public class SimpleShellCommandTestImpl extends TargetableTestComponentImpl implements SimpleShellCommandTest { private static final Logger LOG = LoggerFactory.getLogger(SimpleShellCommandTestImpl.class); private static final int A_LINE = 80; public static final String DEFAULT_NAME = "download.sh"; private static final String CD = "cd"; @Override public void start(Collection<? extends Location> locations) { setExpectedState(this, STARTING); execute(); } @Override public void stop() { LOG.debug("{} Stopping simple command", this); setUpAndRunState(false, STOPPED); } @Override public void restart() { LOG.debug("{} Restarting simple command", this); execute(); } private void setUpAndRunState(boolean up, Lifecycle status) { sensors().set(SERVICE_UP, up); setExpectedState(this, status); } private static class Result { int exitCode; String stdout; String stderr; public Result(final ProcessTaskWrapper<Integer> job) { exitCode = job.get(); stdout = job.getStdout().trim(); stderr = job.getStderr().trim(); } public int getExitCode() { return exitCode; } public String getStdout() { return stdout; } public String getStderr() { return stderr; } } protected void handle(Result result) { LOG.debug("{}, Result is {}\nwith output [\n{}\n] and error [\n{}\n]", new Object[] { this, result.getExitCode(), shorten(result.getStdout()), shorten(result.getStderr()) }); ImmutableMap<String, Duration> flags = ImmutableMap.of("timeout", getConfig(TIMEOUT)); AssertionSupport support = new AssertionSupport(); checkAssertions(support, flags, exitCodeAssertions(), "exit code", Suppliers.ofInstance(result.getExitCode())); checkAssertions(support, flags, getAssertions(this, ASSERT_OUT), "stdout", Suppliers.ofInstance(result.getStdout())); checkAssertions(support, flags, getAssertions(this, ASSERT_ERR), "stderr", Suppliers.ofInstance(result.getStderr())); support.validate(); } private String shorten(String text) { return Strings.maxlenWithEllipsis(text, A_LINE); } public void execute() { try { SshMachineLocation machineLocation = Machines.findUniqueMachineLocation(resolveTarget().getLocations(), SshMachineLocation.class).get(); executeCommand(machineLocation); setUpAndRunState(true, RUNNING); } catch (Throwable t) { setUpAndRunState(false, ON_FIRE); throw Exceptions.propagate(t); } } private void executeCommand(SshMachineLocation machineLocation) { Result result = null; String downloadUrl = getConfig(DOWNLOAD_URL); String command = getConfig(COMMAND); String downloadName = DOWNLOAD_URL.getName(); String commandName = COMMAND.getName(); if (!(isNonBlank(downloadUrl) ^ isNonBlank(command))) { throw illegal("Must specify exactly one of", downloadName, "and", commandName); } if (isNonBlank(downloadUrl)) { String scriptDir = getConfig(SCRIPT_DIR); String scriptPath = calculateDestPath(downloadUrl, scriptDir); result = executeDownloadedScript(machineLocation, downloadUrl, scriptPath); } if (isNonBlank(command)) { result = executeShellCommand(machineLocation, command); } handle(result); } private Result executeDownloadedScript(SshMachineLocation machineLocation, String url, String scriptPath) { TaskFactory<?> install = SshTasks.installFromUrl(ImmutableMap.<String, Object>of(), machineLocation, url, scriptPath); DynamicTasks.queue(install); DynamicTasks.waitForLast(); List<String> commands = new ArrayList<>(); commands.add("chmod u+x " + scriptPath); maybeCdToRunDir(commands); commands.add(scriptPath); return runCommands(machineLocation, commands); } private Result executeShellCommand(SshMachineLocation machineLocation, String command) { List<String> commands = new ArrayList<>(); maybeCdToRunDir(commands); commands.add(command); return runCommands(machineLocation, commands); } private void maybeCdToRunDir(List<String> commands) { String runDir = getConfig(RUN_DIR); if (!isBlank(runDir)) { commands.add(CD + " " + runDir); } } private Result runCommands(SshMachineLocation machine, List<String> commands) { SshEffectorTasks.SshEffectorTaskFactory<Integer> etf = SshEffectorTasks.ssh(commands.toArray(new String[]{})) .machine(machine); ProcessTaskWrapper<Integer> job = DynamicTasks.queue(etf); job.asTask().blockUntilEnded(); return new Result(job); } private IllegalArgumentException illegal(String message, String ...messages) { return new IllegalArgumentException(Joiner.on(' ').join(this.toString() + ":", message, messages)); } private String calculateDestPath(String url, String directory) { try { URL asUrl = new URL(url); Iterable<String> path = Splitter.on("/").split(asUrl.getPath()); String scriptName = getLastPartOfPath(path, DEFAULT_NAME); return Joiner.on("/").join(directory, "test-" + Identifiers.makeRandomId(8), scriptName); } catch (MalformedURLException e) { throw illegal("Malformed URL:", url); } } private static String getLastPartOfPath(Iterable<String> path, String defaultName) { MutableList<String> parts = MutableList.copyOf(path); Collections.reverse(parts); Iterator<String> it = parts.iterator(); String scriptName = null; // strip any trailing "/" parts of URL while (isBlank(scriptName) && it.hasNext()) { scriptName = it.next(); } if (isBlank(scriptName)) { scriptName = defaultName; } return scriptName; } private List<Map<String, Object>> exitCodeAssertions() { List<Map<String, Object>> assertStatus = getAssertions(this, ASSERT_STATUS); List<Map<String, Object>> assertOut = getAssertions(this, ASSERT_OUT); List<Map<String, Object>> assertErr = getAssertions(this, ASSERT_ERR); List<Map<String, Object>> result; if (assertStatus.isEmpty() && assertOut.isEmpty() && assertErr.isEmpty()) { Map<String, Object> shouldSucceed = DEFAULT_ASSERTION; result = MutableList.of(shouldSucceed); } else { result = assertStatus; } return result; } }