e448f1c475f9b874b814b7ba13ff62d46c4c5773
[ccsdk/sli.git] /
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP : SLI
4  * ================================================================================
5  * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  *
19  * ECOMP is a trademark and service mark of AT&T Intellectual Property.
20  * ============LICENSE_END=========================================================
21  */
22
23 package org.onap.ccsdk.sli.adaptors.ansible.model;
24
25 import com.google.common.base.Strings;
26 import java.util.Collections;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.Map;
30 import java.util.Set;
31 import java.util.UUID;
32 import org.apache.commons.lang.StringUtils;
33 import org.json.JSONArray;
34 import org.json.JSONException;
35 import org.json.JSONObject;
36 import org.onap.ccsdk.sli.adaptors.ansible.AnsibleAdapterConstants;
37 import org.onap.ccsdk.sli.core.sli.SvcLogicException;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 import static org.onap.ccsdk.sli.adaptors.ansible.AnsibleAdapterConstants.*;
42
43 /**
44  * Class that validates and constructs requests sent/received from
45  * Ansible Server
46  */
47 public class AnsibleMessageParser {
48
49
50     private static final String JSON_ERROR_MESSAGE = "JSONException: Error parsing response";
51
52     private static final Logger LOGGER = LoggerFactory.getLogger(AnsibleMessageParser.class);
53
54     /**
55      * Accepts a map of strings and
56      * a) validates if all parameters are appropriate (else, throws an exception) and
57      * b) if correct returns a JSON object with appropriate key-value pairs to send to the server.
58      * <p>
59      * Mandatory parameters, that must be in the supplied information to the Ansible Adapter
60      * 1. URL to connect to
61      * 2. credentials for URL (assume user pswd for now)
62      * 3. Playbook name
63      */
64     public JSONObject reqMessage(Map<String, String> params) throws SvcLogicException {
65         final String[] mandatoryTestParams = {AGENT_URL, PLAYBOOK_NAME, USER, PSWD};
66         final String[] optionalTestParams = {ENV_PARAMETERS, NODE_LIST, LOCAL_PARAMETERS, TIMEOUT, VERSION, FILE_PARAMETERS,
67                                              ACTION, INVENTORY_NAMES, AUTO_NODE_LIST};
68         JSONObject jsonPayload = new JSONObject();
69
70         for (String key : mandatoryTestParams) {
71             throwIfMissingMandatoryParam(params, key);
72             jsonPayload.put(key, params.get(key));
73         }
74
75         parseOptionalParams(params, optionalTestParams, jsonPayload);
76
77         // Generate a unique uuid for the test
78         String reqId = UUID.randomUUID().toString();
79         jsonPayload.put(ID, reqId);
80         return jsonPayload;
81     }
82
83     /**
84      * Method that validates that the Map has enough information
85      * to query Ansible server for a result. If so, it returns
86      * the appropriate url, else an empty string.
87      */
88     public String reqUriResult(Map<String, String> params) throws SvcLogicException {
89         final String[] mandatoryTestParams = {AGENT_URL, ID, USER, PSWD};
90         for (String key : mandatoryTestParams) {
91             throwIfMissingMandatoryParam(params, key);
92         }
93         return params.get(AGENT_URL) + "?Id=" + params.get(ID) + "&Type=GetResult";
94     }
95
96     /**
97      * Method that validates that the Map has enough information to query Ansible
98      * server for a result. If so, it returns the appropriate url, else an empty
99      * string.
100      */
101     public String reqUriResultWithIP(Map<String, String> params, String serverIP) throws SvcLogicException {
102         final String[] mandatoryTestParams = {AGENT_URL, ID, USER, PSWD};
103         for (String key : mandatoryTestParams) {
104             throwIfMissingMandatoryParam(params, key);
105         }
106         String[] arr1 = params.get(AGENT_URL).split("//", 2);
107         String[] arr2 = arr1[1].split(":", 2);
108         return arr1[0] + "//" + serverIP + ":" + arr2[1] + "?Id=" + params.get(ID) + "&Type=GetResult";
109     }
110
111     /**
112      * Method that validates that the Map has enough information to query Ansible
113      * server for logs. If so, it populates the appropriate returns the appropriate
114      * url, else an empty string.
115      */
116     public String reqUriLog(Map<String, String> params) throws SvcLogicException {
117         final String[] mandatoryTestParams = {AGENT_URL, ID, USER, PSWD};
118         for (String mandatoryParam : mandatoryTestParams) {
119             throwIfMissingMandatoryParam(params, mandatoryParam);
120         }
121         return params.get(AGENT_URL) + "?Id=" + params.get(ID) + "&Type=GetLog";
122     }
123
124     /**
125      * Method that validates that the Map has enough information
126      * to query Ansible server for an output. If so, it returns
127      * the appropriate url, else an empty string.
128      */
129     public String reqUriOutput(Map<String, String> params) throws SvcLogicException {
130         final String[] mandatoryTestParams = {AGENT_URL, ID, USER, PSWD};
131         for (String mandatoryParam : mandatoryTestParams) {
132             throwIfMissingMandatoryParam(params, mandatoryParam);
133         }
134         return params.get(AGENT_URL) + "?Id=" + params.get(ID) + "&Type=GetOutput";
135     }
136
137     /**
138      * This method parses response from the Ansible Server when we do a post
139      * and returns an AnsibleResult object.
140      */
141     public AnsibleResult parsePostResponse(String input) throws SvcLogicException {
142         AnsibleResult ansibleResult;
143         try {
144             JSONObject postResponse = new JSONObject(input);
145             int code = postResponse.getInt(STATUS_CODE);
146             int initResponseValue = AnsibleResultCodes.INITRESPONSE.getValue();
147             boolean validCode = AnsibleResultCodes.CODE.checkValidCode(initResponseValue, code);
148             if (!validCode) {
149                 throw new SvcLogicException(String.format("Invalid InitResponse code = %s received. MUST be one of %s",
150                         code, AnsibleResultCodes.CODE.getValidCodes(initResponseValue)));
151             }
152
153             ansibleResult = new AnsibleResult(code, postResponse.getString(STATUS_MESSAGE));
154             if (postResponse.has(ANSIBLE_SERVER) && StringUtils.isNotBlank(postResponse.getString(ANSIBLE_SERVER))) {
155                 ansibleResult.setServerIp(postResponse.getString(ANSIBLE_SERVER));
156             }
157             if (!postResponse.isNull(OUTPUT)) {
158                 LOGGER.info("Processing results-output in post response");
159                 JSONObject output = postResponse.getJSONObject(OUTPUT);
160                 ansibleResult.setOutput(output.toString());
161             }
162         } catch (JSONException e) {
163             LOGGER.error(JSON_ERROR_MESSAGE, e);
164             ansibleResult = new AnsibleResult(600, "Error parsing response = " + input + ". Error = " + e.getMessage());
165         }
166         return ansibleResult;
167     }
168
169     /**
170      * This method parses response from an Ansible server when we do a GET for a result
171      * and returns an AnsibleResult object.
172      **/
173     public AnsibleResult parseGetResponse(String input) throws SvcLogicException {
174         AnsibleResult ansibleResult = new AnsibleResult();
175         try {
176             JSONObject postResponse = new JSONObject(input);
177             parseGetResponseNested(ansibleResult, postResponse);
178         } catch (JSONException e) {
179             LOGGER.error(JSON_ERROR_MESSAGE, e);
180             ansibleResult = new AnsibleResult(AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
181                     "Error parsing response = " + input + ". Error = " + e.getMessage(), "");
182         }
183         return ansibleResult;
184     }
185
186     private void parseGetResponseNested(AnsibleResult ansibleResult, JSONObject postRsp) throws SvcLogicException {
187         String messageStatus = postRsp.getString(STATUS_MESSAGE);
188         int codeStatus = postRsp.getInt(STATUS_CODE);
189         int finalCode = AnsibleResultCodes.FINAL_SUCCESS.getValue();
190         boolean valCode = AnsibleResultCodes.CODE.checkValidCode(AnsibleResultCodes.FINALRESPONSE.getValue(), codeStatus);
191         if (!valCode) {
192             throw new SvcLogicException(String.format("Invalid InitResponse code = %s received. MUST be one of %s",
193                     codeStatus, AnsibleResultCodes.CODE.getValidCodes(AnsibleResultCodes.FINALRESPONSE.getValue())));
194         }
195
196         ansibleResult.setStatusCode(codeStatus);
197         ansibleResult.setStatusMessage(messageStatus);
198         ansibleResult.setConfigData("UNKNOWN");
199         LOGGER.info("Received response with code = {}, Message = {}", codeStatus, messageStatus);
200
201         if (!postRsp.isNull("Results")) {
202
203             // Results are available. process them
204             // Results is a dictionary of the form
205
206             LOGGER.info("Processing results in response");
207             JSONObject results = postRsp.getJSONObject("Results");
208
209             LOGGER.info("Get JSON dictionary from Results by Iterating through hosts");
210             Iterator<String> hosts = results.keys();
211             while (hosts.hasNext()) {
212                 String host = hosts.next();
213                 LOGGER.info("Processing host = {}",
214                         (host.matches("^[\\w\\-.]+$")) ? host : "[unexpected value, logging suppressed]");
215                 try {
216                     JSONObject hostResponse = results.getJSONObject(host);
217                     int subCode = hostResponse.getInt(STATUS_CODE);
218                     String message = hostResponse.getString(STATUS_MESSAGE);
219
220                     LOGGER.info("Code = {}, Message = {}", subCode, message);
221
222                     if (subCode != 200 || !"SUCCESS".equals(message)) {
223                         finalCode = AnsibleResultCodes.REQ_FAILURE.getValue();
224                     }
225                     if ((hostResponse.optJSONObject(OUTPUT)) != null) {
226                         JSONObject hostResponseObjectInfo = hostResponse.optJSONObject(OUTPUT).optJSONObject("info");
227                         JSONObject hostResponseConfigData = hostResponseObjectInfo.optJSONObject("configData");
228                         if (hostResponseConfigData != null) {
229                             ansibleResult.setConfigData(hostResponseConfigData.toString());
230                         }
231                     }
232                 } catch (JSONException e) {
233                     LOGGER.error(JSON_ERROR_MESSAGE, e);
234                     ansibleResult.setStatusCode(AnsibleResultCodes.INVALID_RESPONSE.getValue());
235                     ansibleResult.setStatusMessage(String.format("Error processing response message = %s from host %s",
236                             results.getString(host), host));
237                     break;
238                 }
239             }
240             ansibleResult.setStatusCode(finalCode);
241
242             // We return entire Results object as message
243             ansibleResult.setResults(results.toString());
244         } else {
245             ansibleResult.setStatusCode(AnsibleResultCodes.INVALID_RESPONSE.getValue());
246             ansibleResult.setStatusMessage("Results not found in GET for response");
247         }
248         if (!postRsp.isNull(OUTPUT)) {
249             LOGGER.info("Processing results-output in response");
250             JSONObject output = postRsp.getJSONObject(OUTPUT);
251             ansibleResult.setOutput(output.toString());
252         }
253     }
254
255     private void parseOptionalParams(Map<String, String> params, String[] optionalTestParams, JSONObject jsonPayload) {
256
257         Set<String> optionalParamsSet = new HashSet<>();
258         Collections.addAll(optionalParamsSet, optionalTestParams);
259
260         //@formatter:off
261         params.entrySet().stream().filter(entry -> optionalParamsSet.contains(entry.getKey()))
262                 .filter(entry -> !Strings.isNullOrEmpty(entry.getValue()))
263                 .forEach(entry -> parseOptionalParam(entry, jsonPayload));
264         //@formatter:on
265     }
266
267     private void parseOptionalParam(Map.Entry<String, String> params, JSONObject jsonPayload) {
268         String key = params.getKey();
269         String payload = params.getValue();
270
271         switch (key) {
272             case TIMEOUT:
273                 if (dataIsVariable(payload)) {
274                     break;
275                 }
276                 int timeout = Integer.parseInt(payload);
277                 if (timeout < 0) {
278                     throw new NumberFormatException(" : specified negative integer for timeout = " + payload);
279                 }
280                 jsonPayload.put(key, payload);
281                 break;
282             case AUTO_NODE_LIST:
283                 if (payload.equalsIgnoreCase("true") || payload.equalsIgnoreCase("false")) {
284                     jsonPayload.put(key, payload);
285                 } else {
286                     throw new IllegalArgumentException(" : specified invalid boolean value of AutoNodeList = " + payload);
287                 }
288                 break;
289             case VERSION:
290                 if (dataIsVariable(payload)) {
291                     break;
292                 }
293             case INVENTORY_NAMES:
294                 jsonPayload.put(key, payload);
295                 break;
296
297             case LOCAL_PARAMETERS:
298             case ENV_PARAMETERS:
299             case EXTRA_VARS:
300                 JSONObject paramsJson = new JSONObject(payload);
301                 jsonDataIsVariable(paramsJson);
302                 jsonPayload.put(key, paramsJson);
303                 break;
304
305             case NODE_LIST:
306                 if (payload.startsWith("$")) {
307                     break;
308                 }
309                 JSONArray paramsArray = new JSONArray(payload);
310                 jsonPayload.put(key, paramsArray);
311                 break;
312
313             case FILE_PARAMETERS:
314                 if (dataIsVariable(payload)) {
315                     break;
316                 }
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 SvcLogicException {
334         if (!params.containsKey(key)) {
335             throw new SvcLogicException(String.format(
336                     "Ansible: Mandatory AnsibleAdapter key %s not found in parameters provided by calling agent !", key));
337         }
338         if (Strings.isNullOrEmpty(params.get(key))) {
339             throw new SvcLogicException(String.format(
340                     "Ansible: Mandatory AnsibleAdapter key %s not found in parameters provided by calling agent !", key));
341         }
342         if (StringUtils.startsWith(params.get(key), "$")) {
343             throw new SvcLogicException(String.format(
344                     "Ansible: Mandatory AnsibleAdapter key %s is a variable", key));
345         }
346     }
347
348     private boolean dataIsVariable(String payload) {
349         return StringUtils.startsWith(payload, "$") || StringUtils.isEmpty(payload);
350     }
351
352     private void jsonDataIsVariable(JSONObject paramsJson) {
353         LOGGER.info("input json is " + paramsJson);
354         String[] keys = JSONObject.getNames(paramsJson);
355         for (String k : keys) {
356             Object a = paramsJson.get(k);
357             if (a instanceof String) {
358                 if (StringUtils.startsWith(a.toString(), "$") || StringUtils.isEmpty(a.toString())) {
359                     LOGGER.info("removing key " + k);
360                     paramsJson.remove(k);
361                 }
362             }
363         }
364         LOGGER.info("returning json as {}", paramsJson);
365     }
366
367 }