/*
* 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.entity.software.base.test.location;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotEquals;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.brooklyn.api.location.MachineProvisioningLocation;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.location.winrm.WinRmMachineLocation;
import org.apache.brooklyn.util.core.internal.winrm.WinRmToolResponse;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
/**
* Tests execution of commands (batch and powershell) on Windows over WinRM, and of
* file upload.
*
* There are limitations with what is supported by winrm4j. These are highlighted in
* tests marked as "WIP" (see individual tests).
*
* These limitations are documented in docs/guide/yaml/winrm/index.md.
* Please update the docs if you encountered new situations, or change the behaviuor
* of existing use-cases.
*/
public class WinRmMachineLocationLiveTest {
private static final int MAX_EXECUTOR_THREADS = 100;
/*
* TODO: Deferred implementing copyFrom or environment variables.
*/
private static final Logger LOG = LoggerFactory.getLogger(WinRmMachineLocationLiveTest.class);
private static final String INVALID_CMD = "thisCommandDoesNotExistAEFafiee3d";
private static final String PS_ERR_ACTION_PREF_EQ_STOP = "$ErrorActionPreference = \"Stop\"";
protected MachineProvisioningLocation<WinRmMachineLocation> loc;
protected TestApplication app;
protected ManagementContextInternal mgmt;
protected WinRmMachineLocation machine;
private ListeningExecutorService executor;
@BeforeClass(alwaysRun=true)
public void setUpClass() throws Exception {
executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(MAX_EXECUTOR_THREADS));
mgmt = new LocalManagementContextForTests(BrooklynProperties.Factory.newDefault());
loc = newLoc(mgmt);
machine = loc.obtain(ImmutableMap.of());
LOG.info("PROVISIONED: "+machine.getAddress()+":"+machine.config().get(WinRmMachineLocation.WINRM_PORT)
+", "+machine.getUser()+":"+machine.config().get(WinRmMachineLocation.PASSWORD));
}
@AfterClass(alwaysRun=true)
public void tearDownClass() throws Exception {
try {
if (executor != null) executor.shutdownNow();
if (machine != null) loc.release(machine);
if (mgmt != null) Entities.destroyAll(mgmt);
} catch (Throwable t) {
LOG.error("Caught exception in tearDown method", t);
} finally {
executor = null;
mgmt = null;
}
}
/**
* Returns a location for obtaining a single WinRM machine. This method will be called once during
* {@code @BeforeClass}, then {@code loc.obtain()} will be called. The obtained machine will be
* released in {@code @AfterClass}.
*/
protected MachineProvisioningLocation<WinRmMachineLocation> newLoc(ManagementContextInternal mgmt) throws Exception {
return WindowsTestFixture.setUpWindowsLocation(mgmt);
}
@Test(groups="Live")
public void testCopyTo() throws Exception {
String contents = "abcdef";
runCopyTo(contents);
runCopyFileTo(contents);
}
// Takes several minutes to upload/download!
@Test(groups="Live")
public void testLargeFileCopyTo() throws Exception {
String contents = Identifiers.makeRandomId(65537);
runCopyTo(contents);
runCopyFileTo(contents);
}
protected void runCopyTo(String contents) throws Exception {
String remotePath = "C:\\myfile-"+Identifiers.makeRandomId(4)+".txt";
machine.copyTo(new ByteArrayInputStream(contents.getBytes()), remotePath);
WinRmToolResponse response = machine.executeCommand("type "+remotePath);
String msg = "statusCode="+response.getStatusCode()+"; out="+response.getStdOut()+"; err="+response.getStdErr();
assertEquals(response.getStatusCode(), 0, msg);
assertEquals(response.getStdOut().trim(), contents, msg);
}
protected void runCopyFileTo(String contents) throws Exception {
File localFile = File.createTempFile("winrmtest"+Identifiers.makeRandomId(4), ".txt");
try {
Files.write(contents, localFile, Charsets.UTF_8);
String remotePath = "C:\\myfile-"+Identifiers.makeRandomId(4)+".txt";
machine.copyTo(localFile, remotePath);
WinRmToolResponse response = machine.executeCommand("type "+remotePath);
String msg = "statusCode="+response.getStatusCode()+"; out="+response.getStdOut()+"; err="+response.getStdErr();
assertEquals(response.getStatusCode(), 0, msg);
assertEquals(response.getStdOut().trim(), contents, msg);
} finally {
localFile.delete();
}
}
@Test(groups="Live")
public void testExecScript() throws Exception {
assertExecSucceeds("echo myline", "myline", "");
}
/*
* TODO Not supported in winrm4j (or PyWinRM).
*
* Just gives "first", and exit code 1.
*/
@Test(groups={"Live", "WIP"}, enabled=false)
public void testExecMultiLineScript() throws Exception {
assertExecSucceeds("echo first" + "\r\n" + "echo second", "first"+"\r\n"+"second", "");
}
@Test(groups={"Live"})
public void testExecMultiPartScript() throws Exception {
assertExecSucceeds(ImmutableList.of("echo first", "echo second"), "first "+"\r\n"+"second", "");
}
@Test(groups="Live")
public void testExecFailingScript() throws Exception {
final String INVALID_CMD = "thisCommandDoesNotExistAEFafiee3d";
// Single commands
assertExecFails(INVALID_CMD);
assertExecFails(ImmutableList.of(INVALID_CMD));
}
@Test(groups="Live")
public void testExecScriptExit0() throws Exception {
assertExecSucceeds("exit /B 0", "", "");
assertExecSucceeds(ImmutableList.of("exit /B 0"), "", "");
}
/*
* TODO Not supported in winrm4j (or PyWinRM).
*
* Executing (in python):
* import winrm
* s = winrm.Session('52.12.211.247', auth=('Administrator', 'pa55w0rd'))
* r = s.run_cmd("exit /B 1")
* gives exit code 0.
*/
@Test(groups={"Live", "WIP"})
public void testExecScriptExit1() throws Exception {
// Single commands
assertExecFails("exit /B 1");
assertExecFails(ImmutableList.of("exit /B 1"));
}
@Test(groups="Live")
public void testExecBatchFileSingleLine() throws Exception {
String script = "EXIT /B 0";
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecSucceeds(scriptPath, null, "");
}
@Test(groups="Live")
public void testExecBatchFileMultiLine() throws Exception {
String script = Joiner.on("\n").join(
"@ECHO OFF",
"echo first",
"echo second",
"EXIT /B 0");
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecSucceeds(scriptPath, "first"+"\r\n"+"second", "");
}
@Test(groups="Live")
public void testExecBatchFileWithArgs() throws Exception {
String script = Joiner.on("\n").join(
"@ECHO OFF",
"echo got %1",
"echo got %2",
"EXIT /B 0");
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecSucceeds(scriptPath+" first second", "got first"+"\r\n"+"got second", "");
}
@Test(groups="Live")
public void testExecBatchFileWithExit1() throws Exception {
String script = "EXIT /B 1";
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecFails(scriptPath);
}
@Test(groups="Live")
public void testExecCorruptExe() throws Exception {
String exe = "garbage";
String exePath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".exe";
machine.copyTo(new ByteArrayInputStream(exe.getBytes()), exePath);
assertExecFails(exePath);
}
@Test(groups="Live")
public void testExecFilePs() throws Exception {
String script = Joiner.on("\r\n").join(
"Write-Host myline",
"exit 0");
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecPsSucceeds(
"PowerShell -NonInteractive -NoProfile -Command "+scriptPath,
"myline",
"");
}
@Test(groups="Live")
public void testExecFilePsWithExit1() throws Exception {
String script = Joiner.on("\r\n").join(
"Write-Host myline",
"exit 1");
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecFails("PowerShell -NonInteractive -NoProfile -Command "+scriptPath);
}
/*
* TODO Not supported in PyWinRM - single line .ps1 file with "exit 1" gives an
* exit code 0 over PyWinRM, but an exit code 1 when executed locally!
*
* Executing (in python):
* import winrm
* s = winrm.Session('52.12.211.247', auth=('Administrator', 'pa55w0rd'))
* r = s.run_cmd("PowerShell -NonInteractive -NoProfile -Command C:\singleLineExit1.ps1")
* gives exit code 0.
*/
@Test(groups={"Live", "WIP"})
public void testExecFilePsWithSingleLineExit1() throws Exception {
String script = "exit 1";
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecFails("PowerShell -NonInteractive -NoProfile -Command "+scriptPath);
}
@Test(groups="Live")
public void testExecPsScript() throws Exception {
assertExecPsSucceeds("Write-Host myline", "myline", "");
}
@Test(groups="Live")
public void testExecPsMultiLineScript() throws Exception {
// Note stdout is "\n" rather than "\r\n" (the latter is returned for run_cmd, versus run_ps)
assertExecPsSucceeds("Write-Host first" + "\r\n" + "Write-Host second", "first"+"\n"+"second", "");
}
@Test(groups="Live")
public void testExecPsMultiLineScriptWithoutSlashR() throws Exception {
assertExecPsSucceeds("Write-Host first" + "\n" + "Write-Host second", "first"+"\n"+"second", "");
}
@Test(groups="Live")
public void testExecPsMultiPartScript() throws Exception {
assertExecPsSucceeds(ImmutableList.of("Write-Host first", "Write-Host second"), "first"+"\n"+"second", "");
}
@Test(groups="Live")
public void testExecPsBatchFile() throws Exception {
String script = "EXIT /B 0";
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecPsSucceeds("& '"+scriptPath+"'", null, "");
}
@Test(groups="Live")
public void testExecPsBatchFileExit1() throws Exception {
String script = "EXIT /B 1";
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecPsFails("& '"+scriptPath+"'");
}
/*
* TODO Not supported in PyWinRM - gives exit status 1, rather than the 3 from the batch file.
*/
@Test(groups={"Live", "WIP"})
public void testExecPsBatchFileExit3() throws Exception {
String script = "EXIT /B 3";
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".bat";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
WinRmToolResponse response = machine.executePsScript("& '"+scriptPath+"'");
String msg = "statusCode="+response.getStatusCode()+"; out="+response.getStdOut()+"; err="+response.getStdErr();
assertEquals(response.getStatusCode(), 3, msg);
}
@Test(groups="Live")
public void testExecPsCorruptExe() throws Exception {
String exe = "garbage";
String exePath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".exe";
machine.copyTo(new ByteArrayInputStream(exe.getBytes()), exePath);
assertExecPsFails("& '"+exePath+"'");
}
@Test(groups="Live")
public void testExecPsFileWithArg() throws Exception {
String script = Joiner.on("\r\n").join(
"Param(",
" [string]$myarg",
")",
"Write-Host got $myarg",
"exit 0");
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecPsSucceeds("& "+scriptPath+" -myarg myval", "got myval", "");
}
@Test(groups="Live")
public void testExecPsFilePs() throws Exception {
String script = Joiner.on("\r\n").join(
"Write-Host myline",
"exit 0");
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecPsSucceeds("& "+scriptPath, "myline", "");
}
@Test(groups="Live")
public void testExecPsFilePsWithExit1() throws Exception {
String script = Joiner.on("\r\n").join(
"Write-Host myline",
"exit 1");
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
System.out.println(scriptPath);
assertExecPsFails("& "+scriptPath);
}
/*
* TODO Not supported in PyWinRM - single line .ps1 file with "exit 1" gives an
* exit code 0 over PyWinRM, but an exit code 1 when executed locally!
*
* Executing (in python):
* import winrm
* s = winrm.Session('52.12.211.247', auth=('Administrator', 'pa55w0rd'))
* r = s.run_cmd("PowerShell -NonInteractive -NoProfile -Command C:\singleLineExit1.ps1")
* gives exit code 0.
*/
@Test(groups={"Live", "WIP"})
public void testExecPsFilePsSingleLineWithExit1() throws Exception {
String script = "exit 1";
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecPsFails("& "+scriptPath);
}
/*
* TODO Not supported in winrm4j - single line .ps1 file with "exit 1" gives an
* exit code 0 over PyWinRM, but an exit code 1 when executed locally!
*
* Executing (in python):
* import winrm
* s = winrm.Session('52.12.211.247', auth=('Administrator', 'pa55w0rd'))
* r = s.run_cmd("PowerShell -NonInteractive -NoProfile -Command C:\singleLineGarbage.ps1")
* gives exit code 0.
*/
@Test(groups={"Live", "WIP"})
public void testExecPsFilePsSingleLineWithInvalidCommand() throws Exception {
String script = INVALID_CMD;
String scriptPath = "C:\\myscript-"+Identifiers.makeRandomId(4)+".ps1";
machine.copyTo(new ByteArrayInputStream(script.getBytes()), scriptPath);
assertExecPsFails("& "+scriptPath);
}
@Test(groups="Live")
public void testConfirmUseOfErrorActionPreferenceDoesNotCauseErr() throws Exception {
// Confirm that ErrorActionPreference=Stop does not itself cause a failure, and still get output on success.
assertExecPsSucceeds(ImmutableList.of(PS_ERR_ACTION_PREF_EQ_STOP, "Write-Host myline"), "myline", "");
}
/*
* TODO Not supported in PyWinRM
*
* Executing (in python):
* import winrm
* s = winrm.Session('52.12.211.247', auth=('Administrator', 'pa55w0rd'))
* r = s.run_ps("exit 1")
* gives exit code 0.
*/
@Test(groups={"Live", "WIP"})
public void testExecPsExit1() throws Exception {
// Single commands
assertExecPsFails("exit 1");
assertExecPsFails(ImmutableList.of("exit 1"));
// Multi-part
assertExecPsFails(ImmutableList.of(PS_ERR_ACTION_PREF_EQ_STOP, "Write-Host myline", "exit 1"));
// Multi-line
assertExecPsFails(PS_ERR_ACTION_PREF_EQ_STOP + "\n" + "Write-Host myline" + "\n" + "exit 1");
}
@Test(groups="Live")
public void testExecFailingPsScript() throws Exception {
// Single commands
assertExecPsFails(INVALID_CMD);
assertExecPsFails(ImmutableList.of(INVALID_CMD));
// Multi-part commands
assertExecPsFails(ImmutableList.of(PS_ERR_ACTION_PREF_EQ_STOP, "Write-Host myline", INVALID_CMD));
assertExecPsFails(ImmutableList.of(PS_ERR_ACTION_PREF_EQ_STOP, INVALID_CMD, "Write-Host myline"));
}
@Test(groups={"Live", "Acceptance"})
public void testExecConcurrently() throws Exception {
final int NUM_RUNS = 10;
final int TIMEOUT_MINS = 30;
final AtomicInteger counter = new AtomicInteger();
// Find the test methods that are enabled, and that are not WIP
List<Method> methodsToRun = Lists.newArrayList();
Method[] allmethods = WinRmMachineLocationLiveTest.class.getMethods();
for (Method method : allmethods) {
Test annotatn = method.getAnnotation(Test.class);
if (method.getParameterTypes().length != 0) {
continue;
}
if (method.getName().equals("testExecConcurrently")) {
continue;
}
if (annotatn == null || !annotatn.enabled()) {
continue;
}
String[] groups = annotatn.groups();
if (groups != null && Arrays.asList(groups).contains("WIP")) {
continue;
}
methodsToRun.add(method);
}
// Execute all the methods many times
LOG.info("Executing "+methodsToRun.size()+" methods "+NUM_RUNS+" times each, with "+MAX_EXECUTOR_THREADS+" threads for concurrent execution; max permitted time "+TIMEOUT_MINS+"mins; methods="+methodsToRun);
List<ListenableFuture<?>> results = Lists.newArrayList();
for (int i = 0; i < NUM_RUNS; i++) {
for (final Method method : methodsToRun) {
results.add(executor.submit(new Callable<Void>() {
public Void call() throws Exception {
LOG.info("Executing "+method.getName()+" in thread "+Thread.currentThread());
Stopwatch stopwatch = Stopwatch.createStarted();
try {
method.invoke(WinRmMachineLocationLiveTest.this);
LOG.info("Executed "+method.getName()+" in "+Time.makeTimeStringRounded(stopwatch)+", in thread "+Thread.currentThread()+"; total "+counter.incrementAndGet()+" methods done");
return null;
} catch (Exception e) {
LOG.error("Execute failed for "+method.getName()+" after "+Time.makeTimeStringRounded(stopwatch)+", in thread "+Thread.currentThread()+"; total "+counter.incrementAndGet()+" methods done");
throw e;
}
}}));
}
}
Futures.allAsList(results).get(TIMEOUT_MINS, TimeUnit.MINUTES);
}
private void assertExecFails(String cmd) {
Stopwatch stopwatch = Stopwatch.createStarted();
assertFailed(cmd, machine.executeCommand(cmd), stopwatch);
}
private void assertExecFails(List<String> cmds) {
Stopwatch stopwatch = Stopwatch.createStarted();
assertFailed(cmds, machine.executeCommand(cmds), stopwatch);
}
private void assertExecPsFails(String cmd) {
Stopwatch stopwatch = Stopwatch.createStarted();
assertFailed(cmd, machine.executePsScript(cmd), stopwatch);
}
private void assertExecPsFails(List<String> cmds) {
Stopwatch stopwatch = Stopwatch.createStarted();
assertFailed(cmds, machine.executePsScript(cmds), stopwatch);
}
private void assertExecSucceeds(String cmd, String stdout, String stderr) {
Stopwatch stopwatch = Stopwatch.createStarted();
assertSucceeded(cmd, machine.executeCommand(cmd), stdout, stderr, stopwatch);
}
private void assertExecSucceeds(List<String> cmds, String stdout, String stderr) {
Stopwatch stopwatch = Stopwatch.createStarted();
assertSucceeded(cmds, machine.executeCommand(cmds), stdout, stderr, stopwatch);
}
private void assertExecPsSucceeds(String cmd, String stdout, String stderr) {
Stopwatch stopwatch = Stopwatch.createStarted();
assertSucceeded(cmd, machine.executePsScript(cmd), stdout, stderr, stopwatch);
}
private void assertExecPsSucceeds(List<String> cmds, String stdout, String stderr) {
Stopwatch stopwatch = Stopwatch.createStarted();
assertSucceeded(cmds, machine.executePsScript(cmds), stdout, stderr, stopwatch);
}
private void assertFailed(Object cmd, WinRmToolResponse response, Stopwatch stopwatch) {
String msg = "statusCode="+response.getStatusCode()+"; out="+response.getStdOut()+"; err="+response.getStdErr();
LOG.info("Executed in "+Time.makeTimeStringRounded(stopwatch)+" (asserting failed): "+msg+"; cmd="+cmd);
assertNotEquals(response.getStatusCode(), 0, msg);
}
private WinRmToolResponse assertSucceeded(Object cmd, WinRmToolResponse response, String stdout, String stderr, Stopwatch stopwatch) {
String msg = "statusCode="+response.getStatusCode()+"; out="+response.getStdOut()+"; err="+response.getStdErr();
LOG.info("Executed in "+Time.makeTimeStringRounded(stopwatch)+" (asserting success): "+msg+"; cmd="+cmd);
assertEquals(response.getStatusCode(), 0, msg);
if (stdout != null) assertEquals(response.getStdOut().trim(), stdout, msg);
if (stderr != null) assertEquals(response.getStdErr().trim(), stderr, msg);
return response;
}
}