2  * ============LICENSE_START=======================================================
 
   4  * ================================================================================
 
   5  * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved.
 
   6  * ================================================================================
 
   7  * Copyright (C) 2017 Amdocs
 
   8  * =============================================================================
 
   9  * Licensed under the Apache License, Version 2.0 (the "License");
 
  10  * you may not use this file except in compliance with the License.
 
  11  * You may obtain a copy of the License at
 
  13  *      http://www.apache.org/licenses/LICENSE-2.0
 
  15  * Unless required by applicable law or agreed to in writing, software
 
  16  * distributed under the License is distributed on an "AS IS" BASIS,
 
  17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
  18  * See the License for the specific language governing permissions and
 
  19  * limitations under the License.
 
  21  * ECOMP is a trademark and service mark of AT&T Intellectual Property.
 
  22  * ============LICENSE_END=========================================================
 
  25 package org.onap.ccsdk.sli.adaptors.saltstack.model;
 
  28  * This module implements the APP-C/Saltstack Server interface
 
  29  * based on the REST API specifications
 
  32 import com.google.common.base.Strings;
 
  33 import org.json.JSONArray;
 
  34 import org.codehaus.jettison.json.JSONException;
 
  35 import org.json.JSONObject;
 
  36 import org.onap.ccsdk.sli.core.sli.SvcLogicContext;
 
  37 import org.onap.ccsdk.sli.core.sli.SvcLogicException;
 
  38 import org.slf4j.Logger;
 
  39 import org.slf4j.LoggerFactory;
 
  42 import java.io.FileInputStream;
 
  43 import java.io.FileNotFoundException;
 
  44 import java.io.IOException;
 
  45 import java.io.InputStream;
 
  46 import java.util.Collections;
 
  47 import java.util.HashSet;
 
  48 import java.util.Iterator;
 
  50 import java.util.Properties;
 
  52 import java.util.UUID;
 
  55  * Class that validates and constructs requests sent/received from
 
  58 //TODO: This class is to be altered completely based on the SALTSTACK server communicaiton.
 
  59 public class SaltstackMessageParser {
 
  61     private static final String STATUS_MESSAGE_KEY = "StatusMessage";
 
  62     private static final String STATUS_CODE_KEY = "StatusCode";
 
  64     private static final String SALTSTATE_NAME_KEY = "SaltStateName";
 
  65     private static final String SS_AGENT_HOSTNAME_KEY = "HostName";
 
  66     private static final String SS_AGENT_PORT_KEY = "Port";
 
  67     private static final String PASS_KEY = "Password";
 
  68     private static final String USER_KEY = "User";
 
  69     private static final String CMD_EXEC = "cmd";
 
  70     private static final String IS_SLS_EXEC = "slsExec";
 
  71     private static final String SS_REQ_ID = "Id";
 
  72     private static final String SLS_FILE_LOCATION = "slsFile";
 
  73     private static final String SLS_NAME = "slsName";
 
  74     private static final String MINION_TO_APPLY = "applyTo";
 
  76     private static final String LOCAL_PARAMETERS_OPT_KEY = "LocalParameters";
 
  77     private static final String FILE_PARAMETERS_OPT_KEY = "FileParameters";
 
  78     private static final String ENV_PARAMETERS_OPT_KEY = "EnvParameters";
 
  79     private static final String NODE_LIST_OPT_KEY = "NodeList";
 
  80     private static final String TIMEOUT_OPT_KEY = "Timeout";
 
  81     private static final String VERSION_OPT_KEY = "Version";
 
  82     private static final String ACTION_OPT_KEY = "Action";
 
  84     private static final Logger LOGGER = LoggerFactory.getLogger(SaltstackMessageParser.class);
 
  87      * Accepts a map of strings and
 
  88      * a) validates if all parameters are appropriate (else, throws an exception) and
 
  89      * b) if correct returns a JSON object with appropriate key-value pairs to send to the server.
 
  91      * Mandatory parameters, that must be in the supplied information to the Saltstack Adapter
 
  92      * 1. URL to connect to
 
  93      * 2. credentials for URL (assume username password for now)
 
  96     public JSONObject reqMessage(Map<String, String> params) throws SvcLogicException {
 
  97         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SALTSTATE_NAME_KEY, USER_KEY, PASS_KEY};
 
  98         final String[] optionalTestParams = {ENV_PARAMETERS_OPT_KEY, NODE_LIST_OPT_KEY, LOCAL_PARAMETERS_OPT_KEY,
 
  99                 TIMEOUT_OPT_KEY, VERSION_OPT_KEY, FILE_PARAMETERS_OPT_KEY, ACTION_OPT_KEY};
 
 101         JSONObject jsonPayload = new JSONObject();
 
 103         for (String key : mandatoryTestParams) {
 
 104             throwIfMissingMandatoryParam(params, key);
 
 105             jsonPayload.put(key, params.get(key));
 
 108         parseOptionalParams(params, optionalTestParams, jsonPayload);
 
 110         // Generate a unique uuid for the test
 
 111         String reqId = UUID.randomUUID().toString();
 
 112         jsonPayload.put(SS_AGENT_HOSTNAME_KEY, reqId);
 
 118      * Method that validates that the Map has enough information
 
 119      * to query Saltstack server for a result. If so, it returns
 
 120      * the appropriate PORT number.
 
 122     public String reqPortResult(Map<String, String> params) throws SvcLogicException {
 
 124         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY,
 
 127         for (String key : mandatoryTestParams) {
 
 128             throwIfMissingMandatoryParam(params, key);
 
 130         return params.get(SS_AGENT_PORT_KEY);
 
 134      * Method that validates that the Map has enough information
 
 135      * to query Saltstack server for a result. If so, it returns
 
 136      * the appropriate HOST name.
 
 138     public String reqHostNameResult(Map<String, String> params) throws SvcLogicException {
 
 140         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY,
 
 143         for (String key : mandatoryTestParams) {
 
 144             throwIfMissingMandatoryParam(params, key);
 
 146         return params.get(SS_AGENT_HOSTNAME_KEY);
 
 150      * Method that validates that the Map has enough information
 
 151      * to query Saltstack server for a result. If so, it returns
 
 152      * the appropriate request ID.
 
 154     public String reqId(Map<String, String> params) {
 
 156         if (params.get(SaltstackMessageParser.SS_REQ_ID) == null) {
 
 157             return UUID.randomUUID().toString();
 
 158         } else if (params.get(SaltstackMessageParser.SS_REQ_ID).equalsIgnoreCase("")) {
 
 159             return UUID.randomUUID().toString();
 
 161         return params.get(SaltstackMessageParser.SS_REQ_ID);
 
 165      * Method that validates that the Map has enough information
 
 166      * to query Saltstack server for a result. If so, it returns
 
 167      * the appropriate command to execute.
 
 169     public String reqCmd(Map<String, String> params) throws SvcLogicException {
 
 171         final String[] mandatoryTestParams = {CMD_EXEC, IS_SLS_EXEC};
 
 173         for (String key : mandatoryTestParams) {
 
 174             throwIfMissingMandatoryParam(params, key);
 
 177         return params.get(SaltstackMessageParser.CMD_EXEC);
 
 181      * Method that validates that the Map has enough information
 
 182      * to query Saltstack server for a result. If so, it returns
 
 183      * the appropriate SLS file location to execute.
 
 185     public String reqSlsFile(Map<String, String> params) throws SvcLogicException {
 
 187         final String[] mandatoryTestParams = {SLS_FILE_LOCATION};
 
 189         for (String key : mandatoryTestParams) {
 
 190             throwIfMissingMandatoryParam(params, key);
 
 193         return params.get(SaltstackMessageParser.SLS_FILE_LOCATION);
 
 197      * Method that validates that the Map has enough information
 
 198      * to query Saltstack server for a result. If so, it returns
 
 199      * the appropriate SLS file location to execute.
 
 201     public String reqSlsName(Map<String, String> params) throws SvcLogicException {
 
 203         final String[] mandatoryTestParams = {SLS_NAME};
 
 205         for (String key : mandatoryTestParams) {
 
 206             throwIfMissingMandatoryParam(params, key);
 
 208         String slsName = params.get(SaltstackMessageParser.SLS_NAME);
 
 210             if(slsName.substring(slsName.lastIndexOf("."), slsName.length()).equalsIgnoreCase(".sls"))
 
 211                 return stripExtension(slsName);
 
 212         } catch (StringIndexOutOfBoundsException e) {
 
 218     private String stripExtension (String str) {
 
 219         if (str == null) return null;
 
 220         int pos = str.lastIndexOf(".");
 
 221         if (pos == -1) return str;
 
 222         return str.substring(0, pos);
 
 226      * Method that validates that the Map has enough information
 
 227      * to query Saltstack server for a result. If so, it returns
 
 228      * the appropriate minions/vnfc to execute the SLS file to.
 
 230     public String reqApplyToDevices(Map<String, String> params) {
 
 232         if (params.get(SaltstackMessageParser.MINION_TO_APPLY) == null) {
 
 234         } else if (params.get(SaltstackMessageParser.MINION_TO_APPLY).equalsIgnoreCase("")) {
 
 237         return params.get(SaltstackMessageParser.MINION_TO_APPLY);
 
 241      * Method that validates that the Map has enough information
 
 242      * to query Saltstack server for a result. If so, it returns
 
 243      * the appropriate IsSLSExec true or false.
 
 245     public boolean reqIsSLSExec(Map<String, String> params) throws SvcLogicException {
 
 247         final String[] mandatoryTestParams = {CMD_EXEC, IS_SLS_EXEC};
 
 249         for (String key : mandatoryTestParams) {
 
 250             throwIfMissingMandatoryParam(params, key);
 
 253         return params.get(SaltstackMessageParser.IS_SLS_EXEC).equalsIgnoreCase("true");
 
 257      * Method that validates that the Map has enough information
 
 258      * to query Saltstack server for a result. If so, it returns
 
 259      * the appropriate Saltstack server login user name.
 
 261     public String reqUserNameResult(Map<String, String> params) throws SvcLogicException {
 
 263         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY,
 
 266         for (String key : mandatoryTestParams) {
 
 267             throwIfMissingMandatoryParam(params, key);
 
 269         return params.get(USER_KEY);
 
 273      * Method that validates that the Map has enough information
 
 274      * to query Saltstack server for a result. If so, it returns
 
 275      * the appropriate Saltstack server login password.
 
 277     public String reqPasswordResult(Map<String, String> params) throws SvcLogicException {
 
 279         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY,
 
 282         for (String key : mandatoryTestParams) {
 
 283             throwIfMissingMandatoryParam(params, key);
 
 285         return params.get(PASS_KEY);
 
 289      * This method parses response from the Saltstack Server when we do a post
 
 290      * and returns an SaltstackResult object.
 
 292     public SaltstackResult parseResponse(SvcLogicContext ctx, String pfx,
 
 293                                          SaltstackResult saltstackResult, boolean slsExec) throws IOException{
 
 294         int code = saltstackResult.getStatusCode();
 
 295         InputStream in = null;
 
 296         boolean executionStatus = true, retCodeFound = false;
 
 297         if (code != SaltstackResultCodes.SUCCESS.getValue()) {
 
 298             return saltstackResult;
 
 301             File file = new File(saltstackResult.getOutputFileName());
 
 302             in = new FileInputStream(file);
 
 303             byte[] data = new byte[(int) file.length()];
 
 305             String str = new String(data, "UTF-8");
 
 307             Map<String, String> mm = JsonParser.convertToProperties(str);
 
 309                 for (Map.Entry<String, String> entry : mm.entrySet()) {
 
 310                     if (entry.getKey().contains("retcode")) {
 
 312                         if (!entry.getValue().equalsIgnoreCase("0")) {
 
 313                             executionStatus = false;
 
 316                     ctx.setAttribute(pfx + "." + entry.getKey(), entry.getValue());
 
 317                     LOGGER.info("+++ " + pfx + "." + entry.getKey() + ": [" + entry.getValue() + "]");
 
 320         } catch (FileNotFoundException e) {
 
 321             return new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "error parsing response file "
 
 322                     + saltstackResult.getOutputFileName() + " : " + e.getMessage());
 
 323         } catch (JSONException e) {
 
 324             LOGGER.info("Output not in JSON format");
 
 325             return putToProperties(ctx, pfx, saltstackResult);
 
 326         } catch (Exception e) {
 
 327             return new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "error parsing response file "
 
 328                     + saltstackResult.getOutputFileName() + " : " + e.getMessage());
 
 335                 return new SaltstackResult(SaltstackResultCodes.COMMAND_EXEC_FAILED_STATUS.getValue(),
 
 336                                            "error in parsing response Json after SLS file execution in server");
 
 337             if (!executionStatus)
 
 338                 return new SaltstackResult(SaltstackResultCodes.COMMAND_EXEC_FAILED_STATUS.getValue(),
 
 339                                            "error in parsing response Json after SLS file execution in server");
 
 341         saltstackResult.setStatusCode(SaltstackResultCodes.FINAL_SUCCESS.getValue());
 
 342         return saltstackResult;
 
 345     public SaltstackResult putToProperties(SvcLogicContext ctx, String pfx,
 
 346                                            SaltstackResult saltstackResult) throws IOException{
 
 347         InputStream in = null;
 
 349             File file = new File(saltstackResult.getOutputFileName());
 
 350             in = new FileInputStream(file);
 
 351             Properties prop = new Properties();
 
 353             ctx.setAttribute(pfx + "completeResult", prop.toString());
 
 354             for (Object key : prop.keySet()) {
 
 355                 String name = (String) key;
 
 356                 String value = prop.getProperty(name);
 
 357                 if (value != null && value.trim().length() > 0) {
 
 358                     ctx.setAttribute(pfx + name, value.trim());
 
 359                     LOGGER.info("+++ " + pfx + name + ": [" + value + "]");
 
 362         } catch (Exception e) {
 
 363             saltstackResult = new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "Error parsing response file = "
 
 364                     + saltstackResult.getOutputFileName() + ". Error = " + e.getMessage());
 
 369         saltstackResult.setStatusCode(SaltstackResultCodes.FINAL_SUCCESS.getValue());
 
 370         return saltstackResult;
 
 374      * This method parses response from an Saltstack server when we do a GET for a result
 
 375      * and returns an SaltstackResult object.
 
 377     public SaltstackResult parseGetResponse(String input) throws SvcLogicException {
 
 379         SaltstackResult saltstackResult = new SaltstackResult();
 
 382             JSONObject postResponse = new JSONObject(input);
 
 383             saltstackResult = parseGetResponseNested(saltstackResult, postResponse);
 
 384         } catch (Exception e) {
 
 385             saltstackResult = new SaltstackResult(SaltstackResultCodes.INVALID_COMMAND.getValue(),
 
 386                                                   "Error parsing response = " + input + ". Error = " + e.getMessage(), "", -1);
 
 388         return saltstackResult;
 
 391     private SaltstackResult parseGetResponseNested(SaltstackResult saltstackResult, JSONObject postRsp) throws SvcLogicException {
 
 393         int codeStatus = postRsp.getInt(STATUS_CODE_KEY);
 
 394         String messageStatus = postRsp.getString(STATUS_MESSAGE_KEY);
 
 395         int finalCode = SaltstackResultCodes.FINAL_SUCCESS.getValue();
 
 398                 SaltstackResultCodes.CODE.checkValidCode(SaltstackResultCodes.FINALRESPONSE.getValue(), codeStatus);
 
 401             throw new SvcLogicException("Invalid FinalResponse code  = " + codeStatus + " received. MUST be one of "
 
 402                                                 + SaltstackResultCodes.CODE.getValidCodes(SaltstackResultCodes.FINALRESPONSE.getValue()));
 
 405         saltstackResult.setStatusCode(codeStatus);
 
 406         saltstackResult.setStatusMessage(messageStatus);
 
 407         LOGGER.info("Received response with code = {}, Message = {}", codeStatus, messageStatus);
 
 409         if (!postRsp.isNull("Results")) {
 
 411             // Results are available. process them
 
 412             // Results is a dictionary of the form
 
 413             // {host :{status:s, group:g, message:m, hostname:h}, ...}
 
 414             LOGGER.info("Processing results in response");
 
 415             JSONObject results = postRsp.getJSONObject("Results");
 
 416             LOGGER.info("Get JSON dictionary from Results ..");
 
 417             Iterator<String> hosts = results.keys();
 
 418             LOGGER.info("Iterating through hosts");
 
 420             while (hosts.hasNext()) {
 
 421                 String host = hosts.next();
 
 422                 LOGGER.info("Processing host = {}", host);
 
 425                     JSONObject hostResponse = results.getJSONObject(host);
 
 426                     int subCode = hostResponse.getInt(STATUS_CODE_KEY);
 
 427                     String message = hostResponse.getString(STATUS_MESSAGE_KEY);
 
 429                     LOGGER.info("Code = {}, Message = {}", subCode, message);
 
 431                     if (subCode != 200 || !message.equals("SUCCESS")) {
 
 432                         finalCode = SaltstackResultCodes.REQ_FAILURE.getValue();
 
 434                 } catch (Exception e) {
 
 435                     saltstackResult.setStatusCode(SaltstackResultCodes.INVALID_RESPONSE.getValue());
 
 436                     saltstackResult.setStatusMessage(String.format(
 
 437                             "Error processing response message = %s from host %s", results.getString(host), host));
 
 442             saltstackResult.setStatusCode(finalCode);
 
 444             // We return entire Results object as message
 
 445             saltstackResult.setResults(results.toString());
 
 448             saltstackResult.setStatusCode(SaltstackResultCodes.INVALID_RESPONSE.getValue());
 
 449             saltstackResult.setStatusMessage("Results not found in GET for response");
 
 451         return saltstackResult;
 
 454     private void parseOptionalParams(Map<String, String> params, String[] optionalTestParams, JSONObject jsonPayload) {
 
 456         Set<String> optionalParamsSet = new HashSet<>();
 
 457         Collections.addAll(optionalParamsSet, optionalTestParams);
 
 462                 .filter(entry -> optionalParamsSet.contains(entry.getKey()))
 
 463                 .filter(entry -> !Strings.isNullOrEmpty(entry.getValue()))
 
 464                 .forEach(entry -> parseOptionalParam(entry, jsonPayload));
 
 468     private void parseOptionalParam(Map.Entry<String, String> params, JSONObject jsonPayload) {
 
 469         String key = params.getKey();
 
 470         String payload = params.getValue();
 
 473             case TIMEOUT_OPT_KEY:
 
 474                 int timeout = Integer.parseInt(payload);
 
 476                     throw new NumberFormatException(" : specified negative integer for timeout = " + payload);
 
 478                 jsonPayload.put(key, payload);
 
 481             case VERSION_OPT_KEY:
 
 482                 jsonPayload.put(key, payload);
 
 485             case LOCAL_PARAMETERS_OPT_KEY:
 
 486             case ENV_PARAMETERS_OPT_KEY:
 
 487                 JSONObject paramsJson = new JSONObject(payload);
 
 488                 jsonPayload.put(key, paramsJson);
 
 491             case NODE_LIST_OPT_KEY:
 
 492                 JSONArray paramsArray = new JSONArray(payload);
 
 493                 jsonPayload.put(key, paramsArray);
 
 496             case FILE_PARAMETERS_OPT_KEY:
 
 497                 jsonPayload.put(key, getFilePayload(payload));
 
 506      * Return payload with escaped newlines
 
 508     private JSONObject getFilePayload(String payload) {
 
 509         String formattedPayload = payload.replace("\n", "\\n").replace("\r", "\\r");
 
 510         return new JSONObject(formattedPayload);
 
 513     private void throwIfMissingMandatoryParam(Map<String, String> params, String key) throws SvcLogicException {
 
 514         if (!params.containsKey(key)) {
 
 515             throw new SvcLogicException(String.format(
 
 516                     "Saltstack: Mandatory SaltstackAdapter key %s not found in parameters provided by calling agent !",
 
 519         if (Strings.isNullOrEmpty(params.get(key))) {
 
 520             throw new SvcLogicException(String.format(
 
 521                     "Saltstack: Mandatory SaltstackAdapter key %s not found in parameters provided by calling agent !",