saltstack reqExecSlsFile API implemented
[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
32 import com.google.common.base.Strings;
33 import org.json.JSONArray;
34 import org.codehaus.jettison.json.JSONException;
35 import org.json.JSONObject;
36 import org.onap.ccsdk.sli.core.sli.SvcLogicContext;
37 import org.onap.ccsdk.sli.core.sli.SvcLogicException;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileNotFoundException;
44 import java.io.InputStream;
45 import java.util.Collections;
46 import java.util.HashSet;
47 import java.util.Iterator;
48 import java.util.Map;
49 import java.util.Properties;
50 import java.util.Set;
51 import java.util.UUID;
52
53 /**
54  * Class that validates and constructs requests sent/received from
55  * Saltstack Server
56  */
57 //TODO: This class is to be altered completely based on the SALTSTACK server communicaiton.
58 public class SaltstackMessageParser {
59
60     private static final String STATUS_MESSAGE_KEY = "StatusMessage";
61     private static final String STATUS_CODE_KEY = "StatusCode";
62
63     private static final String SALTSTATE_NAME_KEY = "SaltStateName";
64     private static final String SS_AGENT_HOSTNAME_KEY = "HostName";
65     private static final String SS_AGENT_PORT_KEY = "Port";
66     private static final String PASS_KEY = "Password";
67     private static final String USER_KEY = "User";
68     private static final String CMD_EXEC = "cmd";
69     private static final String IS_SLS_EXEC = "slsExec";
70     private static final String SS_REQ_ID = "Id";
71     private static final String SLS_FILE_LOCATION = "slsFile";
72     private static final String SLS_NAME = "slsName";
73     private static final String MINION_TO_APPLY = "applyTo";
74
75     private static final String LOCAL_PARAMETERS_OPT_KEY = "LocalParameters";
76     private static final String FILE_PARAMETERS_OPT_KEY = "FileParameters";
77     private static final String ENV_PARAMETERS_OPT_KEY = "EnvParameters";
78     private static final String NODE_LIST_OPT_KEY = "NodeList";
79     private static final String TIMEOUT_OPT_KEY = "Timeout";
80     private static final String VERSION_OPT_KEY = "Version";
81     private static final String ACTION_OPT_KEY = "Action";
82
83     private static final Logger LOGGER = LoggerFactory.getLogger(SaltstackMessageParser.class);
84
85     /**
86      * Accepts a map of strings and
87      * a) validates if all parameters are appropriate (else, throws an exception) and
88      * b) if correct returns a JSON object with appropriate key-value pairs to send to the server.
89      * <p>
90      * Mandatory parameters, that must be in the supplied information to the Saltstack Adapter
91      * 1. URL to connect to
92      * 2. credentials for URL (assume username password for now)
93      * 3. SaltState name
94      */
95     public JSONObject reqMessage(Map<String, String> params) throws SvcLogicException {
96         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SALTSTATE_NAME_KEY, USER_KEY, PASS_KEY};
97         final String[] optionalTestParams = {ENV_PARAMETERS_OPT_KEY, NODE_LIST_OPT_KEY, LOCAL_PARAMETERS_OPT_KEY,
98                 TIMEOUT_OPT_KEY, VERSION_OPT_KEY, FILE_PARAMETERS_OPT_KEY, ACTION_OPT_KEY};
99
100         JSONObject jsonPayload = new JSONObject();
101
102         for (String key : mandatoryTestParams) {
103             throwIfMissingMandatoryParam(params, key);
104             jsonPayload.put(key, params.get(key));
105         }
106
107         parseOptionalParams(params, optionalTestParams, jsonPayload);
108
109         // Generate a unique uuid for the test
110         String reqId = UUID.randomUUID().toString();
111         jsonPayload.put(SS_AGENT_HOSTNAME_KEY, reqId);
112
113         return jsonPayload;
114     }
115
116     /**
117      * Method that validates that the Map has enough information
118      * to query Saltstack server for a result. If so, it returns
119      * the appropriate PORT number.
120      */
121     public String reqPortResult(Map<String, String> params) throws SvcLogicException {
122
123         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY,
124                 PASS_KEY};
125
126         for (String key : mandatoryTestParams) {
127             throwIfMissingMandatoryParam(params, key);
128         }
129         return params.get(SS_AGENT_PORT_KEY);
130     }
131
132     /**
133      * Method that validates that the Map has enough information
134      * to query Saltstack server for a result. If so, it returns
135      * the appropriate HOST name.
136      */
137     public String reqHostNameResult(Map<String, String> params) throws SvcLogicException {
138
139         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY,
140                 PASS_KEY};
141
142         for (String key : mandatoryTestParams) {
143             throwIfMissingMandatoryParam(params, key);
144         }
145         return params.get(SS_AGENT_HOSTNAME_KEY);
146     }
147
148     /**
149      * Method that validates that the Map has enough information
150      * to query Saltstack server for a result. If so, it returns
151      * the appropriate request ID.
152      */
153     public String reqId(Map<String, String> params) {
154
155         if (params.get(SaltstackMessageParser.SS_REQ_ID) == null) {
156             return UUID.randomUUID().toString();
157         } else if (params.get(SaltstackMessageParser.SS_REQ_ID).equalsIgnoreCase("")) {
158             return UUID.randomUUID().toString();
159         }
160         return params.get(SaltstackMessageParser.SS_REQ_ID);
161     }
162
163     /**
164      * Method that validates that the Map has enough information
165      * to query Saltstack server for a result. If so, it returns
166      * the appropriate command to execute.
167      */
168     public String reqCmd(Map<String, String> params) throws SvcLogicException {
169
170         final String[] mandatoryTestParams = {CMD_EXEC, IS_SLS_EXEC};
171
172         for (String key : mandatoryTestParams) {
173             throwIfMissingMandatoryParam(params, key);
174         }
175
176         return params.get(SaltstackMessageParser.CMD_EXEC);
177     }
178
179     /**
180      * Method that validates that the Map has enough information
181      * to query Saltstack server for a result. If so, it returns
182      * the appropriate SLS file location to execute.
183      */
184     public String reqSlsFile(Map<String, String> params) throws SvcLogicException {
185
186         final String[] mandatoryTestParams = {SLS_FILE_LOCATION};
187
188         for (String key : mandatoryTestParams) {
189             throwIfMissingMandatoryParam(params, key);
190         }
191
192         return params.get(SaltstackMessageParser.SLS_FILE_LOCATION);
193     }
194
195     /**
196      * Method that validates that the Map has enough information
197      * to query Saltstack server for a result. If so, it returns
198      * the appropriate SLS file location to execute.
199      */
200     public String reqSlsName(Map<String, String> params) throws SvcLogicException {
201
202         final String[] mandatoryTestParams = {SLS_NAME};
203
204         for (String key : mandatoryTestParams) {
205             throwIfMissingMandatoryParam(params, key);
206         }
207         String slsName = params.get(SaltstackMessageParser.SLS_NAME);
208         try {
209             if(slsName.substring(slsName.lastIndexOf("."), slsName.length()).equalsIgnoreCase(".sls"))
210                 return stripExtension(slsName);
211         } catch (StringIndexOutOfBoundsException e) {
212             return slsName;
213         }
214         return slsName;
215     }
216
217     private String stripExtension (String str) {
218         if (str == null) return null;
219         int pos = str.lastIndexOf(".");
220         if (pos == -1) return str;
221         return str.substring(0, pos);
222     }
223
224     /**
225      * Method that validates that the Map has enough information
226      * to query Saltstack server for a result. If so, it returns
227      * the appropriate minions/vnfc to execute the SLS file to.
228      */
229     public String reqApplyToDevices(Map<String, String> params) {
230
231         if (params.get(SaltstackMessageParser.MINION_TO_APPLY) == null) {
232             return "*";
233         } else if (params.get(SaltstackMessageParser.MINION_TO_APPLY).equalsIgnoreCase("")) {
234             return "*";
235         }
236         return params.get(SaltstackMessageParser.MINION_TO_APPLY);
237     }
238
239     /**
240      * Method that validates that the Map has enough information
241      * to query Saltstack server for a result. If so, it returns
242      * the appropriate IsSLSExec true or false.
243      */
244     public boolean reqIsSLSExec(Map<String, String> params) throws SvcLogicException {
245
246         final String[] mandatoryTestParams = {CMD_EXEC, IS_SLS_EXEC};
247
248         for (String key : mandatoryTestParams) {
249             throwIfMissingMandatoryParam(params, key);
250         }
251
252         return params.get(SaltstackMessageParser.IS_SLS_EXEC).equalsIgnoreCase("true");
253     }
254
255     /**
256      * Method that validates that the Map has enough information
257      * to query Saltstack server for a result. If so, it returns
258      * the appropriate Saltstack server login user name.
259      */
260     public String reqUserNameResult(Map<String, String> params) throws SvcLogicException {
261
262         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY,
263                 PASS_KEY};
264
265         for (String key : mandatoryTestParams) {
266             throwIfMissingMandatoryParam(params, key);
267         }
268         return params.get(USER_KEY);
269     }
270
271     /**
272      * Method that validates that the Map has enough information
273      * to query Saltstack server for a result. If so, it returns
274      * the appropriate Saltstack server login password.
275      */
276     public String reqPasswordResult(Map<String, String> params) throws SvcLogicException {
277
278         final String[] mandatoryTestParams = {SS_AGENT_HOSTNAME_KEY, SS_AGENT_PORT_KEY, USER_KEY,
279                 PASS_KEY};
280
281         for (String key : mandatoryTestParams) {
282             throwIfMissingMandatoryParam(params, key);
283         }
284         return params.get(PASS_KEY);
285     }
286
287     /**
288      * This method parses response from the Saltstack Server when we do a post
289      * and returns an SaltstackResult object.
290      */
291     public SaltstackResult parseResponse(SvcLogicContext ctx, String pfx,
292                                          SaltstackResult saltstackResult, boolean slsExec) {
293         int code = saltstackResult.getStatusCode();
294         boolean executionStatus = true, retCodeFound = false;
295         if (code != SaltstackResultCodes.SUCCESS.getValue()) {
296             return saltstackResult;
297         }
298         try {
299             File file = new File(saltstackResult.getOutputFileName());
300             InputStream in = new FileInputStream(file);
301             byte[] data = new byte[(int) file.length()];
302             in.read(data);
303             String str = new String(data, "UTF-8");
304             in.close();
305             Map<String, String> mm = JsonParser.convertToProperties(str);
306             if (mm != null) {
307                 for (Map.Entry<String, String> entry : mm.entrySet()) {
308                     if (entry.getKey().contains("retcode")) {
309                         retCodeFound = true;
310                         if (!entry.getValue().equalsIgnoreCase("0")) {
311                             executionStatus = false;
312                         }
313                     }
314                     ctx.setAttribute(pfx + "." + entry.getKey(), entry.getValue());
315                     LOGGER.info("+++ " + pfx + "." + entry.getKey() + ": [" + entry.getValue() + "]");
316                 }
317             }
318         } catch (FileNotFoundException e) {
319             return new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "error parsing response file "
320                     + saltstackResult.getOutputFileName() + " : " + e.getMessage());
321         } catch (JSONException e) {
322             LOGGER.info("Output not in JSON format");
323             return putToProperties(ctx, pfx, saltstackResult);
324         } catch (Exception e) {
325             return new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "error parsing response file "
326                     + saltstackResult.getOutputFileName() + " : " + e.getMessage());
327         }
328         if (slsExec) {
329             if (!retCodeFound)
330                 return new SaltstackResult(SaltstackResultCodes.COMMAND_EXEC_FAILED_STATUS.getValue(),
331                                            "error in parsing response Json after SLS file execution in server");
332             if (!executionStatus)
333                 return new SaltstackResult(SaltstackResultCodes.COMMAND_EXEC_FAILED_STATUS.getValue(),
334                                            "error in parsing response Json after SLS file execution in server");
335         }
336         saltstackResult.setStatusCode(SaltstackResultCodes.FINAL_SUCCESS.getValue());
337         return saltstackResult;
338     }
339
340     public SaltstackResult putToProperties(SvcLogicContext ctx, String pfx, SaltstackResult saltstackResult) {
341         try {
342             File file = new File(saltstackResult.getOutputFileName());
343             InputStream in = new FileInputStream(file);
344             Properties prop = new Properties();
345             prop.load(in);
346             ctx.setAttribute(pfx + "completeResult", prop.toString());
347             for (Object key : prop.keySet()) {
348                 String name = (String) key;
349                 String value = prop.getProperty(name);
350                 if (value != null && value.trim().length() > 0) {
351                     ctx.setAttribute(pfx + name, value.trim());
352                     LOGGER.info("+++ " + pfx + name + ": [" + value + "]");
353                 }
354             }
355         } catch (Exception e) {
356             saltstackResult = new SaltstackResult(SaltstackResultCodes.INVALID_RESPONSE_FILE.getValue(), "Error parsing response file = "
357                     + saltstackResult.getOutputFileName() + ". Error = " + e.getMessage());
358         }
359         saltstackResult.setStatusCode(SaltstackResultCodes.FINAL_SUCCESS.getValue());
360         return saltstackResult;
361     }
362
363     /**
364      * This method parses response from an Saltstack server when we do a GET for a result
365      * and returns an SaltstackResult object.
366      **/
367     public SaltstackResult parseGetResponse(String input) throws SvcLogicException {
368
369         SaltstackResult saltstackResult = new SaltstackResult();
370
371         try {
372             JSONObject postResponse = new JSONObject(input);
373             saltstackResult = parseGetResponseNested(saltstackResult, postResponse);
374         } catch (Exception e) {
375             saltstackResult = new SaltstackResult(SaltstackResultCodes.INVALID_COMMAND.getValue(),
376                                                   "Error parsing response = " + input + ". Error = " + e.getMessage(), "", -1);
377         }
378         return saltstackResult;
379     }
380
381     private SaltstackResult parseGetResponseNested(SaltstackResult saltstackResult, JSONObject postRsp) throws SvcLogicException {
382
383         int codeStatus = postRsp.getInt(STATUS_CODE_KEY);
384         String messageStatus = postRsp.getString(STATUS_MESSAGE_KEY);
385         int finalCode = SaltstackResultCodes.FINAL_SUCCESS.getValue();
386
387         boolean valCode =
388                 SaltstackResultCodes.CODE.checkValidCode(SaltstackResultCodes.FINALRESPONSE.getValue(), codeStatus);
389
390         if (!valCode) {
391             throw new SvcLogicException("Invalid FinalResponse code  = " + codeStatus + " received. MUST be one of "
392                                                 + SaltstackResultCodes.CODE.getValidCodes(SaltstackResultCodes.FINALRESPONSE.getValue()));
393         }
394
395         saltstackResult.setStatusCode(codeStatus);
396         saltstackResult.setStatusMessage(messageStatus);
397         LOGGER.info("Received response with code = {}, Message = {}", codeStatus, messageStatus);
398
399         if (!postRsp.isNull("Results")) {
400
401             // Results are available. process them
402             // Results is a dictionary of the form
403             // {host :{status:s, group:g, message:m, hostname:h}, ...}
404             LOGGER.info("Processing results in response");
405             JSONObject results = postRsp.getJSONObject("Results");
406             LOGGER.info("Get JSON dictionary from Results ..");
407             Iterator<String> hosts = results.keys();
408             LOGGER.info("Iterating through hosts");
409
410             while (hosts.hasNext()) {
411                 String host = hosts.next();
412                 LOGGER.info("Processing host = {}", host);
413
414                 try {
415                     JSONObject hostResponse = results.getJSONObject(host);
416                     int subCode = hostResponse.getInt(STATUS_CODE_KEY);
417                     String message = hostResponse.getString(STATUS_MESSAGE_KEY);
418
419                     LOGGER.info("Code = {}, Message = {}", subCode, message);
420
421                     if (subCode != 200 || !message.equals("SUCCESS")) {
422                         finalCode = SaltstackResultCodes.REQ_FAILURE.getValue();
423                     }
424                 } catch (Exception e) {
425                     saltstackResult.setStatusCode(SaltstackResultCodes.INVALID_RESPONSE.getValue());
426                     saltstackResult.setStatusMessage(String.format(
427                             "Error processing response message = %s from host %s", results.getString(host), host));
428                     break;
429                 }
430             }
431
432             saltstackResult.setStatusCode(finalCode);
433
434             // We return entire Results object as message
435             saltstackResult.setResults(results.toString());
436
437         } else {
438             saltstackResult.setStatusCode(SaltstackResultCodes.INVALID_RESPONSE.getValue());
439             saltstackResult.setStatusMessage("Results not found in GET for response");
440         }
441         return saltstackResult;
442     }
443
444     private void parseOptionalParams(Map<String, String> params, String[] optionalTestParams, JSONObject jsonPayload) {
445
446         Set<String> optionalParamsSet = new HashSet<>();
447         Collections.addAll(optionalParamsSet, optionalTestParams);
448
449         //@formatter:off
450         params.entrySet()
451                 .stream()
452                 .filter(entry -> optionalParamsSet.contains(entry.getKey()))
453                 .filter(entry -> !Strings.isNullOrEmpty(entry.getValue()))
454                 .forEach(entry -> parseOptionalParam(entry, jsonPayload));
455         //@formatter:on
456     }
457
458     private void parseOptionalParam(Map.Entry<String, String> params, JSONObject jsonPayload) {
459         String key = params.getKey();
460         String payload = params.getValue();
461
462         switch (key) {
463             case TIMEOUT_OPT_KEY:
464                 int timeout = Integer.parseInt(payload);
465                 if (timeout < 0) {
466                     throw new NumberFormatException(" : specified negative integer for timeout = " + payload);
467                 }
468                 jsonPayload.put(key, payload);
469                 break;
470
471             case VERSION_OPT_KEY:
472                 jsonPayload.put(key, payload);
473                 break;
474
475             case LOCAL_PARAMETERS_OPT_KEY:
476             case ENV_PARAMETERS_OPT_KEY:
477                 JSONObject paramsJson = new JSONObject(payload);
478                 jsonPayload.put(key, paramsJson);
479                 break;
480
481             case NODE_LIST_OPT_KEY:
482                 JSONArray paramsArray = new JSONArray(payload);
483                 jsonPayload.put(key, paramsArray);
484                 break;
485
486             case FILE_PARAMETERS_OPT_KEY:
487                 jsonPayload.put(key, getFilePayload(payload));
488                 break;
489
490             default:
491                 break;
492         }
493     }
494
495     /**
496      * Return payload with escaped newlines
497      */
498     private JSONObject getFilePayload(String payload) {
499         String formattedPayload = payload.replace("\n", "\\n").replace("\r", "\\r");
500         return new JSONObject(formattedPayload);
501     }
502
503     private void throwIfMissingMandatoryParam(Map<String, String> params, String key) throws SvcLogicException {
504         if (!params.containsKey(key)) {
505             throw new SvcLogicException(String.format(
506                     "Saltstack: Mandatory SaltstackAdapter key %s not found in parameters provided by calling agent !",
507                     key));
508         }
509         if (Strings.isNullOrEmpty(params.get(key))) {
510             throw new SvcLogicException(String.format(
511                     "Saltstack: Mandatory SaltstackAdapter key %s not found in parameters provided by calling agent !",
512                     key));
513         }
514     }
515 }