aboutsummaryrefslogtreecommitdiffstats
path: root/controlloop/m2/test
diff options
context:
space:
mode:
authorStraubs, Ralph (rs8887) <rs8887@att.com>2019-11-19 04:11:23 -0600
committerStraubs, Ralph (rs8887) <rs8887@att.com>2020-01-10 03:20:23 -0600
commit3e05cb41202145e113853392e9837abf3f6ec12c (patch)
tree0c504018436c3933f563caa37c3ea0512c82181e /controlloop/m2/test
parent927c7c177670a812a4a4139281ef84e85b520645 (diff)
Add m2 model, including the LCM application
Issue-ID: POLICY-1948 Change-Id: I18a5231d3102073c928a591c9e91b241b7093680 Signed-off-by: Straubs, Ralph (rs8887) <rs8887@att.com>
Diffstat (limited to 'controlloop/m2/test')
-rw-r--r--controlloop/m2/test/pom.xml100
-rw-r--r--controlloop/m2/test/src/test/java/org/onap/policy/m2/test/AppcLcmTest.java318
-rw-r--r--controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimDmaap.java266
-rw-r--r--controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimGuard.java65
-rw-r--r--controlloop/m2/test/src/test/java/org/onap/policy/m2/test/Util.java502
-rw-r--r--controlloop/m2/test/src/test/resources/appclcm/CLRulevUSPAPPCLCMGuardTemplate.yaml83
-rw-r--r--controlloop/m2/test/src/test/resources/appclcm/M2CLRulevUSPAPPCLCMGuardTemplate.drl649
-rw-r--r--controlloop/m2/test/src/test/resources/appclcm/controller.properties38
-rw-r--r--controlloop/m2/test/src/test/resources/appclcm/kmodule.xml26
-rw-r--r--controlloop/m2/test/src/test/resources/appclcm/pom.xml29
10 files changed, 2076 insertions, 0 deletions
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 @@
+<?xml version="1.0"?>
+<!--
+ ============LICENSE_START=======================================================
+ ONAP Policy Engine - Drools PDP
+ ================================================================================
+ 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=========================================================
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+ <artifactId>m2</artifactId>
+ <version>1.6.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>test</artifactId>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-test</id>
+ <configuration>
+ <systemPropertyVariables>
+ <project.version>${project.version}</project.version>
+ </systemPropertyVariables>
+ </configuration>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <skip>false</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.onap.policy.drools-pdp</groupId>
+ <artifactId>policy-core</artifactId>
+ <version>${version.policy.drools-pdp}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.onap.policy.drools-pdp</groupId>
+ <artifactId>policy-management</artifactId>
+ <version>${version.policy.drools-pdp}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.onap.policy.drools-pdp</groupId>
+ <artifactId>feature-drools-init</artifactId>
+ <version>${version.policy.drools-pdp}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.onap.policy.models.policy-models-interactions</groupId>
+ <artifactId>model-yaml</artifactId>
+ <version>${policy.models.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+ <artifactId>appclcm</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
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<String,Topic> 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<String,Group> 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<String> 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 '<limit>-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<Server> 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<JsonObject> 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ============LICENSE_START=======================================================
+ ONAP Policy Engine - Drools PDP
+ ================================================================================
+ 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=========================================================
+ -->
+
+<kmodule xmlns="http://jboss.org/kie/6.0.0/kmodule">
+ <kbase name="rules">
+ <ksession name="appclcm"/>
+ </kbase>
+</kmodule>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ============LICENSE_START=======================================================
+ ONAP Policy Engine - Drools PDP
+ ================================================================================
+ 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=========================================================
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.onap.policy.m2.test</groupId>
+ <artifactId>appclcm</artifactId>
+ <version>${project.version}</version>
+</project>