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