Skip to content

Commit d518b61

Browse files
committed
Merge pull request #1459 from GabrielBrascher/CLOUDSTACK-8611
This closes #561 CLOUDSTACK-8611:Handle SSH if server "forget" to send exit statusContinuing the work started by @likitha, I did not cherry-picked the commit (b9181c6) from PR <#561> due to the fact that the path of that SshHelper class was different of the current SshHelper; that is because the fact that by cherry-picking it would seem that I had changed all the class as the code is from another file. I made some changes from the cherry-picked commit adding @wilderrodrigues suggestions (create simple methods to have reusable code, make unit tests and create the `WAITING_OPEN_SSH_SESSION` variable to manipulate with the delay of 1000 milliseconds). Also, I tried to simplify the logic by assuming that .... if ((conditions & ChannelCondition.EXIT_STATUS) != 0) { if ((conditions & (ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA)) == 0) { break; } } ... is the same as `((conditions & ChannelCondition.EXIT_STATUS) != 0) && ((conditions & (ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA)) == 0)`. This expression has the following results according to each possible condition. |Condition|Value|result |-----------------|-------|------| TIMEOUT | 0000001|false CLOSED | 0000010 |false STDERR_DATA | 0000100 | false STDERR_DATA | 0001000 | false EOF | 0010000 | false EXIT_STATUS | 0100000 | **true** EXIT_SIGNAL | 1000000 | false After testing all the possibilities we can note that the condition of `(conditions & ChannelCondition.EXIT_STATUS) != 0` is sufficient; thus, the simplified "if" conditional can be: `if ((conditions & ChannelCondition.EXIT_STATUS) != 0) { break; }` This proposed work can be explained by quoting @likitha: >CheckS2SVpnConnectionsCommand execution involves executing a script (checkbatchs2svpn.sh) in the virtual router. Once CS has opened a session to a virtual router and executed a script in the router, it waits indefinitely till the session either times out or the exit status of the remote process is available. But it is possible that an EOF is reached by the process in the router and the router never set the exit status. >References - >1. Some servers never send the exit status, or occasionally "forget" to do so (http://grepcode.com/file/repo1.maven.org/maven2/org.jvnet.hudson/trilead-ssh2/build212-hudson-1/com/trilead/ssh2/ChannelCondition.java). >2. Get the exit code/status from the remote command - if available. Be careful - not all server implementations return this value - (http://grepcode.com/file/repo1.maven.org/maven2/org.jvnet.hudson/trilead-ssh2/build212-hudson-1/com/trilead/ssh2/Session.java#Session.waitForCondition%28int%2Clong%29). * pr/1459: Handle SSH if server "forget" to send exit status Signed-off-by: Will Stevens <[email protected]>
2 parents 5498170 + abae908 commit d518b61

File tree

2 files changed

+241
-18
lines changed

2 files changed

+241
-18
lines changed

utils/src/main/java/com/cloud/utils/ssh/SshHelper.java

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,26 @@
2020
package com.cloud.utils.ssh;
2121

2222
import java.io.File;
23+
import java.io.IOException;
2324
import java.io.InputStream;
2425

2526
import org.apache.log4j.Logger;
2627

2728
import com.trilead.ssh2.ChannelCondition;
28-
29+
import com.trilead.ssh2.Connection;
30+
import com.trilead.ssh2.Session;
2931
import com.cloud.utils.Pair;
3032

