From 43090d8778d60ed62089927c1f6732140036791a Mon Sep 17 00:00:00 2001 From: Ganesh Chandrasekaran Date: Wed, 4 Jul 2018 14:10:57 +0900 Subject: reqExec API implemented for saltstack Issue-ID: CCSDK-320 Change-Id: I5c4eb36924f36ebc9a7786d2bd46c923d0c05ab0 Signed-off-by: Ganesh Chandrasekaran --- .../adaptors/saltstack/impl/ConnectionBuilder.java | 149 ++++++++++-- .../saltstack/impl/SaltstackAdapterImpl.java | 107 +++++++-- .../sli/adaptors/saltstack/impl/SshConnection.java | 250 +++++++++++++++++++++ .../sli/adaptors/saltstack/model/JsonParser.java | 89 ++++++++ .../saltstack/model/SaltstackMessageParser.java | 133 ++++++++--- .../adaptors/saltstack/model/SaltstackResult.java | 27 ++- .../saltstack/model/SaltstackResultCodes.java | 4 +- .../saltstack/model/SaltstackServerEmulator.java | 47 ++-- 8 files changed, 724 insertions(+), 82 deletions(-) create mode 100644 saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/SshConnection.java create mode 100644 saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/JsonParser.java (limited to 'saltstack-adapter/saltstack-adapter-provider/src/main') diff --git a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/ConnectionBuilder.java b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/ConnectionBuilder.java index 7702dc80..5dee9f5e 100644 --- a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/ConnectionBuilder.java +++ b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/ConnectionBuilder.java @@ -24,11 +24,18 @@ package org.onap.ccsdk.sli.adaptors.saltstack.impl; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.RandomStringUtils; import org.onap.ccsdk.sli.adaptors.saltstack.model.SaltstackResult; import org.onap.ccsdk.sli.adaptors.saltstack.model.SaltstackResultCodes; import org.onap.ccsdk.sli.core.sli.SvcLogicException; @@ -47,30 +54,142 @@ import com.att.eelf.configuration.EELFManager; public class ConnectionBuilder { private static final EELFLogger logger = EELFManager.getInstance().getLogger(ConnectionBuilder.class); + SshConnection sshConnection; + /** + * Constructor that initializes an ssh client based on username and password + **/ + public ConnectionBuilder(String host, String port, String userName, String userPasswd) { + sshConnection = new SshConnection(host, Integer.parseInt(port), userName, userPasswd); + } /** - * Constructor that initializes an ssh client based on certificate + * Constructor that initializes an ssh client based on ssh certificate **/ - public ConnectionBuilder(String userName, String userPasswd) throws KeyStoreException, CertificateException, IOException, - KeyManagementException, NoSuchAlgorithmException, SvcLogicException { + public ConnectionBuilder(String host, String port, String certFile) { + sshConnection = new SshConnection(host, Integer.parseInt(port), certFile); + } + /** + * Constructor that initializes an ssh client based on ssh username password and certificate + **/ + public ConnectionBuilder(String host, String port, String userName, String userPasswd, + String certFile) { + sshConnection = new SshConnection(host, Integer.parseInt(port), userName, userPasswd, certFile); } /** - * Constructor which trusts all certificates in a specific java keystore file (assumes a JKS - * file) - **/ - public ConnectionBuilder(String certFile) throws KeyStoreException, IOException, - KeyManagementException, NoSuchAlgorithmException, CertificateException { + * 1. Connect to SSH server. + * 2. Exec remote command over SSH. Return command execution status. + * Command output is written to out or err stream. + * + * @param cmd Commands to execute + * @return command execution status + */ + public SaltstackResult connectNExecute(String cmd) { + return connectNExecute(cmd,-1,-1); + } + + /** + * 1. Connect to SSH server with retry enabled. + * 2. Exec remote command over SSH. Return command execution status. + * Command output is written to out or err stream. + * + * @param cmd Commands to execute + * @param retryDelay delay between retry to make a SSH connection. + * @param retryCount number of count retry to make a SSH connection. + * @return command execution status + */ + public SaltstackResult connectNExecute(String cmd, int retryCount, int retryDelay) { + + SaltstackResult result = new SaltstackResult(); + try { + if (retryCount != -1) { + result = sshConnection.connectWithRetry(retryCount, retryDelay); + } else { + result = sshConnection.connect(); + } + if (result.getStatusCode() != SaltstackResultCodes.SUCCESS.getValue()) { + return result; + } + String outFilePath = "/tmp/"+ RandomStringUtils.random(5,true,true); + String errFilePath = "/tmp/"+ RandomStringUtils.random(5,true,true); + OutputStream out = new FileOutputStream(outFilePath); + OutputStream errs = new FileOutputStream(errFilePath); + result = sshConnection.execCommand(cmd, out, errs); + sshConnection.disconnect(); + out.close(); + errs.close(); + if (result.getSshExitStatus() != 0) { + return sortExitStatus(result.getSshExitStatus(), errFilePath, cmd); + } + if (result.getStatusCode() != SaltstackResultCodes.SUCCESS.getValue()) { + return result; + } + result.setStatusMessage("Success"); + result.setOutputFileName(outFilePath); + } catch (Exception io) { + logger.error("Caught Exception", io); + result.setStatusCode(SaltstackResultCodes.UNKNOWN_EXCEPTION.getValue()); + result.setStatusMessage(io.getMessage()); + } + return result; + } + public SaltstackResult sortExitStatus (int exitStatus, String errFilePath, String cmd) { + SaltstackResult result = new SaltstackResult(); + String err; + StringWriter writer = new StringWriter(); + try { + IOUtils.copy(new FileInputStream(new File(errFilePath)), writer, "UTF-8"); + err = writer.toString(); + } catch (Exception e){ + err = ""; + } + if (exitStatus == 255 || exitStatus == 1) { + String errMessage = "Error executing command [" + cmd + "] over SSH [" + sshConnection.toString() + + "]. Exit Code " + exitStatus + " and Error message : " + + "Malformed configuration. "+ err; + logger.error(errMessage); + result.setStatusCode(SaltstackResultCodes.INVALID_COMMAND.getValue()); + result.setStatusMessage(errMessage); + } else if (exitStatus == 5 || exitStatus == 65) { + String errMessage = "Error executing command [" + cmd + "] over SSH [" + sshConnection.toString() + + "]. Exit Code " + exitStatus + " and Error message : " + + "Host not allowed to connect. "+ err; + logger.error(errMessage); + result.setStatusCode(SaltstackResultCodes.USER_UNAUTHORIZED.getValue()); + result.setStatusMessage(errMessage); + } else if (exitStatus == 67 || exitStatus == 73) { + String errMessage = "Error executing command [" + cmd + "] over SSH [" + sshConnection.toString() + + "]. Exit Code " + exitStatus + " and Error message : " + + "Key exchange failed. "+ err; + logger.error(errMessage); + result.setStatusCode(SaltstackResultCodes.CERTIFICATE_ERROR.getValue()); + result.setStatusMessage(errMessage); + } else { + String errMessage = "Error executing command [" + cmd + "] over SSH [" + sshConnection.toString() + + "]. Exit Code " + exitStatus + " and Error message : "+ err; + logger.error(errMessage); + result.setStatusCode(SaltstackResultCodes.UNKNOWN_EXCEPTION.getValue()); + result.setStatusMessage(errMessage); + } + return result; } /** - * Connect to SSH server. + * 1. Connect to SSH server. + * 2. Exec remote command over SSH. Return command execution status. + * Command output is written to out or err stream. + * + * @param commands list of commands to execute + * @param payloadSLS has the SLS file location that is to be sent to server + * @param retryDelay delay between retry to make a SSH connection. + * @param retryCount number of count retry to make a SSH connection. + * @return command execution status */ - public SaltstackResult connect(String agentUrl, String payload) { + public SaltstackResult connectNExecuteSLS(String commands, String payloadSLS, int retryDelay, int retryCount) { SaltstackResult result = new SaltstackResult(); try { @@ -104,8 +223,6 @@ public class ConnectionBuilder { * Command output is written to out or err stream. * * @param cmd command to execute - * @param out content of sysout will go to this stream - * @param err content of syserr will go to this stream * @return command execution status */ public SaltstackResult execute(String cmd) { diff --git a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/SaltstackAdapterImpl.java b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/SaltstackAdapterImpl.java index 6ff5e574..5fe130fc 100644 --- a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/SaltstackAdapterImpl.java +++ b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/SaltstackAdapterImpl.java @@ -79,7 +79,12 @@ public class SaltstackAdapterImpl implements SaltstackAdapter { private static final String ID_ATTRIBUTE_NAME = "org.onap.appc.adapter.saltstack.Id"; private static final String LOG_ATTRIBUTE_NAME = "org.onap.appc.adapter.saltstack.log"; + public static final String CONNECTION_RETRY_DELAY = "retryDelay"; + public static final String CONNECTION_RETRY_COUNT = "retryCount"; + private static final String CLIENT_TYPE_PROPERTY_NAME = "org.onap.appc.adapter.saltstack.clientType"; + private static final String SS_SERVER_HOSTNAME = "org.onap.appc.adapter.saltstack.host"; + private static final String SS_SERVER_PORT = "org.onap.appc.adapter.saltstack.port"; private static final String SS_SERVER_USERNAME = "org.onap.appc.adapter.saltstack.userName"; private static final String SS_SERVER_PASSWORD = "org.onap.appc.adapter.saltstack.userPasswd"; private static final String SS_SERVER_SSH_KEY = "org.onap.appc.adapter.saltstack.sshKey"; @@ -134,7 +139,7 @@ public class SaltstackAdapterImpl implements SaltstackAdapter { * Returns the symbolic name of the adapter * * @return The adapter name - * @see org.onap.appc.adapter.rest.SaltstackAdapter#getAdapterName() + * @see SaltstackAdapter#getAdapterName() */ @Override public String getAdapterName() { @@ -146,7 +151,7 @@ public class SaltstackAdapterImpl implements SaltstackAdapter { this.timeout = timeout; } /** - * @param rc Method posts info to Context memory in case of an error and throws a + * Method posts info to Context memory in case of an error and throws a * SvcLogicException causing SLI to register this as a failure */ @SuppressWarnings("static-method") @@ -182,22 +187,32 @@ public class SaltstackAdapterImpl implements SaltstackAdapter { String clientType = props.getProperty(CLIENT_TYPE_PROPERTY_NAME); logger.info("Saltstack ssh client type set to " + clientType); - if ("BASIC".equals(clientType)) { + if ("BASIC".equalsIgnoreCase(clientType)) { logger.info("Creating ssh client connection"); // set path to keystore file - String trustStoreFile = props.getProperty(SS_SERVER_USERNAME); - String key = props.getProperty(SS_SERVER_PASSWORD); - //TODO: Connect to SSH Saltstack server (using username and password) and return client to execute command - sshClient = null; - } else if ("SSH_CERT".equals(clientType)) { + String sshHost = props.getProperty(SS_SERVER_HOSTNAME); + String sshPort = props.getProperty(SS_SERVER_PORT); + String sshUserName = props.getProperty(SS_SERVER_USERNAME); + String sshPassword = props.getProperty(SS_SERVER_PASSWORD); + sshClient = new ConnectionBuilder(sshHost, sshPort, sshUserName, sshPassword); + } else if ("SSH_CERT".equalsIgnoreCase(clientType)) { // set path to keystore file - String key = props.getProperty(SS_SERVER_SSH_KEY); - logger.info("Creating ssh client with ssh KEY from " + key); - //TODO: Connect to SSH Saltstack server (using SSH Key) and return client to execute command - sshClient = null; + String sshKey = props.getProperty(SS_SERVER_SSH_KEY); + String sshHost = props.getProperty(SS_SERVER_HOSTNAME); + String sshPort = props.getProperty(SS_SERVER_PORT); + logger.info("Creating ssh client with ssh KEY from " + sshKey); + sshClient = new ConnectionBuilder(sshHost, sshPort, sshKey); + } else if ("BOTH".equalsIgnoreCase(clientType)) { + // set path to keystore file + String sshKey = props.getProperty(SS_SERVER_SSH_KEY); + String sshHost = props.getProperty(SS_SERVER_HOSTNAME); + String sshUserName = props.getProperty(SS_SERVER_USERNAME); + String sshPassword = props.getProperty(SS_SERVER_PASSWORD); + String sshPort = props.getProperty(SS_SERVER_PORT); + logger.info("Creating ssh client with ssh KEY from " + sshKey); + sshClient = new ConnectionBuilder(sshHost, sshPort, sshUserName, sshPassword, sshKey); } else { - logger.info("Creating ssh client without any Auth"); - //TODO: Connect to SSH Saltstack server without any Auth + logger.info("No saltstack-adapter.properties defined so reading from DG props"); sshClient = null; } } catch (Exception e) { @@ -214,7 +229,49 @@ public class SaltstackAdapterImpl implements SaltstackAdapter { // org.onap.appc.adapter.saltstack.req.Id : a unique uuid to reference the request @Override public void reqExecCommand(Map params, SvcLogicContext ctx) throws SvcLogicException { - //TODO: to implement + String reqID; + SaltstackResult testResult; + if (sshClient == null){ + logger.info("saltstack-adapter.properties not defined so reading saltstack host and " + + "auth details from DG's parameters"); + String sshHost = messageProcessor.reqHostNameResult(params); + String sshPort = messageProcessor.reqPortResult(params); + String sshUserName = messageProcessor.reqUserNameResult(params); + String sshPassword = messageProcessor.reqPasswordResult(params); + logger.info("Creating ssh client with BASIC Auth"); + if(!testMode) + sshClient = new ConnectionBuilder(sshHost, sshPort, sshUserName, sshPassword); + } + try { + reqID = params.get("Id"); + String commandToExecute = params.get("cmd"); + testResult = execCommand(params, commandToExecute); + testResult = messageProcessor.parseResponse(ctx, reqID, testResult); + + // Check status of test request returned by Agent + if (testResult.getStatusCode() == SaltstackResultCodes.FINAL_SUCCESS.getValue()) { + logger.info(String.format("Execution of request-ID : %s successful.", reqID)); + testResult.setResults("Success"); + } else { + doFailure(ctx, testResult.getStatusCode(), "Request for execution of command failed. Reason = " + testResult.getStatusMessage()); + return; + } + } catch (SvcLogicException e) { + logger.error(APPC_EXCEPTION_CAUGHT, e); + doFailure(ctx, SaltstackResultCodes.UNKNOWN_EXCEPTION.getValue(), + "Request for execution of command failed. Reason = " + + e.getMessage()); + return; + } catch (Exception e) { + logger.error("Exception caught", e); + doFailure(ctx, SaltstackResultCodes.INVALID_COMMAND.getValue(), + "Request for execution of command failed. Reason = " + + e.getMessage()); + return; + } + ctx.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, Integer.toString(testResult.getStatusCode())); + ctx.setAttribute(MESSAGE_ATTRIBUTE_NAME, testResult.getResults()); + ctx.setAttribute(ID_ATTRIBUTE_NAME, reqID); } /** @@ -243,4 +300,24 @@ public class SaltstackAdapterImpl implements SaltstackAdapter { //TODO: to implement } + + public SaltstackResult execCommand(Map params, String commandToExecute){ + SaltstackResult testResult; + if (params.get(CONNECTION_RETRY_DELAY) != null && params.get(CONNECTION_RETRY_COUNT) != null) { + int retryDelay = Integer.parseInt(params.get(CONNECTION_RETRY_DELAY)); + int retryCount = Integer.parseInt(params.get(CONNECTION_RETRY_COUNT)); + if(!testMode) + testResult = sshClient.connectNExecute(commandToExecute, retryCount, retryDelay); + else { + testResult = testServer.MockReqExec(params); + } + } else { + if(!testMode) + testResult = sshClient.connectNExecute(commandToExecute); + else { + testResult = testServer.MockReqExec(params); + } + } + return testResult; + } } diff --git a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/SshConnection.java b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/SshConnection.java new file mode 100644 index 00000000..41e6102d --- /dev/null +++ b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/SshConnection.java @@ -0,0 +1,250 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP : APPC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Copyright (C) 2017 Amdocs + * ============================================================================= + * Licensed 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. + * + * ECOMP is a trademark and service mark of AT&T Intellectual Property. + * ============LICENSE_END========================================================= + */ + +package org.onap.ccsdk.sli.adaptors.saltstack.impl; + +import org.onap.appc.encryption.EncryptionTool; +import org.apache.sshd.ClientChannel; +import org.apache.sshd.ClientSession; +import org.apache.sshd.SshClient; +import org.apache.sshd.client.channel.ChannelExec; +import org.apache.sshd.client.future.AuthFuture; +import org.apache.sshd.client.future.OpenFuture; +import org.apache.sshd.common.KeyPairProvider; +import org.apache.sshd.common.keyprovider.FileKeyPairProvider; + +import com.att.eelf.configuration.EELFLogger; +import com.att.eelf.configuration.EELFManager; +import org.onap.ccsdk.sli.adaptors.saltstack.model.SaltstackResult; +import org.onap.ccsdk.sli.adaptors.saltstack.model.SaltstackResultCodes; +import org.onap.ccsdk.sli.core.sli.SvcLogicException; + +import java.io.OutputStream; +import java.security.KeyPair; + +/** + * Implementation of SshConnection interface based on Apache MINA SSHD library. + */ +class SshConnection { + + private static final EELFLogger logger = EELFManager.getInstance().getApplicationLogger(); + + private static final long AUTH_TIMEOUT = 60000; + private static final long EXEC_TIMEOUT = 120000; + + private String host; + private int port; + private String username; + private String password; + private long timeout = EXEC_TIMEOUT; + private String keyFile; + private SshClient sshClient; + private ClientSession clientSession; + + public static final int DEFAULT_CONNECTION_RETRY_DELAY = 60; + public static final int DEFAULT_CONNECTION_RETRY_COUNT = 5; + + public SshConnection(String host, int port, String username, String password, String keyFile) { + this.host = host; + this.port = port; + this.username = username; + this.password = password; + this.keyFile = keyFile; + } + + public SshConnection(String host, int port, String username, String password) { + this(host, port, username, password, null); + } + + public SshConnection(String host, int port, String keyFile) { + this(host, port, null, null, keyFile); + } + + public SaltstackResult connect() { + SaltstackResult result = new SaltstackResult(); + sshClient = SshClient.setUpDefaultClient(); + sshClient.start(); + try { + clientSession = + sshClient.connect(EncryptionTool.getInstance().decrypt(username), host, port).await().getSession(); + if (password != null) { + clientSession.addPasswordIdentity(EncryptionTool.getInstance().decrypt(password)); + } + if (keyFile != null) { + KeyPairProvider keyPairProvider = new FileKeyPairProvider(new String[] { + keyFile + }); + KeyPair keyPair = keyPairProvider.loadKeys().iterator().next(); + clientSession.addPublicKeyIdentity(keyPair); + } + AuthFuture authFuture = clientSession.auth(); + authFuture.await(AUTH_TIMEOUT); + if (!authFuture.isSuccess()) { + String errMessage = "Error establishing ssh connection to [" + username + "@" + host + ":" + port + + "]. Authentication failed."; + result.setStatusCode(SaltstackResultCodes.USER_UNAUTHORIZED.getValue()); + result.setStatusMessage(errMessage); + } + } catch (RuntimeException e) { + String errMessage = "Error establishing ssh connection to [" + username + "@" + host + ":" + port + "]." + + "Runtime Exception : "+ e.getMessage(); + result.setStatusCode(SaltstackResultCodes.UNKNOWN_EXCEPTION.getValue()); + result.setStatusMessage(errMessage); + } catch (Exception e) { + String errMessage = "Error establishing ssh connection to [" + username + "@" + host + ":" + port + "]." + + "Host Unknown : " + e.getMessage(); + result.setStatusCode(SaltstackResultCodes.HOST_UNKNOWN.getValue()); + result.setStatusMessage(errMessage); + } + if (logger.isDebugEnabled()) { + logger.debug("SSH: connected to [" + toString() + "]"); + } + result.setStatusCode(SaltstackResultCodes.SUCCESS.getValue()); + return result; + } + + public SaltstackResult connectWithRetry(int retryCount, int retryDelay) { + int retriesLeft; + SaltstackResult result = new SaltstackResult(); + if(retryCount == 0) + retryCount = DEFAULT_CONNECTION_RETRY_COUNT; + if(retryDelay == 0) + retryDelay = DEFAULT_CONNECTION_RETRY_DELAY; + retriesLeft = retryCount + 1; + do { + try { + result = this.connect(); + break; + } catch (RuntimeException e) { + if (retriesLeft > 1) { + logger.debug("SSH Connection failed. Waiting for change in server's state."); + waitForConnection(retryDelay); + retriesLeft--; + logger.debug("Retrying SSH connection. Attempt [" + Integer.toString(retryCount - retriesLeft + 1) + + "] out of [" + retryCount + "]"); + } else { + throw e; + } + } + } while (retriesLeft > 0); + return result; + } + + public void disconnect() { + try { + if (logger.isDebugEnabled()) { + logger.debug("SSH: disconnecting from [" + toString() + "]"); + } + clientSession.close(false); + } finally { + if (sshClient != null) { + sshClient.stop(); + } + } + } + + public void setExecTimeout(long timeout) { + this.timeout = timeout; + } + + public SaltstackResult execCommand(String cmd, OutputStream out, OutputStream err) { + return execCommand(cmd, out, err, false); + } + + public SaltstackResult execCommandWithPty(String cmd, OutputStream out) { + return execCommand(cmd, out, out, true); + } + + private SaltstackResult execCommand(String cmd, OutputStream out, OutputStream err, boolean usePty) { + SaltstackResult result = new SaltstackResult(); + try { + if (logger.isDebugEnabled()) { + logger.debug("SSH: executing command"); + } + ChannelExec client = clientSession.createExecChannel(cmd); + client.setUsePty(usePty); // use pseudo-tty? + client.setOut(out); + client.setErr(err); + OpenFuture openFuture = client.open(); + int exitStatus; + try { + client.waitFor(ClientChannel.CLOSED, timeout); + openFuture.verify(); + Integer exitStatusI = client.getExitStatus(); + if (exitStatusI == null) { + String errMessage = "Error executing command [" + cmd + "] over SSH [" + username + "@" + host + + ":" + port + "]. SSH operation timed out."; + result.setStatusCode(SaltstackResultCodes.OPERATION_TIMEOUT.getValue()); + result.setStatusMessage(errMessage); + return result; + } + exitStatus = exitStatusI; + } finally { + client.close(false); + } + result.setSshExitStatus(exitStatus); + return result; + } catch (RuntimeException e) { + String errMessage = "Error establishing ssh connection to [" + username + "@" + host + ":" + port + "]." + + "Runtime Exception : "+ e.getMessage(); + result.setStatusCode(SaltstackResultCodes.UNKNOWN_EXCEPTION.getValue()); + result.setStatusMessage(errMessage); + } catch (Exception e1) { + String errMessage = "Error executing command [" + cmd + "] over SSH [" + username + "@" + host + ":" + + port + "]"+ e1.getMessage(); + result.setStatusCode(SaltstackResultCodes.UNKNOWN_EXCEPTION.getValue()); + result.setStatusMessage(errMessage); + } + result.setStatusCode(SaltstackResultCodes.SUCCESS.getValue()); + return result; + } + + private void waitForConnection(int retryDelay) { + long time = retryDelay * 1000L; + long future = System.currentTimeMillis() + time; + if (time != 0) { + while (System.currentTimeMillis() < future && time > 0) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + /* + * This is rare, but it can happen if another thread interrupts us while we are sleeping. In that + * case, the thread is resumed before the delay time has actually expired, so re-calculate the + * amount of delay time needed and reenter the sleep until we get to the future time. + */ + time = future - System.currentTimeMillis(); + } + } + } + } + + @Override + public String toString() { + String address = host; + if (username != null) { + address = username + '@' + address + ':' + port; + } + return address; + } +} diff --git a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/JsonParser.java b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/JsonParser.java new file mode 100644 index 00000000..f33799fd --- /dev/null +++ b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/JsonParser.java @@ -0,0 +1,89 @@ +/*- + * ============LICENSE_START======================================================= + * openECOMP : SDN-C + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights + * reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.ccsdk.sli.adaptors.saltstack.model; + +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public final class JsonParser { + + private static final Logger log = LoggerFactory.getLogger(JsonParser.class); + + private JsonParser() { + // Preventing instantiation of the same. + } + + @SuppressWarnings("unchecked") + public static Map convertToProperties(String s) + throws JSONException { + + checkNotNull(s, "Input should not be null."); + + JSONObject json = new JSONObject(s); + Map wm = new HashMap<>(); + Iterator ii = json.keys(); + while (ii.hasNext()) { + String key1 = ii.next(); + wm.put(key1, json.get(key1)); + } + + Map mm = new HashMap<>(); + + while (!wm.isEmpty()) + for (String key : new ArrayList<>(wm.keySet())) { + Object o = wm.get(key); + wm.remove(key); + + if (o instanceof Boolean || o instanceof Number || o instanceof String) { + mm.put(key, o.toString()); + + log.info("Added property: {} : {}", key, o.toString()); + } else if (o instanceof JSONObject) { + JSONObject jo = (JSONObject) o; + Iterator i = jo.keys(); + while (i.hasNext()) { + String key1 = i.next(); + wm.put(key + "." + key1, jo.get(key1)); + } + } else if (o instanceof JSONArray) { + JSONArray ja = (JSONArray) o; + mm.put(key + "_length", String.valueOf(ja.length())); + + log.info("Added property: {}_length: {}", key, String.valueOf(ja.length())); + + for (int i = 0; i < ja.length(); i++) + wm.put(key + '[' + i + ']', ja.get(i)); + } + } + return mm; + } +} diff --git a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackMessageParser.java b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackMessageParser.java index 5a548f84..1ea31516 100644 --- a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackMessageParser.java +++ b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackMessageParser.java @@ -28,15 +28,21 @@ package org.onap.ccsdk.sli.adaptors.saltstack.model; * This module implements the APP-C/Saltstack Server interface * based on the REST API specifications */ +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Map; +import java.util.Properties; import java.util.Set; import java.util.UUID; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.onap.ccsdk.sli.core.sli.SvcLogicContext; import org.onap.ccsdk.sli.core.sli.SvcLogicException; import com.google.common.base.Strings; import org.slf4j.Logger; @@ -53,10 +59,10 @@ public class SaltstackMessageParser { private static final String STATUS_CODE_KEY = "StatusCode"; private static final String SALTSTATE_NAME_KEY = "SaltStateName"; - private static final String AGENT_URL_KEY = "AgentUrl"; + private static final String SS_AGENT_HOSTNAME_KEY = "HostName"; + private static final String SS_AGENT_PORT_KEY = "Port"; private static final String PASS_KEY = "Password"; private static final String USER_KEY = "User"; - private static final String ID_KEY = "Id"; private static final String LOCAL_PARAMETERS_OPT_KEY = "LocalParameters"; private static final String FILE_PARAMETERS_OPT_KEY = "FileParameters"; @@ -80,7 +86,7 @@ public class SaltstackMessageParser { * */ public JSONObject reqMessage(Map params) throws SvcLogicException { - final String[] mandatoryTestParams = {AGENT_URL_KEY, SALTSTATE_NAME_KEY, USER_KEY, PASS_KEY}; + final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SALTSTATE_NAME_KEY, USER_KEY, PASS_KEY}; final String[] optionalTestParams = {ENV_PARAMETERS_OPT_KEY, NODE_LIST_OPT_KEY, LOCAL_PARAMETERS_OPT_KEY, TIMEOUT_OPT_KEY, VERSION_OPT_KEY, FILE_PARAMETERS_OPT_KEY, ACTION_OPT_KEY}; @@ -95,7 +101,7 @@ public class SaltstackMessageParser { // Generate a unique uuid for the test String reqId = UUID.randomUUID().toString(); - jsonPayload.put(ID_KEY, reqId); + jsonPayload.put(SS_AGENT_HOSTNAME_KEY, reqId); return jsonPayload; } @@ -103,60 +109,121 @@ public class SaltstackMessageParser { /** * Method that validates that the Map has enough information * to query Saltstack server for a result. If so, it returns - * the appropriate url, else an empty string. + * the appropriate PORT number. */ - public String reqUriResult(Map params) throws SvcLogicException { + public String reqPortResult(Map params) throws SvcLogicException { - final String[] mandatoryTestParams = {AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY}; + final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY, PASS_KEY}; for (String key : mandatoryTestParams) { throwIfMissingMandatoryParam(params, key); } - return params.get(AGENT_URL_KEY) + "?Id=" + params.get(ID_KEY) + "&Type=GetResult"; + return params.get(SS_AGENT_PORT_KEY); } /** * Method that validates that the Map has enough information - * to query Saltstack server for logs. If so, it populates the appropriate - * returns the appropriate url, else an empty string. + * to query Saltstack server for a result. If so, it returns + * the appropriate HOST name. */ - public String reqUriLog(Map params) throws SvcLogicException { + public String reqHostNameResult(Map params) throws SvcLogicException { - final String[] mandatoryTestParams = {AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY}; + final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY, PASS_KEY}; - for (String mandatoryParam : mandatoryTestParams) { - throwIfMissingMandatoryParam(params, mandatoryParam); + for (String key : mandatoryTestParams) { + throwIfMissingMandatoryParam(params, key); } - return params.get(AGENT_URL_KEY) + "?Id=" + params.get(ID_KEY) + "&Type=GetLog"; + return params.get(SS_AGENT_HOSTNAME_KEY); } /** - * This method parses response from the Saltstack Server when we do a post - * and returns an SaltstackResult object. + * Method that validates that the Map has enough information + * to query Saltstack server for a result. If so, it returns + * the appropriate Saltstack server login user name. */ - public SaltstackResult parsePostResponse(String input) throws SvcLogicException { - SaltstackResult saltstackResult; - try { - JSONObject postResponse = new JSONObject(input); + public String reqUserNameResult(Map params) throws SvcLogicException { - int code = postResponse.getInt(STATUS_CODE_KEY); - String msg = postResponse.getString(STATUS_MESSAGE_KEY); + final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY, PASS_KEY}; - int initResponseValue = SaltstackResultCodes.INITRESPONSE.getValue(); - boolean validCode = SaltstackResultCodes.CODE.checkValidCode(initResponseValue, code); - if (!validCode) { - throw new SvcLogicException("Invalid InitResponse code = " + code + " received. MUST be one of " - + SaltstackResultCodes.CODE.getValidCodes(initResponseValue)); - } + for (String key : mandatoryTestParams) { + throwIfMissingMandatoryParam(params, key); + } + return params.get(USER_KEY); + } + + /** + * Method that validates that the Map has enough information + * to query Saltstack server for a result. If so, it returns + * the appropriate Saltstack server login password. + */ + public String reqPasswordResult(Map params) throws SvcLogicException { - saltstackResult = new SaltstackResult(code, msg); + final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY, PASS_KEY}; + for (String key : mandatoryTestParams) { + throwIfMissingMandatoryParam(params, key); + } + return params.get(PASS_KEY); + } + + /** + * This method parses response from the Saltstack Server when we do a post + * and returns an SaltstackResult object. + */ + public SaltstackResult parseResponse(SvcLogicContext ctx, String pfx, SaltstackResult saltstackResult) { + int code = saltstackResult.getStatusCode(); + if (code != SaltstackResultCodes.SUCCESS.getValue()) { + return saltstackResult; + } + try { + File file = new File(saltstackResult.getOutputFileName()); + InputStream in = new FileInputStream(file); + byte[] data = new byte[(int) file.length()]; + in.read(data); + String str = new String(data, "UTF-8"); + in.close(); + Map mm = JsonParser.convertToProperties(str); + if (mm != null) { + for (Map.Entry entry : mm.entrySet()) { + ctx.setAttribute(pfx + entry.getKey(), entry.getValue()); + LOGGER.info("+++ " + pfx + entry.getKey() + ": [" + entry.getValue() + "]"); + } + } + } catch (FileNotFoundException e){ + return new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "Error parsing response file = " + + saltstackResult.getOutputFileName() + ". Error = " + e.getMessage()); } catch (JSONException e) { - saltstackResult = new SaltstackResult(600, "Error parsing response = " + input + ". Error = " + e.getMessage()); + LOGGER.info("Output not in JSON format"); + return putToProperties(ctx, pfx, saltstackResult); + } catch (Exception e) { + return new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "Error parsing response file = " + + saltstackResult.getOutputFileName() + ". Error = " + e.getMessage()); } + saltstackResult.setStatusCode(SaltstackResultCodes.FINAL_SUCCESS.getValue()); return saltstackResult; } + public SaltstackResult putToProperties(SvcLogicContext ctx, String pfx, SaltstackResult saltstackResult) { + try { + File file = new File(saltstackResult.getOutputFileName()); + InputStream in = new FileInputStream(file); + Properties prop = new Properties(); + prop.load(in); + ctx.setAttribute(pfx + "completeResult", prop.toString()); + for (Object key : prop.keySet()) { + String name = (String) key; + String value = prop.getProperty(name); + if (value != null && value.trim().length() > 0) { + ctx.setAttribute(pfx + name, value.trim()); + LOGGER.info("+++ " + pfx + name + ": [" + value + "]"); + } + } + } catch (Exception e) { + saltstackResult = new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "Error parsing response file = " + + saltstackResult.getOutputFileName() + ". Error = " + e.getMessage()); + } + return saltstackResult; + } /** * This method parses response from an Saltstack server when we do a GET for a result * and returns an SaltstackResult object. @@ -169,8 +236,8 @@ public class SaltstackMessageParser { JSONObject postResponse = new JSONObject(input); saltstackResult = parseGetResponseNested(saltstackResult, postResponse); } catch (JSONException e) { - saltstackResult = new SaltstackResult(SaltstackResultCodes.INVALID_PAYLOAD.getValue(), - "Error parsing response = " + input + ". Error = " + e.getMessage(), ""); + saltstackResult = new SaltstackResult(SaltstackResultCodes.INVALID_COMMAND.getValue(), + "Error parsing response = " + input + ". Error = " + e.getMessage(), "", -1); } return saltstackResult; } diff --git a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackResult.java b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackResult.java index f1fb40d9..05873024 100644 --- a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackResult.java +++ b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackResult.java @@ -24,6 +24,8 @@ package org.onap.ccsdk.sli.adaptors.saltstack.model; +import java.io.OutputStream; + /** * Simple class to store code and message returned by POST/GET to an Saltstack Server */ @@ -34,19 +36,22 @@ public class SaltstackResult { private int statusCode; private String statusMessage; private String results; + private String out; + private int sshExitStatus; public SaltstackResult() { - this(-1, EMPTY_VALUE, EMPTY_VALUE); + this(-1, EMPTY_VALUE, EMPTY_VALUE, -1); } public SaltstackResult(int code, String message) { - this(code, message, EMPTY_VALUE); + this(code, message, EMPTY_VALUE, -1); } - public SaltstackResult(int code, String message, String result) { + public SaltstackResult(int code, String message, String result, int sshCode) { statusCode = code; statusMessage = message; results = result; + sshExitStatus = sshCode; } public void setStatusCode(int code) { @@ -67,6 +72,14 @@ public class SaltstackResult { this.results = results; } + public void setOutputFileName (String out) { + this.out = out; + } + + public String getOutputFileName() { + return out; + } + public int getStatusCode() { return this.statusCode; } @@ -78,4 +91,12 @@ public class SaltstackResult { public String getResults() { return this.results; } + + public int getSshExitStatus() { + return sshExitStatus; + } + + public void setSshExitStatus(int sshExitStatus) { + this.sshExitStatus = sshExitStatus; + } } diff --git a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackResultCodes.java b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackResultCodes.java index e520dda6..ab88c212 100644 --- a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackResultCodes.java +++ b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackResultCodes.java @@ -44,9 +44,11 @@ public enum SaltstackResultCodes { HOST_UNKNOWN(625), USER_UNAUTHORIZED(613), UNKNOWN_EXCEPTION(699), + OPERATION_TIMEOUT(659), SSL_EXCEPTION(697), - INVALID_PAYLOAD(698), + INVALID_COMMAND(698), INVALID_RESPONSE(601), + INVALID_RESPONSE_FILE(600), PENDING(100), REJECTED(101), FINAL_SUCCESS(200), diff --git a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackServerEmulator.java b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackServerEmulator.java index a9bf7e7c..9737efd3 100644 --- a/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackServerEmulator.java +++ b/saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackServerEmulator.java @@ -32,6 +32,7 @@ package org.onap.ccsdk.sli.adaptors.saltstack.model; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; @@ -50,6 +51,28 @@ public class SaltstackServerEmulator { private String saltStateName = "test_saltState.yaml"; + /** + * Method that emulates the response from an Saltstack Server + * when presented with a request to execute a saltState + * Returns an saltstack object result. The response code is always the ssh code 200 (i.e connection successful) + * payload is json string as would be sent back by Saltstack Server + **/ + public SaltstackResult MockReqExec(Map params) { + SaltstackResult result = new SaltstackResult(); + + try { + if (params.get("Test") == "fail") { + result = rejectRequest(result, "Must provide a valid Id"); + } else { + result = acceptRequest(result); + } + } catch (Exception e) { + logger.error("JSONException caught", e); + rejectRequest(result, e.getMessage()); + } + return result; + } + /** * Method that emulates the response from an Saltstack Server * when presented with a request to execute a saltState @@ -57,13 +80,13 @@ public class SaltstackServerEmulator { * payload is json string as would be sent back by Saltstack Server **/ //TODO: This class is to be altered completely based on the SALTSTACK server communicaiton. - public SaltstackResult Connect(String agentUrl, String payload) { + public SaltstackResult Connect(Map params) { SaltstackResult result = new SaltstackResult(); try { // Request must be a JSON object - JSONObject message = new JSONObject(payload); + JSONObject message = new JSONObject(); if (message.isNull("Id")) { rejectRequest(result, "Must provide a valid Id"); } else if (message.isNull(SALTSTATE_NAME)) { @@ -120,19 +143,15 @@ public class SaltstackServerEmulator { return getResult; } - private void rejectRequest(SaltstackResult result, String Message) { - result.setStatusCode(200); - JSONObject response = new JSONObject(); - response.put(STATUS_CODE, SaltstackResultCodes.REJECTED.getValue()); - response.put(STATUS_MESSAGE, Message); - result.setStatusMessage(response.toString()); + private SaltstackResult rejectRequest(SaltstackResult result, String Message) { + result.setStatusCode(SaltstackResultCodes.REJECTED.getValue()); + result.setStatusMessage("Rejected"); + return result; } - private void acceptRequest(SaltstackResult result) { - result.setStatusCode(200); - JSONObject response = new JSONObject(); - response.put(STATUS_CODE, SaltstackResultCodes.PENDING.getValue()); - response.put(STATUS_MESSAGE, "PENDING"); - result.setStatusMessage(response.toString()); + private SaltstackResult acceptRequest(SaltstackResult result) { + result.setStatusCode(SaltstackResultCodes.SUCCESS.getValue()); + result.setStatusMessage("Success"); + return result; } } \ No newline at end of file -- cgit 1.2.3-korg