/* * 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.feed.shell; import static com.google.common.base.Preconditions.checkNotNull; import java.io.File; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import org.apache.brooklyn.api.entity.EntityLocal; import org.apache.brooklyn.api.mgmt.ExecutionContext; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.feed.AbstractFeed; import org.apache.brooklyn.core.feed.AttributePollHandler; import org.apache.brooklyn.core.feed.DelegatingPollHandler; import org.apache.brooklyn.core.feed.Poller; import org.apache.brooklyn.feed.function.FunctionFeed; import org.apache.brooklyn.feed.ssh.SshFeed; import org.apache.brooklyn.feed.ssh.SshPollValue; import org.apache.brooklyn.util.core.task.system.ProcessTaskFactory; import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper; import org.apache.brooklyn.util.core.task.system.internal.SystemProcessTaskFactory.ConcreteSystemProcessTaskFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import com.google.common.reflect.TypeToken; /** * 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): * <pre> * {@code * private ShellFeed feed; * * //@Override * protected void connectSensors() { * super.connectSensors(); * * feed = ShellFeed.builder() * .entity(this) * .machine(mySshMachineLachine) * .poll(new ShellPollConfig<Long>(DISK_USAGE) * .command("df -P | grep /dev") * .failOnNonZeroResultCode(true) * .onSuccess(new Function<SshPollValue, Long>() { * 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(); * } * } * </pre> * * @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); @SuppressWarnings("serial") private static final ConfigKey<SetMultimap<ShellPollIdentifier, ShellPollConfig<?>>> POLLS = ConfigKeys.newConfigKey( new TypeToken<SetMultimap<ShellPollIdentifier, ShellPollConfig<?>>>() {}, "polls"); 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<ShellPollConfig<?>> polls = Lists.newArrayList(); private String uniqueTag; 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 Builder uniqueTag(String uniqueTag) { this.uniqueTag = uniqueTag; return this; } public ShellFeed build() { built = true; ShellFeed result = new ShellFeed(this); result.setEntity(checkNotNull(entity, "entity")); 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<String, String> env; final File dir; final String input; final String context; final long timeout; private ShellPollIdentifier(String command, Map<String, String> 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); } } /** * For rebind; do not call directly; use builder */ public ShellFeed() { } protected ShellFeed(Builder builder) { super(); SetMultimap<ShellPollIdentifier, ShellPollConfig<?>> polls = HashMultimap.<ShellPollIdentifier,ShellPollConfig<?>>create(); for (ShellPollConfig<?> config : builder.polls) { if (!config.isEnabled()) continue; @SuppressWarnings({ "unchecked", "rawtypes" }) ShellPollConfig<?> configCopy = new ShellPollConfig(config); if (configCopy.getPeriod() < 0) configCopy.period(builder.period, builder.periodUnits); String command = config.getCommand(); Map<String, String> 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); } setConfig(POLLS, polls); initUniqueTag(builder.uniqueTag, polls.values()); } @Override protected void preStart() { SetMultimap<ShellPollIdentifier, ShellPollConfig<?>> polls = getConfig(POLLS); for (final ShellPollIdentifier pollInfo : polls.keySet()) { Set<ShellPollConfig<?>> configs = polls.get(pollInfo); long minPeriod = Integer.MAX_VALUE; Set<AttributePollHandler<? super SshPollValue>> handlers = Sets.newLinkedHashSet(); for (ShellPollConfig<?> config : configs) { handlers.add(new AttributePollHandler<SshPollValue>(config, entity, this)); if (config.getPeriod() > 0) minPeriod = Math.min(minPeriod, config.getPeriod()); } final ProcessTaskFactory<?> taskFactory = newTaskFactory(pollInfo.command, pollInfo.env, pollInfo.dir, pollInfo.input, pollInfo.context, pollInfo.timeout); final ExecutionContext executionContext = ((EntityInternal) entity).getManagementSupport().getExecutionContext(); getPoller().scheduleAtFixedRate( new Callable<SshPollValue>() { @Override public SshPollValue call() throws Exception { ProcessTaskWrapper<?> taskWrapper = taskFactory.newTask(); executionContext.submit(taskWrapper); taskWrapper.block(); Optional<Integer> exitCode = Optional.fromNullable(taskWrapper.getExitCode()); return new SshPollValue(null, exitCode.or(-1), taskWrapper.getStdout(), taskWrapper.getStderr()); }}, new DelegatingPollHandler<SshPollValue>(handlers), minPeriod); } } @SuppressWarnings("unchecked") protected Poller<SshPollValue> getPoller() { return (Poller<SshPollValue>) super.getPoller(); } /** * 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) */ protected ProcessTaskFactory<?> newTaskFactory(final String command, Map<String,String> env, File dir, String input, final String summary, final long timeout) { // FIXME Add generic timeout() support to task ExecutionManager if (timeout > 0) { log.warn("Timeout ({}ms) not currently supported for ShellFeed {}", timeout, this); } return new ConcreteSystemProcessTaskFactory<Object>(command) .environmentVariables(env) .loginShell(true) .directory(dir) .runAsCommand() .summary(summary); } }