Added Null check
[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             logger.info("Received response on ansible post request " + testResult.getStatusMessage());
320             // Process if HTTP was successful
321             if (testResult.getStatusCode() == 200) {
322                 testResult = messageProcessor.parsePostResponse(testResult.getStatusMessage());
323             } else {
324                 doFailure(ctx, testResult.getStatusCode(),
325                         "Error posting request. Reason = " + testResult.getStatusMessage());
326             }
327             String output = StringUtils.EMPTY;
328             code = testResult.getStatusCode();
329             message = testResult.getStatusMessage();
330             output = testResult.getOutput();
331             ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, output);
332             String serverIp = testResult.getServerIp();
333             if (StringUtils.isBlank(serverIp))
334                 ctx.setAttribute("ServerIP", serverIp);
335             else
336                 ctx.setAttribute("ServerIP", "");
337             // Check status of test request returned by Agent
338             if (code == AnsibleResultCodes.PENDING.getValue()) {
339                 logger.info(String.format("Submission of Test %s successful.", playbookName));
340                 // test request accepted. We are in asynchronous case
341             } else {
342                 doFailure(ctx, code, "Request for execution of playbook rejected. Reason = " + message);
343             }
344         } catch (APPCException e) {
345             logger.error(APPC_EXCEPTION_CAUGHT, e);
346             doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
347                     "Exception encountered when posting request for execution of playbook. Reason = " + e.getMessage());
348         }
349
350         ctx.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, Integer.toString(code));
351         ctx.setAttribute(MESSAGE_ATTRIBUTE_NAME, message);
352         ctx.setAttribute(ID_ATTRIBUTE_NAME, id);
353     }
354
355     /**
356      * Public method to query status of a specific request It blocks till the
357      * Ansible Server responds or the session times out (non-Javadoc)
358      *
359      * @see org.onap.appc.adapter.ansible.AnsibleAdapter#reqExecResult(java.util.Map,
360      *      org.onap.ccsdk.sli.core.sli.SvcLogicContext)
361      */
362     @Override
363     public void reqExecResult(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
364
365         // Get URI
366         String reqUri = StringUtils.EMPTY;
367
368         try {
369             String serverIp = ctx.getAttribute("ServerIP");
370             if (StringUtils.isNotBlank(serverIp))
371                 reqUri = messageProcessor.reqUriResultWithIP(params, serverIp);
372             else
373                 reqUri = messageProcessor.reqUriResult(params);
374             logger.info("Got uri " + reqUri);
375         } catch (APPCException e) {
376             logger.error(APPC_EXCEPTION_CAUGHT, e);
377             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
378                     "Error constructing request to retrieve result due to missing parameters. Reason = "
379                             + e.getMessage());
380             return;
381         } catch (NumberFormatException e) {
382             logger.error("NumberFormatException caught", e);
383             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
384                     "Error constructing request to retrieve result due to invalid parameters value. Reason = "
385                             + e.getMessage());
386             return;
387         }
388
389         int code = -1;
390         String message = StringUtils.EMPTY;
391         String results = StringUtils.EMPTY;
392         String output = StringUtils.EMPTY;
393         try {
394             // Try to retrieve the test results (modify the URL for that)
395             AnsibleResult testResult = queryServer(reqUri, params.get("User"),
396                     EncryptionTool.getInstance().decrypt(params.get(PASSWORD)), ctx);
397             code = testResult.getStatusCode();
398             message = testResult.getStatusMessage();
399
400             if (code == 200 || code == 400 || "FINISHED".equalsIgnoreCase(message)) {
401                 logger.info("Parsing response from ansible Server = " + message);
402                 // Valid HTTP. process the Ansible message
403                 testResult = messageProcessor.parseGetResponse(message);
404                 code = testResult.getStatusCode();
405                 message = testResult.getStatusMessage();
406                 results = testResult.getResults();
407                 output = testResult.getOutput();
408                 ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, output);
409             }
410             logger.info("Request response = " + message);
411         } catch (APPCException e) {
412             doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
413                     "Exception encountered retrieving result : " + e.getMessage());
414             return;
415         }
416
417         // We were able to get and process the results. Determine if playbook succeeded
418
419         if (code == AnsibleResultCodes.FINAL_SUCCESS.getValue()) {
420             message = String.format("Ansible Request  %s finished with Result = %s, Message = %s", params.get("Id"),
421                     OUTCOME_SUCCESS, message);
422             logger.info(message);
423         } else {
424             logger.info(String.format("Ansible Request  %s finished with Result %s, Message = %s", params.get("Id"),
425                     OUTCOME_FAILURE, message));
426             ctx.setAttribute(RESULTS_ATTRIBUTE_NAME, results);
427             ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, output);
428             doFailure(ctx, code, message);
429             return;
430         }
431
432         // In case of 200,400,FINISHED return 400
433         ctx.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, "400");
434         ctx.setAttribute(MESSAGE_ATTRIBUTE_NAME, message);
435         ctx.setAttribute(RESULTS_ATTRIBUTE_NAME, results);
436         ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, output);
437         ctx.setStatus(OUTCOME_SUCCESS);
438     }
439
440     /**
441      * Public method to get logs from playbook execution for a specific request
442      *
443      * It blocks till the Ansible Server responds or the session times out very
444      * similar to reqExecResult logs are returned in the DG context variable
445      * org.onap.appc.adapter.ansible.log
446      */
447     @Override
448     public void reqExecLog(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
449
450         String reqUri = StringUtils.EMPTY;
451         try {
452             reqUri = messageProcessor.reqUriLog(params);
453             logger.info("Retrieving results from " + reqUri);
454         } catch (Exception e) {
455             logger.error("Exception caught", e);
456             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(), e.getMessage());
457         }
458
459         String message = StringUtils.EMPTY;
460         try {
461             // Try to retrieve the test results (modify the url for that)
462             AnsibleResult testResult = queryServer(reqUri, params.get("User"),
463                     EncryptionTool.getInstance().decrypt(params.get(PASSWORD)), ctx);
464             message = testResult.getStatusMessage();
465             logger.info("Request output = " + message);
466             ctx.setAttribute(LOG_ATTRIBUTE_NAME, message);
467             ctx.setStatus(OUTCOME_SUCCESS);
468         } catch (Exception e) {
469             logger.error("Exception caught", e);
470             doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
471                     "Exception encountered retreiving output : " + e.getMessage());
472         }
473     }
474
475     /**
476      * Method that posts the request
477      */
478     private AnsibleResult postExecRequest(String agentUrl, String payload, String user, String password,
479             SvcLogicContext ctx) {
480
481         AnsibleResult testResult = null;
482         ConnectionBuilder httpClient = getHttpConn(defaultSocketTimeout, "");
483         if (!testMode) {
484           if(httpClient!=null) {
485             httpClient.setHttpContext(user, password);
486             testResult = httpClient.post(agentUrl, payload);
487             httpClient.close();
488           }
489         } else {
490             testResult = testServer.Post(agentUrl, payload);
491         }
492         return testResult;
493     }
494
495     /**
496      * Method to query Ansible server
497      */
498     private AnsibleResult queryServer(String agentUrl, String user, String password, SvcLogicContext ctx) {
499
500         AnsibleResult testResult = new AnsibleResult();
501         int timeout = 600 * 1000;
502         try {
503             timeout = Integer.parseInt(ctx.getAttribute("AnsibleTimeout")) * 1000;
504
505         } catch (Exception e) {
506             timeout = defaultTimeout;
507         }
508         long endTime = System.currentTimeMillis() + timeout;
509
510         while (System.currentTimeMillis() < endTime) {
511             String serverIP = ctx.getAttribute("ServerIP");
512             ConnectionBuilder httpClient = getHttpConn(defaultSocketTimeout, serverIP);
513             logger.info("Querying ansible GetResult URL = " + agentUrl);
514
515             if (!testMode) {
516               if(httpClient!=null) {
517                 httpClient.setHttpContext(user, password);
518                 testResult = httpClient.get(agentUrl);
519                 httpClient.close();
520               }
521             } else {
522                 testResult = testServer.Get(agentUrl);
523             }
524             if (testResult.getStatusCode() != AnsibleResultCodes.IO_EXCEPTION.getValue()
525                     && testResult.getStatusCode() != AnsibleResultCodes.PENDING.getValue()) {
526                 break;
527             }
528             
529             try {
530                 Thread.sleep(defaultPollInterval);
531             } catch (InterruptedException ex) {
532
533             }
534
535         }
536         if (testResult.getStatusCode() == AnsibleResultCodes.PENDING.getValue()) {
537             testResult.setStatusCode(AnsibleResultCodes.IO_EXCEPTION.getValue());
538             testResult.setStatusMessage("Request timed out");
539         }
540
541         return testResult;
542     }
543 }