All Downloads are FREE. Search and download functionalities are using the official Maven repository.

brooklyn.event.feed.shell.ShellFeed Maven / Gradle / Ivy

package brooklyn.event.feed.shell;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import brooklyn.entity.basic.EntityLocal;
import brooklyn.event.feed.AbstractFeed;
import brooklyn.event.feed.AttributePollHandler;
import brooklyn.event.feed.DelegatingPollHandler;
import brooklyn.event.feed.Poller;
import brooklyn.event.feed.function.FunctionFeed;
import brooklyn.event.feed.ssh.SshFeed;
import brooklyn.event.feed.ssh.SshPollValue;
import brooklyn.util.exceptions.Exceptions;
import brooklyn.util.stream.StreamGobbler;
import brooklyn.util.time.Time;

import com.google.common.base.Objects;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;

/**
 * Provides a feed of attribute values, by executing shell commands (on the local machine where 
 * this instance of brooklyn is running). Useful e.g. for paas tools such as Cloud Foundry vmc 
 * which operate against a remote target.
 * 
 * Example usage (e.g. in an entity that extends SoftwareProcessImpl):
 * 
 * {@code
 * private ShellFeed feed;
 * 
 * //@Override
 * protected void connectSensors() {
 *   super.connectSensors();
 *   
 *   feed = ShellFeed.builder()
 *       .entity(this)
 *       .machine(mySshMachineLachine)
 *       .poll(new ShellPollConfig(DISK_USAGE)
 *           .command("df -P | grep /dev")
 *           .failOnNonZeroResultCode(true)
 *           .onSuccess(new Function() {
 *                public Long apply(SshPollValue input) {
 *                  String[] parts = input.getStdout().split("[ \\t]+");
 *                  return Long.parseLong(parts[2]);
 *                }}))
 *       .build();
 * }
 * 
 * {@literal @}Override
 * protected void disconnectSensors() {
 *   super.disconnectSensors();
 *   if (feed != null) feed.stop();
 * }
 * }
 * 
* * @see SshFeed (to run on remote machines) * @see FunctionFeed (for arbitrary functions) * * @author aled */ public class ShellFeed extends AbstractFeed { public static final Logger log = LoggerFactory.getLogger(ShellFeed.class); public static Builder builder() { return new Builder(); } public static class Builder { private EntityLocal entity; private long period = 500; private TimeUnit periodUnits = TimeUnit.MILLISECONDS; private List> polls = Lists.newArrayList(); private volatile boolean built; public Builder entity(EntityLocal val) { this.entity = val; return this; } public Builder period(long millis) { return period(millis, TimeUnit.MILLISECONDS); } public Builder period(long val, TimeUnit units) { this.period = val; this.periodUnits = units; return this; } public Builder poll(ShellPollConfig config) { polls.add(config); return this; } public ShellFeed build() { built = true; ShellFeed result = new ShellFeed(this); result.start(); return result; } @Override protected void finalize() { if (!built) log.warn("ShellFeed.Builder created, but build() never called"); } } private static class ShellPollIdentifier { final String command; final Map env; final File dir; final String input; final String context; final long timeout; private ShellPollIdentifier(String command, Map env, File dir, String input, String context, long timeout) { this.command = checkNotNull(command, "command"); this.env = checkNotNull(env, "env"); this.dir = dir; this.input = input; this.context = checkNotNull(context, "context"); this.timeout = timeout; } @Override public int hashCode() { return Objects.hashCode(command, env, dir, input, timeout); } @Override public boolean equals(Object other) { if (!(other instanceof ShellPollIdentifier)) { return false; } ShellPollIdentifier o = (ShellPollIdentifier) other; return Objects.equal(command, o.command) && Objects.equal(env, o.env) && Objects.equal(dir, o.dir) && Objects.equal(input, o.input) && Objects.equal(timeout, o.timeout); } } // Treat as immutable once built private final SetMultimap> polls = HashMultimap.>create(); protected ShellFeed(Builder builder) { super(builder.entity); for (ShellPollConfig config : builder.polls) { ShellPollConfig configCopy = new ShellPollConfig(config); if (configCopy.getPeriod() < 0) configCopy.period(builder.period, builder.periodUnits); String command = config.getCommand(); Map env = config.getEnv(); File dir = config.getDir(); String input = config.getInput(); String context = config.getSensor().getName(); long timeout = config.getTimeout(); polls.put(new ShellPollIdentifier(command, env, dir, input, context, timeout), configCopy); } } @Override protected void preStart() { for (final ShellPollIdentifier pollInfo : polls.keySet()) { Set> configs = polls.get(pollInfo); long minPeriod = Integer.MAX_VALUE; Set> handlers = Sets.newLinkedHashSet(); for (ShellPollConfig config : configs) { handlers.add(new AttributePollHandler(config, entity, this)); if (config.getPeriod() > 0) minPeriod = Math.min(minPeriod, config.getPeriod()); } getPoller().scheduleAtFixedRate( new Callable() { public SshPollValue call() throws Exception { return exec(pollInfo.command, pollInfo.env, pollInfo.dir, pollInfo.input, pollInfo.context, pollInfo.timeout); }}, new DelegatingPollHandler(handlers), minPeriod); } } @SuppressWarnings("unchecked") private Poller getPoller() { return (Poller) poller; } /** * Executes the given command (using `bash -l -c $command`, so as to have a good path set). * * @param command The command to execute * @param env Environment variable settings, in format name=value * @param dir Working directory, or null to inherit from current process * @param input Input to send to the command (if not null) */ private SshPollValue exec(final String command, Map env, File dir, String input, final String context, final long timeout) { // TODO Implementation duplicates ShellUtils, but captures everything in return value (rather than just stdout) if (log.isTraceEnabled()) log.trace("Shell polling, executing {} with env {}", new Object[] {command, env}); String[] commandFull = new String[] {"bash", "-l", "-c", command}; List envFull = new ArrayList(env.size()); for (Map.Entry entry : env.entrySet()) { envFull.add(entry.getKey() + "=" + (entry.getValue() != null ? entry.getValue() : "")); } try { final Process proc = Runtime.getRuntime().exec(commandFull, envFull.toArray(new String[envFull.size()]), dir); ByteArrayOutputStream stdoutS = new ByteArrayOutputStream(); ByteArrayOutputStream stderrS = new ByteArrayOutputStream(); StreamGobbler stdoutG = new StreamGobbler(proc.getInputStream(), stdoutS, log).setLogPrefix("["+context+":stdout] "); stdoutG.start(); StreamGobbler stderrG = new StreamGobbler(proc.getErrorStream(), stderrS, log).setLogPrefix("["+context+":stderr] "); stderrG.start(); if (input != null && input.length() > 0) { proc.getOutputStream().write(input.getBytes()); proc.getOutputStream().flush(); } final AtomicBoolean ended = new AtomicBoolean(false); final AtomicBoolean killed = new AtomicBoolean(false); Thread t = new Thread(new Runnable() { public void run() { try { if (timeout>0) { Thread.sleep(timeout); if (!ended.get()) { log.debug("Timeout exceeded for {}% {}", context, command); proc.destroy(); killed.set(true); } } } catch (Exception e) { } }}); if (timeout < Long.MAX_VALUE) t.start(); int exitStatus = proc.waitFor(); ended.set(true); t.interrupt(); stdoutG.blockUntilFinished(); stderrG.blockUntilFinished(); String stdout = new String(stdoutS.toByteArray()); String stderr = new String(stderrS.toByteArray()); if (killed.get()) { log.warn("Command timed out after {} (throwing): {}% {}\nstdout={}\nstderr={}", new Object[] {Time.makeTimeString(timeout), context, command, stdout, stderr}); String msg = String.format("Command timed out after %s: %s (details logged)", Time.makeTimeString(timeout), command); throw new IllegalStateException(msg); } if (log.isDebugEnabled()) log.debug("Completed local command: {}% {}: exit code {}", new Object[] {context, command, exitStatus}); return new SshPollValue(null, exitStatus, stdout, stderr); } catch (Exception e) { throw Exceptions.propagate(e); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy