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