Enhance Ansible by parsing of response messages
[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) 2017-2018 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 org.apache.commons.lang.StringUtils;
29 import org.json.JSONException;
30 import org.json.JSONObject;
31 import org.onap.appc.adapter.ansible.AnsibleAdapter;
32 import org.onap.appc.adapter.ansible.model.AnsibleMessageParser;
33 import org.onap.appc.adapter.ansible.model.AnsibleResult;
34 import org.onap.appc.adapter.ansible.model.AnsibleResultCodes;
35 import org.onap.appc.adapter.ansible.model.AnsibleServerEmulator;
36 import org.onap.appc.configuration.Configuration;
37 import org.onap.appc.configuration.ConfigurationFactory;
38 import org.onap.appc.exceptions.APPCException;
39 import org.onap.ccsdk.sli.core.sli.SvcLogicContext;
40 import org.onap.ccsdk.sli.core.sli.SvcLogicException;
41 import com.att.eelf.configuration.EELFLogger;
42 import com.att.eelf.configuration.EELFManager;
43 import org.onap.appc.encryption.EncryptionTool;
44
45 /**
46  * This class implements the {@link AnsibleAdapter} interface. This interface defines the behaviors
47  * that our service provides.
48  */
49 public class AnsibleAdapterImpl implements AnsibleAdapter {
50
51
52     /**
53      * The constant used to define the service name in the mapped diagnostic context
54      */
55     @SuppressWarnings("nls")
56     public static final String MDC_SERVICE = "service";
57
58     /**
59      * The constant for the status code for a failed outcome
60      */
61     @SuppressWarnings("nls")
62     public static final String OUTCOME_FAILURE = "failure";
63
64     /**
65      * The constant for the status code for a successful outcome
66      */
67     @SuppressWarnings("nls")
68     public static final String OUTCOME_SUCCESS = "success";
69
70     /**
71      * Adapter Name
72      */
73     private static final String ADAPTER_NAME = "Ansible Adapter";
74     private static final String APPC_EXCEPTION_CAUGHT = "APPCException caught";
75
76     private static final String RESULT_CODE_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.result.code";
77     private static final String MESSAGE_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.message";
78     private static final String RESULTS_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.results";
79     private static final String OUTPUT_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.output";
80     private static final String ID_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.Id";
81     private static final String LOG_ATTRIBUTE_NAME = "org.onap.appc.adapter.ansible.log";
82
83     private static final String CLIENT_TYPE_PROPERTY_NAME = "org.onap.appc.adapter.ansible.clientType";
84     private static final String TRUSTSTORE_PROPERTY_NAME = "org.onap.appc.adapter.ansible.trustStore";
85     private static final String TRUSTPASSWD_PROPERTY_NAME = "org.onap.appc.adapter.ansible.trustStore.trustPasswd";
86
87     private static final String PASSWORD = "Password";
88
89     /**
90      * The logger to be used
91      */
92     private static final EELFLogger logger = EELFManager.getInstance().getLogger(AnsibleAdapterImpl.class);
93
94     /**
95      * A reference to the adapter configuration object.
96      */
97     private Configuration configuration;
98
99     /**
100      * Connection object
101      **/
102     private ConnectionBuilder httpClient;
103
104     /**
105      * Ansible API Message Handlers
106      **/
107     private AnsibleMessageParser messageProcessor;
108
109     /**
110      * indicator whether in test mode
111      **/
112     private boolean testMode = false;
113
114     /**
115      * server emulator object to be used if in test mode
116      **/
117     private AnsibleServerEmulator testServer;
118
119     /**
120      * This default constructor is used as a work around because the activator wasn't getting called
121      */
122     public AnsibleAdapterImpl() {
123         initialize();
124     }
125
126     /**
127      * Used for jUnit test and testing interface
128      */
129     public AnsibleAdapterImpl(boolean mode) {
130         testMode = mode;
131         testServer = new AnsibleServerEmulator();
132         messageProcessor = new AnsibleMessageParser();
133     }
134
135     /**
136      * Returns the symbolic name of the adapter
137      *
138      * @return The adapter name
139      * @see org.onap.appc.adapter.rest.AnsibleAdapter#getAdapterName()
140      */
141     @Override
142     public String getAdapterName() {
143         return ADAPTER_NAME;
144     }
145
146     /**
147      * @param rc Method posts info to Context memory in case of an error and throws a
148      *        SvcLogicException causing SLI to register this as a failure
149      */
150     @SuppressWarnings("static-method")
151     private void doFailure(SvcLogicContext svcLogic, int code, String message) throws SvcLogicException {
152
153         svcLogic.setStatus(OUTCOME_FAILURE);
154         svcLogic.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, Integer.toString(code));
155         svcLogic.setAttribute(MESSAGE_ATTRIBUTE_NAME, message);
156
157         throw new SvcLogicException("Ansible Adapter Error = " + message);
158     }
159
160     /**
161      * initialize the Ansible adapter based on default and over-ride configuration data
162      */
163     private void initialize() {
164
165         configuration = ConfigurationFactory.getConfiguration();
166         Properties props = configuration.getProperties();
167
168         // Create the message processor instance
169         messageProcessor = new AnsibleMessageParser();
170
171         // Create the http client instance
172         // type of client is extracted from the property file parameter
173         // org.onap.appc.adapter.ansible.clientType
174         // It can be :
175         // 1. TRUST_ALL (trust all SSL certs). To be used ONLY in dev
176         // 2. TRUST_CERT (trust only those whose certificates have been stored in the trustStore file)
177         // 3. DEFAULT (trust only well known certificates). This is standard behavior to which it will
178         // revert. To be used in PROD
179
180         try {
181             String clientType = props.getProperty(CLIENT_TYPE_PROPERTY_NAME);
182             logger.info("Ansible http client type set to " + clientType);
183
184             if ("TRUST_ALL".equals(clientType)) {
185                 logger.info(
186                         "Creating http client to trust ALL ssl certificates. WARNING. This should be done only in dev environments");
187                 httpClient = new ConnectionBuilder(1);
188             } else if ("TRUST_CERT".equals(clientType)) {
189                 // set path to keystore file
190                 String trustStoreFile = props.getProperty(TRUSTSTORE_PROPERTY_NAME);
191                 String key = props.getProperty(TRUSTPASSWD_PROPERTY_NAME);
192                 char[] trustStorePasswd = key.toCharArray();
193                 logger.info("Creating http client with trustmanager from " + trustStoreFile);
194                 httpClient = new ConnectionBuilder(trustStoreFile, trustStorePasswd);
195             } else {
196                 logger.info("Creating http client with default behaviour");
197                 httpClient = new ConnectionBuilder(0);
198             }
199         } catch (Exception e) {
200             logger.error("Error Initializing Ansible Adapter due to Unknown Exception", e);
201         }
202
203         logger.info("Initialized Ansible Adapter");
204     }
205
206     // Public Method to post request to execute playbook. Posts the following back
207     // to Svc context memory
208     //  org.onap.appc.adapter.ansible.req.code : 100 if successful
209     //  org.onap.appc.adapter.ansible.req.messge : any message
210     //  org.onap.appc.adapter.ansible.req.Id : a unique uuid to reference the request
211     @Override
212     public void reqExec(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
213
214         String playbookName = StringUtils.EMPTY;
215         String payload = StringUtils.EMPTY;
216         String agentUrl = StringUtils.EMPTY;
217         String user = StringUtils.EMPTY;
218         String password = StringUtils.EMPTY;
219         String id = StringUtils.EMPTY;
220
221         JSONObject jsonPayload;
222
223         try {
224             // create json object to send request
225             jsonPayload = messageProcessor.reqMessage(params);
226
227             agentUrl = (String) jsonPayload.remove("AgentUrl");
228             user = (String) jsonPayload.remove("User");
229             password = EncryptionTool.getInstance().decrypt((String) jsonPayload.remove(PASSWORD));
230             id = jsonPayload.getString("Id");
231             payload = jsonPayload.toString();
232             logger.info("Updated Payload  = " + payload);
233         } catch (APPCException e) {
234             logger.error(APPC_EXCEPTION_CAUGHT, e);
235             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
236                     "Error constructing request for execution of playbook due to missing mandatory parameters. Reason = "
237                             + e.getMessage());
238         } catch (JSONException e) {
239             logger.error("JSONException caught", e);
240             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
241                     "Error constructing request for execution of playbook due to invalid JSON block. Reason = "
242                             + e.getMessage());
243         } catch (NumberFormatException e) {
244             logger.error("NumberFormatException caught", e);
245             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
246                     "Error constructing request for execution of playbook due to invalid parameter values. Reason = "
247                             + e.getMessage());
248         }
249
250         int code = -1;
251         String message = StringUtils.EMPTY;
252
253         try {
254             // post the test request
255             logger.info("Posting request = " + payload + " to url = " + agentUrl);
256             AnsibleResult testResult = postExecRequest(agentUrl, payload, user, password);
257
258             // Process if HTTP was successful
259             if (testResult.getStatusCode() == 200) {
260                 testResult = messageProcessor.parsePostResponse(testResult.getStatusMessage());
261             } else {
262                 doFailure(ctx, testResult.getStatusCode(),
263                         "Error posting request. Reason = " + testResult.getStatusMessage());
264             }
265
266             code = testResult.getStatusCode();
267             message = testResult.getStatusMessage();
268
269             // Check status of test request returned by Agent
270             if (code == AnsibleResultCodes.PENDING.getValue()) {
271                 logger.info(String.format("Submission of Test %s successful.", playbookName));
272                 // test request accepted. We are in asynchronous case
273             } else {
274                 doFailure(ctx, code, "Request for execution of playbook rejected. Reason = " + message);
275             }
276         } catch (APPCException e) {
277             logger.error(APPC_EXCEPTION_CAUGHT, e);
278             doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
279                     "Exception encountered when posting request for execution of playbook. Reason = " + e.getMessage());
280         }
281
282         ctx.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, Integer.toString(code));
283         ctx.setAttribute(MESSAGE_ATTRIBUTE_NAME, message);
284         ctx.setAttribute(ID_ATTRIBUTE_NAME, id);
285     }
286
287     /**
288      * Public method to query status of a specific request It blocks till the Ansible Server
289      * responds or the session times out (non-Javadoc)
290      *
291      * @see org.onap.appc.adapter.ansible.AnsibleAdapter#reqExecResult(java.util.Map,
292      *      org.onap.ccsdk.sli.core.sli.SvcLogicContext)
293      */
294     @Override
295     public void reqExecResult(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
296
297         // Get URI
298         String reqUri = StringUtils.EMPTY;
299
300         try {
301             reqUri = messageProcessor.reqUriResult(params);
302             logger.info("Got uri ", reqUri );
303         } catch (APPCException e) {
304             logger.error(APPC_EXCEPTION_CAUGHT, e);
305             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
306                     "Error constructing request to retrieve result due to missing parameters. Reason = "
307                             + e.getMessage());
308             return;
309         } catch (NumberFormatException e) {
310             logger.error("NumberFormatException caught", e);
311             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
312                     "Error constructing request to retrieve result due to invalid parameters value. Reason = "
313                             + e.getMessage());
314             return;
315         }
316
317         int code = -1;
318         String message = StringUtils.EMPTY;
319         String results = StringUtils.EMPTY;
320         String output = StringUtils.EMPTY;
321
322         try {
323             // Try to retrieve the test results (modify the URL for that)
324             AnsibleResult testResult = queryServer(reqUri, params.get("User"),  EncryptionTool.getInstance().decrypt(params.get(PASSWORD)));
325             code = testResult.getStatusCode();
326             message = testResult.getStatusMessage();
327
328             if (code == 200 || code == 400) {
329                 logger.info("Parsing response from Server = " + message);
330                 // Valid HTTP. process the Ansible message
331                 testResult = messageProcessor.parseGetResponse(message);
332                 code = testResult.getStatusCode();
333                 message = testResult.getStatusMessage();
334                 results = testResult.getResults();
335                 output = testResult.getOutput();
336
337             }
338
339             logger.info("Request response = " + message);
340         } catch (APPCException e) {
341             doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
342                     "Exception encountered retrieving result : " + e.getMessage());
343             return;
344         }
345
346         // We were able to get and process the results. Determine if playbook succeeded
347
348         if (code == AnsibleResultCodes.FINAL_SUCCESS.getValue()) {
349             message = String.format("Ansible Request  %s finished with Result = %s, Message = %s", params.get("Id"),
350                     OUTCOME_SUCCESS, message);
351             logger.info(message);
352         } else {
353             logger.info(String.format("Ansible Request  %s finished with Result %s, Message = %s", params.get("Id"),
354                     OUTCOME_FAILURE, message));
355             ctx.setAttribute(RESULTS_ATTRIBUTE_NAME, results);
356             doFailure(ctx, code, message);
357             return;
358         }
359
360         ctx.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, Integer.toString(400));
361         ctx.setAttribute(MESSAGE_ATTRIBUTE_NAME, message);
362         ctx.setAttribute(RESULTS_ATTRIBUTE_NAME, results);
363         ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, output);
364         ctx.setStatus(OUTCOME_SUCCESS);
365     }
366
367     /**
368      * Public method to get logs from playbook execution for a specific request
369      *
370      * It blocks till the Ansible Server responds or the session times out very similar to
371      * reqExecResult logs are returned in the DG context variable org.onap.appc.adapter.ansible.log
372      */
373     @Override
374     public void reqExecLog(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
375
376         String reqUri = StringUtils.EMPTY;
377         try {
378             reqUri = messageProcessor.reqUriLog(params);
379             logger.info("Retrieving results from " + reqUri);
380         } catch (Exception e) {
381             logger.error("Exception caught", e);
382             doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(), e.getMessage());
383         }
384
385         String message = StringUtils.EMPTY;
386         try {
387             // Try to retrieve the test results (modify the url for that)
388             AnsibleResult testResult = queryServer(reqUri, params.get("User"), EncryptionTool.getInstance().decrypt(params.get(PASSWORD)));
389             message = testResult.getStatusMessage();
390             logger.info("Request output = " + message);
391             ctx.setAttribute(LOG_ATTRIBUTE_NAME, message);
392             ctx.setStatus(OUTCOME_SUCCESS);
393         } catch (Exception e) {
394             logger.error("Exception caught", e);
395             doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
396                     "Exception encountered retreiving output : " + e.getMessage());
397         }
398     }
399
400     /**
401      * Method that posts the request
402      */
403     private AnsibleResult postExecRequest(String agentUrl, String payload, String user, String password) {
404
405         AnsibleResult testResult;
406
407         if (!testMode) {
408             httpClient.setHttpContext(user, password);
409             testResult = httpClient.post(agentUrl, payload);
410         } else {
411             testResult = testServer.Post(agentUrl, payload);
412         }
413         return testResult;
414     }
415
416     /**
417      * Method to query Ansible server
418      */
419     private AnsibleResult queryServer(String agentUrl, String user, String password) {
420
421         AnsibleResult testResult;
422
423         logger.info("Querying url = " + agentUrl);
424
425         if (!testMode) {
426             testResult = httpClient.get(agentUrl);
427         } else {
428             testResult = testServer.Get(agentUrl);
429         }
430
431         return testResult;
432     }
433 }