2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2021 AT&T Intellectual Property. All rights reserved.
6 * ================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
19 * ECOMP is a trademark and service mark of AT&T Intellectual Property.
20 * ============LICENSE_END=========================================================
23 package org.onap.ccsdk.sli.adaptors.ansible.impl;
25 import com.att.eelf.configuration.EELFLogger;
26 import com.att.eelf.configuration.EELFManager;
28 import java.io.FileInputStream;
29 import java.io.InputStream;
30 import java.util.HashMap;
32 import java.util.Properties;
33 import org.apache.commons.lang.StringUtils;
34 import org.json.JSONArray;
35 import org.json.JSONException;
36 import org.json.JSONObject;
37 import org.onap.ccsdk.sli.adaptors.ansible.AnsibleAdapter;
38 import org.onap.ccsdk.sli.adaptors.ansible.AnsibleAdapterPropertiesProvider;
39 import org.onap.ccsdk.sli.adaptors.ansible.model.AnsibleMessageParser;
40 import org.onap.ccsdk.sli.adaptors.ansible.model.AnsibleResult;
41 import org.onap.ccsdk.sli.adaptors.ansible.model.AnsibleResultCodes;
42 import org.onap.ccsdk.sli.adaptors.ansible.model.AnsibleServerEmulator;
43 import org.onap.ccsdk.sli.core.sli.SvcLogicContext;
44 import org.onap.ccsdk.sli.core.sli.SvcLogicException;
45 import org.onap.ccsdk.sli.core.utils.encryption.EncryptionTool;
47 import static org.onap.ccsdk.sli.adaptors.ansible.AnsibleAdapterConstants.*;
50 * This class implements the {@link AnsibleAdapter} interface. This interface defines the behaviors
51 * that our service provides.
53 public class AnsibleAdapterImpl implements AnsibleAdapter {
58 private static final String ADAPTER_NAME = "Ansible Adapter";
59 private static final String APPC_EXCEPTION_CAUGHT = "APPCException caught";
62 * The logger to be used
64 private static final EELFLogger logger = EELFManager.getInstance().getLogger(AnsibleAdapterImpl.class);
65 private int defaultTimeout = 600 * 1000;
66 private int defaultSocketTimeout = 60 * 1000;
67 private int defaultPollInterval = 60 * 1000;
69 * Ansible API Message Handlers
71 private AnsibleMessageParser messageProcessor;
74 * indicator whether in test mode
76 private boolean testMode = false;
79 * server emulator object to be used if in test mode
81 private AnsibleServerEmulator testServer;
84 * This default constructor is used as a work around because the activator wasn't getting called
86 public AnsibleAdapterImpl() {
87 initialize(new AnsibleAdapterPropertiesProviderImpl());
91 * Instantiates a new Ansible adapter.
93 * @param propProvider the prop provider
95 public AnsibleAdapterImpl(AnsibleAdapterPropertiesProvider propProvider) {
96 initialize(propProvider);
100 * Used for jUnit test and testing interface
102 * @param mode the mode
104 public AnsibleAdapterImpl(boolean mode) {
106 testServer = new AnsibleServerEmulator();
107 messageProcessor = new AnsibleMessageParser();
111 * Returns the symbolic name of the adapter
113 * @return The adapter name
116 public String getAdapterName() {
120 @SuppressWarnings("static-method")
121 private void doFailure(SvcLogicContext svcLogic, int code, String message) throws SvcLogicException {
122 svcLogic.markFailed();
123 svcLogic.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, Integer.toString(code));
124 svcLogic.setAttribute(MESSAGE_ATTRIBUTE_NAME, message);
125 throw new SvcLogicException("Ansible Adapter Error = " + message);
129 * initialize the Ansible adapter based on default and over-ride configuration data
131 private void initialize(AnsibleAdapterPropertiesProvider propProvider) {
132 Properties props = propProvider.getProperties();
133 // Create the message processor instance
134 messageProcessor = new AnsibleMessageParser();
136 //continuing for checking defaultTimeout
138 String timeoutStr = props.getProperty(TIMEOUT_PROPERTY_NAME);
139 defaultTimeout = Integer.parseInt(timeoutStr) * 1000;
140 } catch (Exception e) {
141 defaultTimeout = 600 * 1000;
142 logger.error("Error while reading time out property", e);
144 //continuing for checking defaultSocketTimeout
146 String timeoutStr = props.getProperty(SOCKET_TIMEOUT_PROPERTY_NAME);
147 defaultSocketTimeout = Integer.parseInt(timeoutStr) * 1000;
148 } catch (Exception e) {
149 defaultSocketTimeout = 60 * 1000;
150 logger.error("Error while reading socket time out property", e);
152 //continuing for checking defaultPollInterval
154 String timeoutStr = props.getProperty(POLL_INTERVAL_PROPERTY_NAME);
155 defaultPollInterval = Integer.parseInt(timeoutStr) * 1000;
156 } catch (Exception e) {
157 defaultPollInterval = 60 * 1000;
158 logger.error("Error while reading poll interval property", e);
160 logger.info("Initialized Ansible Adapter");
163 private ConnectionBuilder getHttpConn(int timeout, String serverIP) {
164 String path = PROPDIR + APPC_PROPS;
165 File propFile = new File(path);
166 Properties props = new Properties();
169 input = new FileInputStream(propFile);
171 } catch (Exception ex) {
172 logger.error("Error while reading appc.properties file {}", ex.getMessage());
174 // Create the http client instance
175 // type of client is extracted from the property file parameter
176 // org.onap.appc.adapter.ansible.clientType
178 // 1. TRUST_ALL (trust all SSL certs). To be used ONLY in dev
179 // 2. TRUST_CERT (trust only those whose certificates have been stored in the trustStore file)
180 // 3. DEFAULT (trust only well known certificates). This is standard behavior to which it will
181 // revert. To be used in PROD
182 ConnectionBuilder httpClientLocal = null;
184 String clientType = props.getProperty(CLIENT_TYPE_PROPERTY_NAME);
185 logger.info("Ansible http client type set to {}", clientType);
186 if ("TRUST_ALL".equals(clientType)) {
187 logger.info("Creating http client to trust ALL ssl certificates. WARNING. This should be done only in dev environments");
188 httpClientLocal = new ConnectionBuilder(1, timeout);
189 } else if ("TRUST_CERT".equals(clientType)) {
190 // set path to keystore file
191 String trustStoreFile = props.getProperty(TRUSTSTORE_PROPERTY_NAME);
192 String key = props.getProperty(TRUSTSTORE_PASS_PROPERTY_NAME);
193 char[] trustStorePasswd = EncryptionTool.getInstance().decrypt(key).toCharArray();
194 logger.info("Creating http client with trust manager from {}", trustStoreFile);
195 httpClientLocal = new ConnectionBuilder(trustStoreFile, trustStorePasswd, timeout, serverIP);
197 logger.info("Creating http client with default behaviour");
198 httpClientLocal = new ConnectionBuilder(0, timeout);
200 } catch (Exception e) {
201 logger.error("Error Getting HTTP Connection Builder due to Unknown Exception", e);
204 logger.info("Got HTTP Connection Builder");
205 return httpClientLocal;
208 // Public Method to post request to execute playbook. Posts the following back
209 // to Svc context memory
210 // org.onap.appc.adapter.ansible.req.code : 100 if successful
211 // org.onap.appc.adapter.ansible.req.messge : any message
212 // org.onap.appc.adapter.ansible.req.Id : a unique uuid to reference the request
214 public void reqExec(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
215 String playbookName = StringUtils.EMPTY;
216 String payload = StringUtils.EMPTY;
217 String agentUrl = StringUtils.EMPTY;
218 String user = StringUtils.EMPTY;
219 String pswd = StringUtils.EMPTY;
220 String id = StringUtils.EMPTY;
223 // create json object to send request
224 JSONObject jsonPayload = messageProcessor.reqMessage(params);
225 logger.info("Initial Payload = {}", jsonPayload.toString());
227 agentUrl = (String) jsonPayload.remove("AgentUrl");
228 id = jsonPayload.getString("Id");
229 user = (String) jsonPayload.remove(USER);
230 pswd = (String) jsonPayload.remove(PSWD);
231 if (StringUtils.isNotBlank(pswd)) {
232 pswd = EncryptionTool.getInstance().decrypt(pswd);
234 String timeout = jsonPayload.getString("Timeout");
235 if (StringUtils.isBlank(timeout)) {
239 String autoNodeList = (String) jsonPayload.remove("AutoNodeList");
240 if (Boolean.parseBoolean(autoNodeList)) {
241 JSONArray generatedNodeList = generateNodeList(params, ctx);
242 if (generatedNodeList.length() > 0) {
243 jsonPayload.put("NodeList", generatedNodeList);
244 jsonPayload.put("InventoryNames", "VM");
246 doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
247 "Auto generation of Node List Failed - no elements on the list");
250 logger.debug("Auto Node List is DISABLED");
253 payload = jsonPayload.toString();
254 ctx.setAttribute("AnsibleTimeout", timeout);
255 logger.info("Updated Payload = {} timeout = {}", payload, timeout);
256 } catch (SvcLogicException e) {
257 logger.error(APPC_EXCEPTION_CAUGHT, e);
258 doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
259 "Error constructing request for execution of playbook due to missing mandatory parameters. Reason = "
261 } catch (JSONException e) {
262 logger.error("JSONException caught", e);
263 doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
264 "Error constructing request for execution of playbook due to invalid JSON block. Reason = "
266 } catch (NumberFormatException e) {
267 logger.error("NumberFormatException caught", e);
268 doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
269 "Error constructing request for execution of playbook due to invalid parameter values. Reason = "
274 String message = StringUtils.EMPTY;
277 // post the test request
278 logger.info("Posting ansible request = {} to url = {}", payload, agentUrl);
279 AnsibleResult testResult = postExecRequest(agentUrl, payload, user, pswd);
280 if (testResult != null) {
281 logger.info("Received response on ansible post request {}", testResult.getStatusMessage());
282 // Process if HTTP was successful
283 if (testResult.getStatusCode() == 200) {
284 testResult = messageProcessor.parsePostResponse(testResult.getStatusMessage());
286 doFailure(ctx, testResult.getStatusCode(),
287 "Error posting request. Reason = " + testResult.getStatusMessage());
290 code = testResult.getStatusCode();
291 message = testResult.getStatusMessage();
292 ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, testResult.getOutput());
293 ctx.setAttribute(SERVERIP, StringUtils.defaultIfBlank(testResult.getServerIp(), ""));
294 // Check status of test request returned by Agent
295 if (code == AnsibleResultCodes.PENDING.getValue()) {
296 logger.info("Submission of Test {} successful.", playbookName);
297 // test request accepted. We are in asynchronous case
299 doFailure(ctx, code, "Request for execution of playbook rejected. Reason = " + message);
302 doFailure(ctx, code, "Ansible Test result is null");
304 } catch (SvcLogicException e) {
305 logger.error(APPC_EXCEPTION_CAUGHT, e);
306 doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
307 "Exception encountered when posting request for execution of playbook. Reason = " + e.getMessage());
310 ctx.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, Integer.toString(code));
311 ctx.setAttribute(MESSAGE_ATTRIBUTE_NAME, message);
312 ctx.setAttribute(ID_ATTRIBUTE_NAME, id);
316 * Method is used to automatically generate NodeList section base on the svc context
318 private JSONArray generateNodeList(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
319 String vfModuleId = StringUtils.trimToNull(params.get("vf-module-id"));
320 String vnfcName = StringUtils.trimToNull(params.get("vnfc-name"));
321 String vServerId = StringUtils.trimToNull(params.get("vserver-id"));
322 String vnfcType = StringUtils.trimToNull(params.get("vnfc-type"));
324 JSONArray result = new JSONArray();
325 logger.info("GENERATING NODE LIST");
326 logger.debug("Auto Node List filtering parameter vserver-id {} | vnfc-name {} | vnfc-type {} | vf-module-id {}",
327 vServerId, vnfcName, vnfcType, vfModuleId);
329 Map<String, JSONObject> candidates = new HashMap<>();
330 for (int i = 0; ; i++) {
331 String vmKey = "tmp.vnfInfo.vm[" + i + "]";
332 logger.info("Looking for attributes of: {}", vmKey);
333 if (ctx.getAttribute(vmKey + ".vnfc-name") != null) {
334 String debugText = "Auto Node List candidate ";
335 String vmVnfcName = ctx.getAttribute(vmKey + ".vnfc-name");
336 String vmVnfcIpv4Address = ctx.getAttribute(vmKey + ".vnfc-ipaddress-v4-oam-vip");
337 String vmVnfcType = ctx.getAttribute(vmKey + ".vnfc-type");
339 if (vmVnfcName != null && vmVnfcIpv4Address != null && vmVnfcType != null
340 && !vmVnfcName.equals("") && !vmVnfcIpv4Address.equals("") && !vmVnfcType.equals("")) {
341 if (vServerId != null) {
342 String vmVserverId = ctx.getAttribute(vmKey + ".vserver-id");
343 if (!vServerId.equals(vmVserverId)) {
344 logger.debug("{}{} dropped. vserver-id mismatch", debugText, vmVnfcName);
348 if (vfModuleId != null) {
349 String vmVfModuleId = ctx.getAttribute(vmKey + ".vf-module-id");
350 if (!vfModuleId.equals(vmVfModuleId)) {
351 logger.debug("{}{} dropped. vf-module-id mismatch", debugText, vmVnfcName);
355 if (vnfcName != null && !vmVnfcName.equals(vnfcName)) {
356 logger.debug("{}{} dropped. vnfc-name mismatch", debugText, vmVnfcName);
359 if (vnfcType != null && !vmVnfcType.equals(vnfcType)) {
360 logger.debug("{}{} dropped. vnfc-type mismatch", debugText, vmVnfcType);
364 logger.info("{}{} [{},{}]", debugText, vmVnfcName, vmVnfcIpv4Address, vmVnfcType);
366 JSONObject vnfTypeCandidates;
368 if (!candidates.containsKey(vmVnfcType)) {
369 vnfTypeCandidates = new JSONObject();
370 vmList = new JSONArray();
371 vnfTypeCandidates.put("site", "site");
372 vnfTypeCandidates.put("vnfc-type", vmVnfcType);
373 vnfTypeCandidates.put("vm-info", vmList);
374 candidates.put(vmVnfcType, vnfTypeCandidates);
376 vnfTypeCandidates = candidates.get(vmVnfcType);
377 vmList = (JSONArray) vnfTypeCandidates.get("vm-info");
380 JSONObject candidate = new JSONObject();
381 candidate.put("ne_id", vmVnfcName);
382 candidate.put("fixed_ip_address", vmVnfcIpv4Address);
383 vmList.put(candidate);
385 logger.warn("Incomplete information for Auto Node List candidate {}", vmKey);
392 for (JSONObject vnfcCandidates : candidates.values()) {
393 result.put(vnfcCandidates);
396 logger.info("GENERATING NODE LIST COMPLETED");
401 * Public method to query status of a specific request It blocks till the Ansible Server
402 * responds or the session times out (non-Javadoc)
404 * @see org.onap.ccsdk.sli.adaptors.ansible.AnsibleAdapter#reqExecResult(java.util.Map,
405 * org.onap.ccsdk.sli.core.sli.SvcLogicContext)
408 public void reqExecResult(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
413 String serverIp = ctx.getAttribute(SERVERIP);
414 if (StringUtils.isNotBlank(serverIp)) {
415 reqUri = messageProcessor.reqUriResultWithIP(params, serverIp);
417 reqUri = messageProcessor.reqUriResult(params);
419 logger.info("Got uri {}", reqUri);
420 } catch (SvcLogicException e) {
421 logger.error(APPC_EXCEPTION_CAUGHT, e);
422 doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
423 "Error constructing request to retrieve result due to missing parameters. Reason = "
426 } catch (NumberFormatException e) {
427 logger.error("NumberFormatException caught", e);
428 doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(),
429 "Error constructing request to retrieve result due to invalid parameters value. Reason = "
438 String results = StringUtils.EMPTY;
439 String finalResponse = StringUtils.EMPTY;
441 // Try to retrieve the test results (modify the URL for that)
442 AnsibleResult testResult = queryServer(reqUri, params.get(USER),
443 EncryptionTool.getInstance().decrypt(params.get(PSWD)), ctx);
444 code = testResult.getStatusCode();
445 message = testResult.getStatusMessage();
447 if (code == 200 || code == 400 || "FINISHED".equalsIgnoreCase(message)) {
448 logger.info("Parsing response from ansible Server = {}", message);
449 // Valid HTTP. process the Ansible message
450 testResult = messageProcessor.parseGetResponse(message);
451 code = testResult.getStatusCode();
452 message = testResult.getStatusMessage();
453 results = testResult.getResults();
454 output = testResult.getOutput();
455 configData = testResult.getConfigData();
456 if ((StringUtils.isBlank(output)) || (output.trim().equalsIgnoreCase("{}"))) {
457 finalResponse = results;
459 finalResponse = output;
461 logger.info("configData from ansible's response = {}", configData);
462 ctx.setAttribute("device-running-config", configData);
464 logger.info("Request response = " + message);
465 } catch (SvcLogicException e) {
466 logger.error(APPC_EXCEPTION_CAUGHT, e);
467 ctx.setAttribute(RESULTS_ATTRIBUTE_NAME, results);
468 ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, finalResponse);
469 doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
470 "Exception encountered retrieving result : " + e.getMessage());
474 // We were able to get and process the results. Determine if playbook succeeded
476 if (code == AnsibleResultCodes.FINAL_SUCCESS.getValue()) {
477 message = String.format("Ansible Request %s finished with Result = %s, Message = %s", params.get("Id"),
479 logger.info(message);
481 logger.info(String.format("Ansible Request %s finished with Result %s, Message = %s", params.get("Id"),
483 ctx.setAttribute(RESULTS_ATTRIBUTE_NAME, results);
484 ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, finalResponse);
485 doFailure(ctx, code, message);
489 // In case of 200, 400, FINISHED return 400
490 ctx.setAttribute(RESULT_CODE_ATTRIBUTE_NAME, Integer.toString(400));
491 ctx.setAttribute(MESSAGE_ATTRIBUTE_NAME, message);
492 ctx.setAttribute(RESULTS_ATTRIBUTE_NAME, results);
493 ctx.setAttribute(OUTPUT_ATTRIBUTE_NAME, finalResponse);
498 * Public method to get logs from playbook execution for a specific request
500 * It blocks till the Ansible Server responds or the session times out very similar to
501 * reqExecResult logs are returned in the DG context variable org.onap.appc.adapter.ansible.log
504 public void reqExecLog(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
505 String reqUri = StringUtils.EMPTY;
507 reqUri = messageProcessor.reqUriLog(params);
508 logger.info("Retrieving results from {}", reqUri);
509 } catch (Exception e) {
510 logger.error("Exception caught", e);
511 doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(), e.getMessage());
514 queryServerAndProcessResult(params, ctx, reqUri, LOG_ATTRIBUTE_NAME);
518 * Public method to get output from playbook execution for a specific request
520 * It blocks till the Ansible Server responds or the session times out very similar to
521 * reqExecResult and output is returned in the DG context variable org.onap.appc.adapter.ansible.output
524 public void reqExecOutput(Map<String, String> params, SvcLogicContext ctx) throws SvcLogicException {
525 String reqUri = StringUtils.EMPTY;
527 reqUri = messageProcessor.reqUriOutput(params);
528 logger.info("Retrieving results from {}", reqUri);
529 } catch (Exception e) {
530 logger.error("Exception caught", e);
531 doFailure(ctx, AnsibleResultCodes.INVALID_PAYLOAD.getValue(), e.getMessage());
534 queryServerAndProcessResult(params, ctx, reqUri, OUTPUT_ATTRIBUTE_NAME);
538 * Method that posts the request
540 private AnsibleResult postExecRequest(String agentUrl, String payload, String user, String pswd) {
541 AnsibleResult testResult = null;
542 ConnectionBuilder httpClientLocal = getHttpConn(defaultSocketTimeout, "");
544 if (httpClientLocal != null) {
545 httpClientLocal.setHttpContext(user, pswd);
546 testResult = httpClientLocal.post(agentUrl, payload);
547 httpClientLocal.close();
550 testResult = testServer.post(payload);
555 private void queryServerAndProcessResult(Map<String, String> params, SvcLogicContext ctx, String reqUri, String attributeName)
556 throws SvcLogicException {
558 // Try to retrieve the test results (modify the url for that)
559 AnsibleResult testResult = queryServer(reqUri, params.get(USER),
560 EncryptionTool.getInstance().decrypt(params.get(PSWD)), ctx);
561 String message = testResult.getStatusMessage();
562 logger.info("Request output = {}", message);
563 ctx.setAttribute(attributeName, message);
565 } catch (Exception e) {
566 logger.error("Exception caught: {}", e.getMessage(), e);
567 doFailure(ctx, AnsibleResultCodes.UNKNOWN_EXCEPTION.getValue(),
568 String.format("Exception encountered retrieving output: %s", e.getMessage()));
573 * Method to query Ansible server
575 private AnsibleResult queryServer(String agentUrl, String user, String pswd, SvcLogicContext ctx) {
576 AnsibleResult testResult = new AnsibleResult();
579 timeout = Integer.parseInt(ctx.getAttribute("AnsibleTimeout")) * 1000;
580 } catch (Exception e) {
581 timeout = defaultTimeout;
583 long endTime = System.currentTimeMillis() + timeout;
585 while (System.currentTimeMillis() < endTime) {
586 String serverIP = ctx.getAttribute(SERVERIP);
587 ConnectionBuilder httpClientLocal = getHttpConn(defaultSocketTimeout, serverIP);
588 logger.info("Querying ansible GetResult URL = {}", agentUrl);
591 if (httpClientLocal != null) {
592 httpClientLocal.setHttpContext(user, pswd);
593 testResult = httpClientLocal.get(agentUrl);
594 httpClientLocal.close();
597 testResult = testServer.get(agentUrl);
599 if (testResult.getStatusCode() != AnsibleResultCodes.IO_EXCEPTION.getValue()
600 && testResult.getStatusCode() != AnsibleResultCodes.PENDING.getValue()) {
605 Thread.sleep(defaultPollInterval);
606 } catch (InterruptedException ex) {
607 logger.error("Thread Interrupted Exception", ex);
608 Thread.currentThread().interrupt();
612 if (testResult.getStatusCode() == AnsibleResultCodes.PENDING.getValue()) {
613 testResult.setStatusCode(AnsibleResultCodes.IO_EXCEPTION.getValue());
614 testResult.setStatusMessage("Request timed out");