3133
public class SshHelper {
3234
private static final int DEFAULT_CONNECT_TIMEOUT = 180000;
3335
private static final int DEFAULT_KEX_TIMEOUT = 60000;
3436

37+
/**
38+
* Waiting time to check if the SSH session was successfully opened. This value (of 1000
39+
* milliseconds) represents one (1) second.
40+
*/
41+
private static final long WAITING_OPEN_SSH_SESSION = 1000;
42+
3543
private static final Logger s_logger = Logger.getLogger(SshHelper.class);
3644

3745
public static Pair<Boolean, String> sshExecute(String host, int port, String user, File pemKeyFile, String password, String command) throws Exception {
@@ -40,19 +48,19 @@ public static Pair<Boolean, String> sshExecute(String host, int port, String use
4048
}
4149

4250
public static void scpTo(String host, int port, String user, File pemKeyFile, String password, String remoteTargetDirectory, String localFile, String fileMode)
43-
throws Exception {
51+
throws Exception {
4452

4553
scpTo(host, port, user, pemKeyFile, password, remoteTargetDirectory, localFile, fileMode, DEFAULT_CONNECT_TIMEOUT, DEFAULT_KEX_TIMEOUT);
4654
}
4755

4856
public static void scpTo(String host, int port, String user, File pemKeyFile, String password, String remoteTargetDirectory, byte[] data, String remoteFileName,
49-
String fileMode) throws Exception {
57+
String fileMode) throws Exception {
5058

5159
scpTo(host, port, user, pemKeyFile, password, remoteTargetDirectory, data, remoteFileName, fileMode, DEFAULT_CONNECT_TIMEOUT, DEFAULT_KEX_TIMEOUT);
5260
}
5361

5462
public static void scpTo(String host, int port, String user, File pemKeyFile, String password, String remoteTargetDirectory, String localFile, String fileMode,
55-
int connectTimeoutInMs, int kexTimeoutInMs) throws Exception {
63+
int connectTimeoutInMs, int kexTimeoutInMs) throws Exception {
5664

5765
com.trilead.ssh2.Connection conn = null;
5866
com.trilead.ssh2.SCPClient scpClient = null;
@@ -88,7 +96,7 @@ public static void scpTo(String host, int port, String user, File pemKeyFile, St
8896
}
8997

9098
public static void scpTo(String host, int port, String user, File pemKeyFile, String password, String remoteTargetDirectory, byte[] data, String remoteFileName,
91-
String fileMode, int connectTimeoutInMs, int kexTimeoutInMs) throws Exception {
99+
String fileMode, int connectTimeoutInMs, int kexTimeoutInMs) throws Exception {
92100

93101
com.trilead.ssh2.Connection conn = null;
94102
com.trilead.ssh2.SCPClient scpClient = null;
@@ -123,7 +131,8 @@ public static void scpTo(String host, int port, String user, File pemKeyFile, St
123131
}
124132

125133
public static Pair<Boolean, String> sshExecute(String host, int port, String user, File pemKeyFile, String password, String command, int connectTimeoutInMs,
126-
int kexTimeoutInMs, int waitResultTimeoutInMs) throws Exception {
134+
int kexTimeoutInMs,
135+
int waitResultTimeoutInMs) throws Exception {
127136

128137
com.trilead.ssh2.Connection conn = null;
129138
com.trilead.ssh2.Session sess = null;
@@ -144,7 +153,7 @@ public static Pair<Boolean, String> sshExecute(String host, int port, String use
144153
throw new Exception(msg);
145154
}
146155
}
147-
sess = conn.openSession();
156+
sess = openConnectionSession(conn);
148157

149158
sess.execCommand(command);
150159

@@ -156,22 +165,22 @@ public static Pair<Boolean, String> sshExecute(String host, int port, String use
156165

157166
int currentReadBytes = 0;
158167
while (true) {
168+
throwSshExceptionIfStdoutOrStdeerIsNull(stdout, stderr);
169+
159170
if ((stdout.available() == 0) && (stderr.available() == 0)) {
160-
int conditions =
161-
sess.waitForCondition(ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA | ChannelCondition.EOF | ChannelCondition.EXIT_STATUS,
171+
int conditions = sess.waitForCondition(ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA | ChannelCondition.EOF | ChannelCondition.EXIT_STATUS,
162172
waitResultTimeoutInMs);
163173

164-
if ((conditions & ChannelCondition.TIMEOUT) != 0) {
165-
String msg = "Timed out in waiting SSH execution result";
166-
s_logger.error(msg);
167-
throw new Exception(msg);
168-
}
174+
throwSshExceptionIfConditionsTimeout(conditions);
169175

170176
if ((conditions & ChannelCondition.EXIT_STATUS) != 0) {
171-
if ((conditions & (ChannelCondition.STDOUT_DATA | ChannelCondition.STDERR_DATA)) == 0) {
172-
break;
173-
}
177+
break;
178+
}
179+
180+
if (canEndTheSshConnection(waitResultTimeoutInMs, sess, conditions)) {
181+
break;
174182
}
183+
175184
}
176185

177186
while (stdout.available() > 0) {
@@ -189,11 +198,12 @@ public static Pair<Boolean, String> sshExecute(String host, int port, String use
189198

190199
if (sess.getExitStatus() == null) {
191200
//Exit status is NOT available. Returning failure result.
201+
s_logger.error(String.format("SSH execution of command %s has no exit status set. Result output: %s", command, result));
192202
return new Pair<Boolean, String>(false, result);
193203
}
194204

195205
if (sess.getExitStatus() != null && sess.getExitStatus().intValue() != 0) {
196-
s_logger.error("SSH execution of command " + command + " has an error status code in return. result output: " + result);
206+
s_logger.error(String.format("SSH execution of command %s has an error status code in return. Result output: %s", command, result));
197207
return new Pair<Boolean, String>(false, result);
198208
}
199209

@@ -206,4 +216,66 @@ public static Pair<Boolean, String> sshExecute(String host, int port, String use
206216
conn.close();
207217
}
208218
}
219+
220+
/**
221+
* It gets a {@link Session} from the given {@link Connection}; then, it waits
222+
* {@value #WAITING_OPEN_SSH_SESSION} milliseconds before returning the session, given a time to
223+
* ensure that the connection is open before proceeding the execution.
224+
*/
225+
protected static Session openConnectionSession(Connection conn) throws IOException, InterruptedException {
226+
Session sess = conn.openSession();
227+
Thread.sleep(WAITING_OPEN_SSH_SESSION);
228+
return sess;
229+
}
230+
231+
/**
232+
* Handles the SSH connection in case of timeout or exit. If the session ends with a timeout
233+
* condition, it throws an exception; if the channel reaches an end of file condition, but it
234+
* does not have an exit status, it returns true to break the loop; otherwise, it returns
235+
* false.
236+
*/
237+
protected static boolean canEndTheSshConnection(int waitResultTimeoutInMs, com.trilead.ssh2.Session sess, int conditions) throws SshException {
238+
if (isChannelConditionEof(conditions)) {
239+
int newConditions = sess.waitForCondition(ChannelCondition.EXIT_STATUS, waitResultTimeoutInMs);
240+
throwSshExceptionIfConditionsTimeout(newConditions);
241+
if ((newConditions & ChannelCondition.EXIT_STATUS) != 0) {
242+
return true;
243+
}
244+
}
245+
return false;
246+
}
247+
248+
/**
249+
* It throws a {@link SshException} if the channel condition is {@link ChannelCondition#TIMEOUT}
250+
*/
251+
protected static void throwSshExceptionIfConditionsTimeout(int conditions) throws SshException {
252+
if ((conditions & ChannelCondition.TIMEOUT) != 0) {
253+
String msg = "Timed out in waiting for SSH execution exit status";
254+
s_logger.error(msg);
255+
throw new SshException(msg);
256+
}
257+
}
258+
259+
/**
260+
* Checks if the channel condition mask is of {@link ChannelCondition#EOF} and not
261+
* {@link ChannelCondition#STDERR_DATA} or {@link ChannelCondition#STDOUT_DATA}.
262+
*/
263+
protected static boolean isChannelConditionEof(int conditions) {
264+
if ((conditions & ChannelCondition.EOF) != 0) {
265+
return true;
266+
}
267+
return false;
268+
}
269+
270+
/**
271+
* Checks if the SSH session {@link com.trilead.ssh2.Session#getStdout()} or
272+
* {@link com.trilead.ssh2.Session#getStderr()} is null.
273+
*/
274+
protected static void throwSshExceptionIfStdoutOrStdeerIsNull(InputStream stdout, InputStream stderr) throws SshException {
275+
if (stdout == null || stderr == null) {
276+
String msg = "Stdout or Stderr of SSH session is null";
277+
s_logger.error(msg);
278+
throw new SshException(msg);
279+
}
280+
}
209281
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//
2+
// Licensed to the Apache Software Foundation (ASF) under one
3+
// or more contributor license agreements. See the NOTICE file
4+
// distributed with this work for additional information
5+
// regarding copyright ownership. The ASF licenses this file
6+
// to you under the Apache License, Version 2.0 (the
7+
// "License"); you may not use this file except in compliance
8+
// with the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing,
13+
// software distributed under the License is distributed on an
14+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
// KIND, either express or implied. See the License for the
16+
// specific language governing permissions and limitations
17+
// under the License.
18+
//
19+
20+
package com.cloud.utils.ssh;
21+
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
25+
import org.junit.Assert;
26+
import org.junit.Test;
27+
import org.junit.runner.RunWith;
28+
import org.mockito.Mockito;
29+
import org.powermock.api.mockito.PowerMockito;
30+
import org.powermock.core.classloader.annotations.PrepareForTest;
31+
import org.powermock.modules.junit4.PowerMockRunner;
32+
33+
import com.trilead.ssh2.ChannelCondition;
34+
import com.trilead.ssh2.Connection;
35+
import com.trilead.ssh2.Session;
36+
37+
@PrepareForTest({ Thread.class, SshHelper.class })
38+
@RunWith(PowerMockRunner.class)
39+
public class SshHelperTest {
40+
41+
@Test
42+
public void canEndTheSshConnectionTest() throws Exception {
43+
PowerMockito.spy(SshHelper.class);
44+
Session mockedSession = Mockito.mock(Session.class);
45+
46+
PowerMockito.doReturn(true).when(SshHelper.class, "isChannelConditionEof", Mockito.anyInt());
47+
Mockito.when(mockedSession.waitForCondition(ChannelCondition.EXIT_STATUS, 1l)).thenReturn(0);
48+
PowerMockito.doNothing().when(SshHelper.class, "throwSshExceptionIfConditionsTimeout", Mockito.anyInt());
49+
50+
SshHelper.canEndTheSshConnection(1, mockedSession, 0);
51+
52+
PowerMockito.verifyStatic();
53+
SshHelper.isChannelConditionEof(Mockito.anyInt());
54+
SshHelper.throwSshExceptionIfConditionsTimeout(Mockito.anyInt());
55+
56+
Mockito.verify(mockedSession).waitForCondition(ChannelCondition.EXIT_STATUS, 1l);
57+
}
58+
59+
@Test(expected = SshException.class)
60+
public void throwSshExceptionIfConditionsTimeout() throws SshException {
61+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.TIMEOUT);
62+
}
63+
64+
@Test
65+
public void doNotThrowSshExceptionIfConditionsClosed() throws SshException {
66+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.CLOSED);
67+
}
68+
69+
@Test
70+
public void doNotThrowSshExceptionIfConditionsStdout() throws SshException {
71+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.STDOUT_DATA);
72+
}
73+
74+
@Test
75+
public void doNotThrowSshExceptionIfConditionsStderr() throws SshException {
76+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.STDERR_DATA);
77+
}
78+
79+
@Test
80+
public void doNotThrowSshExceptionIfConditionsEof() throws SshException {
81+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.EOF);
82+
}
83+
84+
@Test
85+
public void doNotThrowSshExceptionIfConditionsExitStatus() throws SshException {
86+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.EXIT_STATUS);
87+
}
88+
89+
@Test
90+
public void doNotThrowSshExceptionIfConditionsExitSignal() throws SshException {
91+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.EXIT_SIGNAL);
92+
}
93+
94+
@Test
95+
public void isChannelConditionEofTestTimeout() {
96+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.TIMEOUT));
97+
}
98+
99+
@Test
100+
public void isChannelConditionEofTestClosed() {
101+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.CLOSED));
102+
}
103+
104+
@Test
105+
public void isChannelConditionEofTestStdout() {
106+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.STDOUT_DATA));
107+
}
108+
109+
@Test
110+
public void isChannelConditionEofTestStderr() {
111+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.STDERR_DATA));
112+
}
113+
114+
@Test
115+
public void isChannelConditionEofTestEof() {
116+
Assert.assertTrue(SshHelper.isChannelConditionEof(ChannelCondition.EOF));
117+
}
118+
119+
@Test
120+
public void isChannelConditionEofTestExitStatus() {
121+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.EXIT_STATUS));
122+
}
123+
124+
@Test
125+
public void isChannelConditionEofTestExitSignal() {
126+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.EXIT_SIGNAL));
127+
}
128+
129+
@Test(expected = SshException.class)
130+
public void throwSshExceptionIfStdoutOrStdeerIsNullTestNull() throws SshException {
131+
SshHelper.throwSshExceptionIfStdoutOrStdeerIsNull(null, null);
132+
}
133+
134+
@Test
135+
public void throwSshExceptionIfStdoutOrStdeerIsNullTestNotNull() throws SshException {
136+
InputStream inputStream = Mockito.mock(InputStream.class);
137+
SshHelper.throwSshExceptionIfStdoutOrStdeerIsNull(inputStream, inputStream);
138+
}
139+
140+
@Test
141+
public void openConnectionSessionTest() throws IOException, InterruptedException {
142+
Connection conn = Mockito.mock(Connection.class);
143+
PowerMockito.mockStatic(Thread.class);
144+
SshHelper.openConnectionSession(conn);
145+
146+
Mockito.verify(conn).openSession();
147+
148+
PowerMockito.verifyStatic();
149+
Thread.sleep(Mockito.anyLong());
150+
}
151+
}

0 commit comments

Comments
 (0)