Skip to content

Commit 4f67128

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 apache#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 apache#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 apache#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 4f67128

File tree

2 files changed

+249
-16
lines changed

2 files changed

+249
-16
lines changed

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

Lines changed: 113 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ public class SshHelper {
3232
private static final int DEFAULT_CONNECT_TIMEOUT = 180000;
3333
private static final int DEFAULT_KEX_TIMEOUT = 60000;
3434

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

3743
public static Pair<Boolean, String> sshExecute(String host, int port, String user, File pemKeyFile, String password, String command) throws Exception {
@@ -40,19 +46,19 @@ public static Pair<Boolean, String> sshExecute(String host, int port, String use
4046
}
4147

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

4551
scpTo(host, port, user, pemKeyFile, password, remoteTargetDirectory, localFile, fileMode, DEFAULT_CONNECT_TIMEOUT, DEFAULT_KEX_TIMEOUT);
4652
}
4753

4854
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 {
55+
String fileMode) throws Exception {
5056

5157
scpTo(host, port, user, pemKeyFile, password, remoteTargetDirectory, data, remoteFileName, fileMode, DEFAULT_CONNECT_TIMEOUT, DEFAULT_KEX_TIMEOUT);
5258
}
5359

5460
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 {
61+
int connectTimeoutInMs, int kexTimeoutInMs) throws Exception {
5662

5763
com.trilead.ssh2.Connection conn = null;
5864
com.trilead.ssh2.SCPClient scpClient = null;
@@ -88,7 +94,7 @@ public static void scpTo(String host, int port, String user, File pemKeyFile, St
8894
}
8995

9096
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 {
97+
String fileMode, int connectTimeoutInMs, int kexTimeoutInMs) throws Exception {
9298

9399
com.trilead.ssh2.Connection conn = null;
94100
com.trilead.ssh2.SCPClient scpClient = null;
@@ -123,7 +129,8 @@ public static void scpTo(String host, int port, String user, File pemKeyFile, St
123129
}
124130

125131
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 {
132+
int kexTimeoutInMs,
133+
int waitResultTimeoutInMs) throws Exception {
127134

128135
com.trilead.ssh2.Connection conn = null;
129136
com.trilead.ssh2.Session sess = null;
@@ -146,6 +153,8 @@ public static Pair<Boolean, String> sshExecute(String host, int port, String use
146153
}
147154
sess = conn.openSession();
148155

156+
throwSshExceptionIfSshConnectionIsNull(sess);
157+
149158
sess.execCommand(command);
150159

151160
InputStream stdout = sess.getStdout();
@@ -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,91 @@ public static Pair<Boolean, String> sshExecute(String host, int port, String use
206216
conn.close();
207217
}
208218
}
219+
220+
/**
221+
* Handles the SSH connection in case of timeout or exit. If the session ends with a timeout
222+
* condition, it throws an exception; if the channel reaches an end of file condition, but it
223+
* does not have an exit status, it returns true to break the loop; otherwise, it returns
224+
* false.
225+
*
226+
* @param waitResultTimeoutInMs
227+
* @param sess
228+
* @param conditions
229+
* @return boolean
230+
* @throws SshException
231+
*/
232+
protected static boolean canEndTheSshConnection(int waitResultTimeoutInMs, com.trilead.ssh2.Session sess, int conditions) throws SshException {
233+
if (isChannelConditionEof(conditions)) {
234+
int newConditions = sess.waitForCondition(ChannelCondition.EXIT_STATUS, waitResultTimeoutInMs);
235+
throwSshExceptionIfConditionsTimeout(newConditions);
236+
if ((newConditions & ChannelCondition.EXIT_STATUS) != 0) {
237+
return true;
238+
}
239+
}
240+
return false;
241+
}
242+
243+
/**
244+
* It throws a {@link SshException} if the channel condition is {@link ChannelCondition#TIMEOUT}
245+
*
246+
* @param conditions
247+
* @throws SshException
248+
*/
249+
protected static void throwSshExceptionIfConditionsTimeout(int conditions) throws SshException {
250+
if ((conditions & ChannelCondition.TIMEOUT) != 0) {
251+
String msg = "Timed out in waiting for SSH execution exit status";
252+
s_logger.error(msg);
253+
throw new SshException(msg);
254+
}
255+
}
256+
257+
/**
258+
* Checks if the channel condition mask is of {@link ChannelCondition#EOF} and not
259+
* {@link ChannelCondition#STDERR_DATA} or {@link ChannelCondition#STDOUT_DATA}.
260+
*
261+
* @param conditions
262+
* @return boolean
263+
*/
264+
protected static boolean isChannelConditionEof(int conditions) {
265+
if ((conditions & ChannelCondition.EOF) != 0) {
266+
return true;
267+
}
268+
return false;
269+
}
270+
271+
/**
272+
* Checks if the SSH session {@link com.trilead.ssh2.Session#getStdout()} or
273+
* {@link com.trilead.ssh2.Session#getStderr()} is null.
274+
*
275+
* @param stdout
276+
* @param stderr
277+
* @throws SshException
278+
*/
279+
protected static void throwSshExceptionIfStdoutOrStdeerIsNull(InputStream stdout, InputStream stderr) throws SshException {
280+
if (stdout == null || stderr == null) {
281+
String msg = "Stdout or Stderr of SSH session is null";
282+
s_logger.error(msg);
283+
throw new SshException(msg);
284+
}
285+
}
286+
287+
/**
288+
* Checks if the session is open. It waits {@link SshHelper#WAITING_OPEN_SSH_SESSION}
289+
* milliseconds; if the session is not open after the waiting time, then it assumes that the
290+
* session could not open a SSH connection.
291+
*
292+
* @param sess
293+
* @throws InterruptedException
294+
* @throws SshException
295+
*/
296+
protected static void throwSshExceptionIfSshConnectionIsNull(com.trilead.ssh2.Session sess) throws InterruptedException, SshException {
297+
Thread.sleep(WAITING_OPEN_SSH_SESSION);
298+
299+
if (sess == null) {
300+
String msg = "Cannot open SSH session";
301+
s_logger.error(msg);
302+
throw new SshException(msg);
303+
}
304+
}
305+
209306
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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.InputStream;
23+
import org.junit.Assert;
24+
import org.junit.Test;
25+
import org.junit.runner.RunWith;
26+
import org.mockito.Mockito;
27+
import org.powermock.api.mockito.PowerMockito;
28+
import org.powermock.core.classloader.annotations.PrepareForTest;
29+
import org.powermock.modules.junit4.PowerMockRunner;
30+
31+
import com.trilead.ssh2.ChannelCondition;
32+
import com.trilead.ssh2.Session;
33+
34+
@PrepareForTest({ Thread.class, SshHelper.class, Session.class })
35+
@RunWith(PowerMockRunner.class)
36+
public class SshHelperTest {
37+
38+
@Test(expected = SshException.class)
39+
public void throwSshExceptionIfConditionsTimeout() throws SshException {
40+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.TIMEOUT);
41+
}
42+
43+
@Test
44+
public void doNotThrowSshExceptionIfConditionsClosed() throws SshException {
45+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.CLOSED);
46+
}
47+
48+
@Test
49+
public void doNotThrowSshExceptionIfConditionsStdout() throws SshException {
50+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.STDOUT_DATA);
51+
}
52+
53+
@Test
54+
public void doNotThrowSshExceptionIfConditionsStderr() throws SshException {
55+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.STDERR_DATA);
56+
}
57+
58+
@Test
59+
public void doNotThrowSshExceptionIfConditionsEof() throws SshException {
60+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.EOF);
61+
}
62+
63+
@Test
64+
public void doNotThrowSshExceptionIfConditionsExitStatus() throws SshException {
65+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.EXIT_STATUS);
66+
}
67+
68+
@Test
69+
public void doNotThrowSshExceptionIfConditionsExitSignal() throws SshException {
70+
SshHelper.throwSshExceptionIfConditionsTimeout(ChannelCondition.EXIT_SIGNAL);
71+
}
72+
73+
@Test
74+
public void isChannelConditionEofTestTimeout() {
75+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.TIMEOUT));
76+
}
77+
78+
@Test
79+
public void isChannelConditionEofTestClosed() {
80+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.CLOSED));
81+
}
82+
83+
@Test
84+
public void isChannelConditionEofTestStdout() {
85+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.STDOUT_DATA));
86+
}
87+
88+
@Test
89+
public void isChannelConditionEofTestStderr() {
90+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.STDERR_DATA));
91+
}
92+
93+
@Test
94+
public void isChannelConditionEofTestEof() {
95+
Assert.assertTrue(SshHelper.isChannelConditionEof(ChannelCondition.EOF));
96+
}
97+
98+
@Test
99+
public void isChannelConditionEofTestExitStatus() {
100+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.EXIT_STATUS));
101+
}
102+
103+
@Test
104+
public void isChannelConditionEofTestExitSignal() {
105+
Assert.assertFalse(SshHelper.isChannelConditionEof(ChannelCondition.EXIT_SIGNAL));
106+
}
107+
108+
@Test(expected = SshException.class)
109+
public void throwSshExceptionIfStdoutOrStdeerIsNullTestNull() throws SshException {
110+
SshHelper.throwSshExceptionIfStdoutOrStdeerIsNull(null, null);
111+
}
112+
113+
@Test
114+
public void throwSshExceptionIfStdoutOrStdeerIsNullTestNotNull() throws SshException {
115+
InputStream inputStream = Mockito.mock(InputStream.class);
116+
SshHelper.throwSshExceptionIfStdoutOrStdeerIsNull(inputStream, inputStream);
117+
}
118+
119+
@Test
120+
public void throwSshExceptionIfSshConnectionIsNullTestNotNull() throws Exception {
121+
Session sess = Mockito.mock(Session.class);
122+
PowerMockito.spy(Thread.class);
123+
PowerMockito.doNothing().when(Thread.class, "sleep", Mockito.anyLong());
124+
125+
SshHelper.throwSshExceptionIfSshConnectionIsNull(sess);
126+
}
127+
128+
@Test(expected = SshException.class)
129+
public void throwSshExceptionIfSshConnectionIsNullTestNull() throws Exception {
130+
PowerMockito.spy(Thread.class);
131+
PowerMockito.doNothing().when(Thread.class, "sleep", Mockito.anyLong());
132+
133+
SshHelper.throwSshExceptionIfSshConnectionIsNull(null);
134+
}
135+
136+
}

0 commit comments

Comments
 (0)