/*
* Copyright (c) 2015 EMC Corporation
* All Rights Reserved
*/
package com.emc.storageos.networkcontroller;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.text.MessageFormat;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.emc.storageos.networkcontroller.exceptions.NetworkDeviceControllerException;
public class SSHDialog {
private static final Logger _log = LoggerFactory.getLogger(SSHDialog.class);
SSHSession session;
protected Integer defaultTimeout; // default timeout in milliseconds
InputStreamReader insr;
OutputStreamWriter oswr;
protected String devname = "__unknown__device__";
// Identifies and exception in the response.
private static final String EXCEPTION_REGEX = "raise Exception\\('.*'\\)";
public SSHDialog(SSHSession session, Integer defaultTimeout) {
this.session = session;
if (defaultTimeout == null) {
defaultTimeout = 60000;
}
this.defaultTimeout = defaultTimeout;
this.insr = new InputStreamReader(session.ins);
this.oswr = new OutputStreamWriter(session.outs);
}
private SSHPrompt checkForPrompt(String buf, SSHPrompt[] prompts) throws NetworkDeviceControllerException {
// First check for an exception, which will be thrown if found.
checkForException(buf);
// If no exceptions, now look for the expected prompt(s).
for (SSHPrompt p : prompts) {
String regex = p.getRegex();
if (regex.contains("<<devname>>")) {
regex = regex.replace("<<devname>>", "\\Q" + devname + "\\E");
}
regex = "(?sm).*" + regex;
// Only check the last few lines of the buffer for the prompt as
// it is always at the end and checking the full buffer can be costly
String substring = buf.substring(Math.max(0, (buf.length() - 1024)));
_log.debug("Checking prompts in " + substring);
if (substring.matches(regex)) {
return p;
}
}
return SSHPrompt.NOMATCH;
}
/**
* Checks the passed response content for an embedded exception.
*
* Content will have the following for an exception:
*
* "raise Exception('A very specific bad thing happened')"
*
* @param buf The response content.
*
* @throws NetworkDeviceControllerException When an exception is found.
*/
private void checkForException(String buf) throws NetworkDeviceControllerException {
Pattern p = Pattern.compile(getResponseExceptionRegex());
Matcher m = p.matcher(buf);
if (m.find()) {
String match = buf.substring(m.start(), m.end());
String message = match.substring(match.indexOf("'")+1, match.lastIndexOf("'"));
_log.error("Found exception in response {}:{}", match, message);
throw NetworkDeviceControllerException.exceptions.exceptionInResponse(message);
}
}
/**
* Wait for the occurrence of a prompt.
*
* @param prompts -- List of possible prompts.
* @param timeout -- Timeout in milliseconds; if null defaultTimeout is used.
* @param delayMatchCheck -- wait to check for prompt match until no input is ready
* @param buf -- OUTPUT parameter returning the entire captured String.
* @return the prompt found
*/
public SSHPrompt waitFor(SSHPrompt[] prompts, Integer timeout, StringBuilder buf, boolean delayMatchCheck)
throws NetworkDeviceControllerException {
if (timeout == null) {
timeout = defaultTimeout;
}
buf.setLength(0);
char[] input = new char[32670];
int nread = 0;
long start = 0;
long lastInputTime = System.currentTimeMillis();
while (System.currentTimeMillis() - lastInputTime < timeout && nread != -1) {
try {
Thread.sleep(10);
if (insr.ready()) {
nread = insr.read(input);
_log.debug("insr is ready and the buffer will be appended by " + String.valueOf(input));
if (nread != -1) {
lastInputTime = System.currentTimeMillis();
buf.append(input, 0, nread);
if (delayMatchCheck) {
Thread.sleep(10);
}
start = System.currentTimeMillis();
_log.debug("Checking for prompts in new input: " + String.valueOf(input));
SSHPrompt px = checkForPrompt(buf.toString(), prompts);
_log.debug("Checking for prompts in new input only took " + (System.currentTimeMillis() - start));
if (px != SSHPrompt.NOMATCH) {
_log.debug("Prompt found " + px);
return px;
}
} else {
_log.debug("Reached EOF. Will check the full buffer for prompts");
start = System.currentTimeMillis();
SSHPrompt px = checkForPrompt(buf.toString(), prompts);
_log.debug("Checking for prompts in the full buffer took " + (System.currentTimeMillis() - start));
if (px != SSHPrompt.NOMATCH) {
return px;
}
}
}
} catch (IOException ex) {
_log.error(ex.getLocalizedMessage());
} catch (InterruptedException ex) {
_log.error(ex.getLocalizedMessage());
}
}
SSHPrompt prompt = checkForPrompt(buf.toString(), prompts);
if (prompt == SSHPrompt.NOMATCH) {
StringBuffer expectedPrompts = new StringBuffer("Expected one of these prompts, but not found: ");
for (SSHPrompt chkPrompt : prompts) {
expectedPrompts.append(chkPrompt.toString()).append("(" + chkPrompt.getRegex() + "), ");
}
throw NetworkDeviceControllerException.exceptions.timeoutWaitingOnPrompt(expectedPrompts.toString());
}
return prompt;
}
protected String[] getLines(StringBuilder buf) {
String[] lines = buf.toString().split("[\n\r]+");
return lines;
}
/**
* Send string and then wait for a prompt in the supplied set.
* All data received is in buf.
*
* @param send
* @param timeout
* @param prompts - An array of MDS prompts. The first one encountered will be returned.
* @param buf - Output: StringBuilder containing characters received (including prompt)
* @return
*/
public SSHPrompt sendWaitFor(String send, Integer timeout, SSHPrompt[] prompts, StringBuilder buf)
throws NetworkDeviceControllerException {
_log.debug(MessageFormat.format("Host: {0}, Port: {1} - sendWaitFor: {2}",
new Object[] { getSession().getSession().getHost(), getSession().getSession().getPort(), send }));
SSHPrompt prompt = null;
try {
oswr.append(send);
oswr.flush();
prompt = waitFor(prompts, timeout, buf, false);
} catch (Exception ex) {
_log.error("Exception sending string: {}, recevied: {}", send, buf);
throw new NetworkDeviceControllerException(ex);
}
_log.debug(MessageFormat.format("Host: {0}, Port: {1} - sendWaitFor: {2} - Received data: {3}",
new Object[] { getSession().getSession().getHost(), getSession().getSession().getPort(), send, buf }));
return prompt;
}
/**
* Send a string without waiting for a reply.
*
* @param send String
* @throws NetworkDeviceControllerException
*/
public void send(String send) {
try {
oswr.append(send);
oswr.flush();
_log.debug("Sent: " + send);
} catch (IOException ex) {
String msg = "Exception sending string: " + send + " " + ex.getLocalizedMessage();
_log.error(msg);
}
}
/**
* Function that overloads {@link #match(String, String[], String[], int)} to make <code>flags</code> an optional parameter.
*
* @param buf - Input string buffer.
* @param regexs - Array of regular expressions to be considered. First one matching
* is returned.
* @param groups - Returns the regex groups for the matching regular expressions.
* Therefore the length of the groups array passed in must be able to accommodate the
* regex with the largest number of groups defined. (0 .. n-1)
* @return the index of the regex that matched (0 .. n-1)
*/
public static int match(String buf, String[] regexs, String[] groups) {
return match(buf, regexs, groups, 0);
}
/**
* Returns the index of the first regex that matches the buffer. If none match,
* returns -1. Any regex group outputs are returned in groups. The matching text is
* removed from the buffer.
*
* @param buf - Input string buffer.
* @param regexs - Array of regular expressions to be considered. First one matching
* is returned.
* @param groups - Returns the regex groups for the matching regular expressions.
* Therefore the length of the groups array passed in must be able to accommodate the
* regex with the largest number of groups defined. (0 .. n-1)
* @param flags
* Match flags, a bit mask that may include {@link Pattern#CASE_INSENSITIVE}, {@link Pattern#MULTILINE}, etc.
* @return the index of the regex that matched (0 .. n-1)
*/
public static int match(String buf, String[] regexs, String[] groups, int flags) {
int index = 0;
for (String regex : regexs) {
Pattern p = Pattern.compile(regex, flags);
Matcher m = p.matcher(buf);
if (m.matches()) {
int ngroups = m.groupCount();
for (int j = 1; j <= ngroups; j++) {
groups[j - 1] = m.group(j);
}
return index; // return the index of the regex that matched
}
index++;
}
return -1; // none of the regular expressions matched
}
public SSHSession getSession() {
return session;
}
/**
* Returns a regular expression that can be used to parse the response and
* identify whether or not an exception occurred handling the request.
*
* @return The regular expression to use when parsing the response for an exception.
*/
protected String getResponseExceptionRegex() {
return EXCEPTION_REGEX;
}
}