/* * 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.core.effector.ssh; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.apache.brooklyn.api.effector.Effector; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.mgmt.Task; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.config.StringConfigMap; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.config.ConfigUtils; import org.apache.brooklyn.core.effector.EffectorBody; import org.apache.brooklyn.core.effector.EffectorTasks; import org.apache.brooklyn.core.effector.EffectorTasks.EffectorTaskFactory; import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.location.internal.LocationInternal; import org.apache.brooklyn.core.mgmt.BrooklynTaskTags; 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.core.task.Tasks; import org.apache.brooklyn.util.core.task.ssh.SshFetchTaskFactory; import org.apache.brooklyn.util.core.task.ssh.SshFetchTaskWrapper; import org.apache.brooklyn.util.core.task.ssh.SshPutTaskFactory; import org.apache.brooklyn.util.core.task.ssh.SshPutTaskWrapper; import org.apache.brooklyn.util.core.task.ssh.SshTasks; import org.apache.brooklyn.util.core.task.ssh.internal.AbstractSshExecTaskFactory; import org.apache.brooklyn.util.core.task.ssh.internal.PlainSshExecTaskFactory; import org.apache.brooklyn.util.core.task.system.ProcessTaskFactory; import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper; import org.apache.brooklyn.util.ssh.BashCommands; import com.google.common.annotations.Beta; import com.google.common.base.Function; import com.google.common.collect.Maps; /** * Conveniences for generating {@link Task} instances to perform SSH activities. * <p> * If the {@link SshMachineLocation machine} is not specified directly it * will be inferred from the {@link Entity} context of either the {@link Effector} * or the current {@link Task}. * * @see SshTasks * @since 0.6.0 */ @Beta public class SshEffectorTasks { private static final Logger log = LoggerFactory.getLogger(SshEffectorTasks.class); public static final ConfigKey<Boolean> IGNORE_ENTITY_SSH_FLAGS = ConfigKeys.newBooleanConfigKey("ignoreEntitySshFlags", "Whether to ignore any ssh flags (behaviour constraints) set on the entity or location " + "where this is running, using only flags explicitly specified", false); /** * Like {@link EffectorBody} but providing conveniences when in an entity with a single machine location. */ public abstract static class SshEffectorBody<T> extends EffectorBody<T> { /** convenience for accessing the machine */ public SshMachineLocation machine() { return EffectorTasks.getSshMachine(entity()); } /** convenience for generating an {@link PlainSshExecTaskFactory} which can be further customised if desired, and then (it must be explicitly) queued */ public ProcessTaskFactory<Integer> ssh(String ...commands) { return new SshEffectorTaskFactory<Integer>(commands).machine(machine()); } } /** variant of {@link PlainSshExecTaskFactory} which fulfills the {@link EffectorTaskFactory} signature so can be used directly as an impl for an effector, * also injects the machine automatically; can also be used outwith effector contexts, and machine is still injected if it is * run from inside a task at an entity with a single SshMachineLocation */ public static class SshEffectorTaskFactory<RET> extends AbstractSshExecTaskFactory<SshEffectorTaskFactory<RET>,RET> implements EffectorTaskFactory<RET> { public SshEffectorTaskFactory(String ...commands) { super(commands); } public SshEffectorTaskFactory(SshMachineLocation machine, String ...commands) { super(machine, commands); } @Override public ProcessTaskWrapper<RET> newTask(Entity entity, Effector<RET> effector, ConfigBag parameters) { markDirty(); if (summary==null) summary(effector.getName()+" (ssh)"); machine(EffectorTasks.getSshMachine(entity)); return newTask(); } @Override public synchronized ProcessTaskWrapper<RET> newTask() { Entity entity = BrooklynTaskTags.getTargetOrContextEntity(Tasks.current()); if (machine==null) { if (log.isDebugEnabled()) log.debug("Using an ssh task not in an effector without any machine; will attempt to infer the machine: "+this); if (entity!=null) machine(EffectorTasks.getSshMachine(entity)); } applySshFlags(getConfig(), entity, getMachine()); return super.newTask(); } @Override public <T2> SshEffectorTaskFactory<T2> returning(ScriptReturnType type) { return (SshEffectorTaskFactory<T2>) super.<T2>returning(type); } @Override public SshEffectorTaskFactory<Boolean> returningIsExitCodeZero() { return (SshEffectorTaskFactory<Boolean>) super.returningIsExitCodeZero(); } public SshEffectorTaskFactory<String> requiringZeroAndReturningStdout() { return (SshEffectorTaskFactory<String>) super.requiringZeroAndReturningStdout(); } public <RET2> SshEffectorTaskFactory<RET2> returning(Function<ProcessTaskWrapper<?>, RET2> resultTransformation) { return (SshEffectorTaskFactory<RET2>) super.returning(resultTransformation); } } public static class SshPutEffectorTaskFactory extends SshPutTaskFactory implements EffectorTaskFactory<Void> { public SshPutEffectorTaskFactory(String remoteFile) { super(remoteFile); } public SshPutEffectorTaskFactory(SshMachineLocation machine, String remoteFile) { super(machine, remoteFile); } @Override public SshPutTaskWrapper newTask(Entity entity, Effector<Void> effector, ConfigBag parameters) { machine(EffectorTasks.getSshMachine(entity)); applySshFlags(getConfig(), entity, getMachine()); return super.newTask(); } @Override public SshPutTaskWrapper newTask() { Entity entity = BrooklynTaskTags.getTargetOrContextEntity(Tasks.current()); if (machine==null) { if (log.isDebugEnabled()) log.debug("Using an ssh put task not in an effector without any machine; will attempt to infer the machine: "+this); if (entity!=null) { machine(EffectorTasks.getSshMachine(entity)); } } applySshFlags(getConfig(), entity, getMachine()); return super.newTask(); } } public static class SshFetchEffectorTaskFactory extends SshFetchTaskFactory implements EffectorTaskFactory<String> { public SshFetchEffectorTaskFactory(String remoteFile) { super(remoteFile); } public SshFetchEffectorTaskFactory(SshMachineLocation machine, String remoteFile) { super(machine, remoteFile); } @Override public SshFetchTaskWrapper newTask(Entity entity, Effector<String> effector, ConfigBag parameters) { machine(EffectorTasks.getSshMachine(entity)); applySshFlags(getConfig(), entity, getMachine()); return super.newTask(); } @Override public SshFetchTaskWrapper newTask() { Entity entity = BrooklynTaskTags.getTargetOrContextEntity(Tasks.current()); if (machine==null) { if (log.isDebugEnabled()) log.debug("Using an ssh fetch task not in an effector without any machine; will attempt to infer the machine: "+this); if (entity!=null) machine(EffectorTasks.getSshMachine(entity)); } applySshFlags(getConfig(), entity, getMachine()); return super.newTask(); } } /** * @since 0.9.0 */ public static SshEffectorTaskFactory<Integer> ssh(SshMachineLocation machine, String ...commands) { return new SshEffectorTaskFactory<Integer>(machine, commands); } public static SshEffectorTaskFactory<Integer> ssh(String ...commands) { return new SshEffectorTaskFactory<Integer>(commands); } public static SshEffectorTaskFactory<Integer> ssh(List<String> commands) { return ssh(commands.toArray(new String[commands.size()])); } public static SshPutTaskFactory put(String remoteFile) { return new SshPutEffectorTaskFactory(remoteFile); } public static SshFetchEffectorTaskFactory fetch(String remoteFile) { return new SshFetchEffectorTaskFactory(remoteFile); } /** task which returns 0 if pid is running */ public static SshEffectorTaskFactory<Integer> codePidRunning(Integer pid) { return ssh("ps -p "+pid).summary("PID "+pid+" is-running check (exit code)").allowingNonZeroExitCode(); } /** task which fails if the given PID is not running */ public static SshEffectorTaskFactory<?> requirePidRunning(Integer pid) { return codePidRunning(pid).summary("PID "+pid+" is-running check (required)").requiringExitCodeZero("Process with PID "+pid+" is required to be running"); } /** as {@link #codePidRunning(Integer)} but returning boolean */ public static SshEffectorTaskFactory<Boolean> isPidRunning(Integer pid) { return codePidRunning(pid).summary("PID "+pid+" is-running check (boolean)").returning(new Function<ProcessTaskWrapper<?>, Boolean>() { public Boolean apply(@Nullable ProcessTaskWrapper<?> input) { return Integer.valueOf(0).equals(input.getExitCode()); } }); } /** task which returns 0 if pid in the given file is running; * method accepts wildcards so long as they match a single file on the remote end * <p> * returns 1 if no matching file, * 1 if matching file but no matching process, * and 2 if 2+ matching files */ public static SshEffectorTaskFactory<Integer> codePidFromFileRunning(final String pidFile) { return ssh(BashCommands.chain( // this fails, but isn't an error BashCommands.requireTest("-f "+pidFile, "The PID file "+pidFile+" does not exist."), // this fails and logs an error picked up later BashCommands.requireTest("`ls "+pidFile+" | wc -w` -eq 1", "ERROR: there are multiple matching PID files"), // this fails and logs an error picked up later BashCommands.require("cat "+pidFile, "ERROR: the PID file "+pidFile+" cannot be read (permissions?)."), // finally check the process "ps -p `cat "+pidFile+"`")).summary("PID file "+pidFile+" is-running check (exit code)") .allowingNonZeroExitCode() .addCompletionListener(new Function<ProcessTaskWrapper<?>,Void>() { public Void apply(ProcessTaskWrapper<?> input) { if (input.getStderr().contains("ERROR:")) throw new IllegalStateException("Invalid or inaccessible PID filespec: "+pidFile); return null; } }); } /** task which fails if the pid in the given file is not running (or if there is no such PID file); * method accepts wildcards so long as they match a single file on the remote end (fails if 0 or 2+ matching files) */ public static SshEffectorTaskFactory<?> requirePidFromFileRunning(String pidFile) { return codePidFromFileRunning(pidFile) .summary("PID file "+pidFile+" is-running check (required)") .requiringExitCodeZero("Process with PID from file "+pidFile+" is required to be running"); } /** as {@link #codePidFromFileRunning(String)} but returning boolean */ public static SshEffectorTaskFactory<Boolean> isPidFromFileRunning(String pidFile) { return codePidFromFileRunning(pidFile).summary("PID file "+pidFile+" is-running check (boolean)"). returning(new Function<ProcessTaskWrapper<?>, Boolean>() { public Boolean apply(@Nullable ProcessTaskWrapper<?> input) { return ((Integer)0).equals(input.getExitCode()); } }); } /** extracts the values for the main brooklyn.ssh.config.* config keys (i.e. those declared in ConfigKeys) * as declared on the entity, and inserts them in a map using the unprefixed state, for ssh. * <p> * currently this is computed for each call, which may be wasteful, but it is reliable in the face of config changes. * we could cache the Map. note that we do _not_ cache (or even own) the SshTool; * the SshTool is created or re-used by the SshMachineLocation making use of these properties */ @Beta public static Map<String, Object> getSshFlags(Entity entity, Location optionalLocation) { ConfigBag allConfig = ConfigBag.newInstance(); StringConfigMap globalConfig = ((EntityInternal)entity).getManagementContext().getConfig(); allConfig.putAll(globalConfig.getAllConfig()); if (optionalLocation!=null) allConfig.putAll(((LocationInternal)optionalLocation).config().getBag()); allConfig.putAll(((EntityInternal)entity).getAllConfig()); Map<String, Object> result = Maps.newLinkedHashMap(); for (String keyS : allConfig.getAllConfig().keySet()) { if (keyS.startsWith(SshTool.BROOKLYN_CONFIG_KEY_PREFIX)) { ConfigKey<?> key = ConfigKeys.newConfigKey(Object.class, keyS); Object val = allConfig.getStringKey(keyS); /* * NOV 2013 changing this to rely on config above being inserted in the right order, * so entity config will be preferred over location, and location over global. * If that is consistent then remove the lines below. * (We can also accept null entity and so combine with SshTasks.getSshFlags.) */ // // have to use raw config to test whether the config is set // Object val = ((EntityInternal)entity).getConfigMap().getRawConfig(key); // if (val!=null) { // val = entity.getConfig(key); // } else { // val = globalConfig.getRawConfig(key); // if (val!=null) val = globalConfig.getConfig(key); // } // if (val!=null) { result.put(ConfigUtils.unprefixedKey(SshTool.BROOKLYN_CONFIG_KEY_PREFIX, key).getName(), val); // } } } return result; } private static void applySshFlags(ConfigBag config, Entity entity, Location machine) { if (entity!=null) { if (!config.get(IGNORE_ENTITY_SSH_FLAGS)) { config.putIfAbsent(getSshFlags(entity, machine)); } } } }