From 3e05cb41202145e113853392e9837abf3f6ec12c Mon Sep 17 00:00:00 2001 From: "Straubs, Ralph (rs8887)" Date: Tue, 19 Nov 2019 04:11:23 -0600 Subject: Add m2 model, including the LCM application Issue-ID: POLICY-1948 Change-Id: I18a5231d3102073c928a591c9e91b241b7093680 Signed-off-by: Straubs, Ralph (rs8887) --- controlloop/m2/test/pom.xml | 100 ++++ .../java/org/onap/policy/m2/test/AppcLcmTest.java | 318 ++++++++++ .../java/org/onap/policy/m2/test/SimDmaap.java | 266 +++++++++ .../java/org/onap/policy/m2/test/SimGuard.java | 65 +++ .../test/java/org/onap/policy/m2/test/Util.java | 502 ++++++++++++++++ .../appclcm/CLRulevUSPAPPCLCMGuardTemplate.yaml | 83 +++ .../appclcm/M2CLRulevUSPAPPCLCMGuardTemplate.drl | 649 +++++++++++++++++++++ .../test/resources/appclcm/controller.properties | 38 ++ .../m2/test/src/test/resources/appclcm/kmodule.xml | 26 + .../m2/test/src/test/resources/appclcm/pom.xml | 29 + 10 files changed, 2076 insertions(+) create mode 100644 controlloop/m2/test/pom.xml create mode 100644 controlloop/m2/test/src/test/java/org/onap/policy/m2/test/AppcLcmTest.java create mode 100644 controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimDmaap.java create mode 100644 controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimGuard.java create mode 100644 controlloop/m2/test/src/test/java/org/onap/policy/m2/test/Util.java create mode 100644 controlloop/m2/test/src/test/resources/appclcm/CLRulevUSPAPPCLCMGuardTemplate.yaml create mode 100644 controlloop/m2/test/src/test/resources/appclcm/M2CLRulevUSPAPPCLCMGuardTemplate.drl create mode 100644 controlloop/m2/test/src/test/resources/appclcm/controller.properties create mode 100644 controlloop/m2/test/src/test/resources/appclcm/kmodule.xml create mode 100644 controlloop/m2/test/src/test/resources/appclcm/pom.xml (limited to 'controlloop/m2/test') diff --git a/controlloop/m2/test/pom.xml b/controlloop/m2/test/pom.xml new file mode 100644 index 000000000..c389be5c7 --- /dev/null +++ b/controlloop/m2/test/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + + org.onap.policy.drools-applications.controlloop.m2 + m2 + 1.6.0-SNAPSHOT + + + test + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + + ${project.version} + + + + test + + + + + false + + + + + + + + org.onap.policy.drools-pdp + policy-core + ${version.policy.drools-pdp} + test + + + + org.onap.policy.drools-pdp + policy-management + ${version.policy.drools-pdp} + test + + + + org.onap.policy.drools-pdp + feature-drools-init + ${version.policy.drools-pdp} + test + + + + org.onap.policy.models.policy-models-interactions + model-yaml + ${policy.models.version} + test + + + + org.onap.policy.drools-applications.controlloop.m2 + appclcm + ${project.version} + test + + + + junit + junit + test + + + diff --git a/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/AppcLcmTest.java b/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/AppcLcmTest.java new file mode 100644 index 000000000..3a8cffb31 --- /dev/null +++ b/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/AppcLcmTest.java @@ -0,0 +1,318 @@ +/*- + * ============LICENSE_START======================================================= + * m2/test + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.m2.test; + +import static org.junit.Assert.assertNotNull; +import static org.onap.policy.guard.Util.ONAP_KEY_PASS; +import static org.onap.policy.guard.Util.ONAP_KEY_URL; +import static org.onap.policy.guard.Util.ONAP_KEY_USER; +import static org.onap.policy.guard.Util.PROP_GUARD_URL; +import static org.onap.policy.m2.test.Util.assertSubset; +import static org.onap.policy.m2.test.Util.json; + +import com.google.gson.JsonObject; + +import java.io.File; +import java.util.Properties; +import java.util.UUID; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.onap.policy.drools.system.PolicyController; +import org.onap.policy.drools.system.PolicyEngineConstants; +import org.onap.policy.drools.util.KieUtils; +import org.onap.policy.drools.utils.PropertyUtil; +import org.onap.policy.m2.test.Util.Input; +import org.onap.policy.m2.test.Util.Output; + +public class AppcLcmTest { + private static String closedLoopControlName = null; + private static Output dcae = null; + private static Output appcResponse = null; + private static Input notification = null; + private static Input appcRequest = null; + private static Properties properties = null; + private static PolicyController policyController = null; + + /** + * Initialization method, which creates the following: + * 1) VUSPLCM artifact + * 2) The associated PolicyController and Drools session + * 3) DMAAP/UEB topic writers and readers + * . + */ + @BeforeClass + public static void init() throws Exception { + Util.commonInit(); + + String projectVersion = System.getProperty("project.version"); + assertNotNull(projectVersion); + closedLoopControlName = "appclcm-" + UUID.randomUUID().toString(); + + File kmodule = new File("src/test/resources/appclcm/kmodule.xml"); + + String pom = Util.openAndReplace("src/test/resources/appclcm/pom.xml", + "${project.version}", projectVersion); + + String yaml = Util.fileToString(new File("src/test/resources/appclcm/CLRulevUSPAPPCLCMGuardTemplate.yaml")); + + // build a '.drl' file (as a String), by replacing '${variable}' names + String drl = Util.openAndReplace( + "src/test/resources/appclcm/M2CLRulevUSPAPPCLCMGuardTemplate.drl", + "${closedLoopControlName}", closedLoopControlName, + "${controlLoopYaml}", Util.convertYaml(yaml), + "${notificationTopic}", "NOTIFICATION-APPCLCM-TOPIC", + "${operationTopic}", "APPC-REQUEST-APPCLCM-TOPIC", + "${policyName}", "appclcm", + "${policyScope}", "service=vUSP;resource=vCTS;type=operational" , + "${policyVersion}", + "org.onap.policy.m2.test:appclcm:" + projectVersion, + "${unique}", "2"); + + // this creates the JAR file, and installs it in the local repository + KieUtils.installArtifact(kmodule, Util.stringToFile(pom, ".xml"), + "src/main/resources/rules/rules.drl", Util.stringToFile(drl, ".drl")); + + properties = PropertyUtil.getProperties("src/test/resources/appclcm/controller.properties"); + properties.setProperty("rules.version", projectVersion); + //properties.setProperty("pdpx.username", ""); + //properties.setProperty("pdpx.password", ""); + + // create PolicyController, which creates the Drools session + PolicyEngineConstants.getManager().setEnvironmentProperty(PROP_GUARD_URL, "http://127.0.71.201:8443/pdp/"); + PolicyEngineConstants.getManager().setEnvironmentProperty(ONAP_KEY_URL, "jdbc:h2:file:./H2DB"); + PolicyEngineConstants.getManager().setEnvironmentProperty(ONAP_KEY_USER, "sa"); + PolicyEngineConstants.getManager().setEnvironmentProperty(ONAP_KEY_PASS, ""); + policyController = + PolicyEngineConstants.getManager().createPolicyController("appclcm", properties); + policyController.start(); + + // create writers + dcae = new Output("org.onap.DCAE-APPCLCM-TOPIC"); + appcResponse = new Output("APPC-RESPONSE-APPCLCM-TOPIC"); + + // create readers + notification = new Input("NOTIFICATION-APPCLCM-TOPIC"); + appcRequest = new Input("APPC-REQUEST-APPCLCM-TOPIC"); + } + + /** + * Clean up. + */ + @AfterClass + public static void cleanup() { + // close readers + notification.close(); + appcRequest.close(); + + // close writers + dcae.close(); + appcResponse.close(); + + // shut down PolicyController and Drools session + policyController.stop(); + PolicyEngineConstants.getManager().stop(); + + // clean up REST servers + Util.commonShutdown(); + } + + /** + * This is a sunny-day scenario. + */ + @Test + public void sunnyDayTest() throws Exception { + Request req = new Request(); + + // send initial ONSET message + dcae.send(req.msg); + + // receive active notification, and restart operation + assertSubset(json("notification", "ACTIVE"), + notification.poll()); + + appcOperation(req, "Restart", 400, "Restart Successful"); + + // send ABATED + req.msg.addProperty("closedLoopEventStatus", "ABATED"); + dcae.send(req.msg); + + // receive final success notification + assertSubset(json("notification", "FINAL: SUCCESS"), + notification.poll()); + + // sleep to allow DB update + Thread.sleep(1000); + } + + /** + * In this scenario, all requests fail until the final 'Evacuate'. + */ + @Test + public void initialFailure() throws Exception { + Request req = new Request(); + + // send initial ONSET message + dcae.send(req.msg); + + // active notification, and restart 1 operation + assertSubset(json("notification", "ACTIVE"), + notification.poll()); + + appcOperation(req, "Restart", 450, "Restart 1 Failed"); + appcOperation(req, "Restart", 450, "Restart 2 Failed"); + appcOperation(req, "Rebuild", 450, "Rebuild Failed"); + appcOperation(req, "Migrate", 450, "Migrate Failed"); + appcOperation(req, "Evacuate", 400, "Evacuate Successful"); + + // send ABATED + req.msg.addProperty("closedLoopEventStatus", "ABATED"); + dcae.send(req.msg); + + // receive final success notification + assertSubset(json("notification", "FINAL: SUCCESS"), + notification.poll()); + + // sleep to allow DB update + Thread.sleep(1000); + } + + private void appcOperation(Request req, String name, int responseCode, String responseMessage) + throws Exception { + String lcName = name.toLowerCase(); + assertSubset(json("notification", "OPERATION", + "message", ".*operation=" + name + ",.*"), + notification.poll()); + + // receive request + JsonObject opRequest = appcRequest.poll(); + assertSubset(json("version", "2.0", + "rpc-name", lcName, + "correlation-id", ".*", + "type", "request", + "body", + json("input", json("common-header", + json("request-id", req.requestId), + "action", name + ))), + opRequest); + + // send response + JsonObject ch = opRequest + .getAsJsonObject("body") + .getAsJsonObject("input") + .getAsJsonObject("common-header"); + JsonObject opResponse = + json("correlation-id", opRequest.get("correlation-id"), + "body", json("output", + json("common-header", + json("flags", json(), + "api-ver", "2.00", + "originator-id", ch.get("originator-id"), + "sub-request-id", ch.get("sub-request-id"), + "request-id", req.requestId, + "timestamp", ch.get("timestamp") + ), + "status", + json("code", responseCode, + "message", responseMessage + ))), + "type", "response", + "version", "2.0", + "rpc-name", lcName + ); + appcResponse.send(opResponse); + + // receive success or failure notification + String expectedNotification = + (responseCode == 400 ? "OPERATION: SUCCESS" : "OPERATION: FAILURE"); + assertSubset(json("notification", expectedNotification, + "message", ".*operation=" + name + ",.*"), + notification.poll()); + } + + /* ============================================================ */ + + /** + * An instance of this class is created for each Transaction. It allocates + * any identifiers, such as 'requestId', and creates the initial ONSET + * message. + */ + class Request { + String requestId; + String triggerId; + JsonObject msg; + + Request() { + long time = System.currentTimeMillis(); + requestId = UUID.randomUUID().toString(); + triggerId = "trigger-" + time; + + msg = json("closedLoopEventClient", "configuration.dcae.microservice.stringmatcher.xml", + "policyVersion", "1610", + "triggerSourceName", "ctsf0002vm014", + "policyName", "vUSP_vCTS_CL_7.Config_MS_ClosedLoop_" + + "104b1445_6b30_11e7_852e_0050568c4ccf_StringMatch_1wo2qh0", + "policyScope", "resource=F5,service=vSCP,type=configuration," + + "closedLoopControlName=vSCP_F5_Firewall_d925ed73-8231-4d02-9545-db4e101f88f8", + "triggerID", triggerId, + "target_type", "VM", + "AAI", + json("vserver.l-interface.l3-interface-ipv6-address-list.l3-inteface-ipv6-address", null, + "vserver.selflink", "https://compute-aic.dpa3.cci.att.com:8774/v2/d0719b845a804b368f8ac0bba39e188b/servers/7953d05b-6698-4aa6-87bd-39bed606133a", + "vserver.is-closed-loop-disabled", "false", + "vserver.l-interface.network-name", "vUSP_DPA3_OAM_3750", + "vserver.l-interface.l3-interface-ipv4-address-list.l3-inteface-ipv4-address", //continues + "135.144.3.50", + "vserver.vserver-id", "78fe4342-8f85-49ba-be9f-d0c1bdf1ba7b", + "generic-vnf.service-id", "e433710f-9217-458d-a79d-1c7aff376d89", + "complex.city", "AAIDefault", + "vserver.in-maint", "N", + "complex.state", "NJ", + "vserver.vserver-name", "ctsf0002vm025", + "complex.physical-location-id", "LSLEILAA", + "tenant.tenant-id", "d0719b845a804b368f8ac0bba39e188b", + "vserver.prov-status", "PROV", + "generic-vnf.vnf-name", "ctsf0002v", + "vserver.l-interface.interface-name", // continues + "ctsf0002v-ALU-LCP-Pair07-oziwyxlxwdyc-1-a4psuz5awjw7-ALU-LCP-ETH2-ygmny7m7rpb5", + "generic-vnf.vnf-type", "vUSP-vCTS", + "cloud-region.identity-url", "https://auth.pdk11.cci.att.com:5000/v2.0" + ), + "closedLoopAlarmStart", "1507143409107000", + "closedLoopEventStatus", "ONSET", + "closedLoopControlName", closedLoopControlName, + "version", "1.0.2", + "target", "vserver.vserver-name", + "resourceInstance", json("resourceName", "", + "resourceInstanceName", "" + ), + "requestID", requestId, + "from", "DCAE", + "serviceInstance", json("serviceInstanceName", "", + "serviceName", "" + ) + ); + } + } +} diff --git a/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimDmaap.java b/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimDmaap.java new file mode 100644 index 000000000..3a80f9581 --- /dev/null +++ b/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimDmaap.java @@ -0,0 +1,266 @@ +/*- + * ============LICENSE_START======================================================= + * m2/test + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.m2.test; + +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class simulates a UEB/DMAAP server. + */ + +@Path("/") +public class SimDmaap { + private static Logger logger = LoggerFactory.getLogger(SimDmaap.class); + + // maps topic name to 'Topic' instance + static Map topicTable = new ConcurrentHashMap<>(); + + /** + * Each instance of this class corresponds to a DMAAP or UEB topic. + */ + static class Topic { + // topic name + String topic; + + // maps group name into group instance + Map groupTable = new ConcurrentHashMap<>(); + + /** + * Create or get a Topic. + * + * @param name the topic name + * @return the associated Topic instance + */ + static Topic createOrGet(String name) { + // look up the topic name + Topic topicObj = topicTable.get(name); + if (topicObj == null) { + // no entry found -- the following will create one, without + // the need for explicit synchronization + topicTable.putIfAbsent(name, new Topic(name)); + topicObj = topicTable.get(name); + } + return topicObj; + } + + /** + * Constructor - initialize the 'topic' field. + * + * @param topic the topic name + */ + private Topic(String topic) { + this.topic = topic; + } + + /** + * Handle an incoming '/events/{topic}' POST REST message. + * + * @param the body of the REST message + * @return the appropriate JSON response + */ + String post(String data) { + // start of message processing + long startTime = System.currentTimeMillis(); + + // current and ending indices to the 'data' field + int cur = 0; + int end = data.length(); + + // the number of messages retrieved so far + int messageCount = 0; + + while (cur < end) { + // The body of the message may consist of multiple JSON messages, + // each preceded by 3 integers separated by '.'. The second one + // is the length, in bytes (the third seems to be some kind of + // channel identifier). + + int leftBrace = data.indexOf('{', cur); + if (leftBrace < 0) { + // no more messages + break; + } + String[] prefix = data.substring(cur,leftBrace).split("\\."); + if (prefix.length == 3) { + try { + // determine length of message, and advance current position + int length = Integer.parseInt(prefix[1]); + cur = leftBrace + length; + + // extract message, and update count -- each '\' is converted + // to '\\', and each double quote has a '\' character placed + // before it, so the overall message can be placed in double + // quotes, and parsed as a literal string + String message = data.substring(leftBrace, cur) + .replace("\\", "\\\\").replace("\"", "\\\"") + .replace("\n", "\\n"); + messageCount += 1; + + // send to all listening groups + for (Group group : groupTable.values()) { + group.messages.add(message); + } + } catch (Exception e) { + logger.info("{}: {}", prefix[1], e); + break; + } + } else if (cur == 0) { + // there is only a single message -- extract it, and update count + String message = data.substring(leftBrace, end) + .replace("\\", "\\\\").replace("\"", "\\\"") + .replace("\n", "\\n"); + messageCount += 1; + + // send to all listening grops + for (Group group : groupTable.values()) { + group.messages.add(message); + } + break; + } else { + // don't know what this is -- toss it + break; + } + } + + // generate response message + long elapsedTime = System.currentTimeMillis() - startTime; + return "{\n" + + " \"count\": " + messageCount + ",\n" + + " \"serverTimeMs\": " + elapsedTime + "\n" + + "}"; + } + + /** + * read one or more incoming messages. + * + * @param group the 'consumerGroup' value + * @param timeout how long to wait for a message, in milliseconds + * @param limit the maximum number of messages to receive + * @return a JSON array, containing 0-limit messages + */ + String get(String group, long timeout, int limit) + throws InterruptedException { + // look up the group -- create one if it doesn't exist + Group groupObj = groupTable.get(group); + if (groupObj == null) { + // no entry found -- the following will create one, without + // the need for explicit synchronization + groupTable.putIfAbsent(group, new Group()); + groupObj = groupTable.get(group); + } + + // pass it on to the 'Group' instance + return groupObj.get(timeout, limit); + } + } + + /* ============================================================ */ + + /** + * Each instance of this class corresponds to a Consumer Group. + */ + static class Group { + // messages queued for this group + private BlockingQueue messages = new LinkedBlockingQueue<>(); + + /** + * Retrieve messages sent to this group. + * + * @param timeout how long to wait for a message, in milliseconds + * @param limit the maximum number of messages to receive + * @return a JSON array, containing 0-limit messages + */ + String get(long timeout, int limit) throws InterruptedException { + String message = messages.poll(timeout, TimeUnit.MILLISECONDS); + if (message == null) { + // timed out without messages + return "[]"; + } + + // use 'StringBuilder' to assemble the response -- add the first message + StringBuilder builder = new StringBuilder(); + builder.append("[\"").append(message); + + // add up to '-1' more messages + for (int i = 1 ; i < limit ; i += 1) { + // fetch the next message -- don't wait if it isn't currently there + message = messages.poll(); + if (message == null) { + // no more currently available + break; + } + builder.append("\",\"").append(message); + } + builder.append("\"]"); + return builder.toString(); + } + } + + /* ============================================================ */ + + /** + * Process an HTTP POST to /events/{topic}. + */ + @POST + @Path("/events/{topic}") + @Consumes("application/cambria") + @Produces(MediaType.APPLICATION_JSON) + public String send(@PathParam("topic") String topic, + String data) { + logger.info("Send: topic={}", topic); + return Topic.createOrGet(topic).post(data); + } + + /** + * Process an HTTP GET to /events/{topic}/{group}/{id}. + */ + @GET + @Path("/events/{topic}/{group}/{id}") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + public String receive(@PathParam("topic") String topic, + @PathParam("group") String group, + @PathParam("id") String id, + @QueryParam("timeout") long timeout, + @QueryParam("limit") int limit) + throws InterruptedException { + + logger.info("Receive: topic={}, group={}, id={}, timeout={}, limit={}", + topic, group, id, timeout, limit); + return Topic.createOrGet(topic).get(group, timeout, limit); + } +} diff --git a/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimGuard.java b/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimGuard.java new file mode 100644 index 000000000..578dd6dc3 --- /dev/null +++ b/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimGuard.java @@ -0,0 +1,65 @@ +/*- + * ============LICENSE_START======================================================= + * m2/test + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.m2.test; + +import com.google.gson.JsonObject; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * minimal Guard Simulator -- just enough to support the current tests. + */ +@Path("/") +public class SimGuard { + private static Logger logger = LoggerFactory.getLogger(SimGuard.class); + + // used for JSON <-> String conversion + private static StandardCoder coder = new StandardCoder(); + + /** + * Process an HTTP POST to /pdp/. + */ + @POST + @Path("/pdp/") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String query(String data) throws CoderException { + + JsonObject msg = coder.decode(data, JsonObject.class); + logger.info("SimGuard query:\n{}", Util.prettyPrint(msg)); + + JsonObject response = new JsonObject(); + response.addProperty("status", "Permit"); + logger.info("Returning:\n{}", Util.prettyPrint(response)); + + return response.toString(); + } +} diff --git a/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/Util.java b/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/Util.java new file mode 100644 index 000000000..393a030f0 --- /dev/null +++ b/controlloop/m2/test/src/test/java/org/onap/policy/m2/test/Util.java @@ -0,0 +1,502 @@ +/*- + * ============LICENSE_START======================================================= + * m2/test + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.m2.test; + +import static org.junit.Assert.assertTrue; + +import com.att.nsa.cambria.client.CambriaBatchingPublisher; +import com.att.nsa.cambria.client.CambriaClientBuilders; +import com.att.nsa.cambria.client.CambriaClientBuilders.ConsumerBuilder; +import com.att.nsa.cambria.client.CambriaClientBuilders.PublisherBuilder; +import com.att.nsa.cambria.client.CambriaConsumer; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.UUID; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.onap.policy.drools.system.PolicyEngineConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Util { + private static Logger logger = LoggerFactory.getLogger(Util.class); + + // used for JSON <-> String conversion + private static StandardCoder coder = new StandardCoder(); + + // used for pretty-printing: gson.toJson(JsonObject obj) + private static Gson gson = + new GsonBuilder().setPrettyPrinting().serializeNulls().create(); + + // contains the currently running set of servers + private static List runningServers = new LinkedList<>(); + + /** + * Read from an 'InputStream' until EOF or until it is closed. This method + * may block, depending on the type of 'InputStream'. + * + * @param input This is the input stream + * @return A 'String' containing the contents of the input stream + */ + public static String inputStreamToString(InputStream input) { + StringBuilder sb = new StringBuilder(); + byte[] buffer = new byte[8192]; + int length; + + try { + while ((length = input.read(buffer)) > 0) { + sb.append(new String(buffer, 0, length)); + } + } catch (IOException e) { + // return what we have so far + } + return sb.toString(); + } + + /** + * Read in a file, converting the contents to a string. + * + * @param file the input file + * @return a String containing the contents of the file + */ + public static String fileToString(File file) + throws IOException, FileNotFoundException { + try (FileInputStream fis = new FileInputStream(file)) { + String string = inputStreamToString(fis); + return string; + } + } + + /** + * Create a file containing the contents of the specified string. + * + * @param string the input string + * @param suffix the suffix to pass to 'createTempFile + * @return a File, whose contents contain the string + */ + public static File stringToFile(String string, String suffix) + throws IOException { + File file = File.createTempFile("templates-util", suffix); + file.deleteOnExit(); + + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(string.getBytes()); + } + return file; + } + + /** + * Create a file containing the contents of the specified string. + * + * @param string the input string + * @return a File, whose contents contain the string + */ + public static File stringToFile(String string) + throws IOException { + return stringToFile(string, ""); + } + + /** + * This method converts a YAML string into one that can be embedded into + * a '.drl' file. + * + * @param yaml the input string, which is typically read from a file + * @return the converted string + */ + public static String convertYaml(String yaml) { + yaml = yaml.replace("\n", "%0A"); + yaml = yaml.replace("\r", ""); + yaml = yaml.replace(":", "%3A"); + yaml = yaml.replace(' ', '+'); + return yaml; + } + + /** + * This is a convenience method which reads a file into a string, and + * then does a set of string replacements on it. The real purpose is to + * make it easy to do '${parameter}' replacements in files, as part of + * building a Drools artifact. + * + * @param fileName this is the input file name + * @param args these parameters come in pairs: + * 'input-string' and 'output-string'. + * @return a String containing the contents of the file, with the parameters + * replaced + */ + public static String openAndReplace(String fileName, String... args) + throws IOException, FileNotFoundException { + String text = fileToString(new File(fileName)); + for (int i = 0 ; i < args.length ; i += 2) { + text = text.replace(args[i], args[i + 1]); + } + return text; + } + + /** + * Convert an Object to a JsonElement. + * + * @param object the object to convert + * @return a JsonElement that corresponds to 'object' + */ + public static JsonElement toJsonElement(Object object) { + if (object == null || object instanceof JsonElement) { + return (JsonElement) object; + } + if (object instanceof Number) { + return new JsonPrimitive((Number) object); + } + if (object instanceof Boolean) { + return new JsonPrimitive((Boolean) object); + } + if (object instanceof Character) { + return new JsonPrimitive((Character) object); + } + return new JsonPrimitive(object.toString()); + } + + /** + * This is a convenience method to build a 'JsonObject', and populate + * it with a set of keyword/value pairs. + * + * @param data this parameter comes in pairs: 'keyword', and 'value' + * @return the populated JsonObject + */ + public static JsonObject json(Object... data) { + JsonObject obj = new JsonObject(); + for (int i = 0 ; i < data.length ; i += 2) { + obj.add(data[i].toString(), toJsonElement(data[i + 1])); + } + return obj; + } + + /** + * Convert a JsonElement to a String (pretty-printing). + * + * @param jsonElement the object to convert + * @return a pretty-printed string + */ + public static String prettyPrint(JsonElement jsonElement) { + return gson.toJson(jsonElement); + } + + /** + * This method is used to check whether a JSON message has a set of fields + * populated with the values expected. + * + * @param subset this is a 'JsonObject', which contains field names and + * values (the values are interpreted as regular expressions). The values + * may also be 'JsonObject' instances, in which case they are compared + * recursively. + * @param whole ordinarily, this will be a 'JsonObject', and will contain + * a superset of the fields in 'subset'. If not, the 'assert' fails. + */ + public static void assertSubset(JsonObject subset, Object whole) { + StringBuilder sb = new StringBuilder(); + assertSubsetAssist(sb, "", subset, toJsonElement(whole)); + String sbString = sb.toString(); + assertTrue(sbString, sbString.isEmpty()); + } + + /** + * This is similar to 'assertSubset', but just returns 'true' if the + * pattern matches. + * + * @param subset this is a 'JsonObject', which contains field names and + * values (the values are interpreted as regular expressions). The values + * may also be 'JsonObject' instances, in which case they are compared + * recursively. + * @param whole ordinarily, this will be a 'JsonObject', and will contain + * a superset of the fields in 'subset'. If not, the 'assert' fails. + * @return 'true' if 'whole' is a superset of 'subset' + */ + public static boolean testSubset(JsonObject subset, Object whole) { + StringBuilder sb = new StringBuilder(); + assertSubsetAssist(sb, "", subset, toJsonElement(whole)); + return sb.length() == 0; + } + + /** + * This is an internal support method for 'assertSubset' and 'testSubset', + * and handles the recursive comparison. + * + * @param sb a 'StringBuilder', which is appended to when there are + * mismatches + * @param prefix the field name being compared (the empty string indicates + * the top-level field). + * @param subset the 'JsonObject' being compared at this level + * @param argWhole the value being tested -- if it is not a 'JsonObject', + * the comparison fails + */ + private static void assertSubsetAssist(StringBuilder sb, String prefix, JsonObject subset, JsonElement argWhole) { + if (!(argWhole.isJsonObject())) { + sb.append(prefix).append(" is not a JsonObject\n"); + return; + } + JsonObject whole = argWhole.getAsJsonObject(); + for (String key : subset.keySet()) { + String fullKey = (prefix.isEmpty() ? key : prefix + "." + key); + JsonElement value = subset.get(key); + JsonElement value2 = whole.get(key); + if (value.isJsonObject()) { + assertSubsetAssist(sb, fullKey, value.getAsJsonObject(), value2); + } else if (!value.equals(value2) + && (value2 == null || !value2.toString().matches(value.toString()))) { + sb.append(fullKey) + .append(": got ") + .append(String.valueOf(value2)) + .append(", expected ") + .append(String.valueOf(value)) + .append("\n"); + } + } + } + + /** + * Do whatever needs to be done to start the server. I don't know exactly + * what abstractions the various pieces provide, but the following code + * ties the pieces together, and starts up the server. + * + * @param name used as the 'ServerConnector' name, and also used to generate + * a name for the server thread + * @param host the host IP address to bind to + * @param port the port to bind to + * @param clazz the class containing the provider methods + */ + public static void startRestServer(String name, String host, int port, Class clazz) { + ServletContextHandler context = + new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/"); + + final Server jettyServer = new Server(); + + ServerConnector connector = new ServerConnector(jettyServer); + connector.setName(name); + connector.setReuseAddress(true); + connector.setPort(port); + connector.setHost(host); + + jettyServer.addConnector(connector); + jettyServer.setHandler(context); + + ServletHolder holder = + context.addServlet(org.glassfish.jersey.servlet.ServletContainer.class.getName(), "/*"); + holder.setInitParameter( + "jersey.config.server.provider.classnames", + "org.onap.policy.common.gson.GsonMessageBodyHandler" + + "," + clazz.getName()); + + synchronized (runningServers) { + runningServers.add(jettyServer); + } + + new Thread(() -> { + try { + jettyServer.start(); + jettyServer.join(); + logger.info("{}: back from jettyServer.join()", name); + } catch (Exception e) { + logger.info(name + ": Exception starting jettyServer", e); + } + }, "REST Server: " + name).start(); + } + + private static boolean initNeeded = true; + + /** + * This method starts services shared by all of the tests. The services are + * started the first time it is invoked -- subsequent invocations have no + * effect. + */ + public static void commonInit() { + if (initNeeded) { + initNeeded = false; + + // start DMAAP Simulator + startRestServer("simdmaap", "127.0.71.250", 3904, SimDmaap.class); + + // start Guard Simulator + startRestServer("simguard", "127.0.71.201", 8443, SimGuard.class); + + // start PolicyEngine + PolicyEngineConstants.getManager().configure(new Properties()); + PolicyEngineConstants.getManager().start(); + } + } + + /** + * This method shuts down all of the servers that were started. + */ + public static void commonShutdown() { + synchronized (runningServers) { + for (Server server : runningServers) { + try { + server.stop(); + } catch (Exception e) { + logger.info("Exception shutting down server: {}", e); + } + } + runningServers.clear(); + initNeeded = true; + } + } + + /* ============================================================ */ + + /** + * This class is used to create an outgoing (publisher) topic message + * channel. 'topic' is the only parameter -- everything else is hard-wired. + */ + public static class Output { + CambriaBatchingPublisher publisher; + String topic; + + /** + * Constructor - create the outgoing topic message channel. + * + * @param topic a DMAAP or UEB topic name + */ + public Output(String topic) throws Exception { + this.topic = topic; + PublisherBuilder builder = + new CambriaClientBuilders.PublisherBuilder(); + builder + .usingHosts("127.0.71.250") + .onTopic(topic) + .withSocketTimeout(5000); + publisher = builder.build(); + } + + /** + * Send a JSON message out this channel. + * + * @param msg a 'JsonObject' containing the message to be sent + */ + public void send(JsonObject msg) throws Exception { + logger.info("Sending message, topic = {}\n{}", + topic, gson.toJson(msg)); + publisher.send("123", msg.toString()); + } + + /** + * Close the channel. + */ + public void close() { + publisher.close(); + } + } + + /* ============================================================ */ + + /** + * This class is used to create an incoming (consumer) topic message channel, + * as well as a Thread that reads from it. Incoming messages are placed in + * a 'LinkedBlockingQueue', which may be polled for messages. + */ + public static class Input extends Thread { + CambriaConsumer consumer; + String topic; + LinkedBlockingQueue queue = new LinkedBlockingQueue<>(); + volatile boolean running = true; + + /** + * Constructor - create the incoming topic message channel. + * + * @param topic a DMAAP or UEB topic name + */ + public Input(String topic) throws Exception { + this.topic = topic; + + // initialize reader + ConsumerBuilder builder = new CambriaClientBuilders.ConsumerBuilder(); + builder + .knownAs(UUID.randomUUID().toString(), "1") + .usingHosts("127.0.71.250") + .onTopic(topic) + .waitAtServer(15000) + .receivingAtMost(100) + .withSocketTimeout(20000); + consumer = builder.build(); + start(); + } + + /** + * This is the Thread main loop. It fetches messages, and queues them. + */ + @Override + public void run() { + while (running) { + try { + for (String message : consumer.fetch()) { + // a message was received -- parse it as JSON + JsonObject msg = coder.decode(message, JsonObject.class); + + // construct a message to print, and print it + logger.info("Received message, topic = {}\n{}", + topic, gson.toJson(msg)); + + // queue the message + queue.add(msg); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * Return the first message in the queue. If none are available, wait up + * to 30 seconds for one to appear. + * + * @return a 'JsonObject' if a message has been received, and 'null' if not + */ + public JsonObject poll() throws InterruptedException { + return queue.poll(30, TimeUnit.SECONDS); + } + + /** + * Stop the thread, and close the channel. + */ + public void close() { + running = false; + consumer.close(); + } + } +} diff --git a/controlloop/m2/test/src/test/resources/appclcm/CLRulevUSPAPPCLCMGuardTemplate.yaml b/controlloop/m2/test/src/test/resources/appclcm/CLRulevUSPAPPCLCMGuardTemplate.yaml new file mode 100644 index 000000000..ee25b03bb --- /dev/null +++ b/controlloop/m2/test/src/test/resources/appclcm/CLRulevUSPAPPCLCMGuardTemplate.yaml @@ -0,0 +1,83 @@ +controlLoop: + version: 2.0.0 + controlLoopName: ControlLoop-vUSP-vCTS-cbed919f-2212-4ef7-8051-fe6308da1bda + services: + - serviceName: vUSP + resources: + - resourceName: vCTS + resourceType: VF + - resourceName: vCOM + resourceType: VF + - resourceName: vRAR + resourceType: VF + - resourceName: vLCS + resourceType: VF + - resourceName: v3CB + resourceType: VF + trigger_policy: unique-policy-id-1-restart + timeout: 60 + +policies: + - id: unique-policy-id-1-restart + name: Restart Policy + description: + actor: APPCLCM + recipe: Restart + target: + type: VM + retry: 1 + timeout: 35 + success: final_success + failure: unique-policy-id-2-rebuild + failure_timeout: unique-policy-id-2-rebuild + failure_retries: unique-policy-id-2-rebuild + failure_guard: unique-policy-id-2-rebuild + failure_exception: final_failure_exception + + - id: unique-policy-id-2-rebuild + name: Rebuild Policy + description: + actor: APPCLCM + recipe: Rebuild + target: + type: VM + retry: 0 + timeout: 35 + success: final_success + failure: unique-policy-id-3-migrate + failure_timeout: unique-policy-id-3-migrate + failure_retries: unique-policy-id-3-migrate + failure_guard: unique-policy-id-3-migrate + failure_exception: final_failure_exception + + - id: unique-policy-id-3-migrate + name: Migrate Policy + description: + actor: APPCLCM + recipe: Migrate + target: + type: VM + retry: 0 + timeout: 35 + success: final_success + failure: unique-policy-id-4-evacuate + failure_timeout: unique-policy-id-4-evacuate + failure_retries: unique-policy-id-4-evacuate + failure_guard: unique-policy-id-4-evacuate + failure_exception: final_failure_exception + + - id: unique-policy-id-4-evacuate + name: Evacuate Policy + description: + actor: APPCLCM + recipe: Evacuate + target: + type: VM + retry: 0 + timeout: 35 + success: final_success + failure: final_failure + failure_timeout: final_failure_timeout + failure_retries: final_failure_retries + failure_guard: final_failure_guard + failure_exception: final_failure_exception diff --git a/controlloop/m2/test/src/test/resources/appclcm/M2CLRulevUSPAPPCLCMGuardTemplate.drl b/controlloop/m2/test/src/test/resources/appclcm/M2CLRulevUSPAPPCLCMGuardTemplate.drl new file mode 100644 index 000000000..2a24f1312 --- /dev/null +++ b/controlloop/m2/test/src/test/resources/appclcm/M2CLRulevUSPAPPCLCMGuardTemplate.drl @@ -0,0 +1,649 @@ +/*- + * ============LICENSE_START======================================================= + * m2/test + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.controlloop.p_${unique}; + +import org.drools.core.spi.KnowledgeHelper; + +import org.onap.policy.appclcm.AppcLcmDmaapWrapper; +import org.onap.policy.controlloop.VirtualControlLoopEvent; +import org.onap.policy.controlloop.ControlLoopEventStatus; +import org.onap.policy.controlloop.ControlLoopException; +import org.onap.policy.controlloop.ControlLoopNotification; +import org.onap.policy.controlloop.ControlLoopNotificationType; +import org.onap.policy.controlloop.VirtualControlLoopNotification; +import org.onap.policy.controlloop.compiler.ControlLoopCompiler; +import org.onap.policy.controlloop.policy.ControlLoop; +import org.onap.policy.controlloop.policy.ControlLoopPolicy; +import org.onap.policy.drools.core.PolicySession; +import org.onap.policy.drools.droolsinit.DroolsInitFeature; +import org.onap.policy.drools.system.PolicyController; +import org.onap.policy.drools.system.PolicyEngineConstants; +import org.onap.policy.guard.GuardContext; +import org.onap.policy.guard.PolicyGuardResponse; + +import org.onap.policy.m2.base.GuardAdjunct; +import org.onap.policy.m2.base.Transaction; +import org.onap.policy.m2.base.Util; +import org.onap.policy.m2.appclcm.AppcLcmOperation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.net.URLDecoder; +import java.time.Instant; +import java.util.LinkedList; +import java.util.Properties; + +declare Params + closedLoopControlName : String + controlLoopYaml : String + notificationTopic : String + operationTopic : String +end + +declare Context + logger : Logger + metricsLogger : Logger + auditLogger : Logger + policy : ControlLoopPolicy + guardContext : GuardContext + maxObjectCount : long + closedLoopControlName : String + controlLoopYaml : String + notificationTopic : String + operationTopic : String +end + +// this object is to provide support for timeouts +// due to a bug in drools' built in timers +declare ControlLoopTimer + closedLoopControlName : String + requestId : String + delay : String + expired : boolean + //timerType is the type of timer: either "ClosedLoop" or "Operation" + timerType : String +end + +function void sendNotification(Context context, + KnowledgeHelper drools, + ControlLoopNotification notification) +{ + if (notification != null) + { + notification.setFrom("policy"); + notification.setPolicyName(drools.getRule().getName()); + notification.setPolicyScope("${policyScope}"); + notification.setPolicyVersion("${policyVersion}"); + + PolicyEngineConstants.getManager().deliver(context.getNotificationTopic(), notification); + } +} + +function Context setParams(Context context) +{ + context.setClosedLoopControlName("${closedLoopControlName}"); + context.setControlLoopYaml("${controlLoopYaml}"); + context.setNotificationTopic("${notificationTopic}"); + context.setOperationTopic("${operationTopic}"); + context.setPolicy(ControlLoopCompiler.compile + (new ByteArrayInputStream + (URLDecoder.decode(context.getControlLoopYaml(), + "UTF-8").getBytes()), null)); + return context; +} + +rule "${policyName}.INIT" + salience 100 + when + $init : DroolsInitFeature.Init() + not(Context(closedLoopControlName == "${closedLoopControlName}")) + then + { + Logger logger = LoggerFactory.getLogger("${policyName}.drl"); + Logger metricsLogger = LoggerFactory.getLogger("com.att.eelf.metrics"); + Logger auditLogger = LoggerFactory.getLogger("com.att.eelf.audit"); + logger.info("This is ${policyName}.INIT"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + + Context context = new Context(); + context.setLogger(logger); + // add metricsLogger to context + context.setMetricsLogger(metricsLogger); + context.setAuditLogger(auditLogger); + context = setParams(context); + + PolicySession session = PolicySession.getCurrentSession(); + + context.setGuardContext(new GuardContext(session)); + + try + { + // initially, set a default maximum object count of 1000 + context.setMaxObjectCount(1000); + + // 'IllegalArgumentException' if the properties can't be found + PolicyController policyController = + Util.getPolicyController(session); + Properties properties = policyController.getProperties(); + + String maxObjectCount = + properties.getProperty("overload.maxObjectCount","").trim(); + if (!maxObjectCount.isEmpty()) + { + // A value has been specified in the properties file -- + // 'NumberFormatException' if the value is bad + context.setMaxObjectCount(Long.valueOf(maxObjectCount)); + } + } + catch (IllegalArgumentException e) + { + logger.error("${policyName}.INIT: Can't locate properties", e); + } + catch (Exception e) + { + logger.error + ("${policyName}.INIT: Can't decode 'overload.maxObjectCount'", e); + } + + insert(context); + } +end + +/* + * This rule fires when a drools update occurs. Its purpose is to + * update the expandable parameters of the context object to reflect + * the new state of the policy. + */ +rule "${policyName}.REINIT" + salience 100 + when + $init : DroolsInitFeature.Init() + $context : Context(closedLoopControlName == "${closedLoopControlName}" + && (controlLoopYaml != "${controlLoopYaml}" + || notificationTopic != "${notificationTopic}" || operationTopic != "${operationTopic}")) + then + { + Logger logger = $context.getLogger(); + // get metricsLogger from context + Logger metricsLogger = $context.getMetricsLogger(); + logger.info("This is ${policyName}.REINIT"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + + $context = setParams($context); + + modify($context) + { + } + } +end + +/* + * Fires when an incoming 'ONSET' event occurs, without an associated + * 'Transaction' instance + */ +rule "${policyName}.EVENT.NO-TRANSACTION" + when + $context : Context(closedLoopControlName == "${closedLoopControlName}") + $event : VirtualControlLoopEvent(closedLoopControlName == $context.closedLoopControlName, closedLoopEventStatus == ControlLoopEventStatus.ONSET) + not(Transaction(closedLoopControlName == $context.closedLoopControlName, requestId == $event.requestId)) + then + { + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + Logger auditLogger = $context.getAuditLogger(); + logger.info("This is ${policyName}.EVENT.NO-TRANSACTION"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + + // check for overload + if (drools.getWorkingMemory().getFactCount() > + $context.getMaxObjectCount()) + { + // send 'Overload' notification + ControlLoopNotification notification = + new VirtualControlLoopNotification($event); + notification.setNotification(ControlLoopNotificationType.REJECTED); + notification.setMessage("Overload in progress"); + sendNotification($context, drools, notification); + + // discard event, and return + auditLogger.info(Instant.now() + "Event End: FAIL: Overload in progress"); + retract($event); + return; + } + + Transaction transaction = + new Transaction(drools.getWorkingMemory(), + $context.getClosedLoopControlName(), + $event.getRequestId(), + $context.getPolicy()); + + // + // Setup the Overall Control Loop timer + // + ControlLoopTimer clTimer = + new ControlLoopTimer($event.getClosedLoopControlName(), $event.getRequestId().toString(), + transaction.getTimeout(), false, "ClosedLoop"); + + insert(clTimer); + + // check that the event and a&ai is valid + if (!transaction.isControlLoopEventValid($event) || + !AppcLcmOperation.isAaiValid(transaction, $event)) { + ControlLoopNotification notification = + new VirtualControlLoopNotification($event); + notification.setNotification(ControlLoopNotificationType.REJECTED); + notification.setMessage(transaction.getNotificationMessage()); + sendNotification($context, drools, notification); + + // discard event, and return + auditLogger.info(Instant.now() + "Event End: FAIL: Invalid event/AAI"); + retract($event); + return; + } + + // this adjunct needs to be in place before the first 'Operation' + // is created + GuardAdjunct.create(transaction, $context.getGuardContext()); + insert(transaction); + + // this creates the initial 'Operation' + transaction.setControlLoopEvent($event); + retract($event); + + // send out an active notification + ControlLoopNotification notification = transaction.getNotification(null); + notification.setNotification(ControlLoopNotificationType.ACTIVE); + sendNotification($context, drools, notification); + } +end + +/* + * Fires when 'ONSET' and 'ABATED' events have occured before an appc request was processed + */ +rule "${policyName}.TRANSACTION.ABATED.NO-REQUEST" + when + $context : Context(closedLoopControlName == "${closedLoopControlName}") + $transaction: Transaction(closedLoopControlName == $context.closedLoopControlName, state != AppcLcmOperation.LCM_PENDING && + state != "COMPLETE" && state != AppcLcmOperation.LCM_COMPLETE) + $event : VirtualControlLoopEvent(closedLoopControlName == $context.closedLoopControlName, closedLoopEventStatus == ControlLoopEventStatus.ABATED, requestId == $transaction.requestId) + then + { + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + logger.info("${policyName}.TRANSACTION.ABATED.NO-REQUEST"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + + $transaction.incomingMessage($event); + modify($transaction) + { + } + } +end + +rule "${policyName}.TRANSACTION.LCM.GUARD_PENDING.RESPONSE" + when + $context : Context(closedLoopControlName == "${closedLoopControlName}") + $transaction: Transaction(closedLoopControlName == $context.closedLoopControlName, state == AppcLcmOperation.LCM_GUARD_PENDING) + $response : PolicyGuardResponse(requestId == $transaction.getRequestId()) + then + { + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + logger.info("This is ${policyName}.TRANSACTION.LCM.GUARD_PENDING.RESPONSE"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + sendNotification($context, drools, + $transaction.incomingMessage($response)); + // + // insert operation timeout object + // + ControlLoopTimer opTimer = + new ControlLoopTimer($transaction.getClosedLoopControlName(), $transaction.getRequestId().toString(), + $transaction.getOperationTimeout(), false, "Operation"); + insert(opTimer); + + modify($transaction) + { + } + retract($response); + } +end + +/* + * Initial state for LCM operations (Restart/Rebuild/Migrate) + */ +rule "${policyName}.TRANSACTION.LCM.BEGIN" + when + $context : Context(closedLoopControlName == "${closedLoopControlName}") + $transaction : Transaction(closedLoopControlName == $context.closedLoopControlName, state == AppcLcmOperation.LCM_BEGIN) + ControlLoopTimer(closedLoopControlName == $context.closedLoopControlName, requestId == $transaction.getRequestId().toString(), !expired, timerType == "Operation") + not(VirtualControlLoopEvent(closedLoopControlName == $context.closedLoopControlName, closedLoopEventStatus == ControlLoopEventStatus.ABATED, requestId == $transaction.requestId)) + then + { + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + logger.info("This is ${policyName}.TRANSACTION.LCM.BEGIN"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + Object request = null; + try { + request = $transaction.getCurrentOperation().getRequest(); + } catch (ControlLoopException e) { + logger.error("request could not be formed due to: "+e.getMessage()); + return; + } + PolicyEngineConstants.getManager().deliver($context.getOperationTopic(), request); + + // send notification + sendNotification($context, drools, + $transaction.initialOperationNotification()); + + modify($transaction) + { + } + } +end + +/* + * We are waiting for an LCM response, and one has occurred + * (it may or may not be the one we were expecting) + */ +rule "${policyName}.TRANSACTION.LCM.PENDING.RESPONSE" + when + $context : Context(closedLoopControlName == "${closedLoopControlName}") + $transaction: Transaction(closedLoopControlName == $context.closedLoopControlName, state == AppcLcmOperation.LCM_PENDING) + $opTimer : ControlLoopTimer(closedLoopControlName == $context.closedLoopControlName, requestId == $transaction.getRequestId().toString(), !expired, timerType == "Operation") + $response : AppcLcmDmaapWrapper(body.output.commonHeader.requestId == $transaction.getRequestId()) + then + { + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + logger.info("This is ${policyName}.TRANSACTION.LCM.PENDING.RESPONSE"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + ControlLoopNotification notification = $transaction.incomingMessage($response); + sendNotification($context, drools, notification); + if (notification != null) { + retract($opTimer); + } + modify($transaction) + { + } + retract($response); + } +end + +/* +* +* This is the timer that manages the timeout for an individual operation. +* Due to a bug in the drools code, the drools timer needed to be split from most of the objects in the when clause +* +*/ +rule "${policyName}.LCM.OPERATION.TIMER.FIRED" + timer (expr: $timeout) + when + $context : Context(closedLoopControlName == "${closedLoopControlName}") + $opTimer : ControlLoopTimer(closedLoopControlName == $context.closedLoopControlName, $timeout : delay, !expired, timerType == "Operation") + then + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + Logger auditLogger = $context.getAuditLogger(); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + modify($opTimer){setExpired(true)}; + end + +/* + * We are waiting for an LCM response, but the timer expired before + * receiving one. + */ +rule "${policyName}.TRANSACTION.LCM.PENDING.TIMEOUT" + when + $context : Context(closedLoopControlName == "${closedLoopControlName}") + $transaction: Transaction(closedLoopControlName == $context.closedLoopControlName, state == AppcLcmOperation.LCM_PENDING, $timeout : getOperationTimeout()) + $opTimer : ControlLoopTimer(closedLoopControlName == $context.closedLoopControlName, requestId == $transaction.getRequestId().toString(), expired, timerType == "Operation") + then + { + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + logger.info("This is ${policyName}.TRANSACTION.LCM.PENDING.TIMEOUT: " + + $transaction.getOperationTimeout()); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + sendNotification($context, drools, $transaction.timeout()); + retract($opTimer); + modify($transaction) + { + } + } +end + +rule "${policyName}.TRANSACTION.LCM.PENDING.ERROR" + when + $context : Context(closedLoopControlName == "${closedLoopControlName}") + $transaction: Transaction(closedLoopControlName == $context.closedLoopControlName, state == AppcLcmOperation.LCM_ERROR) + $timers : LinkedList() + from collect (ControlLoopTimer(closedLoopControlName == $context.closedLoopControlName, requestId == $transaction.getRequestId().toString(), timerType == "Operation")) + then + { + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + logger.info("This is ${policyName}.TRANSACTION.LCM.ERROR"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + $transaction.processError(); + for (Object timer : $timers) { + retract(timer); + } + modify($transaction) + { + } + } +end + +/* + * We are in the 'LCM_COMPLETE' state, meaning no operations are in progress, + * and no 'ABATED' message is expected so the transaction can complete. + */ +rule "${policyName}.TRANSACTION.COMPLETE.NOT-ABATED" + when + $context : Context(closedLoopControlName == "${closedLoopControlName}", getPolicy().getControlLoop().getAbatement() == false) + $transaction: Transaction(closedLoopControlName == $context.closedLoopControlName, state == "COMPLETE") + $clTimer : ControlLoopTimer(closedLoopControlName == $context.closedLoopControlName, requestId == $transaction.getRequestId().toString(), timerType == "ClosedLoop") + then + { + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + Logger auditLogger = $context.getAuditLogger(); + logger.info("This is ${policyName}.TRANSACTION.COMPLETE.NOT-ABATED"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + auditLogger.info(Instant.now() + " Event End: SUCCESS: NOT-ABATED"); + retract($transaction); + retract($clTimer); + $transaction.cleanup(); + + sendNotification($context, drools, $transaction.finalNotification()); + } +end + +/* + * We are in the 'COMPLETE' state, meaning no operations are in progress, + * and we have received an 'ABATED' message. + */ +rule "${policyName}.TRANSACTION.COMPLETE.ABATED" + when + $context : Context(closedLoopControlName == "${closedLoopControlName}", getPolicy().getControlLoop().getAbatement() == true) + $transaction: Transaction(closedLoopControlName == $context.closedLoopControlName, state == "COMPLETE") + $event : VirtualControlLoopEvent(closedLoopControlName == $context.closedLoopControlName, closedLoopEventStatus == ControlLoopEventStatus.ABATED, requestId == $transaction.requestId) + $clTimer : ControlLoopTimer(closedLoopControlName == $context.closedLoopControlName, requestId == $transaction.getRequestId().toString(), timerType == "ClosedLoop") + then + { + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + Logger auditLogger = $context.getAuditLogger(); + logger.info("This is ${policyName}.TRANSACTION.COMPLETE.ABATED"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + auditLogger.info(Instant.now() + "Event End: SUCCESS: ABATED"); + retract($event); + retract($transaction); + retract($clTimer); + $transaction.cleanup(); + + sendNotification($context, drools, $transaction.finalNotification()); + } +end + +/* + * We are in the 'COMPLETE' state, meaning no operations are in progress, + * and the overall transaction failed, so no ABATED message is expected. + */ +rule "${policyName}.TRANSACTION.COMPLETE.FAILED" + when + $context : Context(closedLoopControlName == "${closedLoopControlName}") + $transaction: Transaction(closedLoopControlName == $context.closedLoopControlName, state == "COMPLETE", finalResultFailure) + $clTimer : ControlLoopTimer(closedLoopControlName == $context.closedLoopControlName, requestId == $transaction.getRequestId().toString(), timerType == "ClosedLoop") + then + { + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + Logger auditLogger = $context.getAuditLogger(); + logger.info("This is ${policyName}.TRANSACTION.COMPLETE.FAILED"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + auditLogger.info(Instant.now() + "Transaction End: FAIL: Final Result Failure"); + retract($transaction); + retract($clTimer); + $transaction.cleanup(); + + sendNotification($context, drools, $transaction.finalNotification()); + } +end + +/* +* +* This is the timer that manages the overall control loop timeout. +* Due to a bug in the drools code, the drools timer needed to be split from most of the objects in the when clause +* +*/ +rule "${policyName}.CLOSED_LOOP.TIMER.FIRED" + timer (expr: $timeout) + when + $context : Context(closedLoopControlName == "${closedLoopControlName}") + $clTimer : ControlLoopTimer(closedLoopControlName == $context.closedLoopControlName, $timeout : delay, !expired, timerType == "ClosedLoop") + then + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + logger.info("This is ${policyName}.CLOSED_LOOP.TIMER.FIRED"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + modify($clTimer){setExpired(true)}; + end + +/* + * The overall transaction has timed out. + */ +rule "${policyName}.TRANSACTION.TIMEOUT" + when + $context : Context(closedLoopControlName == "${closedLoopControlName}") + $transaction: Transaction(closedLoopControlName == $context.closedLoopControlName) + $clTimer : ControlLoopTimer(closedLoopControlName == $context.closedLoopControlName, requestId == $transaction.getRequestId().toString(), expired, timerType == "ClosedLoop") + $opTimers : LinkedList() + from collect (ControlLoopTimer(closedLoopControlName == $context.closedLoopControlName, requestId == $transaction.getRequestId().toString(), timerType == "Operation")) + then + { + Logger logger = $context.getLogger(); + Logger metricsLogger = $context.getMetricsLogger(); + Logger auditLogger = $context.getAuditLogger(); + logger.info("This is ${policyName}.TRANSACTION.TIMEOUT"); + metricsLogger.info("{} {} {}", Instant.now(), drools.getRule().getName(), drools.getRule().getPackage()); + auditLogger.info(Instant.now() + "Transaction End: FAIL: Transaction Timeout"); + retract($transaction); + retract($clTimer); + // Drools does not support generics, so the time of the objects in opTimers + // can't be set to OperationTimer. Therefore, timer must be an Object. + for (Object timer : $opTimers) { + retract(timer); + } + $transaction.cleanup(); + $transaction.clTimeout(); + + sendNotification($context, drools, $transaction.finalNotification()); + } +end + +/* ============================================================ */ + +/* + * This rule is an audit runs at a low priority, and cleans up any unclaimed + * 'ControlLoopEvent' instances. The assumption is that all other rules are + * structured so that incoming messages will be processed, and retracted + * from Drools memory (whether or not they are retained within the + * transaction). + */ +rule "${policyName}.UNCLAIMED-EVENT" + salience -1000 + when + $context : Context() + $event : VirtualControlLoopEvent() + then + { + Logger logger = $context.getLogger(); + Logger auditLogger = $context.getAuditLogger(); + logger.debug("${policyName}.UNCLAIMED-EVENT: " + $event); + auditLogger.info(Instant.now() + "Event End: FAIL: UNCLAIMED-EVENT - See error log"); + retract($event); + } +end + +/* + * This rule is an audit runs at a low priority, and cleans up any unclaimed + * 'PolicyGuardResponse' instances. The assumption is that all other rules are + * structured so that incoming messages will be processed, and retracted + * from Drools memory (whether or not they are retained within the + * transaction). + */ +rule "${policyName}.UNCLAIMED-POLICY-GUARD-RESPONSE" + salience -1000 + when + $context : Context() + $response : PolicyGuardResponse() + then + { + Logger logger = $context.getLogger(); + logger.error("${policyName}.UNCLAIMED-POLICY-GUARD-RESPONSE: " + + $response); + retract($response); + } +end + +/* + * This rule is an audit runs at a low priority, and cleans up any unclaimed + * 'Response' instances. The assumption is that all other rules are + * structured so that incoming messages will be processed, and retracted + * from Drools memory (whether or not they are retained within the + * transaction). + */ +rule "${policyName}.UNCLAIMED-LCM-RESPONSE" + salience -1000 + when + $context : Context() + $response : AppcLcmDmaapWrapper() + then + { + Logger logger = $context.getLogger(); + logger.error("${policyName}.UNCLAIMED-LCM-RESPONSE: " + $response); + retract($response); + } +end diff --git a/controlloop/m2/test/src/test/resources/appclcm/controller.properties b/controlloop/m2/test/src/test/resources/appclcm/controller.properties new file mode 100644 index 000000000..e4f1a8c71 --- /dev/null +++ b/controlloop/m2/test/src/test/resources/appclcm/controller.properties @@ -0,0 +1,38 @@ +# CONTROLLER section +controller.name=appclcm + +persistence.type=auto + +dmaap.source.topics=org.onap.DCAE-APPCLCM-TOPIC + +dmaap.source.topics.org.onap.DCAE-APPCLCM-TOPIC.events=org.onap.policy.controlloop.VirtualControlLoopEvent +dmaap.source.topics.org.onap.DCAE-APPCLCM-TOPIC.events.org.onap.policy.controlloop.VirtualControlLoopEvent.filter=[?($.closedLoopControlName =~ /.*/)] +dmaap.source.topics.org.onap.DCAE-APPCLCM-TOPIC.events.custom.gson=org.onap.policy.controlloop.util.Serialization,gson +dmaap.source.topics.org.onap.DCAE-APPCLCM-TOPIC.servers=127.0.71.250 + +ueb.source.topics=APPC-RESPONSE-APPCLCM-TOPIC + +ueb.source.topics.APPC-RESPONSE-APPCLCM-TOPIC.events=org.onap.policy.appclcm.AppcLcmDmaapWrapper +ueb.source.topics.APPC-RESPONSE-APPCLCM-TOPIC.events.org.onap.policy.appclcm.AppcLcmDmaapWrapper.filter=[?($.type == 'response')] +ueb.source.topics.APPC-RESPONSE-APPCLCM-TOPIC.events.custom.gson=org.onap.policy.appclcm.util.Serialization,gson +ueb.source.topics.APPC-RESPONSE-APPCLCM-TOPIC.servers=127.0.71.250 + +ueb.sink.topics=NOTIFICATION-APPCLCM-TOPIC,APPC-REQUEST-APPCLCM-TOPIC + +ueb.sink.topics.APPC-REQUEST-APPCLCM-TOPIC.events=org.onap.policy.appclcm.AppcLcmDmaapWrapper +ueb.sink.topics.APPC-REQUEST-APPCLCM-TOPIC.events.custom.gson=org.onap.policy.appclcm.util.Serialization,gson +ueb.sink.topics.APPC-REQUEST-APPCLCM-TOPIC.servers=127.0.71.250 + +ueb.sink.topics.NOTIFICATION-APPCLCM-TOPIC.events=org.onap.policy.controlloop.VirtualControlLoopNotification +ueb.sink.topics.NOTIFICATION-APPCLCM-TOPIC.events.custom.gson=org.onap.policy.controlloop.util.Serialization,gson +ueb.sink.topics.NOTIFICATION-APPCLCM-TOPIC.servers=127.0.71.250 + +guard.javax.persistence.jdbc.driver=org.h2.Driver +#guard.javax.persistence.jdbc.url=jdbc:h2:file:./H2DB +#guard.javax.persistence.jdbc.user=sa +#guard.javax.persistence.jdbc.password= +#guard.pdp.rest.url=http\://127.0.71.201\:8443/pdp/ + +rules.artifactId=appclcm +rules.groupId=org.onap.policy.m2.test +##rules.version=1.0.0-SNAPSHOT diff --git a/controlloop/m2/test/src/test/resources/appclcm/kmodule.xml b/controlloop/m2/test/src/test/resources/appclcm/kmodule.xml new file mode 100644 index 000000000..fc9b66f3e --- /dev/null +++ b/controlloop/m2/test/src/test/resources/appclcm/kmodule.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/controlloop/m2/test/src/test/resources/appclcm/pom.xml b/controlloop/m2/test/src/test/resources/appclcm/pom.xml new file mode 100644 index 000000000..0cf8b7b92 --- /dev/null +++ b/controlloop/m2/test/src/test/resources/appclcm/pom.xml @@ -0,0 +1,29 @@ + + + + + 4.0.0 + + org.onap.policy.m2.test + appclcm + ${project.version} + -- cgit 1.2.3-korg