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 * Modifications Copyright (C) 2019 Orange
12 * =============================================================================
13 * Licensed under the Apache License, Version 2.0 (the "License");
14 * you may not use this file except in compliance with the License.
15 * You may obtain a copy of the License at
17 * http://www.apache.org/licenses/LICENSE-2.0
19 * Unless required by applicable law or agreed to in writing, software
20 * distributed under the License is distributed on an "AS IS" BASIS,
21 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 * See the License for the specific language governing permissions and
23 * limitations under the License.
25 * ============LICENSE_END=========================================================
28 package org.onap.appc.adapter.ansible.model;
31 * This module implements the APP-C/Ansible Server interface
32 * based on the REST API specifications
34 import java.util.Collections;
35 import java.util.HashSet;
36 import java.util.Iterator;
39 import java.util.UUID;
40 import org.json.JSONArray;
41 import org.json.JSONException;
42 import org.json.JSONObject;
43 import org.onap.appc.exceptions.APPCException;
44 import com.google.common.base.Strings;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47 import org.apache.commons.lang.StringUtils;
50 * Class that validates and constructs requests sent/received from Ansible
53 public class AnsibleMessageParser {
55 private static final String STATUS_MESSAGE_KEY = "StatusMessage";
56 private static final String STATUS_CODE_KEY = "StatusCode";
57 private static final String SERVER_IP_KEY = "AnsibleServer";
58 private static final String PLAYBOOK_NAME_KEY = "PlaybookName";
59 private static final String AGENT_URL_KEY = "AgentUrl";
60 private static final String PASS_KEY = "Password";
61 private static final String USER_KEY = "User";
62 private static final String ID_KEY = "Id";
63 private static final String LOCAL_PARAMETERS_OPT_KEY = "LocalParameters";
64 private static final String FILE_PARAMETERS_OPT_KEY = "FileParameters";
65 private static final String ENV_PARAMETERS_OPT_KEY = "EnvParameters";
66 private static final String NODE_LIST_OPT_KEY = "NodeList";
67 private static final String AUTO_NODE_LIST_OPT_KEY = "AutoNodeList";
68 private static final String TIMEOUT_OPT_KEY = "Timeout";
69 private static final String VERSION_OPT_KEY = "Version";
70 private static final String INVENTORY_NAMES_OPT_KEY = "InventoryNames";
71 private static final String EXTRAVARS_OPT_KEY ="ExtraVars";
72 private static final String ACTION_OPT_KEY = "Action";
73 private static final String OUTPUT_OPT_KEY = "Output";
74 private static final String JSON_ERROR_MESSAGE = "JSONException: Error parsing response";
76 private static final Logger LOGGER = LoggerFactory.getLogger(AnsibleMessageParser.class);
79 * Accepts a map of strings and a) validates if all parameters are appropriate
80 * (else, throws an exception) and b) if correct returns a JSON object with
81 * appropriate key-value pairs to send to the server.
83 * Mandatory parameters, that must be in the supplied information to the Ansible
84 * Adapter 1. URL to connect to 2. credentials for URL (assume username password
85 * for now) 3. Playbook name
88 public JSONObject reqMessage(Map<String, String> params) throws APPCException {
89 final String[] mandatoryTestParams = { AGENT_URL_KEY, PLAYBOOK_NAME_KEY, USER_KEY, PASS_KEY };
90 final String[] optionalTestParams = { ENV_PARAMETERS_OPT_KEY, NODE_LIST_OPT_KEY, LOCAL_PARAMETERS_OPT_KEY,
91 TIMEOUT_OPT_KEY, VERSION_OPT_KEY, FILE_PARAMETERS_OPT_KEY, ACTION_OPT_KEY, INVENTORY_NAMES_OPT_KEY,
92 AUTO_NODE_LIST_OPT_KEY };
94 JSONObject jsonPayload = new JSONObject();
96 for (String key : mandatoryTestParams) {
97 throwIfMissingMandatoryParam(params, key);
98 jsonPayload.put(key, params.get(key));
101 parseOptionalParams(params, optionalTestParams, jsonPayload);
103 // Generate a unique uuid for the test
104 String reqId = UUID.randomUUID().toString();
105 jsonPayload.put(ID_KEY, reqId);
111 * Method that validates that the Map has enough information to query Ansible
112 * server for a result. If so, it returns the appropriate url, else an empty
115 public String reqUriResult(Map<String, String> params) throws APPCException {
117 final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
119 for (String key : mandatoryTestParams) {
120 throwIfMissingMandatoryParam(params, key);
122 return params.get(AGENT_URL_KEY) + "?Id=" + params.get(ID_KEY) + "&Type=GetResult";
126 * Method that validates that the Map has enough information to query Ansible
127 * server for a result. If so, it returns the appropriate url, else an empty
130 public String reqUriResultWithIP(Map<String, String> params, String serverIP) throws APPCException {
132 final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
134 for (String key : mandatoryTestParams) {
135 throwIfMissingMandatoryParam(params, key);
137 String[] arr1 = params.get(AGENT_URL_KEY).split("//", 2);
138 String[] arr2 = arr1[1].split(":", 2);
139 return arr1[0] + "//" + serverIP + ":" + arr2[1] + "?Id=" + params.get(ID_KEY) + "&Type=GetResult";
143 * Method that validates that the Map has enough information to query Ansible
144 * server for logs. If so, it populates the appropriate returns the appropriate
145 * url, else an empty string.
147 public String reqUriLog(Map<String, String> params) throws APPCException {
149 final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
151 for (String mandatoryParam : mandatoryTestParams) {
152 throwIfMissingMandatoryParam(params, mandatoryParam);
154 return params.get(AGENT_URL_KEY) + "?Id=" + params.get(ID_KEY) + "&Type=GetLog";
158 * This method parses response from the Ansible Server when we do a post and
159 * returns an AnsibleResult object.
161 public AnsibleResult parsePostResponse(String input) throws APPCException {
162 AnsibleResult ansibleResult;
164 JSONObject postResponse = new JSONObject(input);
166 int code = postResponse.getInt(STATUS_CODE_KEY);
167 String msg = postResponse.getString(STATUS_MESSAGE_KEY);
168 String serverIP = "";
169 if (postResponse.has(SERVER_IP_KEY))
170 serverIP = postResponse.getString(SERVER_IP_KEY);
173 int initResponseValue = AnsibleResultCodes.INITRESPONSE.getValue();
174 boolean validCode = AnsibleResultCodes.CODE.checkValidCode(initResponseValue, code);
176 throw new APPCException("Invalid InitResponse code = " + code + " received. MUST be one of "
177 + AnsibleResultCodes.CODE.getValidCodes(initResponseValue));
180 ansibleResult = new AnsibleResult(code, msg);
181 if (StringUtils.isNotBlank(serverIP))
182 ansibleResult.setServerIp(serverIP);
184 if (!postResponse.isNull(OUTPUT_OPT_KEY)) {
185 LOGGER.info("Processing results-output in post response");
186 JSONObject output = postResponse.getJSONObject(OUTPUT_OPT_KEY);
187 ansibleResult.setOutput(output.toString());
190 } catch (JSONException e) {
191 LOGGER.error(JSON_ERROR_MESSAGE, e);
192 ansibleResult = new AnsibleResult(600, "Error parsing response = " + input + ". Error = " + e.getMessage());
194 return ansibleResult;
198 * This method parses response from an Ansible server when we do a GET for a
199 * result and returns an AnsibleResult object.
201 public AnsibleResult parseGetResponse(String input) throws APPCException {
203 AnsibleResult ansibleResult = new AnsibleResult();
206 JSONObject postResponse = new JSONObject(input);
207 ansibleResult = parseGetResponseNested(ansibleResult, postResponse);
208 } catch (JSONException e) {
209 LOGGER.error(JSON_ERROR_MESSAGE, e);
210 ansibleResult = new AnsibleResult(AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
211 "Error parsing response = " + input + ". Error = " + e.getMessage(), "");
213 return ansibleResult;
216 private AnsibleResult parseGetResponseNested(AnsibleResult ansibleResult, JSONObject postRsp) throws APPCException {
218 int codeStatus = postRsp.getInt(STATUS_CODE_KEY);
219 String messageStatus = postRsp.getString(STATUS_MESSAGE_KEY);
220 int finalCode = AnsibleResultCodes.FINAL_SUCCESS.getValue();
221 JSONObject config = null;
222 boolean valCode = AnsibleResultCodes.CODE.checkValidCode(AnsibleResultCodes.FINALRESPONSE.getValue(),
226 throw new APPCException("Invalid FinalResponse code = " + codeStatus + " received. MUST be one of "
227 + AnsibleResultCodes.CODE.getValidCodes(AnsibleResultCodes.FINALRESPONSE.getValue()));
230 ansibleResult.setStatusCode(codeStatus);
231 ansibleResult.setStatusMessage(messageStatus);
232 ansibleResult.setconfigData("UNKNOWN");
233 LOGGER.info("Received response with code = {}, Message = {}", codeStatus, messageStatus);
235 if (!postRsp.isNull("Results")) {
237 // Results are available. process them
238 // Results is a dictionary of the form
239 // {host :{status:s, group:g, message:m, hostname:h}, ...}
240 LOGGER.info("Processing results in response");
241 JSONObject results = postRsp.getJSONObject("Results");
242 LOGGER.info("Get JSON dictionary from Results ..");
243 Iterator<String> hosts = results.keys();
244 LOGGER.info("Iterating through hosts");
246 while (hosts.hasNext()) {
247 String host = hosts.next();
248 LOGGER.info("Processing host = {}",
249 (host.matches("^[\\w\\-.]+$")) ? host : "[unexpected value, logging suppressed]");
251 JSONObject hostResponse = results.getJSONObject(host);
252 int subCode = hostResponse.getInt(STATUS_CODE_KEY);
253 String message = hostResponse.getString(STATUS_MESSAGE_KEY);
255 LOGGER.info("Code = {}, Message = {}", subCode, message);
257 if (subCode != 200 || !(("SUCCESS").equals(message))) {
258 finalCode = AnsibleResultCodes.REQ_FAILURE.getValue();
260 if ((hostResponse.optJSONObject(OUTPUT_OPT_KEY)) != null) {
261 JSONObject hostResponseObjectInfo = hostResponse.optJSONObject(OUTPUT_OPT_KEY).optJSONObject("info");
262 JSONObject hostResponseConfigData = hostResponseObjectInfo.optJSONObject("configData");
263 if ((hostResponseObjectInfo != null) && hostResponseConfigData != null) {
264 config = hostResponseConfigData;
265 ansibleResult.setconfigData(config.toString());
268 } catch (JSONException e) {
269 LOGGER.error(JSON_ERROR_MESSAGE, e);
270 ansibleResult.setStatusCode(AnsibleResultCodes.INVALID_RESPONSE.getValue());
271 ansibleResult.setStatusMessage(String.format("Error processing response message = %s from host %s",
272 results.getString(host), host));
277 ansibleResult.setStatusCode(finalCode);
279 // We return entire Results object as message
280 ansibleResult.setResults(results.toString());
283 ansibleResult.setStatusCode(AnsibleResultCodes.INVALID_RESPONSE.getValue());
284 ansibleResult.setStatusMessage("Results not found in GET for response");
286 if (!postRsp.isNull(OUTPUT_OPT_KEY)) {
287 LOGGER.info("Processing results-output in response");
288 JSONObject output = postRsp.getJSONObject(OUTPUT_OPT_KEY);
289 ansibleResult.setOutput(output.toString());
292 return ansibleResult;
295 private void parseOptionalParams(Map<String, String> params, String[] optionalTestParams, JSONObject jsonPayload) {
297 Set<String> optionalParamsSet = new HashSet<>();
298 Collections.addAll(optionalParamsSet, optionalTestParams);
301 params.entrySet().stream().filter(entry -> optionalParamsSet.contains(entry.getKey()))
302 .filter(entry -> !Strings.isNullOrEmpty(entry.getValue()))
303 .forEach(entry -> parseOptionalParam(entry, jsonPayload));
307 private void parseOptionalParam(Map.Entry<String, String> params, JSONObject jsonPayload) {
308 String key = params.getKey();
309 String payload = params.getValue();
312 case TIMEOUT_OPT_KEY:
313 if (dataIsVariable(payload))
315 int timeout = Integer.parseInt(payload);
317 throw new NumberFormatException(" : specified negative integer for timeout = " + payload);
319 jsonPayload.put(key, payload);
321 case AUTO_NODE_LIST_OPT_KEY:
322 if (payload.equalsIgnoreCase("true") || payload.equalsIgnoreCase("false")) {
323 jsonPayload.put(key, payload);
325 throw new IllegalArgumentException(" : specified invalid boolean value of AutoNodeList = " + payload);
328 case VERSION_OPT_KEY:
329 if (dataIsVariable(payload))
331 case INVENTORY_NAMES_OPT_KEY:
332 jsonPayload.put(key, payload);
335 case LOCAL_PARAMETERS_OPT_KEY:
336 case ENV_PARAMETERS_OPT_KEY:
337 case EXTRAVARS_OPT_KEY:
338 JSONObject paramsJson = new JSONObject(payload);
339 jsonDataIsVariable(paramsJson);
340 jsonPayload.put(key, paramsJson);
342 case NODE_LIST_OPT_KEY:
343 if(payload.startsWith("$"))
345 JSONArray paramsArray = new JSONArray(payload);
346 jsonPayload.put(key, paramsArray);
348 case FILE_PARAMETERS_OPT_KEY:
349 if (dataIsVariable(payload))
351 jsonPayload.put(key, getFilePayload(payload));
360 * Return payload with escaped newlines
362 private JSONObject getFilePayload(String payload) {
363 String formattedPayload = payload.replace("\n", "\\n").replace("\r", "\\r");
364 return new JSONObject(formattedPayload);
367 private void throwIfMissingMandatoryParam(Map<String, String> params, String key) throws APPCException {
368 if (!params.containsKey(key)) {
369 throw new APPCException(String.format(
370 "Ansible: Mandatory AnsibleAdapter key %s not found in parameters provided by calling agent !",
373 if (Strings.isNullOrEmpty(params.get(key))) {
374 throw new APPCException(String.format(
375 "Ansible: Mandatory AnsibleAdapter key %s not found in parameters provided by calling agent !",
379 if (StringUtils.startsWith(params.get(key), "$")) {
380 throw new APPCException(String.format(
381 "Ansible: Mandatory AnsibleAdapter key %s is a variable",
387 /*private boolean varObjContainsNoData(Object obj) {
388 if (obj instanceof String) {
389 if (StringUtils.startsWith(obj.toString(), "$") || StringUtils.isEmpty(obj.toString()))
396 private boolean dataIsVariable(String payload) {
397 if (StringUtils.startsWith(payload, "$") || StringUtils.isEmpty(payload))
404 private JSONObject jsonDataIsVariable(JSONObject paramsJson) {
405 LOGGER.info("input json is " + paramsJson);
406 String[] keys = JSONObject.getNames(paramsJson);
407 for (String k : keys) {
408 Object a = paramsJson.get(k);
409 if (a instanceof String) {
410 if (StringUtils.startsWith(a.toString(), "$") || StringUtils.isEmpty(a.toString())) {
411 LOGGER.info("removing key " + k);
412 paramsJson.remove(k);
416 LOGGER.info("returning json as " + paramsJson);