reqExec API implemented for saltstack
[ccsdk/sli/adaptors.git] / saltstack-adapter / saltstack-adapter-provider / src / main / java / org / onap / ccsdk / sli / adaptors / saltstack / model / SaltstackMessageParser.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  * ECOMP is a trademark and service mark of AT&T Intellectual Property.
22  * ============LICENSE_END=========================================================
23  */
24
25 package org.onap.ccsdk.sli.adaptors.saltstack.model;
26
27 /**
28  * This module implements the APP-C/Saltstack Server interface
29  * based on the REST API specifications
30  */
31 import java.io.File;
32 import java.io.FileInputStream;
33 import java.io.FileNotFoundException;
34 import java.io.InputStream;
35 import java.util.Collections;
36 import java.util.HashSet;
37 import java.util.Iterator;
38 import java.util.Map;
39 import java.util.Properties;
40 import java.util.Set;
41 import java.util.UUID;
42 import org.json.JSONArray;
43 import org.json.JSONException;
44 import org.json.JSONObject;
45 import org.onap.ccsdk.sli.core.sli.SvcLogicContext;
46 import org.onap.ccsdk.sli.core.sli.SvcLogicException;
47 import com.google.common.base.Strings;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * Class that validates and constructs requests sent/received from
53  * Saltstack Server
54  */
55 //TODO: This class is to be altered completely based on the SALTSTACK server communicaiton.
56 public class SaltstackMessageParser {
57
58     private static final String STATUS_MESSAGE_KEY = "StatusMessage";
59     private static final String STATUS_CODE_KEY = "StatusCode";
60
61     private static final String SALTSTATE_NAME_KEY = "SaltStateName";
62     private static final String SS_AGENT_HOSTNAME_KEY = "HostName";
63     private static final String SS_AGENT_PORT_KEY = "Port";
64     private static final String PASS_KEY = "Password";
65     private static final String USER_KEY = "User";
66
67     private static final String LOCAL_PARAMETERS_OPT_KEY = "LocalParameters";
68     private static final String FILE_PARAMETERS_OPT_KEY = "FileParameters";
69     private static final String ENV_PARAMETERS_OPT_KEY = "EnvParameters";
70     private static final String NODE_LIST_OPT_KEY = "NodeList";
71     private static final String TIMEOUT_OPT_KEY = "Timeout";
72     private static final String VERSION_OPT_KEY = "Version";
73     private static final String ACTION_OPT_KEY = "Action";
74
75     private static final Logger LOGGER = LoggerFactory.getLogger(SaltstackMessageParser.class);
76
77     /**
78      * Accepts a map of strings and
79      * a) validates if all parameters are appropriate (else, throws an exception) and
80      * b) if correct returns a JSON object with appropriate key-value pairs to send to the server.
81      *
82      * Mandatory parameters, that must be in the supplied information to the Saltstack Adapter
83      * 1. URL to connect to
84      * 2. credentials for URL (assume username password for now)
85      * 3. SaltState name
86      *
87      */
88     public JSONObject reqMessage(Map<String, String> params) throws SvcLogicException {
89         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SALTSTATE_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};
92
93         JSONObject jsonPayload = new JSONObject();
94
95         for (String key : mandatoryTestParams) {
96             throwIfMissingMandatoryParam(params, key);
97             jsonPayload.put(key, params.get(key));
98         }
99
100         parseOptionalParams(params, optionalTestParams, jsonPayload);
101
102         // Generate a unique uuid for the test
103         String reqId = UUID.randomUUID().toString();
104         jsonPayload.put(SS_AGENT_HOSTNAME_KEY, reqId);
105
106         return jsonPayload;
107     }
108
109     /**
110      * Method that validates that the Map has enough information
111      * to query Saltstack server for a result. If so, it returns
112      * the appropriate PORT number.
113      */
114     public String reqPortResult(Map<String, String> params) throws SvcLogicException {
115
116         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY, PASS_KEY};
117
118         for (String key : mandatoryTestParams) {
119             throwIfMissingMandatoryParam(params, key);
120         }
121         return params.get(SS_AGENT_PORT_KEY);
122     }
123
124     /**
125      * Method that validates that the Map has enough information
126      * to query Saltstack server for a result. If so, it returns
127      * the appropriate HOST name.
128      */
129     public String reqHostNameResult(Map<String, String> params) throws SvcLogicException {
130
131         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY, PASS_KEY};
132
133         for (String key : mandatoryTestParams) {
134             throwIfMissingMandatoryParam(params, key);
135         }
136         return params.get(SS_AGENT_HOSTNAME_KEY);
137     }
138
139     /**
140      * Method that validates that the Map has enough information
141      * to query Saltstack server for a result. If so, it returns
142      * the appropriate Saltstack server login user name.
143      */
144     public String reqUserNameResult(Map<String, String> params) throws SvcLogicException {
145
146         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY, PASS_KEY};
147
148         for (String key : mandatoryTestParams) {
149             throwIfMissingMandatoryParam(params, key);
150         }
151         return params.get(USER_KEY);
152     }
153
154     /**
155      * Method that validates that the Map has enough information
156      * to query Saltstack server for a result. If so, it returns
157      * the appropriate Saltstack server login password.
158      */
159     public String reqPasswordResult(Map<String, String> params) throws SvcLogicException {
160
161         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY, PASS_KEY};
162
163         for (String key : mandatoryTestParams) {
164             throwIfMissingMandatoryParam(params, key);
165         }
166         return params.get(PASS_KEY);
167     }
168
169     /**
170      * This method parses response from the Saltstack Server when we do a post
171      * and returns an SaltstackResult object.
172      */
173     public SaltstackResult parseResponse(SvcLogicContext ctx, String pfx, SaltstackResult saltstackResult) {
174         int code = saltstackResult.getStatusCode();
175         if (code != SaltstackResultCodes.SUCCESS.getValue()) {
176             return saltstackResult;
177         }
178         try {
179             File file = new File(saltstackResult.getOutputFileName());
180             InputStream in = new FileInputStream(file);
181             byte[] data = new byte[(int) file.length()];
182             in.read(data);
183             String str = new String(data, "UTF-8");
184             in.close();
185             Map<String, String> mm = JsonParser.convertToProperties(str);
186             if (mm != null) {
187                 for (Map.Entry<String,String> entry : mm.entrySet()) {
188                     ctx.setAttribute(pfx + entry.getKey(), entry.getValue());
189                     LOGGER.info("+++ " + pfx + entry.getKey() + ": [" + entry.getValue() + "]");
190                 }
191             }
192         } catch (FileNotFoundException e){
193             return new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "Error parsing response file = "
194                     + saltstackResult.getOutputFileName() + ". Error = " + e.getMessage());
195         } catch (JSONException e) {
196             LOGGER.info("Output not in JSON format");
197             return putToProperties(ctx, pfx, saltstackResult);
198         } catch (Exception e) {
199             return new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "Error parsing response file = "
200                     + saltstackResult.getOutputFileName() + ". Error = " + e.getMessage());
201         }
202         saltstackResult.setStatusCode(SaltstackResultCodes.FINAL_SUCCESS.getValue());
203         return saltstackResult;
204     }
205
206     public SaltstackResult putToProperties(SvcLogicContext ctx, String pfx, SaltstackResult saltstackResult) {
207         try {
208             File file = new File(saltstackResult.getOutputFileName());
209             InputStream in = new FileInputStream(file);
210             Properties prop = new Properties();
211             prop.load(in);
212             ctx.setAttribute(pfx + "completeResult", prop.toString());
213             for (Object key : prop.keySet()) {
214                 String name = (String) key;
215                 String value = prop.getProperty(name);
216                 if (value != null && value.trim().length() > 0) {
217                     ctx.setAttribute(pfx + name, value.trim());
218                     LOGGER.info("+++ " + pfx + name + ": [" + value + "]");
219                 }
220             }
221         } catch (Exception e) {
222             saltstackResult = new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "Error parsing response file = "
223                     + saltstackResult.getOutputFileName() + ". Error = " + e.getMessage());
224         }
225         return saltstackResult;
226     }
227     /**
228      * This method parses response from an Saltstack server when we do a GET for a result
229      * and returns an SaltstackResult object.
230      **/
231     public SaltstackResult parseGetResponse(String input) throws SvcLogicException {
232
233         SaltstackResult saltstackResult = new SaltstackResult();
234
235         try {
236             JSONObject postResponse = new JSONObject(input);
237             saltstackResult = parseGetResponseNested(saltstackResult, postResponse);
238         } catch (JSONException e) {
239             saltstackResult = new SaltstackResult(SaltstackResultCodes.INVALID_COMMAND.getValue(),
240                     "Error parsing response = " + input + ". Error = " + e.getMessage(), "", -1);
241         }
242         return saltstackResult;
243     }
244
245     private SaltstackResult parseGetResponseNested(SaltstackResult saltstackResult, JSONObject postRsp) throws SvcLogicException  {
246
247         int codeStatus = postRsp.getInt(STATUS_CODE_KEY);
248         String messageStatus = postRsp.getString(STATUS_MESSAGE_KEY);
249         int finalCode = SaltstackResultCodes.FINAL_SUCCESS.getValue();
250
251         boolean valCode =
252                 SaltstackResultCodes.CODE.checkValidCode(SaltstackResultCodes.FINALRESPONSE.getValue(), codeStatus);
253
254         if (!valCode) {
255             throw new SvcLogicException("Invalid FinalResponse code  = " + codeStatus + " received. MUST be one of "
256                     + SaltstackResultCodes.CODE.getValidCodes(SaltstackResultCodes.FINALRESPONSE.getValue()));
257         }
258
259         saltstackResult.setStatusCode(codeStatus);
260         saltstackResult.setStatusMessage(messageStatus);
261         LOGGER.info("Received response with code = {}, Message = {}", codeStatus, messageStatus);
262
263         if (!postRsp.isNull("Results")) {
264
265             // Results are available. process them
266             // Results is a dictionary of the form
267             // {host :{status:s, group:g, message:m, hostname:h}, ...}
268             LOGGER.info("Processing results in response");
269             JSONObject results = postRsp.getJSONObject("Results");
270             LOGGER.info("Get JSON dictionary from Results ..");
271             Iterator<String> hosts = results.keys();
272             LOGGER.info("Iterating through hosts");
273
274             while (hosts.hasNext()) {
275                 String host = hosts.next();
276                 LOGGER.info("Processing host = {}", host);
277
278                 try {
279                     JSONObject hostResponse = results.getJSONObject(host);
280                     int subCode = hostResponse.getInt(STATUS_CODE_KEY);
281                     String message = hostResponse.getString(STATUS_MESSAGE_KEY);
282
283                     LOGGER.info("Code = {}, Message = {}", subCode, message);
284
285                     if (subCode != 200 || !message.equals("SUCCESS")) {
286                         finalCode = SaltstackResultCodes.REQ_FAILURE.getValue();
287                     }
288                 } catch (JSONException e) {
289                     saltstackResult.setStatusCode(SaltstackResultCodes.INVALID_RESPONSE.getValue());
290                     saltstackResult.setStatusMessage(String.format(
291                             "Error processing response message = %s from host %s", results.getString(host), host));
292                     break;
293                 }
294             }
295
296             saltstackResult.setStatusCode(finalCode);
297
298             // We return entire Results object as message
299             saltstackResult.setResults(results.toString());
300
301         } else {
302             saltstackResult.setStatusCode(SaltstackResultCodes.INVALID_RESPONSE.getValue());
303             saltstackResult.setStatusMessage("Results not found in GET for response");
304         }
305         return saltstackResult;
306     }
307
308     private void parseOptionalParams(Map<String, String> params, String[] optionalTestParams, JSONObject jsonPayload) {
309
310         Set<String> optionalParamsSet = new HashSet<>();
311         Collections.addAll(optionalParamsSet, optionalTestParams);
312
313         //@formatter:off
314         params.entrySet()
315             .stream()
316             .filter(entry -> optionalParamsSet.contains(entry.getKey()))
317             .filter(entry -> !Strings.isNullOrEmpty(entry.getValue()))
318              .forEach(entry -> parseOptionalParam(entry, jsonPayload));
319         //@formatter:on
320     }
321
322     private void parseOptionalParam(Map.Entry<String, String> params, JSONObject jsonPayload) {
323         String key = params.getKey();
324         String payload = params.getValue();
325
326         switch (key) {
327             case TIMEOUT_OPT_KEY:
328                 int timeout = Integer.parseInt(payload);
329                 if (timeout < 0) {
330                     throw new NumberFormatException(" : specified negative integer for timeout = " + payload);
331                 }
332                 jsonPayload.put(key, payload);
333                 break;
334
335             case VERSION_OPT_KEY:
336                 jsonPayload.put(key, payload);
337                 break;
338
339             case LOCAL_PARAMETERS_OPT_KEY:
340             case ENV_PARAMETERS_OPT_KEY:
341                 JSONObject paramsJson = new JSONObject(payload);
342                 jsonPayload.put(key, paramsJson);
343                 break;
344
345             case NODE_LIST_OPT_KEY:
346                 JSONArray paramsArray = new JSONArray(payload);
347                 jsonPayload.put(key, paramsArray);
348                 break;
349
350             case FILE_PARAMETERS_OPT_KEY:
351                 jsonPayload.put(key, getFilePayload(payload));
352                 break;
353
354             default:
355                 break;
356         }
357     }
358
359     /**
360      * Return payload with escaped newlines
361      */
362     private JSONObject getFilePayload(String payload) {
363         String formattedPayload = payload.replace("\n", "\\n").replace("\r", "\\r");
364         return new JSONObject(formattedPayload);
365     }
366
367     private void throwIfMissingMandatoryParam(Map<String, String> params, String key) throws SvcLogicException {
368         if (!params.containsKey(key)) {
369             throw new SvcLogicException(String.format(
370                     "Saltstack: Mandatory SaltstackAdapter key %s not found in parameters provided by calling agent !",
371                     key));
372         }
373         if (Strings.isNullOrEmpty(params.get(key))) {
374             throw new SvcLogicException(String.format(
375                     "Saltstack: Mandatory SaltstackAdapter key %s not found in parameters provided by calling agent !",
376                     key));
377         }
378     }
379 }