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