reqExec API implemented for saltstack 39/55739/5
authorGanesh Chandrasekaran <ganesh.c@samsung.com>
Wed, 4 Jul 2018 05:10:57 +0000 (14:10 +0900)
committerGanesh Chandrasekaran <ganesh.c@samsung.com>
Sun, 8 Jul 2018 23:14:16 +0000 (23:14 +0000)
Issue-ID: CCSDK-320
Change-Id: I5c4eb36924f36ebc9a7786d2bd46c923d0c05ab0
Signed-off-by: Ganesh Chandrasekaran <ganesh.c@samsung.com>
12 files changed:
saltstack-adapter/README.md
saltstack-adapter/saltstack-adapter-features/ccsdk-saltstack-adapter/pom.xml
saltstack-adapter/saltstack-adapter-provider/pom.xml
saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/ConnectionBuilder.java
saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/SaltstackAdapterImpl.java
saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/impl/SshConnection.java [new file with mode: 0644]
saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/JsonParser.java [new file with mode: 0644]
saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackMessageParser.java
saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackResult.java
saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackResultCodes.java
saltstack-adapter/saltstack-adapter-provider/src/main/java/org/onap/ccsdk/sli/adaptors/saltstack/model/SaltstackServerEmulator.java
saltstack-adapter/saltstack-adapter-provider/src/test/java/org/onap/appc/adapter/impl/TestSaltstackAdapterImpl.java

index 0c3161a..cf21e10 100644 (file)
@@ -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. 
index fa442ac..92f404e 100644 (file)
     <packaging>feature</packaging>
 
     <name>ccsdk-sli-adaptors :: saltstack-adapter:: ${project.artifactId}</name>
-       <dependencyManagement>
-               <dependencies>
-                       <dependency>
-                               <groupId>org.opendaylight.controller</groupId>
-                               <artifactId>mdsal-artifacts</artifactId>
-                               <version>${odl.mdsal.version}</version>
-                               <type>pom</type>
-                               <scope>import</scope>
-                       </dependency>
-                       <dependency>
-                               <groupId>org.opendaylight.mdsal.model</groupId>
-                               <artifactId>mdsal-model-artifacts</artifactId>
-                               <version>${odl.mdsal.model.version}</version>
-                               <type>pom</type>
-                               <scope>import</scope>
-                       </dependency>
-               </dependencies>
-       </dependencyManagement>
-    <dependencies>
-
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.opendaylight.controller</groupId>
+                <artifactId>mdsal-artifacts</artifactId>
+                <version>${odl.mdsal.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.opendaylight.mdsal.model</groupId>
+                <artifactId>mdsal-model-artifacts</artifactId>
+                <version>${odl.mdsal.model.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+ <!--   <dependencies>
         <dependency>
             <groupId>org.onap.ccsdk.sli.core</groupId>
             <artifactId>ccsdk-sli</artifactId>
             <type>xml</type>
             <classifier>features</classifier>
         </dependency>
-
         <dependency>
             <groupId>${project.groupId}</groupId>
             <artifactId>saltstack-adapter-provider</artifactId>
             <version>${project.version}</version>
         </dependency>
-
-
     </dependencies>
+    -->
 </project>
index f4e0450..41bf7c6 100644 (file)
                        <version>1.2</version>
                </dependency>
 
+               <!-- Needed to run SSH -->
+               <dependency>
+                       <groupId>org.apache.sshd</groupId>
+                       <artifactId>sshd-core</artifactId>
+                       <version>0.12.0</version>
+                       <scope>provided</scope>
+               </dependency>
+
+               <dependency>
+                       <groupId>org.onap.appc</groupId>
+                       <artifactId>appc-common</artifactId>
+                       <version>1.4.0-SNAPSHOT</version>
+               </dependency>
+
                <!-- Needed to run test cases -->
                <dependency>
                        <groupId>org.glassfish.jersey.core</groupId>
                        <scope>test</scope>
                </dependency>
 
+               <dependency>
+                       <groupId>org.apache.commons</groupId>
+                       <artifactId>commons-io</artifactId>
+                       <version>1.3.2</version>
+               </dependency>
+
+               <dependency>
+                       <groupId>org.codehaus.jettison</groupId>
+                       <artifactId>jettison</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+
                <dependency>
                        <groupId>junit</groupId>
                        <artifactId>junit</artifactId>
 
        </dependencies>
 
-
+       <build>
+               <plugins>
+                       <plugin>
+                               <groupId>org.apache.felix</groupId>
+                               <artifactId>maven-bundle-plugin</artifactId>
+                               <extensions>true</extensions>
+                               <configuration>
+                                       <instructions>
+                                               <Export-Service>org.onap.appc.adapter.ssh.SshAdapter</Export-Service>
+                                               <Private-Package>org.onap.appc.adapter.ssh.impl.*</Private-Package>
+                                               <Import-Package>!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.*,*</Import-Package>
+                                               <Embed-Dependency>!dblib-provider,jasypt,eelf-core,logback-core,logback-classic;scope=compile|runtime;inline=false</Embed-Dependency>
+                                               <Embed-Transitive>true</Embed-Transitive>
+                                       </instructions>
+                               </configuration>
+                       </plugin>
+               </plugins>
+       </build>
 </project>
index 7702dc8..5dee9f5 100644 (file)
 
 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) {
index 6ff5e57..5fe130f 100644 (file)
@@ -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<String, String> 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<String, String> 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 (file)
index 0000000..41e6102
--- /dev/null
@@ -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 (file)
index 0000000..f33799f
--- /dev/null
@@ -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<String, String> convertToProperties(String s)
+        throws JSONException {
+
+        checkNotNull(s, "Input should not be null.");
+
+            JSONObject json = new JSONObject(s);
+            Map<String, Object> wm = new HashMap<>();
+            Iterator<String> ii = json.keys();
+            while (ii.hasNext()) {
+                String key1 = ii.next();
+                wm.put(key1, json.get(key1));
+            }
+
+            Map<String, String> 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<String> 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;
+    }
+}
index 5a548f8..1ea3151 100644 (file)
@@ -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<String, String> 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<String, String> params) throws SvcLogicException {
+    public String reqPortResult(Map<String, String> 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<String, String> params) throws SvcLogicException {
+    public String reqHostNameResult(Map<String, String> 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<String, String> 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<String, String> 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<String, String> mm = JsonParser.convertToProperties(str);
+            if (mm != null) {
+                for (Map.Entry<String,String> 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;
     }
index f1fb40d..0587302 100644 (file)
@@ -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;
+    }
 }
index e520dda..ab88c21 100644 (file)
@@ -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),
index a9bf7e7..9737efd 100644 (file)
@@ -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<String, String> 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<String, String> 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
index 5ca6e6e..d7b3303 100644 (file)
@@ -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 {