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