Skip to content

Commit 2949100

Browse files
author
gabrascher
committed
Handle SSH if server "forget" to send exit status
Continued the work started by https://github.com/likitha commit (b9181c6) from PR #561. CS waits indefinitely for CheckS2SVpnConnectionsComm and to return. While remote executing commands through ssh, handle channel condition of EOF because we wait for the the condition. The SshHelper of the PR #561 is of another path from the current master, its path was https://github.com/likitha/cloudstack/commits/CLOUDSTACK-8611/utils/src/com/cloud/utils/ssh/SshHelper.java; thus, although this commit brings changes from PR #561, I did not cherry-picked to keep the master file, otherwise it would look that I had changed all the file. by me.
1 parent 5251eed commit 2949100

File tree

2 files changed

+262
-18
lines changed

2 files changed

+262
-18
lines changed

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

Lines changed: 111 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,87 @@ 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+
* @param conn
226+
* @return {@link Session}
227+
* @throws IOException
228+
* @throws InterruptedException
229+
*/
230+
protected static Session openConnectionSession(Connection conn) throws IOException, InterruptedException {
231+
Session sess = conn.openSession();
232+
Thread.sleep(WAITING_OPEN_SSH_SESSION);
233+
return sess;
234+
}
235+
236+
/**
237+
* Handles the SSH connection in case of timeout or exit. If the session ends with a timeout
238+
* condition, it throws an exception; if the channel reaches an end of file condition, but it
239+
* does not have an exit status, it returns true to break the loop; otherwise, it returns
240+
* false.
241+
*
242+
* @param waitResultTimeoutInMs
243+
* @param sess
244+
* @param conditions
245+
* @return boolean
246+
* @throws SshException
247+
*/
248+
protected static boolean canEndTheSshConnection(int waitResultTimeoutInMs, com.trilead.ssh2.Session sess, int conditions) throws SshException {
249+
if (isChannelConditionEof(conditions)) {
250+
int newConditions = sess.waitForCondition(ChannelCondition.EXIT_STATUS, waitResultTimeoutInMs);
251+
throwSshExceptionIfConditionsTimeout(newConditions);
252+
if ((newConditions & ChannelCondition.EXIT_STATUS) != 0) {
253+
return true;
254+
}
255+
}
256+
return false;
257+
}
258+
259+
/**
260+
* It throws a {@link SshException} if the channel condition is {@link ChannelCondition#TIMEOUT}
261+
*
262+
* @param conditions
263+
* @throws SshException
264+
*/
265+
protected static void throwSshExceptionIfConditionsTimeout(int conditions) throws SshException {
266+
if ((conditions & ChannelCondition.TIMEOUT) != 0) {
267+
String msg = "Timed out in waiting for SSH execution exit status";
268+
s_logger.error(msg);
269+
throw new SshException(msg);
270+
}
271+
}
272+
273+
/**
274+
* Checks if the channel condition mask is of {@link ChannelCondition#EOF} and not
275+
* {@link ChannelCondition#STDERR_DATA} or {@link ChannelCondition#STDOUT_DATA}.
276+
*
277+
* @param conditions
278+
* @return boolean
279+
*/
280+
protected static boolean isChannelConditionEof(int conditions) {
281+
if ((conditions & ChannelCondition.EOF) != 0) {
282+
return true;
283+
}
284+
return false;
285+
}
286+
287+
/**
288+
* Checks if the SSH session {@link com.trilead.ssh2.Session#getStdout()} or
289+
* {@link com.trilead.ssh2.Session#getStderr()} is null.
290+
*
291+
* @param stdout
292+
* @param stderr
293+
* @throws SshException
294+
*/
295+
protected static void throwSshExceptionIfStdoutOrStdeerIsNull(InputStream stdout, InputStream stderr) throws SshException {
296+
if (stdout == null || stderr == null) {
297+
String msg = "Stdout or Stderr of SSH session is null";
298+
s_logger.error(msg);
299+
throw new SshException(msg);
300+
}
301+
}
209302
}
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)