/*
* 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.ssh;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
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.Entity;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
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.core.location.Locations;
import org.apache.brooklyn.core.location.Machines;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.internal.ssh.SshTool;
import org.apache.brooklyn.util.time.Duration;
import com.google.common.base.Objects;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
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 polling over ssh.
*
* Example usage (e.g. in an entity that extends SoftwareProcessImpl):
* <pre>
* {@code
* private SshFeed feed;
*
* //@Override
* protected void connectSensors() {
* super.connectSensors();
*
* feed = SshFeed.builder()
* .entity(this)
* .machine(mySshMachineLachine)
* .poll(new SshPollConfig<Boolean>(SERVICE_UP)
* .command("rabbitmqctl -q status")
* .onSuccess(new Function<SshPollValue, Boolean>() {
* public Boolean apply(SshPollValue input) {
* return (input.getExitStatus() == 0);
* }}))
* .build();
* }
*
* {@literal @}Override
* protected void disconnectSensors() {
* super.disconnectSensors();
* if (feed != null) feed.stop();
* }
* }
* </pre>
*
* @author aled
*/
public class SshFeed extends AbstractFeed {
public static final Logger log = LoggerFactory.getLogger(SshFeed.class);
@SuppressWarnings("serial")
public static final ConfigKey<Supplier<SshMachineLocation>> MACHINE = ConfigKeys.newConfigKey(
new TypeToken<Supplier<SshMachineLocation>>() {},
"machine");
public static final ConfigKey<Boolean> EXEC_AS_COMMAND = ConfigKeys.newBooleanConfigKey("execAsCommand");
@SuppressWarnings("serial")
public static final ConfigKey<SetMultimap<SshPollIdentifier, SshPollConfig<?>>> POLLS = ConfigKeys.newConfigKey(
new TypeToken<SetMultimap<SshPollIdentifier, SshPollConfig<?>>>() {},
"polls");
public static Builder builder() {
return new Builder();
}
public static class Builder {
private EntityLocal entity;
private boolean onlyIfServiceUp = false;
private Supplier<SshMachineLocation> machine;
private Duration period = Duration.of(500, TimeUnit.MILLISECONDS);
private List<SshPollConfig<?>> polls = Lists.newArrayList();
private boolean execAsCommand = false;
private String uniqueTag;
private volatile boolean built;
public Builder entity(EntityLocal val) {
this.entity = val;
return this;
}
public Builder onlyIfServiceUp() { return onlyIfServiceUp(true); }
public Builder onlyIfServiceUp(boolean onlyIfServiceUp) {
this.onlyIfServiceUp = onlyIfServiceUp;
return this;
}
/** optional, to force a machine; otherwise it is inferred from the entity */
public Builder machine(SshMachineLocation val) { return machine(Suppliers.ofInstance(val)); }
/** optional, to force a machine; otherwise it is inferred from the entity */
public Builder machine(Supplier<SshMachineLocation> val) {
this.machine = val;
return this;
}
public Builder period(Duration period) {
this.period = period;
return this;
}
public Builder period(long millis) {
return period(Duration.of(millis, TimeUnit.MILLISECONDS));
}
public Builder period(long val, TimeUnit units) {
return period(Duration.of(val, units));
}
public Builder poll(SshPollConfig<?> config) {
polls.add(config);
return this;
}
public Builder execAsCommand() {
execAsCommand = true;
return this;
}
public Builder execAsScript() {
execAsCommand = false;
return this;
}
public Builder uniqueTag(String uniqueTag) {
this.uniqueTag = uniqueTag;
return this;
}
public SshFeed build() {
built = true;
SshFeed result = new SshFeed(this);
result.setEntity(checkNotNull(entity, "entity"));
result.start();
return result;
}
@Override
protected void finalize() {
if (!built) log.warn("SshFeed.Builder created, but build() never called");
}
}
private static class SshPollIdentifier {
final Supplier<String> command;
final Supplier<Map<String, String>> env;
private SshPollIdentifier(Supplier<String> command, Supplier<Map<String, String>> env) {
this.command = checkNotNull(command, "command");
this.env = checkNotNull(env, "env");
}
@Override
public int hashCode() {
return Objects.hashCode(command, env);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof SshPollIdentifier)) {
return false;
}
SshPollIdentifier o = (SshPollIdentifier) other;
return Objects.equal(command, o.command) &&
Objects.equal(env, o.env);
}
}
/** @deprecated since 0.7.0, use static convenience on {@link Locations} */
@Deprecated
public static SshMachineLocation getMachineOfEntity(Entity entity) {
return Machines.findUniqueMachineLocation(entity.getLocations(), SshMachineLocation.class).orNull();
}
/**
* For rebind; do not call directly; use builder
*/
public SshFeed() {
}
protected SshFeed(final Builder builder) {
setConfig(ONLY_IF_SERVICE_UP, builder.onlyIfServiceUp);
setConfig(MACHINE, builder.machine != null ? builder.machine : null);
setConfig(EXEC_AS_COMMAND, builder.execAsCommand);
SetMultimap<SshPollIdentifier, SshPollConfig<?>> polls = HashMultimap.<SshPollIdentifier,SshPollConfig<?>>create();
for (SshPollConfig<?> config : builder.polls) {
@SuppressWarnings({ "unchecked", "rawtypes" })
SshPollConfig<?> configCopy = new SshPollConfig(config);
if (configCopy.getPeriod() < 0) configCopy.period(builder.period);
polls.put(new SshPollIdentifier(config.getCommandSupplier(), config.getEnvSupplier()), configCopy);
}
setConfig(POLLS, polls);
initUniqueTag(builder.uniqueTag, polls.values());
}
protected SshMachineLocation getMachine() {
Supplier<SshMachineLocation> supplier = getConfig(MACHINE);
if (supplier != null) {
return supplier.get();
} else {
return Locations.findUniqueSshMachineLocation(entity.getLocations()).get();
}
}
@Override
protected void preStart() {
SetMultimap<SshPollIdentifier, SshPollConfig<?>> polls = getConfig(POLLS);
for (final SshPollIdentifier pollInfo : polls.keySet()) {
Set<SshPollConfig<?>> configs = polls.get(pollInfo);
long minPeriod = Integer.MAX_VALUE;
Set<AttributePollHandler<? super SshPollValue>> handlers = Sets.newLinkedHashSet();
for (SshPollConfig<?> config : configs) {
handlers.add(new AttributePollHandler<SshPollValue>(config, entity, this));
if (config.getPeriod() > 0) minPeriod = Math.min(minPeriod, config.getPeriod());
}
getPoller().scheduleAtFixedRate(
new Callable<SshPollValue>() {
public SshPollValue call() throws Exception {
return exec(pollInfo.command.get(), pollInfo.env.get());
}},
new DelegatingPollHandler<SshPollValue>(handlers),
minPeriod);
}
}
@SuppressWarnings("unchecked")
protected Poller<SshPollValue> getPoller() {
return (Poller<SshPollValue>) super.getPoller();
}
private SshPollValue exec(String command, Map<String,String> env) throws IOException {
SshMachineLocation machine = getMachine();
Boolean execAsCommand = getConfig(EXEC_AS_COMMAND);
if (log.isTraceEnabled()) log.trace("Ssh polling for {}, executing {} with env {}", new Object[] {machine, command, env});
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
int exitStatus;
ConfigBag flags = ConfigBag.newInstance()
.configure(SshTool.PROP_NO_EXTRA_OUTPUT, true)
.configure(SshTool.PROP_OUT_STREAM, stdout)
.configure(SshTool.PROP_ERR_STREAM, stderr);
if (Boolean.TRUE.equals(execAsCommand)) {
exitStatus = machine.execCommands(flags.getAllConfig(),
"ssh-feed", ImmutableList.of(command), env);
} else {
exitStatus = machine.execScript(flags.getAllConfig(),
"ssh-feed", ImmutableList.of(command), env);
}
return new SshPollValue(machine, exitStatus, new String(stdout.toByteArray()), new String(stderr.toByteArray()));
}
}