fc38b550a85851e89d4ecc48380c194ae7db85c7
[appc.git] / appc-adapters / appc-ansible-adapter / appc-ansible-adapter-bundle / src / main / java / org / onap / appc / adapter / ansible / model / AnsibleMessageParser.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP : APPC
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
16  *
17  *      http://www.apache.org/licenses/LICENSE-2.0
18  *
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.
24  *
25  * ============LICENSE_END=========================================================
26  */
27
28 package org.onap.appc.adapter.ansible.model;
29
30 /**
31  * This module implements the APP-C/Ansible Server interface
32  * based on the REST API specifications
33  */
34 import java.util.Collections;
35 import java.util.HashSet;
36 import java.util.Iterator;
37 import java.util.Map;
38 import java.util.Set;
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;
48
49 /**
50  * Class that validates and constructs requests sent/received from Ansible
51  * Server
52  */
53 public class AnsibleMessageParser {
54
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";
75
76     private static final Logger LOGGER = LoggerFactory.getLogger(AnsibleMessageParser.class);
77
78     /**
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.
82      *
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
86      *
87      */
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 };
93
94         JSONObject jsonPayload = new JSONObject();
95
96         for (String key : mandatoryTestParams) {
97             throwIfMissingMandatoryParam(params, key);
98             jsonPayload.put(key, params.get(key));
99         }
100
101         parseOptionalParams(params, optionalTestParams, jsonPayload);
102
103         // Generate a unique uuid for the test
104         String reqId = UUID.randomUUID().toString();
105         jsonPayload.put(ID_KEY, reqId);
106
107         return jsonPayload;
108     }
109
110     /**
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
113      * string.
114      */
115     public String reqUriResult(Map<String, String> params) throws APPCException {
116
117         final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
118
119         for (String key : mandatoryTestParams) {
120             throwIfMissingMandatoryParam(params, key);
121         }
122         return params.get(AGENT_URL_KEY) + "?Id=" + params.get(ID_KEY) + "&Type=GetResult";
123     }
124
125     /**
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
128      * string.
129      */
130     public String reqUriResultWithIP(Map<String, String> params, String serverIP) throws APPCException {
131
132         final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
133
134         for (String key : mandatoryTestParams) {
135             throwIfMissingMandatoryParam(params, key);
136         }
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";
140     }
141
142     /**
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.
146      */
147     public String reqUriLog(Map<String, String> params) throws APPCException {
148
149         final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
150
151         for (String mandatoryParam : mandatoryTestParams) {
152             throwIfMissingMandatoryParam(params, mandatoryParam);
153         }
154         return params.get(AGENT_URL_KEY) + "?Id=" + params.get(ID_KEY) + "&Type=GetLog";
155     }
156
157     /**
158      * This method parses response from the Ansible Server when we do a post and
159      * returns an AnsibleResult object.
160      */
161     public AnsibleResult parsePostResponse(String input) throws APPCException {
162         AnsibleResult ansibleResult;
163         try {
164             JSONObject postResponse = new JSONObject(input);
165
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);
171             else
172                 serverIP = "";
173             int initResponseValue = AnsibleResultCodes.INITRESPONSE.getValue();
174             boolean validCode = AnsibleResultCodes.CODE.checkValidCode(initResponseValue, code);
175             if (!validCode) {
176                 throw new APPCException("Invalid InitResponse code  = " + code + " received. MUST be one of "
177                         + AnsibleResultCodes.CODE.getValidCodes(initResponseValue));
178             }
179
180             ansibleResult = new AnsibleResult(code, msg);
181             if (StringUtils.isNotBlank(serverIP))
182                 ansibleResult.setServerIp(serverIP);
183
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());
188             }
189
190         } catch (JSONException e) {
191             LOGGER.error(JSON_ERROR_MESSAGE, e);
192             ansibleResult = new AnsibleResult(600, "Error parsing response = " + input + ". Error = " + e.getMessage());
193         }
194         return ansibleResult;
195     }
196
197     /**
198      * This method parses response from an Ansible server when we do a GET for a
199      * result and returns an AnsibleResult object.
200      **/
201     public AnsibleResult parseGetResponse(String input) throws APPCException {
202
203         AnsibleResult ansibleResult = new AnsibleResult();
204
205         try {
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(), "");
212         }
213         return ansibleResult;
214     }
215
216     private AnsibleResult parseGetResponseNested(AnsibleResult ansibleResult, JSONObject postRsp) throws APPCException {
217
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(),
223                 codeStatus);
224
225         if (!valCode) {
226             throw new APPCException("Invalid FinalResponse code  = " + codeStatus + " received. MUST be one of "
227                     + AnsibleResultCodes.CODE.getValidCodes(AnsibleResultCodes.FINALRESPONSE.getValue()));
228         }
229
230         ansibleResult.setStatusCode(codeStatus);
231         ansibleResult.setStatusMessage(messageStatus);
232         ansibleResult.setconfigData("UNKNOWN");
233         LOGGER.info("Received response with code = {}, Message = {}", codeStatus, messageStatus);
234
235         if (!postRsp.isNull("Results")) {
236
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");
245
246             while (hosts.hasNext()) {
247                 String host = hosts.next();
248                 LOGGER.info("Processing host = {}",
249                         (host.matches("^[\\w\\-.]+$")) ? host : "[unexpected value, logging suppressed]");
250                 try {
251                     JSONObject hostResponse = results.getJSONObject(host);
252                     int subCode = hostResponse.getInt(STATUS_CODE_KEY);
253                     String message = hostResponse.getString(STATUS_MESSAGE_KEY);
254
255                     LOGGER.info("Code = {}, Message = {}", subCode, message);
256
257                     if (subCode != 200 || !(("SUCCESS").equals(message))) {
258                         finalCode = AnsibleResultCodes.REQ_FAILURE.getValue();
259                     }
260                    if ((hostResponse.optJSONObject("Output")) != null) {
261                         if ((hostResponse.optJSONObject("Output").optJSONObject("info")) != null) {
262                             if ((hostResponse.optJSONObject("Output").optJSONObject("info")
263                                     .optJSONObject("configData")) != null) {
264                                 config = hostResponse.optJSONObject("Output").optJSONObject("info")
265                                         .optJSONObject("configData");
266                                 
267                                 ansibleResult.setconfigData(config.toString());
268                             }
269                         }
270                     }
271                 } catch (JSONException e) {
272                     LOGGER.error(JSON_ERROR_MESSAGE, e);
273                     ansibleResult.setStatusCode(AnsibleResultCodes.INVALID_RESPONSE.getValue());
274                     ansibleResult.setStatusMessage(String.format("Error processing response message = %s from host %s",
275                             results.getString(host), host));
276                     break;
277                 }
278             }
279
280             ansibleResult.setStatusCode(finalCode);
281
282             // We return entire Results object as message
283             ansibleResult.setResults(results.toString());
284
285         } else {
286             ansibleResult.setStatusCode(AnsibleResultCodes.INVALID_RESPONSE.getValue());
287             ansibleResult.setStatusMessage("Results not found in GET for response");
288         }
289         if (!postRsp.isNull(OUTPUT_OPT_KEY)) {
290             LOGGER.info("Processing results-output in response");
291             JSONObject output = postRsp.getJSONObject(OUTPUT_OPT_KEY);
292             ansibleResult.setOutput(output.toString());
293         }
294
295         return ansibleResult;
296     }
297
298     private void parseOptionalParams(Map<String, String> params, String[] optionalTestParams, JSONObject jsonPayload) {
299
300         Set<String> optionalParamsSet = new HashSet<>();
301         Collections.addAll(optionalParamsSet, optionalTestParams);
302
303         // @formatter:off
304         params.entrySet().stream().filter(entry -> optionalParamsSet.contains(entry.getKey()))
305                 .filter(entry -> !Strings.isNullOrEmpty(entry.getValue()))
306                 .forEach(entry -> parseOptionalParam(entry, jsonPayload));
307         // @formatter:on
308     }
309
310     private void parseOptionalParam(Map.Entry<String, String> params, JSONObject jsonPayload) {
311         String key = params.getKey();
312         String payload = params.getValue();
313
314         switch (key) {
315         case TIMEOUT_OPT_KEY:
316             if (dataIsVariable(payload))
317                 break;
318             int timeout = Integer.parseInt(payload);
319             if (timeout < 0) {
320                 throw new NumberFormatException(" : specified negative integer for timeout = " + payload);
321             }
322             jsonPayload.put(key, payload);
323             break;
324         case AUTO_NODE_LIST_OPT_KEY:
325             if (payload.equalsIgnoreCase("true") || payload.equalsIgnoreCase("false")) {
326                 jsonPayload.put(key, payload);
327             } else {
328                 throw new IllegalArgumentException(" : specified invalid boolean value of AutoNodeList = " + payload);
329             }
330             break;
331         case VERSION_OPT_KEY:
332             if (dataIsVariable(payload))
333                     break;
334         case INVENTORY_NAMES_OPT_KEY:
335             jsonPayload.put(key, payload);
336             break;
337
338         case LOCAL_PARAMETERS_OPT_KEY:
339         case ENV_PARAMETERS_OPT_KEY:
340         case EXTRAVARS_OPT_KEY:
341                 JSONObject paramsJson = new JSONObject(payload);
342                 jsonDataIsVariable(paramsJson);
343                 jsonPayload.put(key, paramsJson);
344                 break;
345         case NODE_LIST_OPT_KEY:
346               if(payload.startsWith("$"))
347                     break;
348               JSONArray paramsArray = new JSONArray(payload);
349               jsonPayload.put(key, paramsArray);
350               break;
351          case FILE_PARAMETERS_OPT_KEY:
352               if (dataIsVariable(payload))
353                     break;
354             jsonPayload.put(key, getFilePayload(payload));
355             break;
356
357         default:
358             break;
359         }
360     }
361
362     /**
363      * Return payload with escaped newlines
364      */
365     private JSONObject getFilePayload(String payload) {
366         String formattedPayload = payload.replace("\n", "\\n").replace("\r", "\\r");
367         return new JSONObject(formattedPayload);
368     }
369
370     private void throwIfMissingMandatoryParam(Map<String, String> params, String key) throws APPCException {
371         if (!params.containsKey(key)) {
372             throw new APPCException(String.format(
373                     "Ansible: Mandatory AnsibleAdapter key %s not found in parameters provided by calling agent !",
374                     key));
375         }
376         if (Strings.isNullOrEmpty(params.get(key))) {
377             throw new APPCException(String.format(
378                     "Ansible: Mandatory AnsibleAdapter key %s not found in parameters provided by calling agent !",
379                     key));
380         }
381         
382         if (StringUtils.startsWith(params.get(key), "$")) {
383             throw new APPCException(String.format(
384                     "Ansible: Mandatory AnsibleAdapter key %s is a variable",
385                     key));
386         }
387     }
388
389
390     private boolean varObjContainsNoData(Object obj) {
391         if (obj instanceof String) {
392             if (StringUtils.startsWith(obj.toString(), "$") || StringUtils.isEmpty(obj.toString()))
393                 return true;
394         }
395         return false;
396
397     }
398
399     private boolean dataIsVariable(String payload) {
400         if (StringUtils.startsWith(payload, "$") || StringUtils.isEmpty(payload))
401             return true;
402         else
403             return false;
404
405     }
406
407      private JSONObject jsonDataIsVariable(JSONObject paramsJson) {
408         LOGGER.info("input json is " + paramsJson);
409         String[] keys = JSONObject.getNames(paramsJson);
410         for (String k : keys) {
411             Object a = paramsJson.get(k);
412             if (a instanceof String) {
413                 if (StringUtils.startsWith(a.toString(), "$") || StringUtils.isEmpty(a.toString())) {
414                     LOGGER.info("removing key " + k);
415                     paramsJson.remove(k);
416                 }
417             }
418         }
419         LOGGER.info("returning json as " + paramsJson);
420         return paramsJson;
421     }
422 }