From 43090d8778d60ed62089927c1f6732140036791a Mon Sep 17 00:00:00 2001 From: Ganesh Chandrasekaran Date: Wed, 4 Jul 2018 14:10:57 +0900 Subject: [PATCH] reqExec API implemented for saltstack Issue-ID: CCSDK-320 Change-Id: I5c4eb36924f36ebc9a7786d2bd46c923d0c05ab0 Signed-off-by: Ganesh Chandrasekaran --- saltstack-adapter/README.md | 30 ++- .../ccsdk-saltstack-adapter/pom.xml | 43 ++-- .../saltstack-adapter-provider/pom.xml | 45 +++- .../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 ++-- .../adapter/impl/TestSaltstackAdapterImpl.java | 163 +++++++++++++- 12 files changed, 972 insertions(+), 115 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 diff --git a/saltstack-adapter/README.md b/saltstack-adapter/README.md index 0c3161ac..cf21e105 100644 --- a/saltstack-adapter/README.md +++ b/saltstack-adapter/README.md @@ -31,4 +31,32 @@ Create an Adaptor to communicate with the SaltStack server: ***Requirements and benefits of the chosen SSH method:*** 1) The SaltStack server should have it’s SSH enabled. -2) Such execution method will give the DGs and adaptor with more refined control on the SaltStack server. \ No newline at end of file +2) Such execution method will give the DGs and adaptor with more refined control on the SaltStack server. +================================================================================================================== + + +***Defining Saltstack server properties:*** Can be done with 2 different methods. +1) Saltstack server details are found in the property file named saltstack-adapter.properties. Param has to be given with following types. + "org.onap.appc.adapter.saltstack.clientType"; -> Supported types are (BASIC || SSH_CERT || BOTH). + "org.onap.appc.adapter.saltstack.host"; -> Saltstack server's host name IP address. + "org.onap.appc.adapter.saltstack.port"; -> Saltstack server's port to make SSH connection to. + "org.onap.appc.adapter.saltstack.userName"; -> Saltstack server's SSH UserName. + "org.onap.appc.adapter.saltstack.userPasswd"; -> Saltstack server's SSH Password. + "org.onap.appc.adapter.saltstack.sshKey"; -> Saltstack server's SSH KEY file location. +2) All the server related details can also be passed as param to the adaptor from the Directed Graphs. Param has to be given with following types. + "HostName"; -> Saltstack server's host name IP address. + "Port"; -> Saltstack server's port to make SSH connection to. + "Password"; -> Saltstack server's SSH UserName. + "User"; -> Saltstack server's SSH Password. + Note: SSH_CERT based Auth is not supported in this method. + +***Using Saltstack Adaptor Commands and params to pass in:*** reqExecCommand: +Method to execute a single command on SaltState server. The command entered should request the output in JSON format, this can be done by appending json-out outputter as specified in https://docs.saltstack.com/en/latest/ref/output/all/salt.output.json_out.html#module-salt.output.json_out and https://docs.saltstack.com/en/2017.7/ref/cli/salt-call.html The response from Saltstack comes in json format and it is automatically put to context for DGs access, with a certain request-ID as prefix. +Example command will look like: +1) Command to test if all VNFC are running: "salt * test.ping --out=json --static" +2) To check Network interfaces on your minions: "salt '*' network.interfaces --out=json --static" +3) Restart Minion service after upgrade process: "salt minion1 service.restart salt-minion --out=json --static" +Note: If using --out=json, you will probably want --static as well. Without the static option, you will get a separate JSON string per minion which makes JSON output invalid as a whole. This is due to using an iterative outputter. So if you want to feed it to a JSON parser, use --static as well. + +This "reqExecCommand" method gives the Operator/Directed Graphs to execute commands in a fine-tuned manner, which also means the operator/DG-creator should know what to expect as output as a result of command execution. By this way using DGs, the operator can check for success/failure of the executed comment. +If the output is not in JSON format, then the adaptor still tries to convert it into properties, in addition "reqID.completeResult" param will have the whole result for DG access. diff --git a/saltstack-adapter/saltstack-adapter-features/ccsdk-saltstack-adapter/pom.xml b/saltstack-adapter/saltstack-adapter-features/ccsdk-saltstack-adapter/pom.xml index fa442ac3..92f404ec 100644 --- a/saltstack-adapter/saltstack-adapter-features/ccsdk-saltstack-adapter/pom.xml +++ b/saltstack-adapter/saltstack-adapter-features/ccsdk-saltstack-adapter/pom.xml @@ -15,39 +15,36 @@ feature ccsdk-sli-adaptors :: saltstack-adapter:: ${project.artifactId} - - - - org.opendaylight.controller - mdsal-artifacts - ${odl.mdsal.version} - pom - import - - - org.opendaylight.mdsal.model - mdsal-model-artifacts - ${odl.mdsal.model.version} - pom - import - - - - - + + + + org.opendaylight.controller + mdsal-artifacts + ${odl.mdsal.version} + pom + import + + + org.opendaylight.mdsal.model + mdsal-model-artifacts + ${odl.mdsal.model.version} + pom + import + + + + diff --git a/saltstack-adapter/saltstack-adapter-provider/pom.xml b/saltstack-adapter/saltstack-adapter-provider/pom.xml index f4e04500..41bf7c67 100644 --- a/saltstack-adapter/saltstack-adapter-provider/pom.xml +++ b/saltstack-adapter/saltstack-adapter-provider/pom.xml @@ -44,6 +44,20 @@ 1.2 + + + org.apache.sshd + sshd-core + 0.12.0 + provided + + + + org.onap.appc + appc-common + 1.4.0-SNAPSHOT + + org.glassfish.jersey.core @@ -59,6 +73,18 @@ test + + org.apache.commons + commons-io + 1.3.2 + + + + org.codehaus.jettison + jettison + provided + + junit junit @@ -108,5 +134,22 @@ - + + + + org.apache.felix + maven-bundle-plugin + true + + + org.onap.appc.adapter.ssh.SshAdapter + org.onap.appc.adapter.ssh.impl.* + !org.apache.log,!org.apache.commons.logging,!groovy.lang,!javax.jms,!org.codehaus.commons.compiler,!org.codehaus.groovy.*,!org.codehaus.janino,!com.ibm.icu.*,!com.sun.faces.*,!org.jasypt.*,* + !dblib-provider,jasypt,eelf-core,logback-core,logback-classic;scope=compile|runtime;inline=false + true + + + + + 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 diff --git a/saltstack-adapter/saltstack-adapter-provider/src/test/java/org/onap/appc/adapter/impl/TestSaltstackAdapterImpl.java b/saltstack-adapter/saltstack-adapter-provider/src/test/java/org/onap/appc/adapter/impl/TestSaltstackAdapterImpl.java index 5ca6e6ea..d7b33038 100644 --- a/saltstack-adapter/saltstack-adapter-provider/src/test/java/org/onap/appc/adapter/impl/TestSaltstackAdapterImpl.java +++ b/saltstack-adapter/saltstack-adapter-provider/src/test/java/org/onap/appc/adapter/impl/TestSaltstackAdapterImpl.java @@ -71,26 +71,173 @@ public class TestSaltstackAdapterImpl { svcContext = null; } - @Test - public void reqExecCommand_shouldSetPending() throws IllegalStateException, IllegalArgumentException { + @Test(expected = SvcLogicException.class) + public void reqExecCommand_shouldSetFailed() throws SvcLogicException, + IllegalStateException, IllegalArgumentException { + + params.put("HostName", "test"); + params.put("Port", "10"); + params.put("User", "test"); + params.put("Password", "test"); + params.put("Test", "fail"); + adapter.reqExecCommand(params, svcContext); + String status = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.code"); + TestId = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.Id"); + assertEquals("101", status); + } + + @Test(expected = SvcLogicException.class) + public void reqExecCommand_shouldSetUserFailed() throws SvcLogicException, + IllegalStateException, IllegalArgumentException { + + params.put("HostName", "test"); + params.put("Port", "10"); + params.put("Password", "test"); + params.put("Test", "fail"); + adapter.reqExecCommand(params, svcContext); + String status = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.code"); + TestId = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.Id"); + assertEquals("101", status); + } + + @Test(expected = SvcLogicException.class) + public void reqExecCommand_shouldSetHostFailed() throws SvcLogicException, + IllegalStateException, IllegalArgumentException { + + params.put("Port", "10"); + params.put("User", "test"); + params.put("Password", "test"); + params.put("Test", "fail"); + adapter.reqExecCommand(params, svcContext); + String status = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.code"); + TestId = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.Id"); + assertEquals("101", status); + } + + @Test(expected = SvcLogicException.class) + public void reqExecCommand_shouldSetPortFailed() throws SvcLogicException, + IllegalStateException, IllegalArgumentException { + + params.put("HostName", "test"); + params.put("User", "test"); + params.put("Password", "test"); + params.put("Test", "fail"); + adapter.reqExecCommand(params, svcContext); + String status = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.code"); + TestId = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.Id"); + assertEquals("101", status); + } + + @Test(expected = SvcLogicException.class) + public void reqExecCommand_shouldSetPasswordFailed() throws SvcLogicException, + IllegalStateException, IllegalArgumentException { + + params.put("HostName", "test"); + params.put("Port", "10"); + params.put("User", "test"); + params.put("Test", "fail"); + adapter.reqExecCommand(params, svcContext); + String status = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.code"); + TestId = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.Id"); + assertEquals("101", status); + } + + @Test(expected = SvcLogicException.class) + public void reqExecCommand_shouldSetMandatoryFailed() throws SvcLogicException, + IllegalStateException, IllegalArgumentException { + + params.put("Test", "fail"); + adapter.reqExecCommand(params, svcContext); + String status = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.code"); + TestId = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.Id"); + assertEquals("101", status); + } + + @Test(expected = SvcLogicException.class) + public void reqExecCommand_shouldSetSuccess() throws SvcLogicException, + IllegalStateException, IllegalArgumentException { params.put("PlaybookName", "test_playbook.yaml"); + params.put("HostName", "test"); + params.put("Port", "10"); + params.put("User", "test"); + params.put("Password", "test"); + params.put("Test", "success"); + try { + adapter.reqExecCommand(params, svcContext); + String status = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.code"); + TestId = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.Id"); + assertEquals("400", status); + } catch (NullPointerException e) { + fail(e.getMessage() + " Unknown exception encountered "); + } + } + + @Test(expected = SvcLogicException.class) + public void reqExecCommand_shouldSetSuccessWithRetry() throws SvcLogicException, + IllegalStateException, IllegalArgumentException { + params.put("PlaybookName", "test_playbook.yaml"); + params.put("HostName", "test"); + params.put("Port", "10"); + params.put("User", "test"); + params.put("Password", "test"); + params.put("Test", "success"); + params.put("retryDelay", "10"); + params.put("retryCount", "10"); try { adapter.reqExecCommand(params, svcContext); String status = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.code"); TestId = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.Id"); - // System.out.println("Comparing " + PENDING + " and " + status); - //assertEquals(PENDING, status); - assertEquals(null, status); - } catch (SvcLogicException e) { + assertEquals("400", status); + } catch (NullPointerException e) { + fail(e.getMessage() + " Unknown exception encountered "); + } + } + + @Test(expected = SvcLogicException.class) + public void reqExecCommand_shouldSetSuccessWithRetryZero() throws SvcLogicException, + IllegalStateException, IllegalArgumentException { + + params.put("PlaybookName", "test_playbook.yaml"); + params.put("HostName", "test"); + params.put("Port", "10"); + params.put("User", "test"); + params.put("Password", "test"); + params.put("Test", "success"); + params.put("retryDelay", "0"); + params.put("retryCount", "0"); + try { + adapter.reqExecCommand(params, svcContext); String status = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.code"); - fail(e.getMessage() + " Code = " + status); - } catch (Exception e) { + TestId = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.Id"); + assertEquals("400", status); + } catch (NullPointerException e) { fail(e.getMessage() + " Unknown exception encountered "); } } + @Test(expected = SvcLogicException.class) + public void reqExecCommand_shouldSetSuccessWithNoRetry() throws SvcLogicException, + IllegalStateException, IllegalArgumentException { + + params.put("PlaybookName", "test_playbook.yaml"); + params.put("HostName", "test"); + params.put("Port", "10"); + params.put("User", "test"); + params.put("Password", "test"); + params.put("Test", "success"); + params.put("retryDelay", "-1"); + params.put("retryCount", "-1"); + try { + adapter.reqExecCommand(params, svcContext); + String status = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.code"); + TestId = svcContext.getAttribute("org.onap.appc.adapter.saltstack.result.Id"); + assertEquals("400", status); + } catch (NullPointerException e) { + fail(e.getMessage() + " Unknown exception encountered "); + } + } @Test public void reqExecSLS_shouldSetSuccess() throws IllegalStateException, IllegalArgumentException { -- 2.16.6