2  * ============LICENSE_START=======================================================
 
   4  * ================================================================================
 
   5  * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved.
 
   6  * ================================================================================
 
   7  * Copyright (C) 2017 Amdocs
 
   8  * =============================================================================
 
   9  * Modifications Copyright (C) 2019 IBM
 
  10  * =============================================================================
 
  11  * Licensed under the Apache License, Version 2.0 (the "License");
 
  12  * you may not use this file except in compliance with the License.
 
  13  * You may obtain a copy of the License at
 
  15  *      http://www.apache.org/licenses/LICENSE-2.0
 
  17  * Unless required by applicable law or agreed to in writing, software
 
  18  * distributed under the License is distributed on an "AS IS" BASIS,
 
  19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
  20  * See the License for the specific language governing permissions and
 
  21  * limitations under the License.
 
  23  * ============LICENSE_END=========================================================
 
  26 package org.onap.appc.adapter.ansible.model;
 
  29  * This module implements the APP-C/Ansible Server interface
 
  30  * based on the REST API specifications
 
  32 import java.util.Collections;
 
  33 import java.util.HashSet;
 
  34 import java.util.Iterator;
 
  37 import java.util.UUID;
 
  38 import org.json.JSONArray;
 
  39 import org.json.JSONException;
 
  40 import org.json.JSONObject;
 
  41 import org.onap.appc.exceptions.APPCException;
 
  42 import com.google.common.base.Strings;
 
  43 import org.slf4j.Logger;
 
  44 import org.slf4j.LoggerFactory;
 
  45 import org.apache.commons.lang.StringUtils;
 
  48  * Class that validates and constructs requests sent/received from Ansible
 
  51 public class AnsibleMessageParser {
 
  53     private static final String STATUS_MESSAGE_KEY = "StatusMessage";
 
  54     private static final String STATUS_CODE_KEY = "StatusCode";
 
  55     private static final String SERVER_IP_KEY = "AnsibleServer";
 
  56     private static final String PLAYBOOK_NAME_KEY = "PlaybookName";
 
  57     private static final String AGENT_URL_KEY = "AgentUrl";
 
  58     private static final String PASS_KEY = "Password";
 
  59     private static final String USER_KEY = "User";
 
  60     private static final String ID_KEY = "Id";
 
  61     private static final String LOCAL_PARAMETERS_OPT_KEY = "LocalParameters";
 
  62     private static final String FILE_PARAMETERS_OPT_KEY = "FileParameters";
 
  63     private static final String ENV_PARAMETERS_OPT_KEY = "EnvParameters";
 
  64     private static final String NODE_LIST_OPT_KEY = "NodeList";
 
  65     private static final String TIMEOUT_OPT_KEY = "Timeout";
 
  66     private static final String VERSION_OPT_KEY = "Version";
 
  67     private static final String INVENTORY_NAMES_OPT_KEY = "InventoryNames";
 
  68     private static final String ACTION_OPT_KEY = "Action";
 
  69     private static final String OUTPUT_OPT_KEY = "Output";
 
  70     private static final String JSON_ERROR_MESSAGE = "JSONException: Error parsing response";
 
  72     private static final Logger LOGGER = LoggerFactory.getLogger(AnsibleMessageParser.class);
 
  75      * Accepts a map of strings and a) validates if all parameters are appropriate
 
  76      * (else, throws an exception) and b) if correct returns a JSON object with
 
  77      * appropriate key-value pairs to send to the server.
 
  79      * Mandatory parameters, that must be in the supplied information to the Ansible
 
  80      * Adapter 1. URL to connect to 2. credentials for URL (assume username password
 
  81      * for now) 3. Playbook name
 
  84     public JSONObject reqMessage(Map<String, String> params) throws APPCException {
 
  85         final String[] mandatoryTestParams = { AGENT_URL_KEY, PLAYBOOK_NAME_KEY, USER_KEY, PASS_KEY };
 
  86         final String[] optionalTestParams = { ENV_PARAMETERS_OPT_KEY, NODE_LIST_OPT_KEY, LOCAL_PARAMETERS_OPT_KEY,
 
  87                 TIMEOUT_OPT_KEY, VERSION_OPT_KEY, FILE_PARAMETERS_OPT_KEY, ACTION_OPT_KEY, INVENTORY_NAMES_OPT_KEY };
 
  89         JSONObject jsonPayload = new JSONObject();
 
  91         for (String key : mandatoryTestParams) {
 
  92             throwIfMissingMandatoryParam(params, key);
 
  93             jsonPayload.put(key, params.get(key));
 
  96         parseOptionalParams(params, optionalTestParams, jsonPayload);
 
  98         // Generate a unique uuid for the test
 
  99         String reqId = UUID.randomUUID().toString();
 
 100         jsonPayload.put(ID_KEY, reqId);
 
 106      * Method that validates that the Map has enough information to query Ansible
 
 107      * server for a result. If so, it returns the appropriate url, else an empty
 
 110     public String reqUriResult(Map<String, String> params) throws APPCException {
 
 112         final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
 
 114         for (String key : mandatoryTestParams) {
 
 115             throwIfMissingMandatoryParam(params, key);
 
 117         return params.get(AGENT_URL_KEY) + "?Id=" + params.get(ID_KEY) + "&Type=GetResult";
 
 121      * Method that validates that the Map has enough information to query Ansible
 
 122      * server for a result. If so, it returns the appropriate url, else an empty
 
 125     public String reqUriResultWithIP(Map<String, String> params, String serverIP) throws APPCException {
 
 127         final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
 
 129         for (String key : mandatoryTestParams) {
 
 130             throwIfMissingMandatoryParam(params, key);
 
 132         String[] arr1 = params.get(AGENT_URL_KEY).split("//", 2);
 
 133         String[] arr2 = arr1[1].split(":", 2);
 
 134         return arr1[0] + "//" + serverIP + ":" + arr2[1] + "?Id=" + params.get(ID_KEY) + "&Type=GetResult";
 
 138      * Method that validates that the Map has enough information to query Ansible
 
 139      * server for logs. If so, it populates the appropriate returns the appropriate
 
 140      * url, else an empty string.
 
 142     public String reqUriLog(Map<String, String> params) throws APPCException {
 
 144         final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
 
 146         for (String mandatoryParam : mandatoryTestParams) {
 
 147             throwIfMissingMandatoryParam(params, mandatoryParam);
 
 149         return params.get(AGENT_URL_KEY) + "?Id=" + params.get(ID_KEY) + "&Type=GetLog";
 
 153      * This method parses response from the Ansible Server when we do a post and
 
 154      * returns an AnsibleResult object.
 
 156     public AnsibleResult parsePostResponse(String input) throws APPCException {
 
 157         AnsibleResult ansibleResult;
 
 159             JSONObject postResponse = new JSONObject(input);
 
 161             int code = postResponse.getInt(STATUS_CODE_KEY);
 
 162             String msg = postResponse.getString(STATUS_MESSAGE_KEY);
 
 163             String serverIP = "";
 
 164             if (postResponse.has(SERVER_IP_KEY))
 
 165                 serverIP = postResponse.getString(SERVER_IP_KEY);
 
 168             int initResponseValue = AnsibleResultCodes.INITRESPONSE.getValue();
 
 169             boolean validCode = AnsibleResultCodes.CODE.checkValidCode(initResponseValue, code);
 
 171                 throw new APPCException("Invalid InitResponse code  = " + code + " received. MUST be one of "
 
 172                         + AnsibleResultCodes.CODE.getValidCodes(initResponseValue));
 
 175             ansibleResult = new AnsibleResult(code, msg);
 
 176             if (StringUtils.isNotBlank(serverIP))
 
 177                 ansibleResult.setServerIp(serverIP);
 
 179             if (!postResponse.isNull(OUTPUT_OPT_KEY)) {
 
 180                 LOGGER.info("Processing results-output in post response");
 
 181                 JSONObject output = postResponse.getJSONObject(OUTPUT_OPT_KEY);
 
 182                 ansibleResult.setOutput(output.toString());
 
 185         } catch (JSONException e) {
 
 186             LOGGER.error(JSON_ERROR_MESSAGE, e);
 
 187             ansibleResult = new AnsibleResult(600, "Error parsing response = " + input + ". Error = " + e.getMessage());
 
 189         return ansibleResult;
 
 193      * This method parses response from an Ansible server when we do a GET for a
 
 194      * result and returns an AnsibleResult object.
 
 196     public AnsibleResult parseGetResponse(String input) throws APPCException {
 
 198         AnsibleResult ansibleResult = new AnsibleResult();
 
 201             JSONObject postResponse = new JSONObject(input);
 
 202             ansibleResult = parseGetResponseNested(ansibleResult, postResponse);
 
 203         } catch (JSONException e) {
 
 204             LOGGER.error(JSON_ERROR_MESSAGE, e);
 
 205             ansibleResult = new AnsibleResult(AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
 
 206                     "Error parsing response = " + input + ". Error = " + e.getMessage(), "");
 
 208         return ansibleResult;
 
 211     private AnsibleResult parseGetResponseNested(AnsibleResult ansibleResult, JSONObject postRsp) throws APPCException {
 
 213         int codeStatus = postRsp.getInt(STATUS_CODE_KEY);
 
 214         String messageStatus = postRsp.getString(STATUS_MESSAGE_KEY);
 
 215         int finalCode = AnsibleResultCodes.FINAL_SUCCESS.getValue();
 
 217         boolean valCode = AnsibleResultCodes.CODE.checkValidCode(AnsibleResultCodes.FINALRESPONSE.getValue(),
 
 221             throw new APPCException("Invalid FinalResponse code  = " + codeStatus + " received. MUST be one of "
 
 222                     + AnsibleResultCodes.CODE.getValidCodes(AnsibleResultCodes.FINALRESPONSE.getValue()));
 
 225         ansibleResult.setStatusCode(codeStatus);
 
 226         ansibleResult.setStatusMessage(messageStatus);
 
 227         LOGGER.info("Received response with code = {}, Message = {}", codeStatus, messageStatus);
 
 229         if (!postRsp.isNull("Results")) {
 
 231             // Results are available. process them
 
 232             // Results is a dictionary of the form
 
 233             // {host :{status:s, group:g, message:m, hostname:h}, ...}
 
 234             LOGGER.info("Processing results in response");
 
 235             JSONObject results = postRsp.getJSONObject("Results");
 
 236             LOGGER.info("Get JSON dictionary from Results ..");
 
 237             Iterator<String> hosts = results.keys();
 
 238             LOGGER.info("Iterating through hosts");
 
 240             while (hosts.hasNext()) {
 
 241                 String host = hosts.next();
 
 242                 LOGGER.info("Processing host = {}",
 
 243                         (host.matches("^[\\w\\-.]+$")) ? host : "[unexpected value, logging suppressed]");
 
 245                     JSONObject hostResponse = results.getJSONObject(host);
 
 246                     int subCode = hostResponse.getInt(STATUS_CODE_KEY);
 
 247                     String message = hostResponse.getString(STATUS_MESSAGE_KEY);
 
 249                     LOGGER.info("Code = {}, Message = {}", subCode, message);
 
 251                     if (subCode != 200 || !(("SUCCESS").equals(message))) {
 
 252                         finalCode = AnsibleResultCodes.REQ_FAILURE.getValue();
 
 254                 } catch (JSONException e) {
 
 255                     LOGGER.error(JSON_ERROR_MESSAGE, e);
 
 256                     ansibleResult.setStatusCode(AnsibleResultCodes.INVALID_RESPONSE.getValue());
 
 257                     ansibleResult.setStatusMessage(String.format("Error processing response message = %s from host %s",
 
 258                             results.getString(host), host));
 
 263             ansibleResult.setStatusCode(finalCode);
 
 265             // We return entire Results object as message
 
 266             ansibleResult.setResults(results.toString());
 
 269             ansibleResult.setStatusCode(AnsibleResultCodes.INVALID_RESPONSE.getValue());
 
 270             ansibleResult.setStatusMessage("Results not found in GET for response");
 
 272         if (!postRsp.isNull(OUTPUT_OPT_KEY)) {
 
 273             LOGGER.info("Processing results-output in response");
 
 274             JSONObject output = postRsp.getJSONObject(OUTPUT_OPT_KEY);
 
 275             ansibleResult.setOutput(output.toString());
 
 278         return ansibleResult;
 
 281     private void parseOptionalParams(Map<String, String> params, String[] optionalTestParams, JSONObject jsonPayload) {
 
 283         Set<String> optionalParamsSet = new HashSet<>();
 
 284         Collections.addAll(optionalParamsSet, optionalTestParams);
 
 287         params.entrySet().stream().filter(entry -> optionalParamsSet.contains(entry.getKey()))
 
 288                 .filter(entry -> !Strings.isNullOrEmpty(entry.getValue()))
 
 289                 .forEach(entry -> parseOptionalParam(entry, jsonPayload));
 
 293     private void parseOptionalParam(Map.Entry<String, String> params, JSONObject jsonPayload) {
 
 294         String key = params.getKey();
 
 295         String payload = params.getValue();
 
 298         case TIMEOUT_OPT_KEY:
 
 299             int timeout = Integer.parseInt(payload);
 
 301                 throw new NumberFormatException(" : specified negative integer for timeout = " + payload);
 
 303             jsonPayload.put(key, payload);
 
 306         case VERSION_OPT_KEY:
 
 307         case INVENTORY_NAMES_OPT_KEY:
 
 308             jsonPayload.put(key, payload);
 
 311         case LOCAL_PARAMETERS_OPT_KEY:
 
 312         case ENV_PARAMETERS_OPT_KEY:
 
 313             JSONObject paramsJson = new JSONObject(payload);
 
 314             jsonPayload.put(key, paramsJson);
 
 317         case NODE_LIST_OPT_KEY:
 
 318             JSONArray paramsArray = new JSONArray(payload);
 
 319             jsonPayload.put(key, paramsArray);
 
 322         case FILE_PARAMETERS_OPT_KEY:
 
 323             jsonPayload.put(key, getFilePayload(payload));
 
 332      * Return payload with escaped newlines
 
 334     private JSONObject getFilePayload(String payload) {
 
 335         String formattedPayload = payload.replace("\n", "\\n").replace("\r", "\\r");
 
 336         return new JSONObject(formattedPayload);
 
 339     private void throwIfMissingMandatoryParam(Map<String, String> params, String key) throws APPCException {
 
 340         if (!params.containsKey(key)) {
 
 341             throw new APPCException(String.format(
 
 342                     "Ansible: Mandatory AnsibleAdapter key %s not found in parameters provided by calling agent !",
 
 345         if (Strings.isNullOrEmpty(params.get(key))) {
 
 346             throw new APPCException(String.format(
 
 347                     "Ansible: Mandatory AnsibleAdapter key %s not found in parameters provided by calling agent !",