Used constants to replace duplicated literals
[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  * 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
14  *
15  *      http://www.apache.org/licenses/LICENSE-2.0
16  *
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.
22  *
23  * ============LICENSE_END=========================================================
24  */
25
26 package org.onap.appc.adapter.ansible.model;
27
28 /**
29  * This module implements the APP-C/Ansible Server interface
30  * based on the REST API specifications
31  */
32 import java.util.Collections;
33 import java.util.HashSet;
34 import java.util.Iterator;
35 import java.util.Map;
36 import java.util.Set;
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;
46
47 /**
48  * Class that validates and constructs requests sent/received from Ansible
49  * Server
50  */
51 public class AnsibleMessageParser {
52
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 ACTION_OPT_KEY = "Action";
68     private static final String OUTPUT_OPT_KEY = "Output";
69     private static final String JSON_ERROR_MESSAGE = "JSONException: Error parsing response";
70
71     private static final Logger LOGGER = LoggerFactory.getLogger(AnsibleMessageParser.class);
72
73     /**
74      * Accepts a map of strings and a) validates if all parameters are appropriate
75      * (else, throws an exception) and b) if correct returns a JSON object with
76      * appropriate key-value pairs to send to the server.
77      *
78      * Mandatory parameters, that must be in the supplied information to the Ansible
79      * Adapter 1. URL to connect to 2. credentials for URL (assume username password
80      * for now) 3. Playbook name
81      *
82      */
83     public JSONObject reqMessage(Map<String, String> params) throws APPCException {
84         final String[] mandatoryTestParams = { AGENT_URL_KEY, PLAYBOOK_NAME_KEY, USER_KEY, PASS_KEY };
85         final String[] optionalTestParams = { ENV_PARAMETERS_OPT_KEY, NODE_LIST_OPT_KEY, LOCAL_PARAMETERS_OPT_KEY,
86                 TIMEOUT_OPT_KEY, VERSION_OPT_KEY, FILE_PARAMETERS_OPT_KEY, ACTION_OPT_KEY };
87
88         JSONObject jsonPayload = new JSONObject();
89
90         for (String key : mandatoryTestParams) {
91             throwIfMissingMandatoryParam(params, key);
92             jsonPayload.put(key, params.get(key));
93         }
94
95         parseOptionalParams(params, optionalTestParams, jsonPayload);
96
97         // Generate a unique uuid for the test
98         String reqId = UUID.randomUUID().toString();
99         jsonPayload.put(ID_KEY, reqId);
100
101         return jsonPayload;
102     }
103
104     /**
105      * Method that validates that the Map has enough information to query Ansible
106      * server for a result. If so, it returns the appropriate url, else an empty
107      * string.
108      */
109     public String reqUriResult(Map<String, String> params) throws APPCException {
110
111         final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
112
113         for (String key : mandatoryTestParams) {
114             throwIfMissingMandatoryParam(params, key);
115         }
116         return params.get(AGENT_URL_KEY) + "?Id=" + params.get(ID_KEY) + "&Type=GetResult";
117     }
118
119     /**
120      * Method that validates that the Map has enough information to query Ansible
121      * server for a result. If so, it returns the appropriate url, else an empty
122      * string.
123      */
124     public String reqUriResultWithIP(Map<String, String> params, String serverIP) throws APPCException {
125
126         final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
127
128         for (String key : mandatoryTestParams) {
129             throwIfMissingMandatoryParam(params, key);
130         }
131         String[] arr1 = params.get(AGENT_URL_KEY).split("//", 2);
132         String[] arr2 = arr1[1].split(":", 2);
133         return arr1[0] + "//" + serverIP + ":" + arr2[1] + "?Id=" + params.get(ID_KEY) + "&Type=GetResult";
134     }
135
136     /**
137      * Method that validates that the Map has enough information to query Ansible
138      * server for logs. If so, it populates the appropriate returns the appropriate
139      * url, else an empty string.
140      */
141     public String reqUriLog(Map<String, String> params) throws APPCException {
142
143         final String[] mandatoryTestParams = { AGENT_URL_KEY, ID_KEY, USER_KEY, PASS_KEY };
144
145         for (String mandatoryParam : mandatoryTestParams) {
146             throwIfMissingMandatoryParam(params, mandatoryParam);
147         }
148         return params.get(AGENT_URL_KEY) + "?Id=" + params.get(ID_KEY) + "&Type=GetLog";
149     }
150
151     /**
152      * This method parses response from the Ansible Server when we do a post and
153      * returns an AnsibleResult object.
154      */
155     public AnsibleResult parsePostResponse(String input) throws APPCException {
156         AnsibleResult ansibleResult;
157         try {
158             JSONObject postResponse = new JSONObject(input);
159
160             int code = postResponse.getInt(STATUS_CODE_KEY);
161             String msg = postResponse.getString(STATUS_MESSAGE_KEY);
162             String serverIP = "";
163             if (postResponse.has(SERVER_IP_KEY))
164                 serverIP = postResponse.getString(SERVER_IP_KEY);
165             else
166                 serverIP = "";
167             int initResponseValue = AnsibleResultCodes.INITRESPONSE.getValue();
168             boolean validCode = AnsibleResultCodes.CODE.checkValidCode(initResponseValue, code);
169             if (!validCode) {
170                 throw new APPCException("Invalid InitResponse code  = " + code + " received. MUST be one of "
171                         + AnsibleResultCodes.CODE.getValidCodes(initResponseValue));
172             }
173
174             ansibleResult = new AnsibleResult(code, msg);
175             if (StringUtils.isNotBlank(serverIP))
176                 ansibleResult.setServerIp(serverIP);
177
178             if (!postResponse.isNull(OUTPUT_OPT_KEY)) {
179                 LOGGER.info("Processing results-output in post response");
180                 JSONObject output = postResponse.getJSONObject(OUTPUT_OPT_KEY);
181                 ansibleResult.setOutput(output.toString());
182             }
183
184         } catch (JSONException e) {
185             LOGGER.error(JSON_ERROR_MESSAGE, e);
186             ansibleResult = new AnsibleResult(600, "Error parsing response = " + input + ". Error = " + e.getMessage());
187         }
188         return ansibleResult;
189     }
190
191     /**
192      * This method parses response from an Ansible server when we do a GET for a
193      * result and returns an AnsibleResult object.
194      **/
195     public AnsibleResult parseGetResponse(String input) throws APPCException {
196
197         AnsibleResult ansibleResult = new AnsibleResult();
198
199         try {
200             JSONObject postResponse = new JSONObject(input);
201             ansibleResult = parseGetResponseNested(ansibleResult, postResponse);
202         } catch (JSONException e) {
203             LOGGER.error(JSON_ERROR_MESSAGE, e);
204             ansibleResult = new AnsibleResult(AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
205                     "Error parsing response = " + input + ". Error = " + e.getMessage(), "");
206         }
207         return ansibleResult;
208     }
209
210     private AnsibleResult parseGetResponseNested(AnsibleResult ansibleResult, JSONObject postRsp) throws APPCException {
211
212         int codeStatus = postRsp.getInt(STATUS_CODE_KEY);
213         String messageStatus = postRsp.getString(STATUS_MESSAGE_KEY);
214         int finalCode = AnsibleResultCodes.FINAL_SUCCESS.getValue();
215
216         boolean valCode = AnsibleResultCodes.CODE.checkValidCode(AnsibleResultCodes.FINALRESPONSE.getValue(),
217                 codeStatus);
218
219         if (!valCode) {
220             throw new APPCException("Invalid FinalResponse code  = " + codeStatus + " received. MUST be one of "
221                     + AnsibleResultCodes.CODE.getValidCodes(AnsibleResultCodes.FINALRESPONSE.getValue()));
222         }
223
224         ansibleResult.setStatusCode(codeStatus);
225         ansibleResult.setStatusMessage(messageStatus);
226         LOGGER.info("Received response with code = {}, Message = {}", codeStatus, messageStatus);
227
228         if (!postRsp.isNull("Results")) {
229
230             // Results are available. process them
231             // Results is a dictionary of the form
232             // {host :{status:s, group:g, message:m, hostname:h}, ...}
233             LOGGER.info("Processing results in response");
234             JSONObject results = postRsp.getJSONObject("Results");
235             LOGGER.info("Get JSON dictionary from Results ..");
236             Iterator<String> hosts = results.keys();
237             LOGGER.info("Iterating through hosts");
238
239             while (hosts.hasNext()) {
240                 String host = hosts.next();
241                 LOGGER.info("Processing host = {}",
242                         (host.matches("^[\\w\\-.]+$")) ? host : "[unexpected value, logging suppressed]");
243                 try {
244                     JSONObject hostResponse = results.getJSONObject(host);
245                     int subCode = hostResponse.getInt(STATUS_CODE_KEY);
246                     String message = hostResponse.getString(STATUS_MESSAGE_KEY);
247
248                     LOGGER.info("Code = {}, Message = {}", subCode, message);
249
250                     if (subCode != 200 || !(("SUCCESS").equals(message))) {
251                         finalCode = AnsibleResultCodes.REQ_FAILURE.getValue();
252                     }
253                 } catch (JSONException e) {
254                     LOGGER.error(JSON_ERROR_MESSAGE, e);
255                     ansibleResult.setStatusCode(AnsibleResultCodes.INVALID_RESPONSE.getValue());
256                     ansibleResult.setStatusMessage(String.format("Error processing response message = %s from host %s",
257                             results.getString(host), host));
258                     break;
259                 }
260             }
261
262             ansibleResult.setStatusCode(finalCode);
263
264             // We return entire Results object as message
265             ansibleResult.setResults(results.toString());
266
267         } else {
268             ansibleResult.setStatusCode(AnsibleResultCodes.INVALID_RESPONSE.getValue());
269             ansibleResult.setStatusMessage("Results not found in GET for response");
270         }
271         if (!postRsp.isNull(OUTPUT_OPT_KEY)) {
272             LOGGER.info("Processing results-output in response");
273             JSONObject output = postRsp.getJSONObject(OUTPUT_OPT_KEY);
274             ansibleResult.setOutput(output.toString());
275         }
276
277         return ansibleResult;
278     }
279
280     private void parseOptionalParams(Map<String, String> params, String[] optionalTestParams, JSONObject jsonPayload) {
281
282         Set<String> optionalParamsSet = new HashSet<>();
283         Collections.addAll(optionalParamsSet, optionalTestParams);
284
285         // @formatter:off
286         params.entrySet().stream().filter(entry -> optionalParamsSet.contains(entry.getKey()))
287                 .filter(entry -> !Strings.isNullOrEmpty(entry.getValue()))
288                 .forEach(entry -> parseOptionalParam(entry, jsonPayload));
289         // @formatter:on
290     }
291
292     private void parseOptionalParam(Map.Entry<String, String> params, JSONObject jsonPayload) {
293         String key = params.getKey();
294         String payload = params.getValue();
295
296         switch (key) {
297         case TIMEOUT_OPT_KEY:
298             int timeout = Integer.parseInt(payload);
299             if (timeout < 0) {
300                 throw new NumberFormatException(" : specified negative integer for timeout = " + payload);
301             }
302             jsonPayload.put(key, payload);
303             break;
304
305         case VERSION_OPT_KEY:
306             jsonPayload.put(key, payload);
307             break;
308
309         case LOCAL_PARAMETERS_OPT_KEY:
310         case ENV_PARAMETERS_OPT_KEY:
311             JSONObject paramsJson = new JSONObject(payload);
312             jsonPayload.put(key, paramsJson);
313             break;
314
315         case NODE_LIST_OPT_KEY:
316             JSONArray paramsArray = new JSONArray(payload);
317             jsonPayload.put(key, paramsArray);
318             break;
319
320         case FILE_PARAMETERS_OPT_KEY:
321             jsonPayload.put(key, getFilePayload(payload));
322             break;
323
324         default:
325             break;
326         }
327     }
328
329     /**
330      * Return payload with escaped newlines
331      */
332     private JSONObject getFilePayload(String payload) {
333         String formattedPayload = payload.replace("\n", "\\n").replace("\r", "\\r");
334         return new JSONObject(formattedPayload);
335     }
336
337     private void throwIfMissingMandatoryParam(Map<String, String> params, String key) throws APPCException {
338         if (!params.containsKey(key)) {
339             throw new APPCException(String.format(
340                     "Ansible: Mandatory AnsibleAdapter key %s not found in parameters provided by calling agent !",
341                     key));
342         }
343         if (Strings.isNullOrEmpty(params.get(key))) {
344             throw new APPCException(String.format(
345                     "Ansible: Mandatory AnsibleAdapter key %s not found in parameters provided by calling agent !",
346                     key));
347         }
348     }
349 }