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