blob: 0fd93b75adedc4c51c283f60683ea26cc8ccd6cb [file] [log] [blame]
/*
* 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.location.winrm;
import static org.apache.brooklyn.core.config.ConfigKeys.newConfigKeyWithPrefix;
import static org.apache.brooklyn.core.config.ConfigKeys.newStringConfigKey;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.InetAddress;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.MachineDetails;
import org.apache.brooklyn.api.location.MachineLocation;
import org.apache.brooklyn.api.location.OsDetails;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.config.ConfigUtils;
import org.apache.brooklyn.core.config.Sanitizer;
import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
import org.apache.brooklyn.core.location.AbstractMachineLocation;
import org.apache.brooklyn.core.location.access.PortForwardManager;
import org.apache.brooklyn.core.location.access.PortForwardManagerLocationResolver;
import org.apache.brooklyn.core.mgmt.ManagementContextInjectable;
import org.apache.brooklyn.location.ssh.CanResolveOnBoxDir;
import org.apache.brooklyn.util.core.ClassLoaderUtils;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.internal.ssh.SshTool;
import org.apache.brooklyn.util.core.internal.winrm.WinRmTool;
import org.apache.brooklyn.util.core.internal.winrm.WinRmToolResponse;
import org.apache.brooklyn.util.core.internal.winrm.winrm4j.Winrm4jTool;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.ssh.BashCommands;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.Strings;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.Beta;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.net.HostAndPort;
import com.google.common.reflect.TypeToken;
public class WinRmMachineLocation extends AbstractMachineLocation implements MachineLocation, CanResolveOnBoxDir {
private static final Logger LOG = LoggerFactory.getLogger(WinRmMachineLocation.class);
public static final ConfigKey<InetAddress> ADDRESS = ConfigKeys.newConfigKey(
InetAddress.class,
"address",
"Address of the remote machine");
public static final ConfigKey<Integer> WINRM_CONFIG_PORT = newConfigKeyWithPrefix(BrooklynConfigKeys.BROOKLYN_WINRM_CONFIG_KEY_PREFIX, WinRmTool.PROP_PORT);
public static final ConfigKey<String> OPERATION_TIMEOUT = newConfigKeyWithPrefix(BrooklynConfigKeys.BROOKLYN_WINRM_CONFIG_KEY_PREFIX, WinRmTool.OPERATION_TIMEOUT);
public static final ConfigKey<Integer> RETRIES_OF_NETWORK_FAILURES = newConfigKeyWithPrefix(BrooklynConfigKeys.BROOKLYN_WINRM_CONFIG_KEY_PREFIX, WinRmTool.RETRIES_OF_NETWORK_FAILURES);
public static final ConfigKey<Boolean> USE_HTTPS_WINRM = WinRmTool.USE_HTTPS_WINRM;
public static final ConfigKey<String> WAIT_WINDOWS_TO_START = newStringConfigKey("waitWindowsToStart",
"By default Brooklyn will return the machine immediately after Brooklyn is able to WinRM. Sometimes restart could happen after a Windows VM is provisioned. This could be because of System Upgrade or other." +
" By setting this config key to 60s, 5m or other X Duration of time Brooklyn will wait X amount of time for disconnect to occur. If connection failure occurs it will wait X amount of time for the machine to come up.", null);
/**
* Flag which tells winrm whether to use Basic Authentication
* or Negotiate plus NTLM.
* winrm.useNtlm parameter could be a subject to change.
* TODO Winrm supports several authentication mechanisms so it would be better to replace it with a prioritised list of authentication mechanisms to try.
*/
@Beta
public static final ConfigKey<Boolean> USE_NTLM = WinRmTool.USE_NTLM;
// TODO merge with {link SshTool#PROP_USER} and {@link SshMachineLocation#user}?
public static final ConfigKey<String> USER = WinRmTool.PROP_USER;
// TODO merge with {link SshTool#PROP_PASSWORD}?
public static final ConfigKey<String> PASSWORD = WinRmTool.PROP_PASSWORD;
// TODO Delete once winrm4j supports this better?
public static final ConfigKey<Integer> COPY_FILE_CHUNK_SIZE_BYTES = WinRmTool.COPY_FILE_CHUNK_SIZE_BYTES;
// Note that SshTool's implementation class *must* use a different key name. Both may be used
// within a location's configuration to indicate the implementation to use for WinRmTool and
// for SshTool - that will require two different configuration values.
public static final ConfigKey<String> WINRM_TOOL_CLASS = ConfigKeys.newConfigKeyWithPrefixRemoved(
BrooklynConfigKeys.BROOKLYN_WINRM_CONFIG_KEY_PREFIX,
Preconditions.checkNotNull(BrooklynConfigKeys.WINRM_TOOL_CLASS, "static final initializer classload ordering problem"));
/**
* Prefix for config key:values to be passed to the winrm tool on construction. For example,
* one could define the location below. When executing winrm commands, it would instantiate
* an instance of {@code com.acme.brooklyn.MyCustomWinrmTool}, calling its constructor with a
* {@code Map<String, Object>} that contained the configuration. In this case, the map would
* include: {@code address=1.2.3.4}; {@code user=myname}; and {@code myparam=myvalue}.
*
* <pre>
* {@code
* brooklyn.location.named.myLocation = byon:(hosts=1.2.3.4,user=myname)
* brooklyn.location.named.myLocation.winrmToolClass = com.acme.brooklyn.MyCustomWinrmTool
* brooklyn.location.named.myLocation.winrmToolClass.myparam = myvalue
* }
* }
* </pre>
* <p>
*/
public static final String WINRM_TOOL_CLASS_PROPERTIES_PREFIX = WINRM_TOOL_CLASS.getName()+".";
/**
* @deprecated since 0.9.0; config never read; will be removed in future version.
*/
@Deprecated
public static final ConfigKey<Integer> EXECUTION_ATTEMPTS = ConfigKeys.newIntegerConfigKey(
"windows.exec.attempts",
"Number of attempts to execute a remote command",
1);
// TODO See SshTool#PROP_SSH_TRIES, where it was called "sshTries"; remove duplication? Merge into one well-named thing?
public static final ConfigKey<Integer> EXEC_TRIES = WinRmTool.PROP_EXEC_TRIES;
@SuppressWarnings("serial")
public static final ConfigKey<Iterable<String>> PRIVATE_ADDRESSES = ConfigKeys.newConfigKey(
new TypeToken<Iterable<String>>() {},
"privateAddresses",
"Private addresses of this machine, e.g. those within the private network",
null);
@SuppressWarnings("serial")
public static final ConfigKey<Map<Integer, String>> TCP_PORT_MAPPINGS = ConfigKeys.newConfigKey(
new TypeToken<Map<Integer, String>>() {},
"tcpPortMappings",
"NAT'ed ports, giving the mapping from private TCP port to a public host:port",
null);
public static final Set<HasConfigKey<?>> ALL_WINRM_CONFIG_KEYS =
ImmutableSet.<HasConfigKey<?>>builder()
.addAll(ConfigUtils.getStaticKeysOnClass(WinRmMachineLocation.class))
.addAll(ConfigUtils.getStaticKeysOnClass(WinRmTool.class))
.build();
public static final Set<String> ALL_WINRM_CONFIG_KEY_NAMES =
ImmutableSet.copyOf(Iterables.transform(ALL_WINRM_CONFIG_KEYS, new Function<HasConfigKey<?>,String>() {
@Override
public String apply(HasConfigKey<?> input) {
return input.getConfigKey().getName();
}
}));
@Override
public void init() {
super.init();
// Register any pre-existing port-mappings with the PortForwardManager
Map<Integer, String> tcpPortMappings = getConfig(TCP_PORT_MAPPINGS);
if (tcpPortMappings != null) {
PortForwardManager pfm = (PortForwardManager) getManagementContext().getLocationRegistry().getLocationManaged(PortForwardManagerLocationResolver.PFM_GLOBAL_SPEC);
for (Map.Entry<Integer, String> entry : tcpPortMappings.entrySet()) {
int targetPort = entry.getKey();
HostAndPort publicEndpoint = HostAndPort.fromString(entry.getValue());
if (!publicEndpoint.hasPort()) {
throw new IllegalArgumentException("Invalid portMapping ('"+entry.getValue()+"') for port "+targetPort+" in machine "+this);
}
pfm.associate(publicEndpoint.getHostText(), publicEndpoint, this, targetPort);
}
}
}
@Override
protected MachineDetails detectMachineDetails() {
// TODO: detect actual machine details via winRM
return UNKNOWN_MACHINE_DETAILS;
}
public String getUser() {
return config().get(USER);
}
@Override
public InetAddress getAddress() {
return getConfig(ADDRESS);
}
@Override
public OsDetails getOsDetails() {
return null;
}
@Override
public MachineDetails getMachineDetails() {
return null;
}
@Nullable
@Override
public String getHostname() {
InetAddress address = getAddress();
return (address != null) ? address.getHostAddress() : null;
}
@Nullable
protected String getHostAndPort() {
String host = getHostname();
return (host == null) ? null : host + ":" + getDefaultPort();
}
public int getPort() {
Maybe<Object> raw = config().getRaw(WinRmTool.PROP_PORT);
if (raw.orNull() == null && config().getRaw(WINRM_CONFIG_PORT).orNull() != null) {
return config().get(WINRM_CONFIG_PORT);
} else {
Integer result = config().get(WinRmTool.PROP_PORT);
return (result != null) ? result : getDefaultPort();
}
}
private int getDefaultPort() {
return getConfig(USE_HTTPS_WINRM) ? 5986 : 5985;
}
@Override
public Set<String> getPublicAddresses() {
InetAddress address = getAddress();
return (address == null) ? ImmutableSet.<String>of() : ImmutableSet.of(address.getHostAddress());
}
@Override
public Set<String> getPrivateAddresses() {
Iterable<String> result = getConfig(PRIVATE_ADDRESSES);
return (result == null) ? ImmutableSet.<String>of() : ImmutableSet.copyOf(result);
}
/**
* @deprecated since 0.9.0; use {@link #executeCommand(String)}
*/
@Deprecated
public WinRmToolResponse executeScript(String script) {
return executeCommand(script);
}
/**
* @deprecated since 0.9.0; use {@link #executeCommand(List)}
*/
@Deprecated
public WinRmToolResponse executeScript(List<String> script) {
return executeCommand(script);
}
/**
* @deprecated since 0.9.0; use {@link #executeCommand(Map, List)}
*/
@Deprecated
public WinRmToolResponse executeScript(Map<?,?> props, List<String> script) {
return executeCommand(props, script);
}
/**
* @since 0.9.0 (previously was {@code executeScript(String)}
*/
public WinRmToolResponse executeCommand(String script) {
return executeCommand(ImmutableList.of(script));
}
/**
* @since 0.9.0 (previously was {@code executeScript(List)}
*/
public WinRmToolResponse executeCommand(List<String> script) {
return executeCommand(ImmutableMap.of(), script);
}
/**
* @since 0.9.0 (previously was {@code executeScript(Map, List)}
*/
public WinRmToolResponse executeCommand(Map<?,?> props, List<String> script) {
WinRmTool tool = newWinRmTool(props);
return tool.executeCommand(script);
}
public WinRmToolResponse executePsScript(String psScript) {
return executePsScript(ImmutableMap.of(), ImmutableList.of(psScript));
}
public WinRmToolResponse executePsScript(List<String> psScript) {
return executePsScript(ImmutableMap.of(), psScript);
}
public WinRmToolResponse executePsScript(Map<?,?> props, List<String> psScript) {
WinRmTool tool = newWinRmTool(props);
return tool.executePs(psScript);
}
protected WinRmTool newWinRmTool(Map<?,?> props) {
// TODO See comments/TODOs in SshMachineLocation.connectSsh()
try {
ConfigBag args = new ConfigBag();
for (Map.Entry<ConfigKey<?>, ?> entry: config().getBag().getAllConfigAsConfigKeyMap().entrySet()) {
boolean include = false;
String keyName = entry.getKey().getName();
if (keyName.startsWith(WinRmTool.BROOKLYN_CONFIG_KEY_PREFIX)) {
keyName = Strings.removeFromStart(keyName, WinRmTool.BROOKLYN_CONFIG_KEY_PREFIX);
include = true;
}
if (keyName.startsWith(WINRM_TOOL_CLASS_PROPERTIES_PREFIX)) {
keyName = Strings.removeFromStart(keyName, WINRM_TOOL_CLASS_PROPERTIES_PREFIX);
include = true;
}
if (ALL_WINRM_CONFIG_KEY_NAMES.contains(keyName)) {
// key should be included, and does not need to be changed
// TODO make this config-setting mechanism more universal
// currently e.g. it will not admit a tool-specific property.
// thinking either we know about the tool here,
// or we don't allow unadorned keys to be set
// (require use of BROOKLYN_CONFIG_KEY_PREFIX)
include = true;
}
if (include) {
args.putStringKey(keyName, config().get(entry.getKey()));
}
}
args.putAll(props);
args.configure(SshTool.PROP_HOST, getAddress().getHostAddress());
args.configure(WinRmTool.USE_NTLM, getConfig(WinRmMachineLocation.USE_NTLM));
args.configure(WinRmTool.PROP_PORT, getPort());
if (LOG.isTraceEnabled()) LOG.trace("creating WinRM session for "+Sanitizer.sanitize(args));
// look up tool class
String toolClass = args.get(WINRM_TOOL_CLASS);
if (toolClass == null) toolClass = Winrm4jTool.class.getName();
WinRmTool tool = (WinRmTool) new ClassLoaderUtils(this, getManagementContext()).loadClass(toolClass).getConstructor(Map.class).newInstance(args.getAllConfig());
if (tool instanceof ManagementContextInjectable) {
((ManagementContextInjectable)tool).setManagementContext(getManagementContext());
}
if (LOG.isTraceEnabled()) LOG.trace("using ssh-tool {} (of type {}); props ", tool, toolClass);
return tool;
} catch (Exception e) {
throw Exceptions.propagate(e);
}
}
public int copyTo(File source, String destination) {
FileInputStream sourceStream = null;
try {
sourceStream = new FileInputStream(source);
return copyTo(sourceStream, destination);
} catch (FileNotFoundException e) {
throw Exceptions.propagate(e);
} finally {
if (sourceStream != null) {
Streams.closeQuietly(sourceStream);
}
}
}
public int copyTo(InputStream source, String destination) {
return copyTo(ImmutableMap.of(), source, destination);
}
public int copyTo(Map<?,?> props, InputStream source, String destination) {
WinRmTool tool = newWinRmTool(props);
WinRmToolResponse response = tool.copyToServer(source, destination);
return response.getStatusCode();
}
public static String getDefaultUserMetadataString(ConfigurationSupportInternal config) {
// Using an encoded command obviates the need to escape
String unencodePowershell = Joiner.on("\r\n").join(ImmutableList.of(
// Allow TS connections
"$RDP = Get-WmiObject -Class Win32_TerminalServiceSetting -ComputerName $env:computername -Namespace root\\CIMV2\\TerminalServices -Authentication PacketPrivacy",
"$RDP.SetAllowTSConnections(1,1)",
"Set-ExecutionPolicy Unrestricted -Force",
// Set unlimited values for remote execution limits
"Set-Item WSMan:\\localhost\\Shell\\MaxConcurrentUsers 100",
"Set-Item WSMan:\\localhost\\Shell\\MaxMemoryPerShellMB 0",
"Set-Item WSMan:\\localhost\\Shell\\MaxProcessesPerShell 0",
"Set-Item WSMan:\\localhost\\Shell\\MaxShellsPerUser 0",
"New-ItemProperty \"HKLM:\\System\\CurrentControlSet\\Control\\LSA\" -Name \"SuppressExtendedProtection\" -Value 1 -PropertyType \"DWord\"",
// The following allows scripts to re-authenticate with local credential - this is required
// as certain operations cannot be performed with remote credentials
"$allowed = @('WSMAN/*')",
"$key = 'hklm:\\SOFTWARE\\Policies\\Microsoft\\Windows\\CredentialsDelegation'",
"if (!(Test-Path $key)) {",
" md $key",
"}",
"New-ItemProperty -Path $key -Name AllowFreshCredentials -Value 1 -PropertyType Dword -Force",
"New-ItemProperty -Path $key -Name AllowFreshCredentialsWhenNTLMOnly -Value 1 -PropertyType Dword -Force",
"$credKey = Join-Path $key 'AllowFreshCredentials'",
"if (!(Test-Path $credKey)) {",
" md $credkey",
"}",
"$ntlmKey = Join-Path $key 'AllowFreshCredentialsWhenNTLMOnly'",
"if (!(Test-Path $ntlmKey)) {",
" md $ntlmKey",
"}",
"$i = 1",
"$allowed |% {",
" # Script does not take into account existing entries in this key",
" New-ItemProperty -Path $credKey -Name $i -Value $_ -PropertyType String -Force",
" New-ItemProperty -Path $ntlmKey -Name $i -Value $_ -PropertyType String -Force",
" $i++",
"}"
));
// FIXME USE_HTTPS_WINRM
// Missing generate certificate step.
// https://support.microsoft.com/en-us/kb/2019527
//
// One possible approach is to generate a certificate and append it to this command.
// http://stackoverflow.com/questions/1615871/creating-an-x509-certificate-in-java-without-bouncycastle
//
// @neykov:
// The certificate is best generated on the machine, without leaving it ever (for self-signed case). On the other hand it's not possible to get the public part at this step.
// I see this setup step dissappearing longer term (or keeping it minimalistic).
// Instead do something like the jclouds init sequence where it connects with whatever is provided by the cloud, then configuring it to our liking.
boolean useSecureWinrm = config.getBag().get(USE_HTTPS_WINRM);
boolean basicAuth = !config.getBag().get(USE_NTLM), allowUnencrypted = !useSecureWinrm;
int port = useSecureWinrm ? 5986 : 5985;
String encoded = new String(Base64.encodeBase64(unencodePowershell.getBytes(Charsets.UTF_16LE)));
return String.format("winrm quickconfig -q & " +
"winrm set winrm/config/service/auth @{Basic=\"%1$s\"} & " +
"winrm set winrm/config/service @{AllowUnencrypted=\"%2$s\"} & " +
"winrm set winrm/config/winrs @{MaxConcurrentUsers=\"100\"} & " +
"winrm set winrm/config/winrs @{MaxMemoryPerShellMB=\"0\"} & " +
"winrm set winrm/config/winrs @{MaxProcessesPerShell=\"0\"} & " +
"winrm set winrm/config/winrs @{MaxShellsPerUser=\"0\"} & " +
"netsh advfirewall firewall add rule name=RDP dir=in protocol=tcp localport=3389 action=allow profile=any & " +
"netsh advfirewall firewall add rule name=WinRM dir=in protocol=tcp localport=%3$d action=allow profile=any & " +
"powershell -EncodedCommand ",
basicAuth,
allowUnencrypted,
port
)
+ encoded;
/* TODO: Find out why scripts with new line characters aren't working on AWS. The following appears as if it *should*
work but doesn't - the script simply isn't run. By connecting to the machine via RDP, you can get the script
from 'http://169.254.169.254/latest/user-data', and running it at the command prompt works, but for some
reason the script isn't run when the VM is provisioned
*/
// return Joiner.on("\r\n").join(ImmutableList.of(
// "winrm quickconfig -q",
// "winrm set winrm/config/service/auth @{Basic=\"true\"}",
// "winrm set winrm/config/client @{AllowUnencrypted=\"true\"}",
// "winrm set winrm/config/service @{AllowUnencrypted=\"true\"}",
// "netsh advfirewall firewall add rule name=RDP dir=in protocol=tcp localport=3389 action=allow profile=any",
// "netsh advfirewall firewall add rule name=WinRM dir=in protocol=tcp localport=5985 action=allow profile=any",
// // Using an encoded command necessitates the need to escape. The unencoded command is as follows:
// // $RDP = Get-WmiObject -Class Win32_TerminalServiceSetting -ComputerName $env:computername -Namespace root\CIMV2\TerminalServices -Authentication PacketPrivacy
// // $Result = $RDP.SetAllowTSConnections(1,1)
// "powershell -EncodedCommand JABSAEQAUAAgAD0AIABHAGUAdAAtAFcAbQBpAE8AYgBqAGUAYwB0ACAALQBDAGwAYQBzAHMAI" +
// "ABXAGkAbgAzADIAXwBUAGUAcgBtAGkAbgBhAGwAUwBlAHIAdgBpAGMAZQBTAGUAdAB0AGkAbgBnACAALQBDAG8AbQBwA" +
// "HUAdABlAHIATgBhAG0AZQAgACQAZQBuAHYAOgBjAG8AbQBwAHUAdABlAHIAbgBhAG0AZQAgAC0ATgBhAG0AZQBzAHAAY" +
// "QBjAGUAIAByAG8AbwB0AFwAQwBJAE0AVgAyAFwAVABlAHIAbQBpAG4AYQBsAFMAZQByAHYAaQBjAGUAcwAgAC0AQQB1A" +
// "HQAaABlAG4AdABpAGMAYQB0AGkAbwBuACAAUABhAGMAawBlAHQAUAByAGkAdgBhAGMAeQANAAoAJABSAGUAcwB1AGwAd" +
// "AAgAD0AIAAkAFIARABQAC4AUwBlAHQAQQBsAGwAbwB3AFQAUwBDAG8AbgBuAGUAYwB0AGkAbwBuAHMAKAAxACwAMQApAA=="
// ));
}
@Override
public String resolveOnBoxDirFor(Entity entity, String unresolvedPath) {
// TODO this is simplistic, writes at c:\ for HOME
if (unresolvedPath.startsWith("./") || unresolvedPath.startsWith("~/")) {
unresolvedPath = "C:\\"+unresolvedPath.substring(2);
}
return unresolvedPath.replaceAll("/", "\\");
}
}