/* * 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.util.core.task.ssh; import java.util.Map; import javax.annotation.Nullable; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.api.mgmt.Task; import org.apache.brooklyn.api.mgmt.TaskAdaptable; import org.apache.brooklyn.api.mgmt.TaskFactory; import org.apache.brooklyn.api.mgmt.TaskQueueingContext; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.config.ConfigUtils; import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks; import org.apache.brooklyn.core.location.AbstractLocation; 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.ResourceUtils; import org.apache.brooklyn.util.core.config.ConfigBag; import org.apache.brooklyn.util.core.internal.ssh.SshTool; import org.apache.brooklyn.util.core.task.DynamicTasks; import org.apache.brooklyn.util.core.task.Tasks; 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.net.Urls; import org.apache.brooklyn.util.ssh.BashCommands; import org.apache.brooklyn.util.stream.Streams; import org.apache.brooklyn.util.text.Identifiers; import org.apache.brooklyn.util.text.Strings; import com.google.common.annotations.Beta; import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; /** * Conveniences for generating {@link Task} instances to perform SSH activities on an {@link SshMachineLocation}. * <p> * To infer the {@link SshMachineLocation} and take properties from entities and global management context the * {@link SshEffectorTasks} should be preferred over this class. * * @see SshEffectorTasks * @since 0.6.0 */ @Beta public class SshTasks { private static final Logger log = LoggerFactory.getLogger(SshTasks.class); public static ProcessTaskFactory<Integer> newSshExecTaskFactory(SshMachineLocation machine, String ...commands) { return newSshExecTaskFactory(machine, true, commands); } public static ProcessTaskFactory<Integer> newSshExecTaskFactory(SshMachineLocation machine, final boolean useMachineConfig, String ...commands) { return new PlainSshExecTaskFactory<Integer>(machine, commands) { { if (useMachineConfig) config.putIfAbsent(getSshFlags(machine)); } }; } public static SshPutTaskFactory newSshPutTaskFactory(SshMachineLocation machine, String remoteFile) { return newSshPutTaskFactory(machine, true, remoteFile); } public static SshPutTaskFactory newSshPutTaskFactory(SshMachineLocation machine, final boolean useMachineConfig, String remoteFile) { return new SshPutTaskFactory(machine, remoteFile) { { if (useMachineConfig) config.putIfAbsent(getSshFlags(machine)); } }; } public static SshFetchTaskFactory newSshFetchTaskFactory(SshMachineLocation machine, String remoteFile) { return newSshFetchTaskFactory(machine, true, remoteFile); } public static SshFetchTaskFactory newSshFetchTaskFactory(SshMachineLocation machine, final boolean useMachineConfig, String remoteFile) { return new SshFetchTaskFactory(machine, remoteFile) { { if (useMachineConfig) config.putIfAbsent(getSshFlags(machine)); } }; } private static Map<String, Object> getSshFlags(Location location) { ConfigBag allConfig = ConfigBag.newInstance(); if (location instanceof AbstractLocation) { ManagementContext mgmt = ((AbstractLocation)location).getManagementContext(); if (mgmt!=null) allConfig.putAll(mgmt.getConfig().getAllConfig()); } allConfig.putAll(((LocationInternal)location).config().getBag()); Map<String, Object> result = Maps.newLinkedHashMap(); for (String keyS : allConfig.getAllConfig().keySet()) { ConfigKey<?> key = ConfigKeys.newConfigKey(Object.class, keyS); if (key.getName().startsWith(SshTool.BROOKLYN_CONFIG_KEY_PREFIX)) { result.put(ConfigUtils.unprefixedKey(SshTool.BROOKLYN_CONFIG_KEY_PREFIX, key).getName(), allConfig.get(key)); } } return result; } @Beta public static enum OnFailingTask { FAIL, /** issues a warning, sometimes implemented as marking the task inessential and failing it if it appears * we are in a dynamic {@link TaskQueueingContext}; * useful because this way the warning appears to the user; * but note that the check is done against the calling thread so use with some care * (and thus this enum is currently here rather then elsewhere) */ WARN_OR_IF_DYNAMIC_FAIL_MARKING_INESSENTIAL, /** issues a warning in the log if the task fails, otherwise swallows it */ WARN_IN_LOG_ONLY, /** not even a warning if the task fails (the caller is expected to handle it as appropriate) */ IGNORE } public static ProcessTaskFactory<Boolean> dontRequireTtyForSudo(SshMachineLocation machine, final boolean failIfCantSudo) { return dontRequireTtyForSudo(machine, failIfCantSudo ? OnFailingTask.FAIL : OnFailingTask.WARN_IN_LOG_ONLY); } /** creates a task which returns modifies sudoers to ensure non-tty access is permitted; * also gives nice warnings if sudo is not permitted */ public static ProcessTaskFactory<Boolean> dontRequireTtyForSudo(SshMachineLocation machine, OnFailingTask onFailingTaskRequested) { final OnFailingTask onFailingTask; if (onFailingTaskRequested==OnFailingTask.WARN_OR_IF_DYNAMIC_FAIL_MARKING_INESSENTIAL) { if (DynamicTasks.getTaskQueuingContext()!=null) onFailingTask = onFailingTaskRequested; else onFailingTask = OnFailingTask.WARN_IN_LOG_ONLY; } else { onFailingTask = onFailingTaskRequested; } final String id = Identifiers.makeRandomId(6); return newSshExecTaskFactory(machine, BashCommands.dontRequireTtyForSudo(), // strange quotes are to ensure we don't match against echoed stdin BashCommands.sudo("echo \"sudo\"-is-working-"+id)) .summary("setting up sudo") .configure(SshTool.PROP_ALLOCATE_PTY, true) .allowingNonZeroExitCode() .returning(new Function<ProcessTaskWrapper<?>,Boolean>() { public Boolean apply(ProcessTaskWrapper<?> task) { if (task.getExitCode()==0 && task.getStdout().contains("sudo-is-working-"+id)) return true; Entity entity = BrooklynTaskTags.getTargetOrContextEntity(Tasks.current()); if (onFailingTask!=OnFailingTask.IGNORE) { // TODO if in a queueing context can we mark this task inessential and throw? // that way user sees the message... String message = "Error setting up sudo for "+task.getMachine().getUser()+"@"+task.getMachine().getAddress().getHostName()+" "+ " (exit code "+task.getExitCode()+(entity!=null ? ", entity "+entity : "")+")"; DynamicTasks.queueIfPossible(Tasks.warning(message, null)); } Streams.logStreamTail(log, "STDERR of sudo setup problem", Streams.byteArrayOfString(task.getStderr()), 1024); if (onFailingTask==OnFailingTask.WARN_OR_IF_DYNAMIC_FAIL_MARKING_INESSENTIAL) { Tasks.markInessential(); } if (onFailingTask==OnFailingTask.FAIL || onFailingTask==OnFailingTask.WARN_OR_IF_DYNAMIC_FAIL_MARKING_INESSENTIAL) { throw new IllegalStateException("Passwordless sudo is required for "+task.getMachine().getUser()+"@"+task.getMachine().getAddress().getHostName()+ (entity!=null ? " ("+entity+")" : "")); } return false; } }); } /** Function for use in {@link ProcessTaskFactory#returning(Function)} which logs all information, optionally requires zero exit code, * and then returns stdout */ public static Function<ProcessTaskWrapper<?>, String> returningStdoutLoggingInfo(final Logger logger, final boolean requireZero) { return new Function<ProcessTaskWrapper<?>, String>() { public String apply(@Nullable ProcessTaskWrapper<?> input) { if (logger!=null) logger.info(input+" COMMANDS:\n"+Strings.join(input.getCommands(),"\n")); if (logger!=null) logger.info(input+" STDOUT:\n"+input.getStdout()); if (logger!=null) logger.info(input+" STDERR:\n"+input.getStderr()); if (requireZero && input.getExitCode()!=0) throw new IllegalStateException("non-zero exit code in "+input.getSummary()+": see log for more details!"); return input.getStdout(); } }; } /** task to install a file given a url, where the url is resolved remotely first then locally */ public static TaskFactory<?> installFromUrl(final SshMachineLocation location, final String url, final String destPath) { return installFromUrl(ResourceUtils.create(SshTasks.class), ImmutableMap.<String,Object>of(), location, url, destPath); } /** task to install a file given a url, where the url is resolved remotely first then locally */ public static TaskFactory<?> installFromUrl(final Map<String, ?> props, final SshMachineLocation location, final String url, final String destPath) { return installFromUrl(ResourceUtils.create(SshTasks.class), props, location, url, destPath); } /** task to install a file given a url, where the url is resolved remotely first then locally */ public static TaskFactory<?> installFromUrl(final ResourceUtils utils, final Map<String, ?> props, final SshMachineLocation location, final String url, final String destPath) { return new TaskFactory<TaskAdaptable<?>>() { @Override public TaskAdaptable<?> newTask() { return Tasks.<Void>builder().displayName("installing "+Urls.getBasename(url)).description("installing "+url+" to "+destPath).body(new Runnable() { @Override public void run() { int result = location.installTo(utils, props, url, destPath); if (result!=0) throw new IllegalStateException("Failed to install '"+url+"' to '"+destPath+"' at "+location+": exit code "+result); } }).build(); } }; } }