Added null check to avoid null pointer exception
[appc.git] / appc-adapters / appc-ansible-adapter / appc-ansible-adapter-bundle / src / main / java / org / onap / appc / adapter / ansible / impl / AnsibleAdapterImpl.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP : APPC
4  * ================================================================================
5  * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Copyright (C) 2017 Amdocs
8  * =============================================================================
9  * Modifications Copyright (C) 2019 IBM
10  * =============================================================================
11  * Licensed under the Apache License, Version 2.0 (the "License");
12  * you may not use this file except in compliance with the License.
13  * You may obtain a copy of the License at
14  *
15  *      http://www.apache.org/licenses/LICENSE-2.0
16  *
17  * Unless required by applicable law or agreed to in writing, software
18  * distributed under the License is distributed on an "AS IS" BASIS,
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20  * See the License for the specific language governing permissions and
21  * limitations under the License.
22  *
23  * ============LICENSE_END=========================================================
24  */
25
26 package org.onap.appc.adapter.ansible.impl;
27
28 import java.util.Map;
29 import java.util.Properties;
30 import java.io.File;
31 import java.io.FileInputStream;
32 import java.io.InputStream;
33 import org.apache.commons.lang.StringUtils;
34 import org.json.JSONException;
35 import org.json.JSONObject;
36 import org.onap.appc.adapter.ansible.AnsibleAdapter;
37 import org.onap.appc.adapter.ansible.model.AnsibleMessageParser;
38 import org.onap.appc.adapter.ansible.model.AnsibleResult;
39 import org.onap.appc.adapter.ansible.model.AnsibleResultCodes;
40 import org.onap.appc.adapter.ansible.model.AnsibleServerEmulator;
41 import org.onap.appc.exceptions.APPCException;
42 import org.onap.ccsdk.sli.core.sli.SvcLogicContext;
43 import org.onap.ccsdk.sli.core.sli.SvcLogicException;
44
45 import com.att.eelf.configuration.EELFLogger;
46 import com.att.eelf.configuration.EELFManager;
47 import org.onap.appc.encryption.EncryptionTool;
48
49 /**
50  * This class implements the {@link AnsibleAdapter} interface. This interface
51  * defines the behaviors that our service provides.
52  */
53 public class AnsibleAdapterImpl implements AnsibleAdapter {
54
55     /**
56      * The constant used to define the service name in the mapped diagnostic context
57      */
58     @SuppressWarnings("nls")
59     public static final String MDC_SERVICE = "service";
60
61     /**
62      * The constant for the status code for a failed outcome
63      */
64     @SuppressWarnings("nls")
65     public static final String OUTCOME_FAILURE = "failure";
66
67     /**
68      * The constant for the status code for a successful outcome
69      */
70     @SuppressWarnings("nls")
71     public static final String OUTCOME_SUCCESS = "success";
72
73     /**
74      * Adapter Name
75      */
76     private static final String ADAPTER_NAME = "Ansible Adapter";
77     private static final String APPC_EXCEPTION_CAUGHT = "APPCException caught";
78
79     private static final String RESULT_CODE_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.result.code";
80     private static final String MESSAGE_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.message";
81     private static final String RESULTS_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.results";
82     private static final String OUTPUT_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.output";
83     private static final String ID_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.Id";
84     private static final String LOG_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.log";
85
86     private static final String CLIENT_TYPE_PROPERTY_NAME = "org.onap.appc.adapter.ansible.clientType";
87     private static final String TRUSTSTORE_PROPERTY_NAME = "org.onap.appc.adapter.ansible.trustStore";
88     private static final String TRUSTPASSWD_PROPERTY_NAME = "org.onap.appc.adapter.ansible.trustStore.trustPasswd";
89     private static final String TIMEOUT_PROPERTY_NAME = "org.onap.appc.adapter.ansible.timeout";
90     private static final String POLL_INTERVAL_PROPERTY_NAME = "org.onap.appc.adapter.ansible.pollInterval";
91     private static final String SOCKET_TIMEOUT_PROPERTY_NAME = "org.onap.appc.adapter.ansible.socketTimeout";
92     private static final String PASSWORD = "Password";
93     private static final String APPC_PROPS = "/appc.properties";
94     private static final String SDNC_CONFIG_DIR = "SDNC_CONFIG_DIR";
95     private static final String propDir = System.getenv(SDNC_CONFIG_DIR);
96     private Properties props;
97     private int defaultTimeout = 600 * 1000;
98     private int defaultSocketTimeout = 60 * 1000;
99     private int defaultPollInterval = 60 * 1000;
100
101     /**
102      * The logger to be used
103      */
104     private static final EELFLogger logger = EELFManager.getInstance().getLogger(AnsibleAdapterImpl.class);
105     /**
106      * Connection object
107      **/
108     private ConnectionBuilder httpClient;
109
110     /**
111      * Ansible API Message Handlers
112      **/
113     private AnsibleMessageParser messageProcessor;
114
115     /**
116      * indicator whether in test mode
117      **/
118     private boolean testMode = false;
119
120     /**
121      * server emulator object to be used if in test mode
122      **/
123     private AnsibleServerEmulator testServer;
124
125     /**
126      * This default constructor is used as a work around because the activator
127      * wasn't getting called
128      */
129     public AnsibleAdapterImpl() {
130         initialize();
131     }
132
133     /**
134      * Used for jUnit test and testing interface
135      */
136     public AnsibleAdapterImpl(boolean mode) {
137         testMode = mode;
138         testServer = new AnsibleServerEmulator();
139         messageProcessor = new AnsibleMessageParser();
140     }
141
142     /**
143      * Returns the symbolic name of the adapter
144      *
145      * @return The adapter name
146      * @see org.onap.appc.adapter.rest.AnsibleAdapter#getAdapterName()
147      */
148     @Override
149     public String getAdapterName() {
150         return ADAPTER_NAME;
151     }
152
153     /**
154      * @param rc
155      *            Method posts info to Context memory in case of an error and throws
156      *            a SvcLogicException causing SLI to register this as a failure
157      */
158     @SuppressWarnings("static-method")
159     private void doFailure(SvcLogicContext svcLogic, int code, String message) throws SvcLogicException {
160
161         svcLogic.setStatus(OUTCOME_FAILURE);
162         svcLogic.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, Integer.toString(code));
163         svcLogic.setAttribute(MESSAGE_ATTRIBUTE_NAME, message);
164
165         throw new SvcLogicException("Ansible Adapter Error = " + message);
166     }
167
168     /**
169      * initialize the Ansible adapter based on default and over-ride configuration
170      * data
171      */
172     private void initialize() {
173         String path = propDir + APPC_PROPS;
174         File propFile = new File(path);
175         props = new Properties();
176         try {
177             InputStream input = new FileInputStream(propFile);
178             props.load(input);
179         } catch (Exception ex) {
180             logger.error("Error while reading appc.properties file" + ex.getMessage());
181         }
182         // Create the message processor instance 
183         messageProcessor = new AnsibleMessageParser();
184         //continuing for checking timeout
185         try {
186             String timeoutStr = props.getProperty(TIMEOUT_PROPERTY_NAME);
187             defaultTimeout = Integer.parseInt(timeoutStr) * 1000;
188
189         } catch (Exception e) {
190             defaultTimeout = 600 * 1000;
191         }
192         //continuing for checking timeout
193         try {
194             String timeoutStr = props.getProperty(SOCKET_TIMEOUT_PROPERTY_NAME);
195             defaultSocketTimeout = Integer.parseInt(timeoutStr) * 1000;
196
197         } catch (Exception e) {
198             defaultSocketTimeout = 60 * 1000;
199         }
200         //continuing for checking timeout
201         try {
202             String timeoutStr = props.getProperty(POLL_INTERVAL_PROPERTY_NAME);
203             defaultPollInterval = Integer.parseInt(timeoutStr) * 1000;
204
205         } catch (Exception e) {
206             defaultPollInterval = 60 * 1000;
207         }
208         logger.info("Initialized Ansible Adapter");
209     }
210
211     private ConnectionBuilder getHttpConn(int timeout, String serverIP) {
212
213         String path = propDir + APPC_PROPS;
214         File propFile = new File(path);
215         props = new Properties();
216         InputStream input;
217         try {
218             input = new FileInputStream(propFile);
219             props.load(input);
220         } catch (Exception ex) {
221             // TODO Auto-generated catch block
222             logger.error("Error while reading appc.properties file" + ex.getMessage());
223         }
224         // Create the http client instance
225         // type of client is extracted from the property file parameter
226         // org.onap.appc.adapter.ansible.clientType
227         // It can be :
228         // 1. TRUST_ALL (trust all SSL certs). To be used ONLY in dev
229         // 2. TRUST_CERT (trust only those whose certificates have been stored in the
230         // trustStore file)
231         // 3. DEFAULT (trust only well known certificates). This is standard behavior to
232         // which it will
233         // revert. To be used in PROD
234         ConnectionBuilder httpClient = null;
235         try {
236             String clientType = props.getProperty(CLIENT_TYPE_PROPERTY_NAME);
237             logger.info("Ansible http client type set to " + clientType);
238
239             if ("TRUST_ALL".equals(clientType)) {
240                 logger.info(
241                         "Creating http client to trust ALL ssl certificates. WARNING. This should be done only in dev environments");
242                 httpClient = new ConnectionBuilder(1, timeout);
243             } else if ("TRUST_CERT".equals(clientType)) {
244                 // set path to keystore file
245                 String trustStoreFile = props.getProperty(TRUSTSTORE_PROPERTY_NAME);
246                 String key = props.getProperty(TRUSTPASSWD_PROPERTY_NAME);
247                 char[] trustStorePasswd = EncryptionTool.getInstance().decrypt(key).toCharArray();
248                 logger.info("Creating http client with trustmanager from " + trustStoreFile);
249                 httpClient = new ConnectionBuilder(trustStoreFile, trustStorePasswd, timeout, serverIP);
250             } else {
251                 logger.info("Creating http client with default behaviour");
252                 httpClient = new ConnectionBuilder(0, timeout);
253             }
254         } catch (Exception e) {
255             logger.error("Error Getting HTTP Connection Builder due to Unknown Exception", e);
256         }
257
258         logger.info("Got HTTP Connection Builder");
259         return httpClient;
260     }
261
262     // Public Method to post request to execute playbook. Posts the following back
263     // to Svc context memory
264     // org.onap.appc.adapter.ansible.req.code : 100 if successful
265     // org.onap.appc.adapter.ansible.req.messge : any message
266     // org.onap.appc.adapter.ansible.req.Id : a unique uuid to reference the request
267     @Override
268     public void reqExec(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
269
270         String playbookName = StringUtils.EMPTY;
271         String payload = StringUtils.EMPTY;
272         String agentUrl = StringUtils.EMPTY;
273         String user = StringUtils.EMPTY;
274         String password = StringUtils.EMPTY;
275         String id = StringUtils.EMPTY;
276         String timeout = StringUtils.EMPTY;
277         JSONObject jsonPayload;
278
279         try {
280             // create json object to send request
281             jsonPayload = messageProcessor.reqMessage(params);
282
283             agentUrl = (String) jsonPayload.remove("AgentUrl");
284             user = (String) jsonPayload.remove("User");
285             password = (String)jsonPayload.remove(PASSWORD);
286             if(StringUtils.isNotBlank(password))
287             password = EncryptionTool.getInstance().decrypt(password);
288             id = jsonPayload.getString("Id");
289             timeout = jsonPayload.getString("Timeout");
290             if (StringUtils.isBlank(timeout))
291                 timeout = "600";
292             payload = jsonPayload.toString();
293             ctx.setAttribute("AnsibleTimeout", timeout);
294             logger.info("Updated Payload  = " + payload + " timeout = " + timeout);
295         } catch (APPCException e) {
296             logger.error(APPC_EXCEPTION_CAUGHT, e);
297             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
298                     "Error constructing request for execution of playbook due to missing mandatory parameters. Reason = "
299                             + e.getMessage());
300         } catch (JSONException e) {
301             logger.error("JSONException caught", e);
302             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
303                     "Error constructing request for execution of playbook due to invalid JSON block. Reason = "
304                             + e.getMessage());
305         } catch (NumberFormatException e) {
306             logger.error("NumberFormatException caught", e);
307             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
308                     "Error constructing request for execution of playbook due to invalid parameter values. Reason = "
309                             + e.getMessage());
310         }
311
312         int code = -1;
313         String message = StringUtils.EMPTY;
314
315         try {
316             // post the test request
317             logger.info("Posting ansible request = " + payload + " to url = " + agentUrl);
318             AnsibleResult testResult = postExecRequest(agentUrl, payload, user, password,ctx);
319           if (testResult != null) {
320             logger.info("Received response on ansible post request " + testResult.getStatusMessage());
321             // Process if HTTP was successful
322             if (testResult.getStatusCode() == 200) {
323               testResult = messageProcessor.parsePostResponse(testResult.getStatusMessage());
324             } else {
325               doFailure(ctx, testResult.getStatusCode(),
326                         "Error posting request. Reason = " + testResult.getStatusMessage());
327             }
328             String output = StringUtils.EMPTY;
329             code = testResult.getStatusCode();
330             message = testResult.getStatusMessage();
331             output = testResult.getOutput();
332             ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, output);
333             String serverIp = testResult.getServerIp();
334             if (StringUtils.isBlank(serverIp))
335             ctx.setAttribute("ServerIP", serverIp);
336             else
337               ctx.setAttribute("ServerIP", "");
338             // Check status of test request returned by Agent
339             if (code == AnsibleResultCodes.PENDING.getValue()) {
340               logger.info(String.format("Submission of Test %s successful.", playbookName));
341               // test request accepted. We are in asynchronous case
342             } else {
343               doFailure(ctx, code, "Request for execution of playbook rejected. Reason = " + message);
344             }
345           } else {
346             doFailure(ctx, code, "Ansible Test result is null");
347           }
348         } catch (APPCException e) {
349             logger.error(APPC_EXCEPTION_CAUGHT, e);
350             doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
351                     "Exception encountered when posting request for execution of playbook. Reason = " + e.getMessage());
352         }
353
354         ctx.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, Integer.toString(code));
355         ctx.setAttribute(MESSAGE_ATTRIBUTE_NAME, message);
356         ctx.setAttribute(ID_ATTRIBUTE_NAME, id);
357     }
358
359     /**
360      * Public method to query status of a specific request It blocks till the
361      * Ansible Server responds or the session times out (non-Javadoc)
362      *
363      * @see org.onap.appc.adapter.ansible.AnsibleAdapter#reqExecResult(java.util.Map,
364      *      org.onap.ccsdk.sli.core.sli.SvcLogicContext)
365      */
366     @Override
367     public void reqExecResult(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
368
369         // Get URI
370         String reqUri = StringUtils.EMPTY;
371
372         try {
373             String serverIp = ctx.getAttribute("ServerIP");
374             if (StringUtils.isNotBlank(serverIp))
375                 reqUri = messageProcessor.reqUriResultWithIP(params, serverIp);
376             else
377                 reqUri = messageProcessor.reqUriResult(params);
378             logger.info("Got uri " + reqUri);
379         } catch (APPCException e) {
380             logger.error(APPC_EXCEPTION_CAUGHT, e);
381             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
382                     "Error constructing request to retrieve result due to missing parameters. Reason = "
383                             + e.getMessage());
384             return;
385         } catch (NumberFormatException e) {
386             logger.error("NumberFormatException caught", e);
387             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
388                     "Error constructing request to retrieve result due to invalid parameters value. Reason = "
389                             + e.getMessage());
390             return;
391         }
392
393         int code = -1;
394         String message = StringUtils.EMPTY;
395         String results = StringUtils.EMPTY;
396         String output = StringUtils.EMPTY;
397         try {
398             // Try to retrieve the test results (modify the URL for that)
399             AnsibleResult testResult = queryServer(reqUri, params.get("User"),
400                     EncryptionTool.getInstance().decrypt(params.get(PASSWORD)), ctx);
401             code = testResult.getStatusCode();
402             message = testResult.getStatusMessage();
403
404             if (code == 200 || code == 400 || "FINISHED".equalsIgnoreCase(message)) {
405                 logger.info("Parsing response from ansible Server = " + message);
406                 // Valid HTTP. process the Ansible message
407                 testResult = messageProcessor.parseGetResponse(message);
408                 code = testResult.getStatusCode();
409                 message = testResult.getStatusMessage();
410                 results = testResult.getResults();
411                 output = testResult.getOutput();
412                 ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, output);
413             }
414             logger.info("Request response = " + message);
415         } catch (APPCException e) {
416             doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
417                     "Exception encountered retrieving result : " + e.getMessage());
418             return;
419         }
420
421         // We were able to get and process the results. Determine if playbook succeeded
422
423         if (code == AnsibleResultCodes.FINAL_SUCCESS.getValue()) {
424             message = String.format("Ansible Request  %s finished with Result = %s, Message = %s", params.get("Id"),
425                     OUTCOME_SUCCESS, message);
426             logger.info(message);
427         } else {
428             logger.info(String.format("Ansible Request  %s finished with Result %s, Message = %s", params.get("Id"),
429                     OUTCOME_FAILURE, message));
430             ctx.setAttribute(RESULTS_ATTRIBUTE_NAME, results);
431             ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, output);
432             doFailure(ctx, code, message);
433             return;
434         }
435
436         // In case of 200,400,FINISHED return 400
437         ctx.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, "400");
438         ctx.setAttribute(MESSAGE_ATTRIBUTE_NAME, message);
439         ctx.setAttribute(RESULTS_ATTRIBUTE_NAME, results);
440         ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, output);
441         ctx.setStatus(OUTCOME_SUCCESS);
442     }
443
444     /**
445      * Public method to get logs from playbook execution for a specific request
446      *
447      * It blocks till the Ansible Server responds or the session times out very
448      * similar to reqExecResult logs are returned in the DG context variable
449      * org.onap.appc.adapter.ansible.log
450      */
451     @Override
452     public void reqExecLog(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
453
454         String reqUri = StringUtils.EMPTY;
455         try {
456             reqUri = messageProcessor.reqUriLog(params);
457             logger.info("Retrieving results from " + reqUri);
458         } catch (Exception e) {
459             logger.error("Exception caught", e);
460             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(), e.getMessage());
461         }
462
463         String message = StringUtils.EMPTY;
464         try {
465             // Try to retrieve the test results (modify the url for that)
466             AnsibleResult testResult = queryServer(reqUri, params.get("User"),
467                     EncryptionTool.getInstance().decrypt(params.get(PASSWORD)), ctx);
468             message = testResult.getStatusMessage();
469             logger.info("Request output = " + message);
470             ctx.setAttribute(LOG_ATTRIBUTE_NAME, message);
471             ctx.setStatus(OUTCOME_SUCCESS);
472         } catch (Exception e) {
473             logger.error("Exception caught", e);
474             doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
475                     "Exception encountered retreiving output : " + e.getMessage());
476         }
477     }
478
479     /**
480      * Method that posts the request
481      */
482     private AnsibleResult postExecRequest(String agentUrl, String payload, String user, String password,
483             SvcLogicContext ctx) {
484
485         AnsibleResult testResult = null;
486         ConnectionBuilder httpClient = getHttpConn(defaultSocketTimeout, "");
487         if (!testMode) {
488           if(httpClient!=null) {
489             httpClient.setHttpContext(user, password);
490             testResult = httpClient.post(agentUrl, payload);
491             httpClient.close();
492           }
493         } else {
494             testResult = testServer.Post(agentUrl, payload);
495         }
496         return testResult;
497     }
498
499     /**
500      * Method to query Ansible server
501      */
502     private AnsibleResult queryServer(String agentUrl, String user, String password, SvcLogicContext ctx) {
503
504         AnsibleResult testResult = new AnsibleResult();
505         int timeout = 600 * 1000;
506         try {
507             timeout = Integer.parseInt(ctx.getAttribute("AnsibleTimeout")) * 1000;
508
509         } catch (Exception e) {
510             timeout = defaultTimeout;
511         }
512         long endTime = System.currentTimeMillis() + timeout;
513
514         while (System.currentTimeMillis() < endTime) {
515             String serverIP = ctx.getAttribute("ServerIP");
516             ConnectionBuilder httpClient = getHttpConn(defaultSocketTimeout, serverIP);
517             logger.info("Querying ansible GetResult URL = " + agentUrl);
518
519             if (!testMode) {
520               if(httpClient!=null) {
521                 httpClient.setHttpContext(user, password);
522                 testResult = httpClient.get(agentUrl);
523                 httpClient.close();
524               }
525             } else {
526                 testResult = testServer.Get(agentUrl);
527             }
528             if (testResult.getStatusCode() != AnsibleResultCodes.IO_EXCEPTION.getValue()
529                     && testResult.getStatusCode() != AnsibleResultCodes.PENDING.getValue()) {
530                 break;
531             }
532             
533             try {
534                 Thread.sleep(defaultPollInterval);
535             } catch (InterruptedException ex) {
536
537             }
538
539         }
540         if (testResult.getStatusCode() == AnsibleResultCodes.PENDING.getValue()) {
541             testResult.setStatusCode(AnsibleResultCodes.IO_EXCEPTION.getValue());
542             testResult.setStatusMessage("Request timed out");
543         }
544
545         return testResult;
546     }
547 }