Add m2 model, including the LCM application 89/98589/11
authorStraubs, Ralph (rs8887) <rs8887@att.com>
Tue, 19 Nov 2019 10:11:23 +0000 (04:11 -0600)
committerStraubs, Ralph (rs8887) <rs8887@att.com>
Fri, 10 Jan 2020 09:20:23 +0000 (03:20 -0600)
Issue-ID: POLICY-1948
Change-Id: I18a5231d3102073c928a591c9e91b241b7093680
Signed-off-by: Straubs, Ralph (rs8887) <rs8887@att.com>
46 files changed:
controlloop/m2/adapters/pom.xml [new file with mode: 0644]
controlloop/m2/adapters/src/main/java/org/onap/policy/m2/adapters/VirtualOnsetAdapter.java [new file with mode: 0644]
controlloop/m2/adapters/src/test/java/org/onap/policy/m2/adapters/VirtualOnsetAdapterTest.java [new file with mode: 0644]
controlloop/m2/appclcm/pom.xml [new file with mode: 0644]
controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/AppcLcmActor.java [new file with mode: 0644]
controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/AppcLcmHealthCheckOperation.java [new file with mode: 0644]
controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/AppcLcmOperation.java [new file with mode: 0644]
controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/model/AppcLcmResponseCode.java [new file with mode: 0644]
controlloop/m2/appclcm/src/main/resources/META-INF/services/org.onap.policy.m2.base.Actor [new file with mode: 0644]
controlloop/m2/appclcm/src/test/java/appclcm/AppcLcmHealthCheckOperationTest.java [new file with mode: 0644]
controlloop/m2/appclcm/src/test/java/appclcm/AppcLcmOperationTest.java [new file with mode: 0644]
controlloop/m2/appclcm/src/test/java/model/AppcLcmResponseCodeTest.java [new file with mode: 0644]
controlloop/m2/base/pom.xml [new file with mode: 0644]
controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Actor.java [new file with mode: 0644]
controlloop/m2/base/src/main/java/org/onap/policy/m2/base/GuardAdjunct.java [new file with mode: 0644]
controlloop/m2/base/src/main/java/org/onap/policy/m2/base/OnsetAdapter.java [new file with mode: 0644]
controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Operation.java [new file with mode: 0644]
controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Transaction.java [new file with mode: 0644]
controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Util.java [new file with mode: 0644]
controlloop/m2/base/src/test/java/org/onap/policy/m2/base/ActorOperationTest.java [new file with mode: 0644]
controlloop/m2/base/src/test/java/org/onap/policy/m2/base/GuardAdjunctTest.java [new file with mode: 0644]
controlloop/m2/base/src/test/java/org/onap/policy/m2/base/TransactionTest.java [new file with mode: 0644]
controlloop/m2/base/src/test/java/org/onap/policy/m2/base/UtilTest.java [new file with mode: 0644]
controlloop/m2/feature-controlloop-m2/pom.xml [new file with mode: 0644]
controlloop/m2/feature-controlloop-m2/src/assembly/assemble_zip.xml [new file with mode: 0644]
controlloop/m2/guard/pom.xml [new file with mode: 0644]
controlloop/m2/guard/src/main/java/org/onap/policy/guard/GuardContext.java [new file with mode: 0644]
controlloop/m2/guard/src/test/java/org/onap/policy/guard/GuardContextTest.java [new file with mode: 0644]
controlloop/m2/lock/pom.xml [new file with mode: 0644]
controlloop/m2/lock/src/main/java/org/onap/policy/drools/m2/lock/LockAdjunct.java [new file with mode: 0644]
controlloop/m2/lock/src/test/java/org/onap/policy/drools/m2/lock/LockAdjunctTest.java [new file with mode: 0644]
controlloop/m2/pom.xml [new file with mode: 0644]
controlloop/m2/test/pom.xml [new file with mode: 0644]
controlloop/m2/test/src/test/java/org/onap/policy/m2/test/AppcLcmTest.java [new file with mode: 0644]
controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimDmaap.java [new file with mode: 0644]
controlloop/m2/test/src/test/java/org/onap/policy/m2/test/SimGuard.java [new file with mode: 0644]
controlloop/m2/test/src/test/java/org/onap/policy/m2/test/Util.java [new file with mode: 0644]
controlloop/m2/test/src/test/resources/appclcm/CLRulevUSPAPPCLCMGuardTemplate.yaml [new file with mode: 0644]
controlloop/m2/test/src/test/resources/appclcm/M2CLRulevUSPAPPCLCMGuardTemplate.drl [new file with mode: 0644]
controlloop/m2/test/src/test/resources/appclcm/controller.properties [new file with mode: 0644]
controlloop/m2/test/src/test/resources/appclcm/kmodule.xml [new file with mode: 0644]
controlloop/m2/test/src/test/resources/appclcm/pom.xml [new file with mode: 0644]
controlloop/m2/util/pom.xml [new file with mode: 0644]
controlloop/m2/util/src/main/java/org/onap/policy/util/DroolsSessionCommonSerializable.java [new file with mode: 0644]
controlloop/m2/util/src/test/java/org/onap/policy/util/DroolsSessionCommonSerializableTest.java [new file with mode: 0644]
controlloop/pom.xml

diff --git a/controlloop/m2/adapters/pom.xml b/controlloop/m2/adapters/pom.xml
new file mode 100644 (file)
index 0000000..b4cd290
--- /dev/null
@@ -0,0 +1,46 @@
+<?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 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <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>adapters</artifactId>
+  <name>adapters</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>base</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/controlloop/m2/adapters/src/main/java/org/onap/policy/m2/adapters/VirtualOnsetAdapter.java b/controlloop/m2/adapters/src/main/java/org/onap/policy/m2/adapters/VirtualOnsetAdapter.java
new file mode 100644 (file)
index 0000000..e9ca116
--- /dev/null
@@ -0,0 +1,62 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/adapters
+ * ================================================================================
+ * 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.adapters;
+
+import java.io.Serializable;
+
+import org.onap.policy.controlloop.ControlLoopEvent;
+import org.onap.policy.controlloop.ControlLoopNotification;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.VirtualControlLoopNotification;
+
+import org.onap.policy.m2.base.OnsetAdapter;
+
+public class VirtualOnsetAdapter extends OnsetAdapter implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private static VirtualOnsetAdapter instance = new VirtualOnsetAdapter();
+
+    /**
+     * This method is called to register the 'VirtualOnsetAdapter' instance
+     * under the 'VirtualControlLoopEvent' class. This method called in the
+     * static initialization code of the 'Actor' classes that use this
+     * adapter -- namely, 'AppcLcmActor'.
+     */
+    public static void register() {
+        OnsetAdapter.register(VirtualControlLoopEvent.class, instance);
+    }
+
+    /**
+     * This method overrides the associated 'OnsetAdapter' method.
+     */
+    @Override
+    public ControlLoopNotification createNotification(ControlLoopEvent event) {
+        if (event instanceof VirtualControlLoopEvent) {
+            return new VirtualControlLoopNotification((VirtualControlLoopEvent)event);
+        }
+
+        // Right now, the onset event from the transaction is used to locate
+        // the adapter. It is expected that the 'event' passed here will
+        // be of the same class, but that isn't always guaranteed. If this
+        // is not the case, the appropriate adapter is located in this way.
+        return OnsetAdapter.get(event).createNotification(event);
+    }
+}
diff --git a/controlloop/m2/adapters/src/test/java/org/onap/policy/m2/adapters/VirtualOnsetAdapterTest.java b/controlloop/m2/adapters/src/test/java/org/onap/policy/m2/adapters/VirtualOnsetAdapterTest.java
new file mode 100644 (file)
index 0000000..0ecbda6
--- /dev/null
@@ -0,0 +1,53 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * adapters
+ * ================================================================================
+ * 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.adapters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import org.onap.policy.controlloop.ControlLoopEvent;
+import org.onap.policy.controlloop.ControlLoopNotification;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.VirtualControlLoopNotification;
+import org.onap.policy.m2.base.OnsetAdapter;
+
+public class VirtualOnsetAdapterTest {
+
+    @Test
+    public void test() {
+        VirtualOnsetAdapter.register();
+        VirtualControlLoopEvent virtualControlLoopEvent = new VirtualControlLoopEvent();
+        VirtualOnsetAdapter virtualOnsetAdapter =
+            VirtualOnsetAdapter.class.cast(OnsetAdapter.get(virtualControlLoopEvent));
+        assertTrue(virtualOnsetAdapter != null);
+
+        ControlLoopNotification notification = virtualOnsetAdapter.createNotification(virtualControlLoopEvent);
+        assertTrue(notification != null);
+        // we want an exact class match, so 'instanceOf' is not being used
+        assertEquals(VirtualControlLoopNotification.class, notification.getClass());
+
+        ControlLoopEvent controlLoopEvent = new ControlLoopEvent() {};
+        notification = virtualOnsetAdapter.createNotification(controlLoopEvent);
+        assertTrue(notification != null);
+    }
+}
diff --git a/controlloop/m2/appclcm/pom.xml b/controlloop/m2/appclcm/pom.xml
new file mode 100644 (file)
index 0000000..7820045
--- /dev/null
@@ -0,0 +1,86 @@
+<?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/maven-v4_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>appclcm</artifactId>
+  <name>Experimental Control Loop Model - appclcm</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>adapters</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>base</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>lock</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+      <artifactId>appclcm</artifactId>
+      <version>${policy.models.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-pdp</groupId>
+      <artifactId>policy-management</artifactId>
+      <version>${version.policy.drools-pdp}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.projectlombok</groupId>
+      <artifactId>lombok</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-api-mockito</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/AppcLcmActor.java b/controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/AppcLcmActor.java
new file mode 100644 (file)
index 0000000..f89d3b8
--- /dev/null
@@ -0,0 +1,76 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/appclcm
+ * ================================================================================
+ * 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.appclcm;
+
+import java.io.Serializable;
+
+import org.onap.policy.controlloop.ControlLoopEvent;
+import org.onap.policy.controlloop.policy.Policy;
+
+import org.onap.policy.m2.adapters.VirtualOnsetAdapter;
+import org.onap.policy.m2.base.Actor;
+import org.onap.policy.m2.base.Operation;
+import org.onap.policy.m2.base.Transaction;
+
+/**
+ * A single instance of this class is created, and resides within the
+ * 'nameToActor' table within class 'Transaction', under the key 'APPC'.
+ */
+public class AppcLcmActor implements Actor, Serializable {
+    /* *******************/
+    /* 'Actor' interface */
+    /* *******************/
+
+    private static final long serialVersionUID = -593438898257647144L;
+
+
+    static {
+        // ensures that 'VirtualOnsetAdapter' has an entry in the
+        // 'OnsetAdapter' table
+        VirtualOnsetAdapter.register();
+    }
+
+    /**
+     * Return the name associated with this 'Actor'.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public String getName() {
+        return "APPCLCM";
+    }
+
+    /**
+     * Create an 'Operation' for this 'Actor'.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public Operation createOperation(
+        Transaction transaction, Policy policy, ControlLoopEvent onset,
+        int attempt) {
+
+        if ("healthcheck".equalsIgnoreCase(policy.getRecipe())) {
+            return new AppcLcmHealthCheckOperation(transaction, policy, onset, attempt);
+        }
+        return new AppcLcmOperation(transaction, policy, onset, attempt);
+    }
+}
diff --git a/controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/AppcLcmHealthCheckOperation.java b/controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/AppcLcmHealthCheckOperation.java
new file mode 100644 (file)
index 0000000..42e06f9
--- /dev/null
@@ -0,0 +1,248 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/appclcm
+ * ================================================================================
+ * 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.appclcm;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.onap.policy.appclcm.AppcLcmDmaapWrapper;
+import org.onap.policy.appclcm.AppcLcmOutput;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.controlloop.ControlLoopEvent;
+import org.onap.policy.controlloop.ControlLoopException;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.onap.policy.guard.PolicyGuardResponse;
+import org.onap.policy.m2.appclcm.model.AppcLcmResponseCode;
+import org.onap.policy.m2.base.Transaction;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AppcLcmHealthCheckOperation extends AppcLcmOperation {
+    public static final String DCAE_IPV4_ADDR =
+        "vserver.l-interface.l3-interface-ipv4-address-list.l3-inteface-ipv4-address";
+
+    private static Logger logger = LoggerFactory.getLogger(AppcLcmHealthCheckOperation.class);
+
+    private static final long serialVersionUID = 4969322301462776173L;
+
+    public AppcLcmHealthCheckOperation(Transaction transaction, Policy policy,
+                                   ControlLoopEvent onset, int attempt) {
+        super(transaction, policy, onset, attempt);
+    }
+
+    /**
+     * This method will attempt to deserialize the json payload from appc and
+     * then parse the response to determine if the vnf is healthy or unhealthy.
+     * The "state" field in the payload will contain "healthy" or "unhealthy"
+     * based on the condition of the vnf.
+     *
+     * @param jsonPayload
+     *            the appc lcm response json payload
+     * @return the string that contains the state of the vnf
+     */
+    private String getVnfHealthState(String jsonPayload) {
+        HashMap<String, Object> healthCheckPayloadMap;
+        try {
+            healthCheckPayloadMap = coder.decode(jsonPayload, HashMap.class);
+        } catch (CoderException e) {
+            return null;
+        }
+
+        String stateOfHealth = null;
+        if (healthCheckPayloadMap.containsKey("state")) {
+            stateOfHealth = healthCheckPayloadMap.get("state").toString();
+        } else {
+            return null;
+        }
+        return stateOfHealth;
+    }
+
+    /**
+     * An incoming message is being delivered to the operation.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public void incomingMessage(Object object) {
+        if (!(object instanceof AppcLcmDmaapWrapper)) {
+            if (object instanceof PolicyGuardResponse) {
+                incomingGuardMessage((PolicyGuardResponse) object);
+                return;
+            }
+            // ignore this message (not sure why we even got it)
+            return;
+        }
+
+        // If we reach this point, we have a 'AppcLcmDmaapWrapper' instance.
+        // The rest of this method is mostly copied from
+        // 'ControlLoopOperationManager.onResponse'.
+
+        AppcLcmOutput response = ((AppcLcmDmaapWrapper)object).getBody().getOutput();
+
+        //
+        // Determine which subrequestID (ie. attempt)
+        //
+        int operationAttempt;
+        try {
+            operationAttempt = Integer
+                               .parseInt(response.getCommonHeader().getSubRequestId());
+        } catch (NumberFormatException e) {
+            //
+            // We cannot tell what happened if this doesn't exist
+            //
+            this.completeOperation(
+                this.getAttempt(),
+                "Policy was unable to parse APP-C SubRequestID (it was null).",
+                PolicyResult.FAILURE_EXCEPTION);
+            return;
+        }
+        //
+        // Sanity check the response message
+        //
+        if (response.getStatus() == null) {
+            //
+            // We cannot tell what happened if this doesn't exist
+            //
+            this.completeOperation(
+                operationAttempt,
+                "Policy was unable to parse APP-C response status field (it was null).",
+                PolicyResult.FAILURE_EXCEPTION);
+            return;
+        }
+        //
+        // Get the Response Code
+        //
+        AppcLcmResponseCode responseValue = AppcLcmResponseCode
+                                        .toResponseValue(response.getStatus().getCode());
+        if (responseValue == null) {
+            //
+            // We are unaware of this code
+            //
+            this.completeOperation(
+                operationAttempt,
+                "Policy was unable to parse APP-C response status code field.",
+                PolicyResult.FAILURE_EXCEPTION);
+            return;
+        }
+        //
+        // Ok, let's figure out what APP-C's response is
+        //
+        switch (responseValue) {
+            case ACCEPTED:
+                //
+                // This is good, they got our original message and
+                // acknowledged it.
+                //
+                // Is there any need to track this?
+                //
+                return;
+            case ERROR:
+            case REJECT:
+                //
+                // We'll consider these two codes as exceptions
+                //
+                this.completeOperation(operationAttempt,
+                                       response.getStatus().getMessage(),
+                                       PolicyResult.FAILURE_EXCEPTION);
+                return;
+            case FAILURE:
+                //
+                // APPC could not do a healthcheck
+                //
+                this.completeOperation(operationAttempt,
+                                       response.getStatus().getMessage(),
+                                       PolicyResult.FAILURE);
+                return;
+            case SUCCESS:
+                //
+                // This means APPC was able to execute the health check.
+                // The payload has to be parsed to see if the VNF is
+                // healthy or unhealthy
+                //
+
+                //
+                // sanity check the payload
+                //
+                if (response.getPayload() == null || response.getPayload().isEmpty()) {
+                    //
+                    // We are cannot parse the payload
+                    //
+                    this.completeOperation(
+                        operationAttempt,
+                        "Policy was unable to parse APP-C response payload because it was null.",
+                        PolicyResult.FAILURE_EXCEPTION);
+                    return;
+                }
+
+                //
+                // parse the payload to see if the VNF is healthy/unhealthy
+                //
+                String vnfHealthState = getVnfHealthState(response.getPayload());
+                if ("healthy".equalsIgnoreCase(vnfHealthState)) {
+                    this.completeOperation(operationAttempt, "VNF is healthy",
+                                           PolicyResult.SUCCESS);
+                } else if ("unhealthy".equalsIgnoreCase(vnfHealthState)) {
+                    this.completeOperation(operationAttempt, "VNF is unhealthy",
+                                           PolicyResult.FAILURE);
+                } else {
+                    this.completeOperation(
+                        operationAttempt,
+                        "Error: Could not determine the state of the VNF."
+                        + " The state field in the APPC response payload was unrecognized or null.",
+                        PolicyResult.FAILURE_EXCEPTION);
+                }
+                return;
+            default:
+                return;
+        }
+    }
+
+    /**
+       * This method will construct a payload for a health check.
+       * The payload must be an escaped json string so gson is used
+       * to convert the payload hashmap into json
+       *
+       * @return an escaped json string representation of the payload
+       * @throws ControlLoopException if it occurs
+       */
+    @Override
+    protected String setPayload(Map<String, String> aai, String recipe) throws ControlLoopException {
+        Map<String, String> payload = new HashMap<>();
+
+        // Extract oam ip address from the onset
+        String ipAddr = aai.get(DCAE_IPV4_ADDR);
+        if (ipAddr != null) {
+            payload.put("host-ip-address", ipAddr);
+        } else {
+            logger.error("Error - IPv4 Address not found in the onset");
+            setErrorStatus("Error - IPv4 Address not found in the onset");
+        }
+
+        try {
+            return coder.encode(payload);
+        } catch (CoderException e) {
+            return null;
+        }
+    }
+}
diff --git a/controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/AppcLcmOperation.java b/controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/AppcLcmOperation.java
new file mode 100644 (file)
index 0000000..6a2518f
--- /dev/null
@@ -0,0 +1,703 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/appclcm
+ * ================================================================================
+ * 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.appclcm;
+
+import com.google.common.collect.ImmutableList;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.Getter;
+
+import org.onap.policy.appclcm.AppcLcmBody;
+import org.onap.policy.appclcm.AppcLcmCommonHeader;
+import org.onap.policy.appclcm.AppcLcmDmaapWrapper;
+import org.onap.policy.appclcm.AppcLcmInput;
+import org.onap.policy.appclcm.AppcLcmOutput;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.controlloop.ControlLoopEvent;
+import org.onap.policy.controlloop.ControlLoopException;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.onap.policy.controlloop.policy.TargetType;
+import org.onap.policy.drools.m2.lock.LockAdjunct;
+import org.onap.policy.guard.PolicyGuardResponse;
+import org.onap.policy.m2.appclcm.model.AppcLcmResponseCode;
+import org.onap.policy.m2.base.GuardAdjunct;
+import org.onap.policy.m2.base.Operation;
+import org.onap.policy.m2.base.Transaction;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class is used for all APPC LCM operations. The only difference between
+ * operation types (Restart, Rebuild, Migrate, Evacuate, or HealthCheck) as
+ * far as DroolsPDP is concerned, is the operation name (policy.recipe).
+ * It is up to APPC to interpret these operations.
+ */
+public class AppcLcmOperation implements Operation, LockAdjunct.Requestor, Serializable {
+    public static final String DCAE_CLOSEDLOOP_DISABLED_FIELD = "vserver.is-closed-loop-disabled";
+    public static final String DCAE_VSERVER_SELF_LINK_FIELD = "vserver.selflink";
+    public static final String DCAE_IDENTITY_FIELD = "cloud-region.identity-url";
+    public static final String DCAE_VNF_NAME_FIELD = "generic-vnf.vnf-name";
+    public static final String DCAE_VNF_ID_FIELD = "generic-vnf.vnf-id";
+    public static final String DCAE_VSERVER_ID_FIELD = "vserver.vserver-id";
+    public static final String DCAE_TENANT_ID_FIELD = "tenant.tenant-id";
+
+    public static final String APPC_LCM_VM_ID_FIELD = "vm-id";
+    public static final String APPC_LCM_IDENTITY_URL_FIELD = "identity-url";
+    public static final String APPC_LCM_TENANT_ID_FIELD = "tenant-id";
+
+    private static Logger logger = LoggerFactory.getLogger(AppcLcmOperation.class);
+
+    private static final long serialVersionUID = 5062964240000304989L;
+
+    // state when waiting for a lock
+    public static final String LCM_WAIT_FOR_LOCK = "LCM.WAIT_FOR_LOCK";
+
+    // state when waiting for a response from 'guard'
+    public static final String LCM_GUARD_PENDING = "LCM.GUARD_PENDING";
+
+    // state when ready to send out the LCM message
+    public static final String LCM_BEGIN = "LCM.BEGIN";
+
+    // state when waiting for a response from APPC
+    public static final String LCM_PENDING = "LCM.PENDING";
+
+    // state when processing can't continue due to errors
+    public static final String LCM_ERROR = "LCM.ERROR";
+
+    // state when the operation has completed (success, failure, or timeout)
+    public static final String LCM_COMPLETE = "LCM.COMPLETE";
+
+    // the APPC LCM recipes supported by Policy
+    private static final ImmutableList<String> recipes = ImmutableList.of(
+                "Restart", "Rebuild", "Migrate", "Evacuate",
+                "HealthCheck", "Reboot", "Start", "Stop");
+
+    // used for JSON <-> String conversion
+    protected static StandardCoder coder = new StandardCoder();
+
+    // current state -- one of the 6 values, above
+    @Getter(onMethod = @__({@Override}))
+    private String state;
+
+    // transaction associated with this operation
+    private Transaction transaction;
+
+    // policy associated with this operation
+    @Getter(onMethod = @__({@Override}))
+    private Policy policy;
+
+    // initial onset message
+    private VirtualControlLoopEvent onset;
+
+    // attempt associated with this operation
+    @Getter(onMethod = @__({@Override}))
+    private int attempt;
+
+    // message, if any, associated with the result of this operation
+    @Getter(onMethod = @__({@Override}))
+    private String message = null;
+
+    // operation result -- set to a non-null value when the operation completes
+    @Getter(onMethod = @__({@Override}))
+    private PolicyResult result = null;
+
+    // the APPC LCM 'target' derived from the onset
+    private String target;
+
+    // reference to a Transaction adjunct supporting guard operations
+    private GuardAdjunct guardAdjunct;
+
+    // counter for how many partial failures were received from appc
+    private int partialFailureCount = 0;
+
+    // counter for how many partial success were received from appc
+    private int partialSuccessCount = 0;
+
+    /**
+     * Constructor -- initialize an LCM operation instance,
+     * try to acquire a lock, and start the guard query if we are ready.
+     *
+     * @param transaction the transaction the operation is running under
+     * @param policy the policy associated with this operation
+     * @param onset the initial onset event that triggered the transaction
+     * @param attempt this value starts at 1, and is incremented for each retry
+     */
+    public AppcLcmOperation(Transaction transaction, Policy policy, ControlLoopEvent onset,
+        int attempt) {
+        // state prior to aquiring the lock
+        // (will be changed when the lock is acquired)
+        this.state = LCM_WAIT_FOR_LOCK;
+        this.transaction = transaction;
+        this.policy = policy;
+        this.attempt = attempt;
+
+        if (!(onset instanceof VirtualControlLoopEvent)) {
+            // we need the correct 'onset' event type
+            state = LCM_COMPLETE;
+            result = PolicyResult.FAILURE;
+            message = "Onset event has the wrong type";
+            return;
+        }
+
+        this.onset = (VirtualControlLoopEvent)onset;
+
+        // fetch or create the guard adjunct -- note that 'guard' operations are
+        // only performed if a 'GuardContext' is present, and the adjunct was
+        // created by the Drools rules prior to creating this operation
+        this.guardAdjunct = transaction.getAdjunct(GuardAdjunct.class);
+
+        // attempt to get a lock for the VM -- if we get it immediately,
+        // we can go to the 'LCM_GUARD_PENDING' or 'LCM_BEGIN' state
+
+        target = this.onset.getAai().get(onset.getTarget()).toString();
+        String key = onset.getTargetType() + ":" + target;
+        if (transaction.getAdjunct(LockAdjunct.class).getLock(this, key,
+                transaction.getRequestId().toString(), false)) {
+            // lock was acquired immediately -- move on to the 'guard query'
+            // phase
+            guardQuery();
+        }
+    }
+
+    /**
+     * A method returning true if the A&AI subtag exists
+     * and the control loop exists and is not disabled and
+     * the target field exists as a key in the A&AI subtag.
+     *
+     * @param transaction the transaction corresponding to an event
+     * @param event the onset containing the A&AI subtag
+     * @return true if the A&AI subtag is valid, false otherwise
+     */
+    public static boolean isAaiValid(Transaction transaction, VirtualControlLoopEvent event) {
+        if (event.getAai() == null) {
+            transaction.setNotificationMessage("No A&AI Subtag");
+            return false;
+        } else if (!event.getAai().containsKey(DCAE_CLOSEDLOOP_DISABLED_FIELD)) {
+            transaction.setNotificationMessage(DCAE_CLOSEDLOOP_DISABLED_FIELD
+                                               + " information missing");
+            return false;
+        } else if (isClosedLoopDisabled(event.getAai())) {
+            transaction.setNotificationMessage(DCAE_CLOSEDLOOP_DISABLED_FIELD
+                                               + " is set to true");
+            return false;
+        } else if (!event.getAai().containsKey(event.getTarget())) {
+            transaction.setNotificationMessage("target field invalid - must have corresponding AAI value");
+            return false;
+        }
+        return true;
+    }
+
+    private static boolean isClosedLoopDisabled(Map<String, String> map) {
+        if (!map.containsKey(DCAE_CLOSEDLOOP_DISABLED_FIELD)) {
+            return false;
+        }
+        String disabled = map.get(DCAE_CLOSEDLOOP_DISABLED_FIELD);
+        return ("true".equalsIgnoreCase(disabled) || "y".equalsIgnoreCase(disabled));
+    }
+
+    /**
+     * trigger an asynchronous guard query -- if guard is not enabled,
+     * we go directly to the 'LCM_BEGIN' state.
+     */
+    private void guardQuery() {
+        if (guardAdjunct.asyncQuery(policy, target, onset.getRequestId().toString())) {
+            // 'GuardContext' is available --
+            // wait for an incoming 'PolicyGuardResponse' message
+            this.state = LCM_GUARD_PENDING;
+        } else {
+            // no 'GuardContext' is available -- go directly to the 'begin' state
+            this.state = LCM_BEGIN;
+            transaction.modify();
+        }
+    }
+
+    /*=====================================*/
+    /* 'LockAdjunct.Requestor' interface   */
+    /*=====================================*/
+
+    /**
+     * This method is called by 'LockAdjunct' if we initially had to wait for
+     * the lock, but it has now became available.
+     */
+    public void lockAvailable() {
+        if (this.state == LCM_WAIT_FOR_LOCK) {
+            // we have the lock -- invoke 'quardQuery()',
+            // go to the appropriate state, and mark the transaction as modified
+            guardQuery();
+
+            // the 'lockAvailable' method was presumably triggered by the
+            // release
+            // of the lock by an unrelated transaction -- 'transaction.modify'
+            // is
+            // called to let Drools know that our transaction has gone through a
+            // state change
+            transaction.modify();
+        }
+    }
+
+    /**
+     * This method is called by 'LockAdjunct' if the lock was unable to be
+     * obtained.
+     */
+    public void lockUnavailable() {
+        if (this.state == LCM_WAIT_FOR_LOCK) {
+            try {
+                setErrorStatus("Already processing event with this target");
+            } catch (ControlLoopException e) {
+                logger.debug("Lock could not be obtained for this operation");
+            }
+        }
+    }
+
+    /*=======================*/
+    /* 'Operation' interface */
+    /*=======================*/
+
+    /**
+     * This method maps the recipe to the correct rpc-name syntax.
+     */
+    private String toRpcName(String recipe) {
+        String rpcName = recipe.toLowerCase();
+        if ("healthcheck".equals(rpcName)) {
+            rpcName = "health-check";
+        }
+        return rpcName;
+    }
+
+    /**
+     * This method forwards the construction of the recipe's
+     * payload to the proper handler.
+     *
+     * @return a json representation of the payload
+     * @throws ControlLoopException if it occurs
+     */
+    protected String setPayload(Map<String, String> aai, String recipe) throws ControlLoopException {
+        Map<String, String> payload = null;
+
+        switch (recipe) {
+            case "restart":
+            case "rebuild":
+            case "migrate":
+            case "evacuate":
+            case "start":
+            case "stop":
+                if (this.policy.getTarget().getType() == TargetType.VM) {
+                    payload = setCommonPayload(aai);
+                }
+                break;
+            case "reboot":
+                payload = setRebootPayload();
+                break;
+            default:
+                payload = null;
+                break;
+        }
+
+        if (payload == null) {
+            return null;
+        }
+
+        try {
+            return coder.encode(payload);
+        } catch (CoderException e) {
+            return null;
+        }
+    }
+
+    /**
+     * This method will construct a payload for a restart, rebuild,
+     * migrate, or evacuate. The payload must be an escaped json
+     * string so gson is used to convert the payload hashmap into
+     * json
+     *
+     * @return a hashmap representation of the payload
+     * @throws ControlLoopException if it occurs
+     */
+    private Map<String, String> setCommonPayload(Map<String, String> aai) throws ControlLoopException {
+        Map<String, String> payload = new HashMap<>();
+
+        for (Map.Entry<String, String> entry : aai.entrySet()) {
+            switch (entry.getKey()) {
+                case DCAE_VSERVER_SELF_LINK_FIELD:
+                    if (entry.getValue() != null) {
+                        payload.put(APPC_LCM_VM_ID_FIELD, entry.getValue());
+                    } else {
+                        setErrorStatus("dcae onset is missing " + DCAE_VSERVER_SELF_LINK_FIELD);
+                    }
+                    break;
+                case DCAE_IDENTITY_FIELD:
+                    if (entry.getValue() != null) {
+                        payload.put(APPC_LCM_IDENTITY_URL_FIELD, entry.getValue());
+                    } else {
+                        setErrorStatus("dcae onset is missing " + DCAE_IDENTITY_FIELD);
+                    }
+                    break;
+                case DCAE_TENANT_ID_FIELD:
+                    if (entry.getValue() != null) {
+                        payload.put(APPC_LCM_TENANT_ID_FIELD, entry.getValue());
+                    } else {
+                        setErrorStatus("dcae onset is missing " + DCAE_TENANT_ID_FIELD);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        return payload;
+    }
+
+    /**
+     * This method will construct a payload for a reboot.
+     * The payload must be an escaped json string so gson is used
+     * to convert the payload hashmap into json. The reboot payload
+     * requires a type of "HARD" or "SOFT" reboot from the policy
+     * defined through CLAMP.
+     *
+     * @return an escaped json string representation of the payload
+     */
+    private Map<String, String> setRebootPayload() throws ControlLoopException {
+        Map<String, String> payload = new HashMap<>();
+
+        if (this.policy.getTarget().getType() == TargetType.VM) {
+            payload = setCommonPayload(onset.getAai());
+            // The tenant-id is not used for the reboot request so we can remove
+            // it after being added by the common payload
+            payload.remove(APPC_LCM_TENANT_ID_FIELD);
+        }
+
+        // Extract "HARD" or "SOFT" from YAML policy
+        String type = this.policy.getPayload().get("type").toUpperCase();
+        payload.put("type", type);
+
+        return payload;
+    }
+
+    /**
+     * Return the request message associated with this operation.
+     *
+     * {@inheritDoc}
+     *
+     * @throws ControlLoopException if it occurs
+     */
+    @Override
+    public Object getRequest() throws ControlLoopException {
+        AppcLcmCommonHeader commonHeader = new AppcLcmCommonHeader();
+        commonHeader.setRequestId(onset.getRequestId());
+        commonHeader.setOriginatorId("POLICY");
+        commonHeader.setSubRequestId(String.valueOf(attempt));
+
+        // Policy will send a default ttl of 10 minutes (600 seconds)
+        Map<String, String> flags = new HashMap<>();
+        flags.put("ttl", "600");
+        commonHeader.setFlags(flags);
+
+        String action = null;
+        for (String recipe: recipes) {
+            if (recipe.equalsIgnoreCase(policy.getRecipe())) {
+                action = recipe;
+                break;
+            }
+        }
+
+        if (action == null) {
+            setErrorStatus("Error - invalid recipe");
+        }
+
+        Map<String, String> actionIdentifiers = new HashMap<>();
+
+        // The vnf-id is needed for both VNF and VM level operations
+        if (onset.getAai().containsKey(DCAE_VNF_NAME_FIELD)) {
+            actionIdentifiers.put("vnf-id", onset.getAai().get(DCAE_VNF_ID_FIELD));
+        } else {
+            logger.error("Error - no AAI DCAE VNF NAME key in the onset");
+            setErrorStatus("Error - no VNF NAME key in the onset");
+        }
+
+        if (this.policy.getTarget().getType() == TargetType.VM) {
+            if (onset.getAai().containsKey(DCAE_VSERVER_ID_FIELD)) {
+                actionIdentifiers.put("vserver-id", onset.getAai().get(DCAE_VSERVER_ID_FIELD));
+            } else {
+                logger.error("Error - no DCAE VSERVER ID key in the onset AAI\n");
+                setErrorStatus("Error - no VSERVER ID key in the onset");
+            }
+        }
+
+        String payload = setPayload(onset.getAai(), action.toLowerCase());
+
+        // construct an APPC LCM 'Request' message
+        AppcLcmInput request = new AppcLcmInput();
+
+        request.setCommonHeader(commonHeader);
+        request.setAction(action);
+        request.setActionIdentifiers(actionIdentifiers);
+        request.setPayload(payload);
+
+        // Pass the LCM request to the LCM wrapper
+        AppcLcmDmaapWrapper dmaapWrapper = new AppcLcmDmaapWrapper();
+        dmaapWrapper.setVersion("2.0");
+        AppcLcmBody appcBody = new AppcLcmBody();
+        appcBody.setInput(request);
+        dmaapWrapper.setBody(appcBody);
+        dmaapWrapper.setCorrelationId(onset.getRequestId() + "-" + attempt);
+        dmaapWrapper.setRpcName(toRpcName(action));
+        dmaapWrapper.setType("request");
+
+        // go to the LCM_PENDING state, under the assumption that the
+        // calling Drools code will send out the message we are returning
+        this.state = LCM_PENDING;
+        transaction.modify();
+        return dmaapWrapper;
+    }
+
+    /**
+     * This method is called by 'incomingMessage' when the message is a
+     * 'PolicyGuardResponse' message (leaving 'incomingMessage' to focus on
+     * 'Response' messages).
+     *
+     * @param response the received guard response message
+     */
+    void incomingGuardMessage(PolicyGuardResponse response) {
+        // this message is only meaningful if we are waiting for a
+        // 'guard' response -- ignore it, if this isn't the case
+        if (this.state == LCM_GUARD_PENDING) {
+            if ("Deny".equals(response.getResult())) {
+                // this is a guard failure
+                logger.error("LCM operation denied by 'Guard'");
+                this.message = "Denied by Guard";
+                this.result = PolicyResult.FAILURE_GUARD;
+                this.state = LCM_COMPLETE;
+            } else {
+                // everything else is treated as 'Permit'
+                this.state = LCM_BEGIN;
+                transaction.modify();
+            }
+        }
+    }
+
+    /**
+     * An incoming message is being delivered to the operation.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public void incomingMessage(Object object) {
+        if (! (object instanceof AppcLcmDmaapWrapper)) {
+            if (object instanceof PolicyGuardResponse) {
+                incomingGuardMessage((PolicyGuardResponse)object);
+                return;
+            } else if (object instanceof ControlLoopEvent) {
+                incomingAbatedEvent((ControlLoopEvent) object);
+                return;
+            }
+            // ignore this message (not sure why we even got it)
+            return;
+        }
+
+        // If we reach this point, we have a 'AppcLcmDmaapWrapper' instance.
+        // The rest of this method is mostly copied from
+        // 'ControlLoopOperationManager.onResponse'.
+
+        AppcLcmOutput response = ((AppcLcmDmaapWrapper)object).getBody().getOutput();
+
+        //
+        // Determine which subrequestID (ie. attempt)
+        //
+        int operationAttempt;
+        try {
+            operationAttempt = Integer.parseInt(response.getCommonHeader()
+                                                .getSubRequestId());
+        } catch (NumberFormatException e) {
+            //
+            // We cannot tell what happened if this doesn't exist
+            // If the attempt cannot be parsed then we assume it is
+            // the current attempt
+            //
+            this.completeOperation(this.attempt, "Policy was unable to parse APP-C SubRequestID (it was null).",
+                PolicyResult.FAILURE_EXCEPTION);
+            return;
+        }
+        //
+        // Sanity check the response message
+        //
+        if (response.getStatus() == null) {
+            //
+            // We cannot tell what happened if this doesn't exist
+            //
+            this.completeOperation(operationAttempt,
+                "Policy was unable to parse APP-C response status field (it was null).",
+                PolicyResult.FAILURE_EXCEPTION);
+            return;
+        }
+        //
+        // Get the Response Code
+        //
+        AppcLcmResponseCode responseValue = AppcLcmResponseCode.toResponseValue(response.getStatus().getCode());
+        if (responseValue == null) {
+            //
+            // We are unaware of this code
+            //
+            this.completeOperation(operationAttempt, "Policy was unable to parse APP-C response status code field.",
+                PolicyResult.FAILURE_EXCEPTION);
+            return;
+        }
+        //
+        // Ok, let's figure out what APP-C's response is
+        //
+        switch (responseValue) {
+            case ACCEPTED:
+                //
+                // This is good, they got our original message and
+                // acknowledged it.
+                //
+                // Is there any need to track this?
+                //
+                return;
+            case PARTIAL_SUCCESS:
+                //
+                // Keep count of partial successes to determine
+                // if retries should be done at the vnf level
+                //
+                this.partialSuccessCount++;
+                return;
+            case PARTIAL_FAILURE:
+                //
+                // Keep count of partial failures to determine
+                // if no retries should be done
+                //
+                this.partialFailureCount++;
+                return;
+            case ERROR:
+            case REJECT:
+                //
+                // We'll consider these two codes as exceptions
+                //
+                this.completeOperation(operationAttempt, response.getStatus()
+                                       .getMessage(), PolicyResult.FAILURE_EXCEPTION);
+                return;
+            case SUCCESS:
+                //
+                //
+                //
+                this.completeOperation(operationAttempt, response.getStatus()
+                                       .getMessage(), PolicyResult.SUCCESS);
+                return;
+            case FAILURE:
+                // For the VNF level operations, retries will be attempted only
+                // if ALL individual VMs failed the operation
+                if (this.partialSuccessCount == 0) {
+                    // Since there are no partial successes, that means all VMs failed
+                    // if all vms fail, we can retry the VNF level action
+                    this.completeOperation(operationAttempt, response.getStatus()
+                                           .getMessage(), PolicyResult.FAILURE);
+                } else if (this.partialFailureCount > 0) {
+                    // Since only a subset of VMs had partial failures,
+                    // the result should go to final failure and not
+                    // retry or move on to the next policy in the chain.
+                    this.completeOperation(operationAttempt, response.getStatus()
+                                           .getMessage(), PolicyResult.FAILURE_EXCEPTION);
+                }
+                return;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * This method is called by 'incomingMessage' only when an 'ABATED' event is received before an APPC
+     * request is sent.
+     *
+     * @param event the control loop event that was received
+     */
+    private void incomingAbatedEvent(ControlLoopEvent event) {
+        // check if ClosedLoopEventStatus is 'abated'
+        if (event.isEventStatusValid() && "ABATED".equalsIgnoreCase(event.getClosedLoopEventStatus().toString())) {
+            this.result = PolicyResult.SUCCESS;
+            this.message = "Abatement received before APPC request was sent";
+            this.state = LCM_COMPLETE;
+        }
+    }
+
+    /**
+       * This method is called by 'incomingMessage' in order to complete the
+       * operation.
+       *
+       * @param attempt the operation attempt indicated in the response message
+       * @param message the value to store in the 'message' field'
+       * @param result the value to store in the 'result' field
+       */
+    void completeOperation(int attempt, String message, PolicyResult result) {
+        logger.debug("LCM: completeOperation("
+                     + "this.attempt=" + this.attempt
+                     + ", attempt=" + attempt
+                     + ", result=" + result
+                     + ", message=" + message);
+        if (this.attempt == attempt) {
+            // we need to verify that the attempt matches in order to reduce the
+            // chances that we are reacting to a prior 'Response' message that
+            // was received after we timed out (unfortunately, we can't guarantee
+            // this, because we have no reliable way to verify the 'recipe')
+
+            this.message = message;
+            this.result = result;
+            state = LCM_COMPLETE;
+        }
+    }
+
+    /**
+     * The operation has timed out.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public void timeout() {
+        result = PolicyResult.FAILURE_TIMEOUT;
+        state = LCM_COMPLETE;
+    }
+
+    void setErrorStatus(String message) throws ControlLoopException {
+        result = PolicyResult.FAILURE_EXCEPTION;
+        state = LCM_ERROR;
+        this.message = message;
+        transaction.modify();
+        throw new ControlLoopException(message);
+    }
+
+    /**
+     * This is called right after it's history entry has been completed.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public void histEntryCompleted(ControlLoopOperation histEntry) {
+        // give 'guard' a chance to create a DB entry (this only happens if
+        // we really have a 'GuardContext', and all of the needed parameters
+        // were provided in the '*-controller.properties' file)
+        guardAdjunct.asyncCreateDbEntry(histEntry, target);
+    }
+}
diff --git a/controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/model/AppcLcmResponseCode.java b/controlloop/m2/appclcm/src/main/java/org/onap/policy/m2/appclcm/model/AppcLcmResponseCode.java
new file mode 100644 (file)
index 0000000..ca812a4
--- /dev/null
@@ -0,0 +1,58 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/appclcm
+ * ================================================================================
+ * 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.appclcm.model;
+
+import java.io.Serializable;
+
+public enum AppcLcmResponseCode implements Serializable {
+    ACCEPTED, ERROR, REJECT, SUCCESS, FAILURE, PARTIAL_SUCCESS, PARTIAL_FAILURE;
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Translates the code to a string value that represents the meaning of the
+     * code.
+     *
+     * @param code
+     *            the numeric value that is returned by APPC based on success,
+     *            failure, etc. of the action requested
+     * @return the enum value equivalent of the APPC response code
+     */
+    public static AppcLcmResponseCode toResponseValue(int code) {
+        if (code == 100) {
+            return ACCEPTED;
+        } else if (code == 200) {
+            return ERROR;
+        } else if (code >= 300 && code <= 316) {
+            return REJECT;
+        } else if (code == 400) {
+            return SUCCESS;
+        } else if (code == 450 || (code >= 401 && code <= 406)) {
+            return FAILURE;
+        } else if (code == 500) {
+            return PARTIAL_SUCCESS;
+        } else if (code >= 501 && code <= 599) {
+            return PARTIAL_FAILURE;
+        }
+        return null;
+    }
+
+}
diff --git a/controlloop/m2/appclcm/src/main/resources/META-INF/services/org.onap.policy.m2.base.Actor b/controlloop/m2/appclcm/src/main/resources/META-INF/services/org.onap.policy.m2.base.Actor
new file mode 100644 (file)
index 0000000..2e60656
--- /dev/null
@@ -0,0 +1 @@
+org.onap.policy.m2.appclcm.AppcLcmActor
diff --git a/controlloop/m2/appclcm/src/test/java/appclcm/AppcLcmHealthCheckOperationTest.java b/controlloop/m2/appclcm/src/test/java/appclcm/AppcLcmHealthCheckOperationTest.java
new file mode 100644 (file)
index 0000000..0748b6e
--- /dev/null
@@ -0,0 +1,281 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/appclcm
+ * ================================================================================
+ * 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 appclcm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+
+import java.util.Properties;
+import java.util.UUID;
+
+import org.drools.core.WorkingMemory;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.onap.policy.appclcm.AppcLcmDmaapWrapper;
+import org.onap.policy.appclcm.AppcLcmInput;
+import org.onap.policy.appclcm.util.Serialization;
+import org.onap.policy.controlloop.ControlLoopEventStatus;
+import org.onap.policy.controlloop.ControlLoopException;
+import org.onap.policy.controlloop.ControlLoopTargetType;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.onap.policy.controlloop.policy.Target;
+import org.onap.policy.controlloop.policy.TargetType;
+import org.onap.policy.drools.m2.lock.LockAdjunct;
+import org.onap.policy.drools.system.PolicyEngineConstants;
+import org.onap.policy.m2.appclcm.AppcLcmHealthCheckOperation;
+import org.onap.policy.m2.base.Transaction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AppcLcmHealthCheckOperationTest {
+    private static Logger logger = LoggerFactory.getLogger(AppcLcmHealthCheckOperationTest.class);
+
+    public static Policy policy;
+    public static VirtualControlLoopEvent event;
+    public static Transaction transaction;
+    public static AppcLcmHealthCheckOperation operation;
+
+    /**
+     * Class-level setup.
+     */
+    @BeforeClass
+    public static void setup() {
+        PolicyEngineConstants.getManager().configure(new Properties());
+        PolicyEngineConstants.getManager().start();
+
+        policy = new Policy();
+        policy.setActor("APPCLCM");
+        policy.setTarget(new Target(TargetType.VM));
+
+        event = new VirtualControlLoopEvent();
+        event.setClosedLoopEventStatus(ControlLoopEventStatus.ONSET);
+        event.setRequestId(UUID.randomUUID());
+        event.setTarget("vserver.vserver-name");
+        event.setTargetType(ControlLoopTargetType.VM);
+        event.getAai().put("vserver.is-closed-loop-disabled", "false");
+        event.getAai().put("complex.state", "NJ");
+        event.getAai().put("vserver.l-interface.interface-name", "89ee9ee6-1e96-4063-b690-aa5ca9f73b32");
+        event.getAai().put("vserver.l-interface.l3-interface-ipv4-address-list.l3-inteface-ipv4-address",
+            "135.144.3.49");
+        event.getAai().put("vserver.l-interface.l3-interface-ipv6-address-list.l3-inteface-ipv6-address", null);
+        event.getAai().put("vserver.in-maint", "N");
+        event.getAai().put("complex.city", "AAIDefault");
+        event.getAai().put("vserver.vserver-id", "aa7a24f9-8791-491f-b31a-c8ba5ad9e2aa");
+        event.getAai().put("vserver.l-interface.network-name", "vUSP_DPA3_OAM_3750");
+        event.getAai().put("vserver.vserver-name", "ctsf0002vm013");
+        event.getAai().put("generic-vnf.vnf-name", "ctsf0002v");
+        event.getAai().put("generic-vnf.vnf-id", "0f551f1b-e4e5-4ce2-84da-eda916e06e1c");
+        event.getAai().put("generic-vnf.service-id", "e433710f-9217-458d-a79d-1c7aff376d89");
+        event.getAai().put("vserver.selflink", "https://compute-aic.dpa3.cci.att.com:8774/v2/d0719b845a804b368f8ac0bba39e188b/servers/aa7a24f9-8791-491f-b31a-c8ba5ad9e2aa");
+        event.getAai().put("generic-vnf.vnf-type", "vUSP - vCTS");
+        event.getAai().put("tenant.tenant-id", "d0719b845a804b368f8ac0bba39e188b");
+        event.getAai().put("cloud-region.identity-url", "https://compute-aic.dpa3.cci.att.com:8774/");
+        event.getAai().put("vserver.prov-status", "PROV");
+        event.getAai().put("complex.physical-location-id", "LSLEILAA");
+
+        WorkingMemory wm = mock(WorkingMemory.class);
+        transaction = new Transaction(wm, "clvusptest", event.getRequestId(), null);
+
+    }
+
+    @AfterClass
+    public static void cleanup() {
+        transaction.cleanup();
+        PolicyEngineConstants.getManager().stop();
+    }
+
+    @Test
+    public void getVnfHealthCheckRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("HEALTHCHECK");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmHealthCheckOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("health-check", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("HealthCheck", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertNotNull(appcRequest.getPayload());
+        assertTrue(appcRequest.getPayload().contains("host-ip-address"));
+
+        logger.info("health-check request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void incomingHealthCheckMessageHealthyStateTest() {
+        policy.setRecipe("HEALTHCHECK");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmHealthCheckOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"body\":{\"output\":{\"common-header\":{\"timestamp\":\"2017-08-25T21:06:23.037Z\","
+            + "\"api-ver\":\"5.00\",\"originator-id\":\"POLICY\","
+            + "\"request-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200\",\"sub-request-id\":\"1\",\"flags\":{}},"
+            + "\"status\":{\"code\":400,\"message\":\"HealthCheckSuccessful\"},"
+            + "\"payload\":\"{\\\"identifier\\\":\\\"scoperepresented\\\",\\\"state\\\":\\\"healthy\\\","
+            + "\\\"time\\\":\\\"01-01-1000:0000\\\"}\"}},\"version\":\"2.0\",\"rpc-name\":\"health-check\","
+            + "\"correlation-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200-1\",\"type\":\"response\"}";
+        AppcLcmDmaapWrapper healthCheckResp = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        operation.incomingMessage(healthCheckResp);
+        assertEquals(operation.getResult(), PolicyResult.SUCCESS);
+    }
+
+    @Test
+    public void incomingHealthCheckMessageUnhealthyStateTest() {
+        policy.setRecipe("HEALTHCHECK");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmHealthCheckOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"body\":{\"output\":{\"common-header\":{\"timestamp\":\"2017-08-25T21:06:23.037Z\","
+            + "\"api-ver\":\"5.00\",\"originator-id\":\"POLICY\","
+            + "\"request-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200\",\"sub-request-id\":\"1\",\"flags\":{}},"
+            + "\"status\":{\"code\":400,\"message\":\"VNF is unhealthy\"},"
+            + "\"payload\":\"{\\\"identifier\\\":\\\"scoperepresented\\\",\\\"state\\\":\\\"unhealthy\\\","
+            + "\\\"info\\\":\\\"Systemthresholdexceededdetails\\\",\\\"fault\\\":{\\\"cpuOverall\\\":0.80,"
+            + "\\\"cpuThreshold\\\":0.45},\\\"time\\\":\\\"01-01-1000:0000\\\"}\"}},\"version\":\"2.0\","
+            + "\"rpc-name\":\"health-check\",\"correlation-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200-1\","
+            + "\"type\":\"response\"}";
+        AppcLcmDmaapWrapper healthCheckResp = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        operation.incomingMessage(healthCheckResp);
+        assertEquals(operation.getResult(), PolicyResult.FAILURE);
+    }
+
+    @Test
+    public void incomingHealthCheckMessageUnknownStateTest() {
+        policy.setRecipe("HEALTHCHECK");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmHealthCheckOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"body\":{\"output\":{\"common-header\":{\"timestamp\":\"2017-08-25T21:06:23.037Z\","
+            + "\"api-ver\":\"5.00\",\"originator-id\":\"POLICY\","
+            + "\"request-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200\",\"sub-request-id\":\"1\",\"flags\":{}},"
+            + "\"status\":{\"code\":400,\"message\":\"VNF is unhealthy\"},"
+            + "\"payload\":\"{\\\"identifier\\\":\\\"scoperepresented\\\",\\\"state\\\":\\\"unknown\\\","
+            + "\\\"info\\\":\\\"Systemthresholdexceededdetails\\\",\\\"fault\\\":{\\\"cpuOverall\\\":0.80,"
+            + "\\\"cpuThreshold\\\":0.45},\\\"time\\\":\\\"01-01-1000:0000\\\"}\"}},\"version\":\"2.0\","
+            + "\"rpc-name\":\"health-check\",\"correlation-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200-1\","
+            + "\"type\":\"response\"}";
+        AppcLcmDmaapWrapper healthCheckResp = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        operation.incomingMessage(healthCheckResp);
+        assertEquals(operation.getResult(), PolicyResult.FAILURE_EXCEPTION);
+    }
+
+    @Test
+    public void incomingHealthCheckMessageNoStateTest() {
+        policy.setRecipe("HEALTHCHECK");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmHealthCheckOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"body\":{\"output\":{\"common-header\":{\"timestamp\":\"2017-08-25T21:06:23.037Z\","
+            + "\"api-ver\":\"5.00\",\"originator-id\":\"POLICY\","
+            + "\"request-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200\",\"sub-request-id\":\"1\",\"flags\":{}},"
+            + "\"status\":{\"code\":400,\"message\":\"VNF is unhealthy\"},"
+            + "\"payload\":\"{\\\"identifier\\\":\\\"scoperepresented\\\","
+            + "\\\"info\\\":\\\"Systemthresholdexceededdetails\\\",\\\"fault\\\":{\\\"cpuOverall\\\":0.80,"
+            + "\\\"cpuThreshold\\\":0.45},\\\"time\\\":\\\"01-01-1000:0000\\\"}\"}},\"version\":\"2.0\","
+            + "\"rpc-name\":\"health-check\",\"correlation-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200-1\","
+            + "\"type\":\"response\"}";
+        AppcLcmDmaapWrapper healthCheckResp = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        operation.incomingMessage(healthCheckResp);
+        assertEquals(operation.getResult(), PolicyResult.FAILURE_EXCEPTION);
+    }
+
+    @Test
+    public void incomingHealthCheckMessageUnsuccessfulTest() {
+        policy.setRecipe("HEALTHCHECK");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmHealthCheckOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"body\":{\"output\":{\"common-header\":{\"timestamp\":\"2017-08-25T21:06:23.037Z\","
+            + "\"api-ver\":\"5.00\",\"originator-id\":\"POLICY\","
+            + "\"request-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200\",\"sub-request-id\":\"1\",\"flags\":{}},"
+            + "\"status\":{\"code\":401,\"message\":\"Could not complete HealthCheck\"},"
+            + "\"payload\":\"{\\\"identifier\\\":\\\"scoperepresented\\\","
+            + "\\\"info\\\":\\\"Systemthresholdexceededdetails\\\",\\\"fault\\\":{\\\"cpuOverall\\\":0.80,"
+            + "\\\"cpuThreshold\\\":0.45},\\\"time\\\":\\\"01-01-1000:0000\\\"}\"}},\"version\":\"2.0\","
+            + "\"rpc-name\":\"health-check\",\"correlation-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200-1\","
+            + "\"type\":\"response\"}";
+        AppcLcmDmaapWrapper healthCheckResp = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        operation.incomingMessage(healthCheckResp);
+        assertEquals(operation.getResult(), PolicyResult.FAILURE);
+    }
+
+    @Test
+    public void incomingHealthCheckMessageNoPayloadTest() {
+        policy.setRecipe("HEALTHCHECK");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmHealthCheckOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"body\":{\"output\":{\"common-header\":{\"timestamp\":\"2017-08-25T21:06:23.037Z\","
+            + "\"api-ver\":\"5.00\",\"originator-id\":\"POLICY\","
+            + "\"request-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200\",\"sub-request-id\":\"1\",\"flags\":{}},"
+            + "\"status\":{\"code\":400,\"message\":\"VNF is unhealthy\"}}},\"version\":\"2.0\","
+            + "\"rpc-name\":\"health-check\",\"correlation-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200-1\","
+            + "\"type\":\"response\"}";
+        AppcLcmDmaapWrapper healthCheckResp = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        operation.incomingMessage(healthCheckResp);
+        assertEquals(operation.getResult(), PolicyResult.FAILURE_EXCEPTION);
+    }
+
+    @Test
+    public void incomingHealthCheckMessageEmptyPayloadTest() {
+        policy.setRecipe("HEALTHCHECK");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmHealthCheckOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"body\":{\"output\":{\"common-header\":{\"timestamp\":\"2017-08-25T21:06:23.037Z\","
+            + "\"api-ver\":\"5.00\",\"originator-id\":\"POLICY\","
+            + "\"request-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200\",\"sub-request-id\":\"1\",\"flags\":{}},"
+            + "\"status\":{\"code\":400,\"message\":\"VNF is unhealthy\"},\"payload\":\"\"}},\"version\":\"2.0\","
+            + "\"rpc-name\":\"health-check\",\"correlation-id\":\"664be3d2-6c12-4f4b-a3e7-c349acced200-1\","
+            + "\"type\":\"response\"}";
+        AppcLcmDmaapWrapper healthCheckResp = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        operation.incomingMessage(healthCheckResp);
+        assertEquals(operation.getResult(), PolicyResult.FAILURE_EXCEPTION);
+    }
+}
diff --git a/controlloop/m2/appclcm/src/test/java/appclcm/AppcLcmOperationTest.java b/controlloop/m2/appclcm/src/test/java/appclcm/AppcLcmOperationTest.java
new file mode 100644 (file)
index 0000000..0ddfb5b
--- /dev/null
@@ -0,0 +1,708 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/appclcm
+ * ================================================================================
+ * 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 appclcm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+
+import java.util.HashMap;
+import java.util.Properties;
+import java.util.UUID;
+
+import org.drools.core.WorkingMemory;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.onap.policy.appclcm.AppcLcmDmaapWrapper;
+import org.onap.policy.appclcm.AppcLcmInput;
+import org.onap.policy.appclcm.util.Serialization;
+import org.onap.policy.controlloop.ControlLoopEventStatus;
+import org.onap.policy.controlloop.ControlLoopException;
+import org.onap.policy.controlloop.ControlLoopTargetType;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.onap.policy.controlloop.policy.Target;
+import org.onap.policy.controlloop.policy.TargetType;
+import org.onap.policy.drools.m2.lock.LockAdjunct;
+import org.onap.policy.drools.system.PolicyEngineConstants;
+import org.onap.policy.m2.appclcm.AppcLcmOperation;
+import org.onap.policy.m2.base.Transaction;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AppcLcmOperationTest {
+    private static Logger logger = LoggerFactory.getLogger(AppcLcmOperationTest.class);
+
+    public static Policy policy;
+    public static VirtualControlLoopEvent event;
+    public static Transaction transaction;
+    public static AppcLcmOperation operation;
+
+    /**
+     * Class-level setup.
+     */
+    @BeforeClass
+    public static void start() {
+        PolicyEngineConstants.getManager().configure(new Properties());
+        PolicyEngineConstants.getManager().start();
+
+        policy = new Policy();
+        policy.setActor("APPCLCM");
+        policy.setTarget(new Target(TargetType.VM));
+
+        event = new VirtualControlLoopEvent();
+        event.setClosedLoopEventStatus(ControlLoopEventStatus.ONSET);
+        event.setRequestId(UUID.randomUUID());
+        event.setTarget("vserver.vserver-name");
+        event.setTargetType(ControlLoopTargetType.VM);
+        event.getAai().put("vserver.is-closed-loop-disabled", "false");
+        event.getAai().put("complex.state", "NJ");
+        event.getAai().put("vserver.l-interface.interface-name", "89ee9ee6-1e96-4063-b690-aa5ca9f73b32");
+        event.getAai().put("vserver.l-interface.l3-interface-ipv4-address-list.l3-inteface-ipv4-address",
+            "135.144.3.49");
+        event.getAai().put("vserver.l-interface.l3-interface-ipv6-address-list.l3-inteface-ipv6-address", null);
+        event.getAai().put("vserver.in-maint", "N");
+        event.getAai().put("complex.city", "AAIDefault");
+        event.getAai().put("vserver.vserver-id", "aa7a24f9-8791-491f-b31a-c8ba5ad9e2aa");
+        event.getAai().put("vserver.l-interface.network-name", "vUSP_DPA3_OAM_3750");
+        event.getAai().put("vserver.vserver-name", "ctsf0002vm013");
+        event.getAai().put("generic-vnf.vnf-name", "ctsf0002v");
+        event.getAai().put("generic-vnf.vnf-id", "0f551f1b-e4e5-4ce2-84da-eda916e06e1c");
+        event.getAai().put("generic-vnf.service-id", "e433710f-9217-458d-a79d-1c7aff376d89");
+        event.getAai().put("vserver.selflink", "https://compute-aic.dpa3.cci.att.com:8774/v2/d0719b845a804b368f8ac0bba39e188b/servers/aa7a24f9-8791-491f-b31a-c8ba5ad9e2aa");
+        event.getAai().put("generic-vnf.vnf-type", "vUSP - vCTS");
+        event.getAai().put("tenant.tenant-id", "d0719b845a804b368f8ac0bba39e188b");
+        event.getAai().put("cloud-region.identity-url", "https://compute-aic.dpa3.cci.att.com:8774/");
+        event.getAai().put("vserver.prov-status", "PROV");
+        event.getAai().put("complex.physical-location-id", "LSLEILAA");
+
+        WorkingMemory wm = mock(WorkingMemory.class);
+        transaction = new Transaction(wm, "clvusptest", event.getRequestId(), null);
+    }
+
+    @AfterClass
+    public static void cleanup() {
+        transaction.cleanup();
+        PolicyEngineConstants.getManager().stop();
+    }
+
+    @Test
+    public void getVmRestartRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("RESTART");
+        policy.getTarget().setType(TargetType.VM);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("restart", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Restart", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(event.getAai().get("vserver.vserver-id"), appcRequest.getActionIdentifiers().get("vserver-id"));
+        assertNotNull(appcRequest.getPayload());
+
+        logger.info("vm restart request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVnfRestartRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("RESTART");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("restart", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Restart", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(appcRequest.getActionIdentifiers().get("vserver-id"), null);
+        assertNull(appcRequest.getPayload());
+
+        logger.info("vnf restart request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVmRebuildRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("REBUILD");
+        policy.getTarget().setType(TargetType.VM);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("rebuild", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Rebuild", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(event.getAai().get("vserver.vserver-id"), appcRequest.getActionIdentifiers().get("vserver-id"));
+        assertNotNull(appcRequest.getPayload());
+
+        logger.info("vm rebuild request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVnfRebuildRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("REBUILD");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("rebuild", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Rebuild", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(appcRequest.getActionIdentifiers().get("vserver-id"), null);
+        assertNull(appcRequest.getPayload());
+
+        logger.info("vnf rebuild request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVmMigrateRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("MIGRATE");
+        policy.getTarget().setType(TargetType.VM);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("migrate", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Migrate", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(event.getAai().get("vserver.vserver-id"), appcRequest.getActionIdentifiers().get("vserver-id"));
+        assertNotNull(appcRequest.getPayload());
+
+        logger.info("vm migrate request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVnfMigrateRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("MIGRATE");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("migrate", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Migrate", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(appcRequest.getActionIdentifiers().get("vserver-id"), null);
+        assertNull(appcRequest.getPayload());
+
+        logger.info("vnf migrate request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVmEvacuateRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("EVACUATE");
+        policy.getTarget().setType(TargetType.VM);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("evacuate", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Evacuate", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(event.getAai().get("vserver.vserver-id"), appcRequest.getActionIdentifiers().get("vserver-id"));
+        assertNotNull(appcRequest.getPayload());
+
+        logger.info("vm evacuate request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVnfEvacuateRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("EVACUATE");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("evacuate", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Evacuate", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(appcRequest.getActionIdentifiers().get("vserver-id"), null);
+        assertNull(appcRequest.getPayload());
+
+        logger.info("vnf evacuate request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVmRebootRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("REBOOT");
+        policy.getTarget().setType(TargetType.VM);
+        policy.setPayload(new HashMap<String, String>());
+        policy.getPayload().put("type", "HARD");
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("reboot", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Reboot", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(event.getAai().get("vserver.vserver-id"), appcRequest.getActionIdentifiers().get("vserver-id"));
+        assertNotNull(appcRequest.getPayload());
+
+        logger.info("vm reboot request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVnfRebootRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("REBOOT");
+        policy.getTarget().setType(TargetType.VNF);
+        policy.setPayload(new HashMap<String, String>());
+        policy.getPayload().put("type", "HARD");
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("reboot", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Reboot", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(appcRequest.getActionIdentifiers().get("vserver-id"), null);
+        assertNotNull(appcRequest.getPayload());
+
+        logger.info("vnf reboot request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVnfStartRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("START");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("start", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Start", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(appcRequest.getActionIdentifiers().get("vserver-id"), null);
+        assertNull(appcRequest.getPayload());
+
+        logger.info("vnf start request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVmStartRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("START");
+        policy.getTarget().setType(TargetType.VM);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("start", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Start", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(event.getAai().get("vserver.vserver-id"), appcRequest.getActionIdentifiers().get("vserver-id"));
+        assertNotNull(appcRequest.getPayload());
+
+        logger.info("vm start request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVnfStopRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("STOP");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+        assertEquals("stop", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Stop", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(appcRequest.getActionIdentifiers().get("vserver-id"), null);
+        assertNull(appcRequest.getPayload());
+
+        logger.info("vnf stop request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    @Test
+    public void getVmStopRequestTest() throws ControlLoopException {
+
+        policy.setRecipe("STOP");
+        policy.getTarget().setType(TargetType.VM);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        Object request = operation.getRequest();
+        assertTrue(request instanceof AppcLcmDmaapWrapper);
+
+        AppcLcmDmaapWrapper dmaapRequest = (AppcLcmDmaapWrapper) request;
+        assertEquals("request", dmaapRequest.getType());
+
+        assertEquals("stop", dmaapRequest.getRpcName());
+        assertNotNull(dmaapRequest.getBody());
+
+        AppcLcmInput appcRequest = dmaapRequest.getBody().getInput();
+        assertNotNull(appcRequest.getCommonHeader());
+        assertEquals("2.00", appcRequest.getCommonHeader().getApiVer());
+        assertEquals("POLICY", appcRequest.getCommonHeader().getOriginatorId());
+        assertNotNull(appcRequest.getAction());
+        assertEquals("Stop", appcRequest.getAction());
+        assertNotNull(appcRequest.getActionIdentifiers());
+        assertEquals(event.getAai().get("generic-vnf.vnf-id"), appcRequest.getActionIdentifiers().get("vnf-id"));
+        assertEquals(event.getAai().get("vserver.vserver-id"), appcRequest.getActionIdentifiers().get("vserver-id"));
+        assertNotNull(appcRequest.getPayload());
+
+        logger.info("vm stop request: {}", Serialization.gson.toJson(request, AppcLcmDmaapWrapper.class));
+
+    }
+
+    /* ===================================================================== */
+
+    /*
+     * these tests are for ensuring the incoming response messages process
+     * properly and translate to the expected policy result
+     */
+
+    @Test
+    public void incomingVmSuccessMessageTest() {
+        policy.setRecipe("RESTART");
+        policy.getTarget().setType(TargetType.VM);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"version\": \"2.0\",\"rpc-name\": \"Restart\",\"correlation-id\": \"baf5ba32-6b8c-430c-a91b-02d2c0ba3064-1\",\"type\": \"response\",\"body\": {\"output\": {\"status\": {\"code\": 400,\"message\": \"Restart Successful\"},\"common-header\": {\"timestamp\": \"2017-07-18T16:52:06.186Z\",\"api-ver\": \"2.01\",\"request-id\": \"baf5ba32-6b8c-430c-a91b-02d2c0ba3064\",\"sub-request-id\": \"1\",\"flags\": {\"ttl\": 600}},\"payload\": \"{\\\"vm-id\\\":\\\"http://135.25.246.131:8774/v2/81fc2bc61f974de1b5a49e8c2ec090bb/servers/75dce20c-97f9-454d-abcc-aa904a33df5a\\\",\\\"tenant-id\\\":\\\"test2\\\"}\"}}}";
+        AppcLcmDmaapWrapper restartResponse = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        operation.incomingMessage(restartResponse);
+        assertEquals(operation.getResult(), PolicyResult.SUCCESS);
+    }
+
+    @Test
+    public void incomingVnfSuccessMessageTest() {
+        policy.setRecipe("RESTART");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"version\": \"2.0\",\"rpc-name\": \"Restart\",\"correlation-id\": \"baf5ba32-6b8c-430c-a91b-02d2c0ba3064-1\",\"type\": \"response\",\"body\": {\"output\": {\"status\": {\"code\": 500,\"message\": \"Restart Successful\"},\"common-header\": {\"timestamp\": \"2017-07-18T16:52:06.186Z\",\"api-ver\": \"2.01\",\"request-id\": \"baf5ba32-6b8c-430c-a91b-02d2c0ba3064\",\"sub-request-id\": \"1\",\"flags\": {\"ttl\": 600}},\"payload\": \"{\\\"vm-id\\\":\\\"http://135.25.246.131:8774/v2/81fc2bc61f974de1b5a49e8c2ec090bb/servers/75dce20c-97f9-454d-abcc-aa904a33df5a\\\",\\\"tenant-id\\\":\\\"test2\\\"}\"}}}";
+        AppcLcmDmaapWrapper restartResponse = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        /* Send in several partial success messages */
+        for (int i = 0; i < 5; i++) {
+            operation.incomingMessage(restartResponse);
+            assertEquals(operation.getResult(), null);
+        }
+
+        /* Send in an operation success */
+        restartResponse.getBody().getOutput().getStatus().setCode(400);
+        operation.incomingMessage(restartResponse);
+        assertEquals(operation.getResult(), PolicyResult.SUCCESS);
+    }
+
+    @Test
+    public void incomingVmFailureMessageTest() {
+        policy.setRecipe("RESTART");
+        policy.getTarget().setType(TargetType.VM);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"version\": \"2.0\",\"rpc-name\": \"Restart\",\"correlation-id\": \"baf5ba32-6b8c-430c-a91b-02d2c0ba3064-1\",\"type\": \"response\",\"body\": {\"output\": {\"status\": {\"code\": 401,\"message\": \"Restart Successful\"},\"common-header\": {\"timestamp\": \"2017-07-18T16:52:06.186Z\",\"api-ver\": \"2.01\",\"request-id\": \"baf5ba32-6b8c-430c-a91b-02d2c0ba3064\",\"sub-request-id\": \"1\",\"flags\": {\"ttl\": 600}},\"payload\": \"{\\\"vm-id\\\":\\\"http://135.25.246.131:8774/v2/81fc2bc61f974de1b5a49e8c2ec090bb/servers/75dce20c-97f9-454d-abcc-aa904a33df5a\\\",\\\"tenant-id\\\":\\\"test2\\\"}\"}}}";
+        AppcLcmDmaapWrapper restartResponse = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        operation.incomingMessage(restartResponse);
+        assertEquals(operation.getResult(), PolicyResult.FAILURE);
+    }
+
+    @Test
+    public void incomingAllVnfFailureMessageTest() {
+        policy.setRecipe("RESTART");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"version\": \"2.0\",\"rpc-name\": \"Restart\",\"correlation-id\": \"baf5ba32-6b8c-430c-a91b-02d2c0ba3064-1\",\"type\": \"response\",\"body\": {\"output\": {\"status\": {\"code\": 501,\"message\": \"Restart Successful\"},\"common-header\": {\"timestamp\": \"2017-07-18T16:52:06.186Z\",\"api-ver\": \"2.01\",\"request-id\": \"baf5ba32-6b8c-430c-a91b-02d2c0ba3064\",\"sub-request-id\": \"1\",\"flags\": {\"ttl\": 600}},\"payload\": \"{\\\"vm-id\\\":\\\"http://135.25.246.131:8774/v2/81fc2bc61f974de1b5a49e8c2ec090bb/servers/75dce20c-97f9-454d-abcc-aa904a33df5a\\\",\\\"tenant-id\\\":\\\"test2\\\"}\"}}}";
+        AppcLcmDmaapWrapper restartResponse = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        /* Send in ALL failure messages */
+        for (int i = 0; i < 5; i++) {
+            operation.incomingMessage(restartResponse);
+            assertEquals(operation.getResult(), null);
+        }
+
+        /* Send in an operation failure */
+        restartResponse.getBody().getOutput().getStatus().setCode(401);
+        operation.incomingMessage(restartResponse);
+
+        /* Because every VM failed in the VNF, it should be failure result */
+        assertEquals(operation.getResult(), PolicyResult.FAILURE);
+    }
+
+    @Test
+    public void incomingPartialVnfFailureMessageTest() {
+        policy.setRecipe("RESTART");
+        policy.getTarget().setType(TargetType.VNF);
+        operation = new AppcLcmOperation(transaction, policy, event, 1);
+
+        String lcmRespJson = "{\"version\": \"2.0\",\"rpc-name\": \"Restart\",\"correlation-id\": \"baf5ba32-6b8c-430c-a91b-02d2c0ba3064-1\",\"type\": \"response\",\"body\": {\"output\": {\"status\": {\"code\": 500,\"message\": \"Restart Successful\"},\"common-header\": {\"timestamp\": \"2017-07-18T16:52:06.186Z\",\"api-ver\": \"2.01\",\"request-id\": \"baf5ba32-6b8c-430c-a91b-02d2c0ba3064\",\"sub-request-id\": \"1\",\"flags\": {\"ttl\": 600}},\"payload\": \"{\\\"vm-id\\\":\\\"http://135.25.246.131:8774/v2/81fc2bc61f974de1b5a49e8c2ec090bb/servers/75dce20c-97f9-454d-abcc-aa904a33df5a\\\",\\\"tenant-id\\\":\\\"test2\\\"}\"}}}";
+        AppcLcmDmaapWrapper restartResponse = Serialization.gson.fromJson(lcmRespJson, AppcLcmDmaapWrapper.class);
+
+        /* Send in several partial success messages */
+        for (int i = 0; i < 5; i++) {
+            operation.incomingMessage(restartResponse);
+            assertEquals(operation.getResult(), null);
+        }
+
+        /* Change status to partial failure */
+        restartResponse.getBody().getOutput().getStatus().setCode(501);
+
+        /* Send in several partial failures messages */
+        for (int i = 0; i < 5; i++) {
+            operation.incomingMessage(restartResponse);
+            assertEquals(operation.getResult(), null);
+        }
+
+        /* Send in an operation failure */
+        restartResponse.getBody().getOutput().getStatus().setCode(401);
+        operation.incomingMessage(restartResponse);
+
+        /*
+         * Only a subset of VMs failed in the VNF so the
+         * result will be failure_exception
+         */
+        assertEquals(operation.getResult(), PolicyResult.FAILURE_EXCEPTION);
+    }
+
+    /* ===================================================================== */
+
+    /*
+     * these tests are for validating the A&AI subtag and target in an onset
+     */
+
+    @Test
+    public void validAaiSubtagTest() {
+        transaction.setNotificationMessage(null);
+        VirtualControlLoopEvent validEvent = new VirtualControlLoopEvent();
+        validEvent.setTarget("vserver.vserver-name");
+        validEvent.getAai().put(AppcLcmOperation.DCAE_CLOSEDLOOP_DISABLED_FIELD, "false");
+        validEvent.getAai().put(validEvent.getTarget(), "VM001");
+        assertTrue(AppcLcmOperation.isAaiValid(transaction, validEvent));
+        assertNull(transaction.getNotificationMessage());
+    }
+
+    @Test
+    public void noAaiSubtagTest() {
+        transaction.setNotificationMessage(null);
+        VirtualControlLoopEvent noAaiTag = new VirtualControlLoopEvent();
+        noAaiTag.setAai(null);
+        assertFalse(AppcLcmOperation.isAaiValid(transaction, noAaiTag));
+        assertEquals(transaction.getNotificationMessage(), "No A&AI Subtag");
+    }
+
+    @Test
+    public void noClosedLoopDisabledInAaiTest() {
+        transaction.setNotificationMessage(null);
+        VirtualControlLoopEvent invalidEvent = new VirtualControlLoopEvent();
+        assertFalse(AppcLcmOperation.isAaiValid(transaction, invalidEvent));
+        assertEquals(AppcLcmOperation.DCAE_CLOSEDLOOP_DISABLED_FIELD
+                     + " information missing", transaction.getNotificationMessage());
+    }
+
+    @Test
+    public void closedLoopDisabledInAaiTest() {
+        transaction.setNotificationMessage(null);
+        VirtualControlLoopEvent invalidEvent = new VirtualControlLoopEvent();
+        invalidEvent.getAai().put(AppcLcmOperation.DCAE_CLOSEDLOOP_DISABLED_FIELD, "true");
+        assertFalse(AppcLcmOperation.isAaiValid(transaction, invalidEvent));
+        assertEquals(AppcLcmOperation.DCAE_CLOSEDLOOP_DISABLED_FIELD
+                     + " is set to true", transaction.getNotificationMessage());
+    }
+
+    @Test
+    public void targetMismatchInAaiTest() {
+        transaction.setNotificationMessage(null);
+        VirtualControlLoopEvent validEvent = new VirtualControlLoopEvent();
+        validEvent.setTarget("vserver.vserver-name");
+        validEvent.getAai().put(AppcLcmOperation.DCAE_CLOSEDLOOP_DISABLED_FIELD, "false");
+        assertFalse(AppcLcmOperation.isAaiValid(transaction, validEvent));
+        assertEquals("target field invalid - must have corresponding AAI value",
+                     transaction.getNotificationMessage());
+    }
+}
diff --git a/controlloop/m2/appclcm/src/test/java/model/AppcLcmResponseCodeTest.java b/controlloop/m2/appclcm/src/test/java/model/AppcLcmResponseCodeTest.java
new file mode 100644 (file)
index 0000000..fc33492
--- /dev/null
@@ -0,0 +1,44 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * appclcm
+ * ================================================================================
+ * 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 model;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+import org.onap.policy.m2.appclcm.model.AppcLcmResponseCode;
+
+
+public class AppcLcmResponseCodeTest {
+
+    @Test
+    public void test() {
+        assertEquals(AppcLcmResponseCode.ACCEPTED, AppcLcmResponseCode.toResponseValue(100));
+        assertEquals(AppcLcmResponseCode.ERROR, AppcLcmResponseCode.toResponseValue(200));
+        assertEquals(AppcLcmResponseCode.REJECT, AppcLcmResponseCode.toResponseValue(300));
+        assertEquals(AppcLcmResponseCode.SUCCESS, AppcLcmResponseCode.toResponseValue(400));
+        assertEquals(AppcLcmResponseCode.FAILURE, AppcLcmResponseCode.toResponseValue(450));
+        assertEquals(AppcLcmResponseCode.PARTIAL_SUCCESS, AppcLcmResponseCode.toResponseValue(500));
+        assertEquals(AppcLcmResponseCode.PARTIAL_FAILURE, AppcLcmResponseCode.toResponseValue(501));
+        assertNull(AppcLcmResponseCode.toResponseValue(600));
+    }
+}
diff --git a/controlloop/m2/base/pom.xml b/controlloop/m2/base/pom.xml
new file mode 100644 (file)
index 0000000..e59025c
--- /dev/null
@@ -0,0 +1,105 @@
+<?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/maven-v4_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>base</artifactId>
+  <name>Experimental Control Loop Model - base</name>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>guard</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+      <artifactId>eventmanager</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.models.policy-models-interactions</groupId>
+      <artifactId>model-yaml</artifactId>
+      <version>${policy.models.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+      <artifactId>events</artifactId>
+      <version>${policy.models.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+      <artifactId>aai</artifactId>
+      <version>${policy.models.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+      <artifactId>sdc</artifactId>
+      <version>${policy.models.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-pdp</groupId>
+      <artifactId>policy-management</artifactId>
+      <version>${version.policy.drools-pdp}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.projectlombok</groupId>
+      <artifactId>lombok</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-api-mockito</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.powermock</groupId>
+      <artifactId>powermock-module-junit4</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Actor.java b/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Actor.java
new file mode 100644 (file)
index 0000000..00eb181
--- /dev/null
@@ -0,0 +1,52 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/base
+ * ================================================================================
+ * 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.base;
+
+import org.onap.policy.controlloop.ControlLoopEvent;
+import org.onap.policy.controlloop.policy.Policy;
+
+/**
+ * This is the 'Actor' interface -- objects implementing this interface
+ * are placed within the 'nameToActor' table within class 'Transaction'.
+ * All of the instances are created and inserted in the table at initialization
+ * time, and are located using the 'Policy.actor' field as a key.
+ */
+public interface Actor {
+    /**
+     * Return the name associated with this Actor.
+     *
+     * @return the name associated with this Actor (as it appears in the 'yaml')
+     */
+    String getName();
+
+    /**
+     * Create an operation for this actor, based on the supplied policy.
+     *
+     * @param transaction the transaction the operation is running under
+     * @param policy the policy associated with this operation
+     * @param onset the initial onset event that triggered the transaction
+     * @param attempt this value starts at 1, and is incremented for each retry
+     * @return the Operation instance
+     */
+    Operation createOperation(
+        Transaction transaction, Policy policy, ControlLoopEvent onset,
+        int attempt);
+}
diff --git a/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/GuardAdjunct.java b/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/GuardAdjunct.java
new file mode 100644 (file)
index 0000000..34397e2
--- /dev/null
@@ -0,0 +1,123 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/base
+ * ================================================================================
+ * 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.base;
+
+import java.io.Serializable;
+
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.guard.GuardContext;
+
+/**
+ * This adjunct class provides a way of accessing 'GuardContext' for any
+ * operations that use 'guard'. It needs to be created and inserted into the
+ * transaction 'adjunct' list prior to creating any operations that use it.
+ *
+ * <p>TBD: Serialization will need to factor in the fact that 'GuardContext'
+ * is a shared object.
+ */
+public class GuardAdjunct implements Transaction.Adjunct, Serializable {
+    private static final long serialVersionUID = 1L;
+
+    // the associated transaction
+    private Transaction transaction = null;
+
+    // the 'GuardContext' instance
+    private GuardContext context = null;
+
+    /**
+     * Constructor -- just in case 'getInstance()' is used to create this
+     * instance (in which case 'guard' will not be used).
+     */
+    public GuardAdjunct() {
+        // This comment is here to keep SONAR from getting upset
+    }
+
+    /**
+     * This method is called to create the adjunct, and insert it into the
+     * transaction.
+     *
+     * @param transaction the associated transaction
+     * @param context the GuardContext derived from the controller properties
+     */
+    public static void create(Transaction transaction, GuardContext context) {
+        GuardAdjunct ga = new GuardAdjunct();
+        ga.transaction = transaction;
+        ga.context = context;
+        transaction.putAdjunct(ga);
+    }
+
+    /**
+     * Return the GuardContext instance.
+     *
+     * @return the GuardContext instance
+     */
+    public GuardContext get() {
+        return context;
+    }
+
+    /**
+     * Do an asynchronous 'guard' query, and place the result in Drools memory.
+     *
+     * @param policy the policy associated with the operation
+     * @param target the target in a form meaningful to 'guard'
+     * @param requestId the transaction's request id
+     * @return 'true' if we have a 'GuardContext', and a response is expected,
+     *     'false' if 'guard' was not used, and should be skipped
+     */
+    public boolean asyncQuery(Policy policy, String target, String requestId) {
+        if (context != null) {
+            // note that we still return 'true' as long as we have a
+            // 'GuardContext', even when 'guard.disabled' is set -- as long
+            // as there is an asynchronous response coming
+            context.asyncQuery(transaction.getWorkingMemory(),
+                               policy.getActor(),
+                               policy.getRecipe(),
+                               target,
+                               requestId,
+                               transaction.getClosedLoopControlName());
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Create a DB entry describing this operation.
+     *
+     * @param op the history entry associated with the operation
+     * @param target the same target that was passed on the 'asyncQuery' call
+     */
+    public void asyncCreateDbEntry(ControlLoopOperation op, String target) {
+        if (context != null) {
+            context.asyncCreateDbEntry(
+                op.getStart(),
+                op.getEnd(),
+                transaction.getClosedLoopControlName(),
+                op.getActor(),
+                op.getOperation(),
+                target,
+                transaction.getRequestId().toString(),
+                op.getSubRequestId(),
+                op.getMessage(),
+                op.getOutcome());
+        }
+    }
+}
diff --git a/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/OnsetAdapter.java b/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/OnsetAdapter.java
new file mode 100644 (file)
index 0000000..37a999f
--- /dev/null
@@ -0,0 +1,156 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/base
+ * ================================================================================
+ * 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.base;
+
+import java.io.Serializable;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.onap.policy.controlloop.ControlLoopEvent;
+import org.onap.policy.controlloop.ControlLoopNotification;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/*
+ * This class maps the class of an incoming ONSET message into the
+ * appropriate adapter. The default adapter is included here as well.
+ */
+public class OnsetAdapter implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private static Logger logger = LoggerFactory.getLogger(OnsetAdapter.class);
+
+    // table mapping onset message class to 'OnsetAdapter' instance
+    private static Map<Class,OnsetAdapter> map = new ConcurrentHashMap<>();
+
+    /**
+     * This method is called to add an entry to the table.
+     *
+     * @param clazz the class of the ONSET message
+     * @param value an instance of 'OnsetAdapter' that should be
+     *     associated with 'clazz'
+     */
+    public static void register(Class clazz, OnsetAdapter value) {
+        // only create an entry if one doesn't already exist
+        map.putIfAbsent(clazz, value);
+    }
+
+    /**
+     * Map an incoming event's class into the appropriate 'OnsetAdapter'
+     * to use.
+     *
+     * @param event this is the onset event
+     * @return an adapter appropriate for the 'event'
+     */
+    public static OnsetAdapter get(ControlLoopEvent event) {
+        Class<?> clazz = event.getClass();
+        OnsetAdapter rval = map.get(clazz);
+        if (rval != null) {
+            return rval;
+        }
+
+        // This algorithm below is generic, in the sense that it can be used
+        // to find a "best match" for any class out of a set of classes
+        // using the class inheritance relationships. In the general case,
+        // it is possible that there could be multiple best matches, but this
+        // can only happen if all of the matching keys are interfaces,
+        // except perhaps one. If there are multiple matches,
+        // one will be chosen "at random".
+
+        // we need to look for the best match of 'clazz'
+        HashSet<Class> matches = new HashSet<>();
+        Class chosenMatch = null;
+        synchronized (map) {
+            for (Class<?> possibleMatch : map.keySet()) {
+                if (possibleMatch.isAssignableFrom(clazz)) {
+                    // we have a match -- see if it is the best match
+                    boolean add = true;
+                    for (Class<?> match : new ArrayList<Class>(matches)) {
+                        if (match.isAssignableFrom(possibleMatch)) {
+                            // 'possibleMatch' is a better match than 'match'
+                            matches.remove(match);
+                        } else if (possibleMatch.isAssignableFrom(match)) {
+                            // we already have a better match
+                            add = false;
+                            break;
+                        }
+                    }
+                    if (add) {
+                        matches.add(possibleMatch);
+                    }
+                }
+            }
+            if (!matches.isEmpty()) {
+                // we have at least one match
+                chosenMatch = matches.iterator().next();
+                rval = map.get(chosenMatch);
+
+                // add this entry back into the table -- this means we can
+                // now use this cached entry, and don't have to run through
+                // the algorithm again for this class
+                map.put(clazz, rval);
+            }
+        }
+
+        if (matches.isEmpty()) {
+            logger.error("no matches for {}", clazz);
+        } else if (matches.size() != 1) {
+            logger.warn("multiple matches for {}: {} -- chose {}",
+                clazz, matches, chosenMatch);
+        }
+
+        return rval;
+    }
+
+    /* ============================================================ */
+
+    // the following code creates an initial entry in the table
+    private static OnsetAdapter instance = new OnsetAdapter();
+
+    static {
+        register(ControlLoopEvent.class, instance);
+    }
+
+    // the new 'ControlLoopNotification' is abstract
+    public static class BaseControlLoopNotification extends ControlLoopNotification {
+        BaseControlLoopNotification(ControlLoopEvent event) {
+            super(event);
+        }
+    }
+
+    /**
+     * This method is what all of the fuss is about -- we want to create
+     * a 'ControlLoopNotification' instance compatible with the type of the
+     * 'event' argument. This is the default implementation -- subclasses of
+     * 'ControlLoopEvent' may have entries in the table that are specialized
+     * generate objects that are a subclass of 'ControlLoopNotification'
+     * appropriate for the transaction type.
+     *
+     * @param event this is the event in question
+     * @return a 'ControlLoopNotification' instance based upon this event
+     */
+    public ControlLoopNotification createNotification(ControlLoopEvent event) {
+        return new BaseControlLoopNotification(event);
+    }
+}
diff --git a/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Operation.java b/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Operation.java
new file mode 100644 (file)
index 0000000..5ca62fa
--- /dev/null
@@ -0,0 +1,125 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/base
+ * ================================================================================
+ * 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.base;
+
+import java.io.Serializable;
+
+import org.onap.policy.controlloop.ControlLoopException;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.policy.PolicyResult;
+
+/**
+ * This is the 'Operation' interface -- each object implementing this
+ * interface exists for the duration of a single operation.
+ *
+ * <p>An operation typically includes some of the following steps:
+ * 1) Acquiring locks
+ * 2) 'Guard' query to see if the operation should proceed (some operations)
+ * 3) Outgoing request (usually using DMAAP or UEB, but possibly HTTP or HTTPS)
+ * 4) Incoming response
+ */
+public interface Operation extends Serializable {
+    /**
+     * This method is used as part of sending out the request. In the case of
+     * DMAAP or UEB interfaces, the method returns the message to be sent,
+     * but leaves it to the Drools code to do the actual sending. In the case
+     * of HTTP or HTTPS (e.g. AOTS), the method itself may run the operation in
+     * a different thread.
+     *
+     * @return an object containing the message
+     * @throws ControlLoopException if it occurs
+     */
+    Object getRequest() throws ControlLoopException;
+
+    /**
+     * Return the 'Policy' instance associated with this operation.
+     *
+     * @return the 'Policy' instance associated with this operation
+     */
+    Policy getPolicy();
+
+    /**
+     * The 'state' of an operation is also the state of the 'Transaction'
+     * instance, while that operation is active. The state is often referenced
+     * in the 'when' clause of Drools rules, with the rules resembling state
+     * transition routines (state + event -> operation). In order to avoid
+     * confusion, the state names should be unique across all operations --
+     * this is managed by having each state name begin with 'ACTOR.', where
+     * 'ACTOR' is the actor associated with the operation.
+     *
+     * @return a string value indicating the state of the operation
+     */
+    String getState();
+
+    /**
+     * This is set to '1' for the initial attempt, and is incremented by one
+     * for each retry. Note that a new 'Operation' instance is created for
+     * each attempt.
+     *
+     * @return '1' for the initial attempt of an operation, and incremented
+     *     for each retry
+     */
+    int getAttempt();
+
+    /**
+     * Return the result of the operation.
+     *
+     * @return the result of the operation
+     *     ('null' if the operation is still in progress)
+     */
+    PolicyResult getResult();
+
+    /**
+     * Return the message associated with the completed operation.
+     *
+     * @return the message associated with the completed operation
+     *     ('null' if the operation is still in progress)
+     */
+    String getMessage();
+
+    /**
+     * An incoming message is being delivered to the operation. The type of
+     * the message is operation-dependent, and an operation will typically
+     * understand only one or two message types, and ignore the rest. The
+     * calling Drools code is written to assume that the transaction has been
+     * modified -- frequently, a state transition occurs as a result of
+     * the message.
+     *
+     * @param object the incoming message
+     */
+    void incomingMessage(Object object);
+
+    /**
+     * The operation has timed out. This typically results in the operation
+     * completing, but that is not enforced.
+     */
+    void timeout();
+
+    /**
+     * This method is called on every operation right after its history
+     * entry has been completed. It gives the operation a chance to do some
+     * processing based on this entry (e.g. create a 'guard' entry in the DB).
+     *
+     * @param histEntry the history entry for this particular operation
+     */
+    default void histEntryCompleted(ControlLoopOperation histEntry) {}
+}
diff --git a/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Transaction.java b/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Transaction.java
new file mode 100644 (file)
index 0000000..65bff68
--- /dev/null
@@ -0,0 +1,717 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/base
+ * ================================================================================
+ * 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.base;
+
+import java.io.Serializable;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.UUID;
+
+import lombok.Getter;
+import org.drools.core.WorkingMemory;
+import org.kie.api.runtime.rule.FactHandle;
+
+import org.onap.policy.controlloop.ControlLoopEvent;
+import org.onap.policy.controlloop.ControlLoopNotification;
+import org.onap.policy.controlloop.ControlLoopNotificationType;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.policy.ControlLoopPolicy;
+import org.onap.policy.controlloop.policy.FinalResult;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.policy.PolicyResult;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/*
+ * Each instance of this class corresonds to a transaction that is in
+ * progress. While active, it resides within Drools memory.
+ */
+
+public class Transaction implements Serializable {
+
+    private static Logger logger = LoggerFactory.getLogger(Transaction.class);
+
+    // This table maps 'actor' names to objects implementing the
+    // 'Actor' interface. 'ServiceLoader' is used to locate and create
+    // these objects, and populate the table.
+    private static Map<String,Actor> nameToActor = new HashMap<>();
+
+    static {
+        // use 'ServiceLoader' to locate all of the 'Actor' implementations
+        for (Actor actor :
+                ServiceLoader.load(Actor.class, Actor.class.getClassLoader())) {
+            logger.debug("Actor: " + actor.getName() + ", "
+                         + actor.getClass());
+            nameToActor.put(actor.getName(), actor);
+        }
+    }
+
+    private static final long serialVersionUID = 4389211793631707360L;
+
+    // Drools working memory containing this transaction
+    @Getter
+    private transient WorkingMemory workingMemory;
+
+    // a service-identifier specified on the associated onset message
+    @Getter
+    private String closedLoopControlName;
+
+    // identifies this transaction
+    @Getter
+    private UUID requestId;
+
+    // the decoded YAML file for the policy
+    private ControlLoopPolicy policy;
+
+    // the initial incoming event
+    private ControlLoopEvent onset = null;
+
+    // operations specific to the type of 'event'
+    private OnsetAdapter onsetAdapter = null;
+
+    // the current (or most recent) policy in effect
+    @Getter
+    private Policy currentPolicy = null;
+
+    // the operation currently in progress
+    @Getter
+    private Operation currentOperation = null;
+
+    // a history entry being constructed that is associated with the
+    // currently running operation
+    private ControlLoopOperation histEntry = null;
+
+    // a list of completed history entries
+    @Getter
+    private List<ControlLoopOperation> history = new LinkedList<>();
+
+    // when the transaction completes, this is the final transaction result
+    @Getter
+    private FinalResult finalResult = null;
+
+    //message, if any, associated with the result of this operation
+    private String message = null;
+
+    // this table maps a class name into the associated adjunct
+    private Map<Class, Adjunct> adjuncts = new HashMap<>();
+
+    /**
+     * Constructor - initialize a 'Transaction' instance
+     * (typically invoked from 'Drools').
+     *
+     * @param workingMemory Drools working memory containing this Transaction
+     * @param closedLoopControlName a string identifying the associated service
+     * @param requestId uniquely identifies this transaction
+     * @param policy decoded YAML file containing the policy
+     */
+    public Transaction(
+        WorkingMemory workingMemory,
+        String closedLoopControlName,
+        UUID requestId,
+        ControlLoopPolicy policy) {
+
+        logger.info("Transaction constructor");
+        this.workingMemory = workingMemory;
+        this.closedLoopControlName = closedLoopControlName;
+        this.requestId = requestId;
+        this.policy = policy;
+    }
+
+    /**
+     * Return a string indicating the current state of this transaction.
+     * If there is an operation in progress, the state indicates the operation
+     * state. Otherwise, the state is 'COMPLETE'.
+     *
+     * @return a string indicating the current state of this transaction
+     */
+    public String getState() {
+        return currentOperation == null
+            ? "COMPLETE" : currentOperation.getState();
+    }
+
+    /**
+     * Return 'true' if the transaction has completed, and the final result
+     * indicates failure.
+     *
+     * @return 'true' if the transaction has completed, and the final result
+     *     indicates failure
+     */
+    public boolean finalResultFailure() {
+        return FinalResult.FINAL_SUCCESS != finalResult
+               && FinalResult.FINAL_OPENLOOP != finalResult
+               && finalResult != null;
+    }
+
+    /**
+     * Return the overall policy timeout value as a String that can be used
+     * in a Drools timer.
+     *
+     * @return the overall policy timeout value as a String that can be used
+     *     in a Drools timer
+     */
+    public String getTimeout() {
+        return String.valueOf(policy.getControlLoop().getTimeout()) + "s";
+    }
+
+    /**
+     * Return the current operation timeout value as a String that can be used
+     * in a Drools timer.
+     *
+     * @return the current operation timeout value as a String that can be used
+     *     in a Drools timer
+     */
+    public String getOperationTimeout() {
+        return String.valueOf(currentPolicy.getTimeout()) + "s";
+    }
+
+    /**
+     * Let Drools know the transaction has been modified.
+     *
+     * <p>It is not necessary for Java code to call this method when an incoming
+     * message is received for an operation, or an operation timeout occurs --
+     * the Drools code has been written with the assumption that the transaction
+     * is modified in these cases. Instead, this method should be called when
+     * some type of internal occurrence results in a state change, such as when
+     * an operation acquires a lock after initially being blocked.
+     */
+    public void modify() {
+        FactHandle handle = workingMemory.getFactHandle(this);
+        if (handle != null) {
+            workingMemory.update(handle, this);
+        }
+    }
+
+    /**
+     * Set the initial 'onset' event that started this transaction.
+     *
+     * @param event the initial 'onset' event
+     */
+    public void setControlLoopEvent(ControlLoopEvent event) {
+        if (onset != null) {
+            logger.error("'Transaction' received unexpected event");
+            return;
+        }
+
+        onset = event;
+
+        // fetch associated 'OnsetAdapter'
+        onsetAdapter = OnsetAdapter.get(onset);
+
+        // check trigger policy type
+        if (isOpenLoop(policy.getControlLoop().getTrigger_policy())) {
+            // no operation is needed for open loops
+            finalResult = FinalResult.FINAL_OPENLOOP;
+            modify();
+        } else {
+            // fetch current policy
+            setPolicyId(policy.getControlLoop().getTrigger_policy());
+        }
+    }
+
+    /**
+     * Validates the onset by ensuring fields that are required
+     * for processing are included in the onset. The fields needed
+     * include the requestId, targetType, and target.
+     *
+     * @param onset the initial message that triggers processing
+     */
+    public boolean isControlLoopEventValid(ControlLoopEvent onset) {
+        if (onset.getRequestId() == null) {
+            this.message = "No requestID";
+            return false;
+        } else if (onset.getTargetType() == null) {
+            this.message = "No targetType";
+            return false;
+        } else if (onset.getTarget() == null || onset.getTarget().isEmpty()) {
+            this.message = "No target field";
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Create a 'ControlLoopNotification' from the specified event. Note thet
+     * the type of the initial 'onset' event is used to determine the type
+     * of the 'ControlLoopNotification', rather than the event passed to the
+     * method.
+     *
+     * @param event the event used to generate the notification
+     *     (if 'null' is passed, the 'onset' event is used)
+     * @return the created 'ControlLoopNotification' (or subclass) instance
+     */
+    public ControlLoopNotification getNotification(ControlLoopEvent event) {
+        ControlLoopNotification notification =
+            onsetAdapter.createNotification(event == null ? this.onset : event);
+
+        // include entire history
+        notification.setHistory(new ArrayList<ControlLoopOperation>(history));
+
+        return notification;
+    }
+
+    /**
+     * This method is called when additional incoming messages are received
+     * for the transaction. Messages are routed to the current operation,
+     * any results are processed, and a notification may be returned to
+     * the caller.
+     *
+     * @param object an incoming message, which should be meaningful to the
+     *     operation currently in progress
+     * @return a notification message if the operation completed,
+     *     or 'null' if it is still in progress
+     */
+    public ControlLoopNotification incomingMessage(Object object) {
+        ControlLoopNotification notification = null;
+        if (currentOperation != null) {
+            currentOperation.incomingMessage(object);
+            notification = processResult(currentOperation.getResult());
+        } else {
+            logger.error("'Transaction' received unexpected message: "
+                         + object);
+        }
+        return notification;
+    }
+
+    /**
+     * This method is called from Drools when the current operation times out.
+     *
+     * @return a notification message if there is an operation in progress,
+     *     or 'null' if not
+     */
+    public ControlLoopNotification timeout() {
+        ControlLoopNotification notification = null;
+        if (currentOperation != null) {
+            // notify the current operation
+            currentOperation.timeout();
+
+            // process the timeout within the transaction
+            notification = processResult(currentOperation.getResult());
+        } else {
+            logger.error("'Transaction' received unexpected timeout");
+        }
+        return notification;
+    }
+
+    /**
+     *  This method is called from Drools during a control loop timeout
+     *  to ensure the correct final notification is sent.
+     */
+    public void clTimeout() {
+        this.finalResult = FinalResult.FINAL_FAILURE_TIMEOUT;
+        message = "Control Loop timed out";
+        currentOperation = null;
+    }
+
+    /**
+     * This method is called from Drools to generate a notification message
+     * when an operation is started.
+     *
+     * @return an initial notification message if there is an operation in
+     *     progress, or 'null' if not
+     */
+    public ControlLoopNotification initialOperationNotification() {
+        if (currentOperation == null || histEntry == null) {
+            return null;
+        }
+
+        ControlLoopNotification notification =
+            onsetAdapter.createNotification(onset);
+        notification.setNotification(ControlLoopNotificationType.OPERATION);
+        notification.setMessage(histEntry.toHistory());
+        notification.setHistory(new LinkedList<ControlLoopOperation>());
+        for (ControlLoopOperation clo : history) {
+            if (histEntry.getOperation().equals(clo.getOperation())
+                    && histEntry.getActor().equals(clo.getActor())) {
+                notification.getHistory().add(clo);
+            }
+        }
+        return notification;
+    }
+
+    /**
+     * Return a final notification message for the entire transaction.
+     *
+     * @return a final notification message for the entire transaction,
+     *     or 'null' if we don't have a final result yet
+     */
+    public ControlLoopNotification finalNotification() {
+        if (finalResult == null) {
+            return null;
+        }
+
+        ControlLoopNotification notification =
+            onsetAdapter.createNotification(onset);
+        switch (finalResult) {
+            case FINAL_SUCCESS:
+                notification.setNotification(
+                    ControlLoopNotificationType.FINAL_SUCCESS);
+                break;
+            case FINAL_OPENLOOP:
+                notification.setNotification(
+                    ControlLoopNotificationType.FINAL_OPENLOOP);
+                break;
+            default:
+                notification.setNotification(
+                    ControlLoopNotificationType.FINAL_FAILURE);
+                notification.setMessage(this.message);
+                break;
+        }
+        notification.setHistory(history);
+        return notification;
+    }
+
+    /**
+     * Return a 'ControlLoopNotification' instance describing the current operation error.
+     *
+     * @return a 'ControlLoopNotification' instance describing the current operation error
+     */
+    public ControlLoopNotification processError() {
+        ControlLoopNotification notification = null;
+        if (currentOperation != null) {
+            // process the error within the transaction
+            notification = processResult(currentOperation.getResult());
+        }
+        return notification;
+    }
+
+    /**
+     * Update the state of the transaction based upon the result of an operation.
+     *
+     * @param result if not 'null', this is the result of the current operation
+     *     (if 'null', the operation is still in progress,
+     *     and no changes are made)
+     * @return if not 'null', this is a notification message that should be
+     *     sent to RUBY
+     */
+    private ControlLoopNotification processResult(PolicyResult result) {
+        if (result == null) {
+            modify();
+            return null;
+        }
+        String nextPolicy = null;
+
+        ControlLoopOperation saveHistEntry = histEntry;
+        completeHistEntry(result);
+
+        final ControlLoopNotification notification = processResult_HistEntry(saveHistEntry, result);
+
+        // If there is a message from the operation then we set it to be
+        // used by the control loop notifications
+        message = currentOperation.getMessage();
+
+        // set the value 'nextPolicy' based upon the result of the operation
+        switch (result) {
+            case SUCCESS:
+                nextPolicy = currentPolicy.getSuccess();
+                break;
+
+            case FAILURE:
+                nextPolicy = processResult_Failure();
+                break;
+
+            case FAILURE_TIMEOUT:
+                nextPolicy = currentPolicy.getFailure_timeout();
+                message = "Operation timed out";
+                break;
+
+            case FAILURE_RETRIES:
+                nextPolicy = currentPolicy.getFailure_retries();
+                message = "Control Loop reached failure retry limit";
+                break;
+
+            case FAILURE_EXCEPTION:
+                nextPolicy = currentPolicy.getFailure_exception();
+                break;
+
+            case FAILURE_GUARD:
+                nextPolicy = currentPolicy.getFailure_guard();
+                break;
+
+            default:
+                break;
+        }
+
+        if (nextPolicy != null) {
+            finalResult = FinalResult.toResult(nextPolicy);
+            if (finalResult == null) {
+                // it must be the next state
+                logger.debug("advancing to next operation");
+                setPolicyId(nextPolicy);
+            } else {
+                logger.debug("moving to COMPLETE state");
+                currentOperation = null;
+            }
+        } else {
+            logger.debug("doing retry with current actor");
+        }
+
+        modify();
+        return notification;
+    }
+
+    // returns a notification message based on the history entry
+    private ControlLoopNotification processResult_HistEntry(ControlLoopOperation hist, PolicyResult result) {
+        if (hist == null) {
+            return null;
+        }
+
+        // generate notification, containing operation history
+        ControlLoopNotification notification = onsetAdapter.createNotification(onset);
+        notification.setNotification(
+            result == PolicyResult.SUCCESS
+            ? ControlLoopNotificationType.OPERATION_SUCCESS
+            : ControlLoopNotificationType.OPERATION_FAILURE);
+        notification.setMessage(hist.toHistory());
+
+        // include the subset of history that pertains to this
+        // actor and operation
+        notification.setHistory(new LinkedList<ControlLoopOperation>());
+        for (ControlLoopOperation clo : history) {
+            if (hist.getOperation().equals(clo.getOperation())
+                    && hist.getActor().equals(clo.getActor())) {
+                notification.getHistory().add(clo);
+            }
+        }
+
+        return notification;
+    }
+
+    // returns the next policy if the current operation fails
+    private String processResult_Failure() {
+        String nextPolicy = null;
+        int attempt = currentOperation.getAttempt();
+        if (attempt <= currentPolicy.getRetry()) {
+            // operation failed, but there are retries left
+            Actor actor = nameToActor.get(currentPolicy.getActor());
+            if (actor != null) {
+                attempt += 1;
+                logger.debug("found Actor, attempt " + attempt);
+                currentOperation =
+                    actor.createOperation(this, currentPolicy, onset, attempt);
+                createHistEntry();
+            } else {
+                logger.error("'Transaction' can't find actor "
+                             + currentPolicy.getActor());
+            }
+        } else {
+            // operation failed, and no retries (or no retries left)
+            nextPolicy = (attempt == 1
+                ? currentPolicy.getFailure()
+                : currentPolicy.getFailure_retries());
+            logger.debug("moving to policy " + nextPolicy);
+        }
+        return nextPolicy;
+    }
+
+    /**
+     * Create a history entry at the beginning of an operation, and store it
+     * in the 'histEntry' instance variable.
+     */
+    private void createHistEntry() {
+        histEntry = new ControlLoopOperation();
+        histEntry.setActor(currentPolicy.getActor());
+        histEntry.setOperation(currentPolicy.getRecipe());
+        histEntry.setTarget(currentPolicy.getTarget().toString());
+        histEntry.setSubRequestId(String.valueOf(currentOperation.getAttempt()));
+
+        // histEntry.end - we will set this one later
+        // histEntry.outcome - we will set this one later
+        // histEntry.message - we will set this one later
+    }
+
+    /**
+     * Finish up the history entry at the end of an operation, and add it
+     * to the history list.
+     *
+     * @param result this is the result of the operation, which can't be 'null'
+     */
+    private void completeHistEntry(PolicyResult result) {
+        if (histEntry == null) {
+            return;
+        }
+
+        // append current entry to history
+        histEntry.setEnd(Instant.now());
+        histEntry.setOutcome(result.toString());
+        histEntry.setMessage(currentOperation.getMessage());
+        history.add(histEntry);
+
+        // give current operation a chance to act on it
+        currentOperation.histEntryCompleted(histEntry);
+        logger.debug("histEntry = {}", histEntry);
+        histEntry = null;
+    }
+
+    /**
+     * Look up the identifier for the next policy, and prepare to start that
+     * operation.
+     *
+     * @param id this is the identifier associated with the policy
+     */
+    private void setPolicyId(String id) {
+        currentPolicy = null;
+        currentOperation = null;
+
+        // search through the policies for a matching 'id'
+        for (Policy tmp : policy.getPolicies()) {
+            if (id.equals(tmp.getId())) {
+                // found a match
+                currentPolicy = tmp;
+                break;
+            }
+        }
+
+        if (currentPolicy != null) {
+            // locate the 'Actor' associated with 'currentPolicy'
+            Actor actor = nameToActor.get(currentPolicy.getActor());
+            if (actor != null) {
+                // found the associated 'Actor' instance
+                currentOperation =
+                    actor.createOperation(this, currentPolicy, onset, 1);
+                createHistEntry();
+            } else {
+                logger.error("'Transaction' can't find actor "
+                             + currentPolicy.getActor());
+            }
+        } else {
+            logger.error("Transaction' can't find policy " + id);
+        }
+
+        if (currentOperation == null) {
+
+            // either we couldn't find the actor or the operation --
+            // the transaction fails
+            finalResult = FinalResult.FINAL_FAILURE;
+        }
+    }
+
+    private boolean isOpenLoop(String policyId) {
+        return FinalResult.FINAL_OPENLOOP.name().equalsIgnoreCase(policyId);
+    }
+
+    /**
+     * This method sets the message for a control loop notification
+     * in the case where a custom message wants to be sent due to
+     * error processing, etc.
+     *
+     * @param message the message to be set for the control loop notification
+     */
+    public void setNotificationMessage(String message) {
+        this.message = message;
+    }
+
+    /**
+     * Return the notification message of this transaction.
+     *
+     * @return the notification message of this transaction
+     */
+    public String getNotificationMessage() {
+        return this.message;
+    }
+
+    /* ============================================================ */
+
+    /**
+     * Subclasses of 'Adjunct' provide data and methods to support one or
+     * more Actors/Operations, but are stored within the 'Transaction'
+     * instance.
+     */
+    public static interface Adjunct extends Serializable {
+        /**
+         * Called when an adjunct is automatically created as a result of
+         * a 'getAdjunct' call.
+         *
+         * @param transaction the transaction containing the adjunct
+         */
+        public default void init(Transaction transaction) {}
+
+        /**
+         * Called for each adjunct when the transaction completes, and is
+         * removed from Drools memory. Any adjunct-specific cleanup can be
+         * done at this point (e.g. freeing locks).
+         */
+        public default void cleanup(Transaction transaction) {}
+    }
+
+    /**
+     * This is a method of class 'Transaction', and returns an adjunct of
+     * the specified class (it is created if it doesn't exist).
+     *
+     * @param clazz this is the class of the adjunct
+     * @return an adjunct of the specified class ('null' may be returned if
+     *     the 'newInstance' method is unable to create the adjunct)
+     */
+    public <T extends Adjunct> T getAdjunct(final Class<T> clazz) {
+        return clazz.cast(adjuncts.computeIfAbsent(clazz, cl -> {
+            T adjunct = null;
+            try {
+                // create the adjunct (may trigger an exception)
+                adjunct = clazz.newInstance();
+
+                // initialize the adjunct (may also trigger an exception */
+                adjunct.init(Transaction.this);
+            } catch (Exception e) {
+                logger.error("Transaction can't create adjunct of {}", cl, e);
+            }
+            return adjunct;
+        }));
+    }
+
+    /**
+     * Explicitly create an adjunct -- this is useful when the adjunct
+     * initialization requires that some parameters be passed.
+     *
+     * @param adjunct this is the adjunct to insert into the table
+     * @return 'true' if successful
+     *     ('false' is returned if an adjunct with this class already exists)
+     */
+    public boolean putAdjunct(Adjunct adjunct) {
+        return adjuncts.putIfAbsent(adjunct.getClass(), adjunct) == null;
+    }
+
+    /**
+     * This method needs to be called when the transaction completes, which
+     * is typically right after it is removed from Drools memory.
+     */
+    public void cleanup() {
+        // create a list containing all of the adjuncts (in no particular order)
+        List<Adjunct> values;
+        synchronized (adjuncts) {
+            values = new LinkedList<>(adjuncts.values());
+        }
+
+        // iterate over the list
+        for (Adjunct a : values) {
+            try {
+                // call the 'cleanup' method on the adjunct
+                a.cleanup(this);
+            } catch (Exception e) {
+                logger.error("Transaction.cleanup exception", e);
+            }
+        }
+    }
+}
diff --git a/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Util.java b/controlloop/m2/base/src/main/java/org/onap/policy/m2/base/Util.java
new file mode 100644 (file)
index 0000000..8e6d98b
--- /dev/null
@@ -0,0 +1,62 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/base
+ * ================================================================================
+ * 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.base;
+
+import java.util.List;
+
+import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager;
+import org.onap.policy.common.endpoints.event.comm.TopicSink;
+import org.onap.policy.drools.core.PolicyContainer;
+import org.onap.policy.drools.core.PolicySession;
+import org.onap.policy.drools.system.PolicyController;
+import org.onap.policy.drools.system.PolicyControllerConstants;
+import org.onap.policy.drools.system.PolicyEngineConstants;
+
+/**
+ * This class contains static utility methods.
+ */
+public class Util {
+    /**
+     * Find the PolicyController associated with the specified PolicySession.
+     *
+     * @param session the current PolicySession
+     * @return the associated PolicyController (or 'null' if not found)
+     */
+    public static PolicyController getPolicyController(PolicySession session) {
+        PolicyContainer container = session.getPolicyContainer();
+        return PolicyControllerConstants.getFactory().get(
+                   container.getGroupId(), container.getArtifactId());
+    }
+
+    /**
+     * Send a UEB/DMAAP message to the specified topic, using the specified
+     * PolicyController.
+     *
+     * @param topic UEB/DMAAP topic
+     * @param event the message to encode, and send
+     * @return 'true' if successful, 'false' if delivery failed
+     * @throws IllegalStateException if the topic can't be found,
+     *     or there are multiple topics with the same name
+     */
+    public static boolean deliver(String topic, Object event) {
+        return PolicyEngineConstants.getManager().deliver(topic, event);
+    }
+}
diff --git a/controlloop/m2/base/src/test/java/org/onap/policy/m2/base/ActorOperationTest.java b/controlloop/m2/base/src/test/java/org/onap/policy/m2/base/ActorOperationTest.java
new file mode 100644 (file)
index 0000000..4cbd05e
--- /dev/null
@@ -0,0 +1,116 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/base
+ * ================================================================================
+ * 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.base;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+import org.onap.policy.controlloop.ControlLoopEvent;
+import org.onap.policy.controlloop.ControlLoopException;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.policy.PolicyResult;
+
+public class ActorOperationTest {
+
+    public static final String ACTOR_NAME = "test";
+    public static final String STATE = "COMPLETE";
+
+    public static class TestOperation implements Operation {
+
+        @Override
+        public Object getRequest() throws ControlLoopException {
+            return "request";
+        }
+
+        @Override
+        public Policy getPolicy() {
+            return null;
+        }
+
+        @Override
+        public String getState() {
+            return STATE;
+        }
+
+        @Override
+        public int getAttempt() {
+            return 0;
+        }
+
+        @Override
+        public PolicyResult getResult() {
+            return PolicyResult.SUCCESS;
+        }
+
+        @Override
+        public String getMessage() {
+            return "success";
+        }
+
+        @Override
+        public void incomingMessage(Object object) {
+            return;
+        }
+
+        @Override
+        public void timeout() {
+            return;
+        }
+
+    }
+
+    public static class TestActor implements Actor {
+
+        @Override
+        public String getName() {
+            return ACTOR_NAME;
+        }
+
+        @Override
+        public Operation createOperation(Transaction transaction, Policy policy, ControlLoopEvent onset, int attempt) {
+            return new TestOperation();
+        }
+
+    }
+
+    @Test
+    public void getNameTest() {
+        Actor actor = new TestActor();
+        assertEquals(ACTOR_NAME, actor.getName());
+    }
+
+    @Test
+    public void operationTest() throws ControlLoopException {
+        Actor actor = new TestActor();
+        Operation operation = actor.createOperation(null, null, null, 0);
+        assertNotNull(operation);
+        assertEquals("request", operation.getRequest());
+        assertNull(operation.getPolicy());
+        assertEquals(STATE, operation.getState());
+        assertEquals(0, operation.getAttempt());
+        assertEquals(PolicyResult.SUCCESS, operation.getResult());
+        assertEquals("success", operation.getMessage());
+    }
+
+}
diff --git a/controlloop/m2/base/src/test/java/org/onap/policy/m2/base/GuardAdjunctTest.java b/controlloop/m2/base/src/test/java/org/onap/policy/m2/base/GuardAdjunctTest.java
new file mode 100644 (file)
index 0000000..eacfc8f
--- /dev/null
@@ -0,0 +1,103 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/base
+ * ================================================================================
+ * 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.base;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.time.Instant;
+import java.util.UUID;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.guard.GuardContext;
+import org.powermock.reflect.Whitebox;
+
+public class GuardAdjunctTest {
+    private static final String ADJUNCT_CONTEXT_FIELD = "context";
+    private static final String ADJUNCT_TRANSACTION_FIELD = "transaction";
+
+    private static GuardAdjunct adjunct;
+    private static Transaction transaction;
+    private static GuardContext context;
+
+    /**
+     * Class-level initialization.
+     */
+    @BeforeClass
+    public static void setup() {
+        transaction = new Transaction(null, "testCL", UUID.randomUUID(), null);
+        context = mock(GuardContext.class);
+
+        GuardAdjunct.create(transaction, context);
+        adjunct = transaction.getAdjunct(GuardAdjunct.class);
+    }
+
+    @Test
+    public void createGuardAdjunctTest() {
+        assertEquals(context, adjunct.get());
+    }
+
+    @Test
+    public void asyncQueryTest() {
+        Policy policy = new Policy();
+        policy.setActor("APPCLCM");
+        policy.setRecipe("test");
+
+        assertTrue(adjunct.asyncQuery(policy, "testTarget", UUID.randomUUID().toString()));
+
+        GuardContext savedContext = Whitebox.getInternalState(adjunct, ADJUNCT_CONTEXT_FIELD);
+        Whitebox.setInternalState(adjunct, ADJUNCT_CONTEXT_FIELD, (GuardContext)null);
+
+        try {
+            assertFalse(adjunct.asyncQuery(policy, "testTarget", UUID.randomUUID().toString()));
+        } finally {
+            Whitebox.setInternalState(adjunct, ADJUNCT_CONTEXT_FIELD, savedContext);
+        }
+    }
+
+    @Test
+    public void asyncCreateDbEntryTest() {
+        ControlLoopOperation op = new ControlLoopOperation();
+        op.setStart(Instant.now().minusSeconds(1));
+        op.setEnd(Instant.now());
+        op.setActor("testActor");
+        op.setOperation("testOperation");
+        op.setSubRequestId("0");
+        op.setMessage("test");
+        op.setOutcome("success");
+
+        adjunct.asyncCreateDbEntry(op, "testTarget");
+        verify(context, times(1)).asyncCreateDbEntry(op.getStart(), op.getEnd(),
+            transaction.getClosedLoopControlName(), op.getActor(),
+            op.getOperation(), "testTarget", transaction.getRequestId().toString(),
+            op.getSubRequestId(), op.getMessage(), op.getOutcome());
+
+    }
+}
diff --git a/controlloop/m2/base/src/test/java/org/onap/policy/m2/base/TransactionTest.java b/controlloop/m2/base/src/test/java/org/onap/policy/m2/base/TransactionTest.java
new file mode 100644 (file)
index 0000000..3c186ce
--- /dev/null
@@ -0,0 +1,230 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/base
+ * ================================================================================
+ * 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.base;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.time.Instant;
+import java.util.LinkedList;
+import java.util.UUID;
+
+import org.drools.core.WorkingMemory;
+import org.drools.core.impl.StatefulKnowledgeSessionImpl;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.onap.policy.controlloop.ControlLoopEventStatus;
+import org.onap.policy.controlloop.ControlLoopNotification;
+import org.onap.policy.controlloop.ControlLoopTargetType;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.policy.ControlLoop;
+import org.onap.policy.controlloop.policy.ControlLoopPolicy;
+import org.onap.policy.controlloop.policy.FinalResult;
+import org.onap.policy.controlloop.policy.Policy;
+
+public class TransactionTest {
+
+    private VirtualControlLoopEvent setControlLoopEvent(UUID requestId, String closedLoopControlName,
+            Instant eventStart, String targetType, String target) {
+
+        VirtualControlLoopEvent event = new VirtualControlLoopEvent();
+        event.setClosedLoopControlName(closedLoopControlName);
+        event.setClosedLoopEventStatus(ControlLoopEventStatus.ONSET);
+        event.setRequestId(requestId);
+        event.setClosedLoopAlarmStart(eventStart);
+        event.setTarget(target);
+        event.setTargetType(targetType);
+        event.getAai().put("vserver.is-closed-loop-disabled", "false");
+        event.getAai().put("complex.state", "NJ");
+        event.getAai().put("vserver.l-interface.interface-name", "89ee9ee6-1e96-4063-b690-aa5ca9f73b32");
+        event.getAai().put("vserver.l-interface.l3-interface-ipv4-address-list.l3-inteface-ipv4-address",
+            "135.144.3.49");
+        event.getAai().put("vserver.l-interface.l3-interface-ipv6-address-list.l3-inteface-ipv6-address", null);
+        event.getAai().put("vserver.in-maint", "N");
+        event.getAai().put("complex.city", "AAIDefault");
+        event.getAai().put("vserver.vserver-id", "aa7a24f9-8791-491f-b31a-c8ba5ad9e2aa");
+        event.getAai().put("vserver.l-interface.network-name", "vUSP_DPA3_OAM_3750");
+        event.getAai().put("vserver.vserver-name", "ctsf0002vm013");
+        event.getAai().put("generic-vnf.vnf-name", "ctsf0002v");
+        event.getAai().put("generic-vnf.service-id", "e433710f-9217-458d-a79d-1c7aff376d89");
+        event.getAai().put("vserver.selflink", "https://compute-aic.dpa3.cci.att.com:8774/v2/d0719b845a804b368f8ac0bba39e188b/servers/aa7a24f9-8791-491f-b31a-c8ba5ad9e2aa");
+        event.getAai().put("generic-vnf.vnf-type", "vUSP - vCTS");
+        event.getAai().put("tenant.tenant-id", "d0719b845a804b368f8ac0bba39e188b");
+        event.getAai().put("cloud-region.identity-url", "");
+        event.getAai().put("vserver.prov-status", "PROV");
+        event.getAai().put("complex.physical-location-id", "LSLEILAA");
+
+        return event;
+    }
+
+    private ControlLoopPolicy createControlLoop() {
+
+        ControlLoop controlLoop = new ControlLoop();
+        controlLoop.setControlLoopName("cltest");
+        controlLoop.setTrigger_policy("testid");
+        controlLoop.setTimeout(15);
+
+        Policy policy = new Policy();
+        policy.setActor("APPCLCM");
+        policy.setId("testid");
+        policy.setName("policytest");
+        policy.setRecipe("restart");
+        policy.setRetry(1);
+        policy.setTimeout(10);
+        policy.setSuccess(FinalResult.FINAL_SUCCESS.toString());
+        policy.setFailure(FinalResult.FINAL_FAILURE.toString());
+        policy.setFailure_exception(FinalResult.FINAL_FAILURE_EXCEPTION.toString());
+        policy.setFailure_guard(FinalResult.FINAL_FAILURE_GUARD.toString());
+        policy.setFailure_retries(FinalResult.FINAL_FAILURE_RETRIES.toString());
+        policy.setFailure_timeout(FinalResult.FINAL_FAILURE_TIMEOUT.toString());
+
+        LinkedList<Policy> policies = new LinkedList<>();
+        policies.add(policy);
+
+        ControlLoopPolicy controlLoopPolicy = new ControlLoopPolicy();
+        controlLoopPolicy.setControlLoop(controlLoop);
+        controlLoopPolicy.setPolicies(policies);
+
+        return controlLoopPolicy;
+    }
+
+    @Test
+    public void validControlLoopEventTest() {
+        VirtualControlLoopEvent event = setControlLoopEvent(UUID.randomUUID(),
+                                        "cltest", Instant.now(), ControlLoopTargetType.VM, "vserver.vserver-name");
+        Transaction transaction = new Transaction(null, "clvusptest", event.getRequestId(), createControlLoop());
+        assertTrue(transaction.isControlLoopEventValid(event));
+    }
+
+    @Test
+    public void noRequestIdControlLoopEventTest() {
+        VirtualControlLoopEvent event = setControlLoopEvent(null,
+                                        "cltest", Instant.now(), ControlLoopTargetType.VM, "vserver.vserver-name");
+        Transaction transaction = new Transaction(null, "clvusptest", event.getRequestId(), createControlLoop());
+        assertFalse(transaction.isControlLoopEventValid(event));
+        assertEquals("No requestID", transaction.getNotificationMessage());
+    }
+
+    @Test
+    public void noTargetTypeControlLoopEventTest() {
+        VirtualControlLoopEvent event = setControlLoopEvent(UUID.randomUUID(),
+                                        "cltest", Instant.now(), null, "vserver.vserver-name");
+        Transaction transaction = new Transaction(null, "clvusptest", event.getRequestId(), createControlLoop());
+        assertFalse(transaction.isControlLoopEventValid(event));
+        assertEquals("No targetType", transaction.getNotificationMessage());
+    }
+
+    @Test
+    public void noTargetControlLoopEventTest() {
+        VirtualControlLoopEvent event = setControlLoopEvent(UUID.randomUUID(),
+                                        "cltest", Instant.now(), ControlLoopTargetType.VM, null);
+        Transaction transaction = new Transaction(null, "clvusptest", event.getRequestId(), createControlLoop());
+        assertFalse(transaction.isControlLoopEventValid(event));
+        assertEquals("No target field", transaction.getNotificationMessage());
+    }
+
+    @Test
+    public void getClosedLoopControlNameTest() {
+        Transaction transaction = new Transaction(null, "clvusptest", UUID.randomUUID(), createControlLoop());
+        assertEquals("clvusptest", transaction.getClosedLoopControlName());
+    }
+
+    @Test
+    public void getRequestIdTest() {
+        UUID requestId = UUID.randomUUID();
+        Transaction transaction = new Transaction(null, "clvusptest", requestId, createControlLoop());
+        assertEquals(requestId, transaction.getRequestId());
+    }
+
+    @Test
+    public void getWorkingMemoryTest() {
+        // Create mock working session
+        StatefulKnowledgeSessionImpl mockWorkingMemory = mock(StatefulKnowledgeSessionImpl.class);
+        Transaction transaction = new Transaction(mockWorkingMemory, "clvusptest",
+            UUID.randomUUID(), createControlLoop());
+        assertEquals(mockWorkingMemory, transaction.getWorkingMemory());
+    }
+
+    @Test
+    public void getStateCompleteTest() {
+        Transaction transaction = new Transaction(null, "clvusptest", UUID.randomUUID(), createControlLoop());
+        assertEquals("COMPLETE", transaction.getState());
+    }
+
+    @Test
+    public void getFinalResultTest() {
+        Transaction transaction = new Transaction(null, "clvusptest", UUID.randomUUID(), createControlLoop());
+        assertEquals(null, transaction.getFinalResult());
+    }
+
+    @Test
+    public void finalResultFailureTest() {
+        Transaction transaction = new Transaction(null, "clvusptest", UUID.randomUUID(), createControlLoop());
+        assertFalse(transaction.finalResultFailure());
+    }
+
+    @Test
+    public void getTimeoutTest() {
+        Transaction transaction = new Transaction(null, "clvusptest", UUID.randomUUID(), createControlLoop());
+        assertEquals("15s", transaction.getTimeout());
+    }
+
+    @Test
+    public void getOperationTimeoutTest() {
+        Transaction transaction = new Transaction(null, "clvusptest", UUID.randomUUID(), createControlLoop());
+        VirtualControlLoopEvent onset = setControlLoopEvent(UUID.randomUUID(),
+                                        "cltest", null, ControlLoopTargetType.VM, "vserver.vserver-name");
+        transaction.setControlLoopEvent(onset);
+        assertEquals("10s", transaction.getOperationTimeout());
+    }
+
+    @Test
+    public void getCurrentPolicy() {
+        ControlLoopPolicy controlLoopPolicy = createControlLoop();
+        Transaction transaction = new Transaction(null, "clvusptest", UUID.randomUUID(), controlLoopPolicy);
+        VirtualControlLoopEvent onset = setControlLoopEvent(UUID.randomUUID(),
+                                        "cltest", null, ControlLoopTargetType.VM, "vserver.vserver-name");
+        transaction.setControlLoopEvent(onset);
+        assertEquals(controlLoopPolicy.getPolicies().get(0), transaction.getCurrentPolicy());
+    }
+
+    @Test
+    public void getOperationTest() {
+        Transaction transaction = new Transaction(null, "clvusptest", UUID.randomUUID(), createControlLoop());
+        assertEquals(null, transaction.getCurrentOperation());
+    }
+
+    @Test
+    public void getNotificationTest() {
+        Transaction transaction = new Transaction(null, "clvusptest", UUID.randomUUID(), createControlLoop());
+        VirtualControlLoopEvent onset = setControlLoopEvent(UUID.randomUUID(),
+                                        "cltest", null, ControlLoopTargetType.VM, "vserver.vserver-name");
+        transaction.setControlLoopEvent(onset);
+        ControlLoopNotification notification = transaction.getNotification(onset);
+        assertEquals(onset.getClosedLoopControlName(), notification.getClosedLoopControlName());
+        assertEquals(onset.getRequestId(), notification.getRequestId());
+        assertEquals(onset.getTarget(), notification.getTarget());
+    }
+}
diff --git a/controlloop/m2/base/src/test/java/org/onap/policy/m2/base/UtilTest.java b/controlloop/m2/base/src/test/java/org/onap/policy/m2/base/UtilTest.java
new file mode 100644 (file)
index 0000000..53d9720
--- /dev/null
@@ -0,0 +1,73 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/base
+ * ================================================================================
+ * 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.base;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Properties;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager;
+import org.onap.policy.drools.core.PolicyContainer;
+import org.onap.policy.drools.core.PolicySession;
+import org.onap.policy.drools.system.PolicyController;
+
+public class UtilTest {
+
+    private static PolicySession session;
+
+    /**
+     * Class-level initialization.
+     */
+    @BeforeClass
+    public static void setup() {
+        PolicyContainer container = mock(PolicyContainer.class);
+        when(container.getGroupId()).thenReturn("org.onap.policy");
+        when(container.getArtifactId()).thenReturn("test");
+        when(container.getVersion()).thenReturn("1.0.0");
+
+        session = mock(PolicySession.class);
+        when(session.getPolicyContainer()).thenReturn(container);
+    }
+
+    @Test
+    public void deliverTest() {
+        Properties prop = new Properties();
+        prop.put("noop.sink.topics", "testTopic");
+        TopicEndpointManager.getManager().addTopicSinks(prop);
+
+        // throws an exception:
+        //     java.lang.IllegalStateException: Policy Engine is stopped
+        // if policy engine is started, it still throws an exception:
+        //     java.lang.IllegalArgumentException: no reverse coder has been found
+        //assertTrue(Util.deliver("testTopic", "test"));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void deliverNoTopicTest() {
+        Util.deliver("noTopic", "test");
+    }
+}
diff --git a/controlloop/m2/feature-controlloop-m2/pom.xml b/controlloop/m2/feature-controlloop-m2/pom.xml
new file mode 100644 (file)
index 0000000..7cedfa3
--- /dev/null
@@ -0,0 +1,126 @@
+<?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>feature-controlloop-m2</artifactId>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>src/main/feature</directory>
+        <filtering>true</filtering>
+      </resource>
+    </resources>
+    <plugins>
+      <plugin>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>zipfile</id>
+            <goals>
+              <goal>single</goal>
+            </goals>
+            <phase>package</phase>
+            <configuration>
+              <attach>true</attach>
+              <finalName>${project.artifactId}-${project.version}</finalName>
+              <descriptors>
+                <descriptor>src/assembly/assemble_zip.xml</descriptor>
+              </descriptors>
+              <appendAssemblyId>false</appendAssemblyId>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>copy-dependencies</id>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+            <phase>prepare-package</phase>
+            <configuration>
+              <outputDirectory>${project.build.directory}/assembly/lib</outputDirectory>
+              <overWriteReleases>false</overWriteReleases>
+              <overWriteSnapshots>true</overWriteSnapshots>
+              <overWriteIfNewer>true</overWriteIfNewer>
+              <useRepositoryLayout>false</useRepositoryLayout>
+              <addParentPoms>false</addParentPoms>
+              <copyPom>false</copyPom>
+              <includeScope>runtime</includeScope>
+              <excludeTransitive>true</excludeTransitive>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>util</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>guard</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>base</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>appclcm</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>lock</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>adapters</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/controlloop/m2/feature-controlloop-m2/src/assembly/assemble_zip.xml b/controlloop/m2/feature-controlloop-m2/src/assembly/assemble_zip.xml
new file mode 100644 (file)
index 0000000..bf006dd
--- /dev/null
@@ -0,0 +1,80 @@
+<!--
+  ============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=========================================================
+  -->
+
+<!-- Defines how we build the .zip file which is our distribution. -->
+
+<assembly
+  xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+  <id>feature-controlloop-m2-package</id>
+  <formats>
+    <format>zip</format>
+  </formats>
+
+  <includeBaseDirectory>false</includeBaseDirectory>
+
+  <fileSets>
+    <fileSet>
+      <directory>target</directory>
+      <outputDirectory>lib/feature</outputDirectory>
+      <includes>
+        <include>feature-controlloop-m2-${project.version}.jar</include>
+      </includes>
+    </fileSet>
+
+    <fileSet>
+      <directory>target/assembly/lib</directory>
+      <outputDirectory>lib/dependencies</outputDirectory>
+      <includes>
+        <include>*.jar</include>
+      </includes>
+    </fileSet>
+
+    <fileSet>
+      <directory>target/classes/config</directory>
+      <outputDirectory>config</outputDirectory>
+      <fileMode>0644</fileMode>
+      <excludes/>
+    </fileSet>
+
+    <fileSet>
+      <directory>src/main/feature/bin</directory>
+      <outputDirectory>bin</outputDirectory>
+      <fileMode>0755</fileMode>
+      <excludes/>
+    </fileSet>
+
+    <fileSet>
+      <directory>src/main/feature/db</directory>
+      <outputDirectory>db</outputDirectory>
+      <fileMode>0755</fileMode>
+      <excludes/>
+    </fileSet>
+
+    <fileSet>
+      <directory>src/main/feature/install</directory>
+      <outputDirectory>install</outputDirectory>
+      <fileMode>0755</fileMode>
+      <excludes/>
+    </fileSet>
+  </fileSets>
+
+</assembly>
diff --git a/controlloop/m2/guard/pom.xml b/controlloop/m2/guard/pom.xml
new file mode 100644 (file)
index 0000000..501151a
--- /dev/null
@@ -0,0 +1,84 @@
+<?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>guard</artifactId>
+
+  <dependencies>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <version>2.13.0</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>util</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-pdp</groupId>
+      <artifactId>policy-core</artifactId>
+      <version>${version.policy.drools-pdp}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-pdp</groupId>
+      <artifactId>policy-management</artifactId>
+      <version>${version.policy.drools-pdp}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+      <artifactId>guard</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+      <artifactId>database</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.h2database</groupId>
+      <artifactId>h2</artifactId>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/controlloop/m2/guard/src/main/java/org/onap/policy/guard/GuardContext.java b/controlloop/m2/guard/src/main/java/org/onap/policy/guard/GuardContext.java
new file mode 100644 (file)
index 0000000..d0d1b83
--- /dev/null
@@ -0,0 +1,400 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * guard
+ * ================================================================================
+ * 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.guard;
+
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.function.Supplier;
+
+import javax.persistence.EntityManager;
+import javax.persistence.EntityManagerFactory;
+import javax.persistence.Persistence;
+
+import org.drools.core.WorkingMemory;
+
+import org.onap.policy.database.operationshistory.Dbao;
+import org.onap.policy.drools.controller.DroolsController;
+import org.onap.policy.drools.core.PolicyContainer;
+import org.onap.policy.drools.core.PolicySession;
+import org.onap.policy.drools.system.PolicyController;
+import org.onap.policy.drools.system.PolicyControllerConstants;
+import org.onap.policy.drools.system.PolicyEngineConstants;
+import org.onap.policy.guard.Util;
+import org.onap.policy.util.DroolsSessionCommonSerializable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Each instance of this class is initialized from a 'Properties' object,
+ * which is most likely a '*-controller.properties' file. The expectation is
+ * that it will be initialized within a '.drl' file, and be referenced by
+ * the associated 'Context' object.
+ */
+public class GuardContext implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private static final String ECLIPSE_LINK_KEY_DRIVER = "javax.persistence.jdbc.driver";
+
+    private static Logger logger = LoggerFactory.getLogger(GuardContext.class);
+
+    // object that should be serialized
+    private Object namedSerializable;
+
+    /*==================================*/
+    /* fields extracted from properties */
+    /*==================================*/
+    // contains the four database properties, 'javax.persistence.jdbc.*',
+    private Properties dbProperties = null;
+
+    // initialized from 'guard.disabled', but may also be set to 'true' if
+    // there is an initialization error
+    private boolean disabled = false;
+
+    // errors that forced 'disabled' to be set to 'true'
+    private String errorMessage = null;
+
+    /*======================================================*/
+    /* fields that shouldn't be included in serialized data */
+    /*======================================================*/
+
+    // derived from DB properties
+    private transient EntityManagerFactory emf = null;
+
+    /**
+     * Constructor - initialize the 'GuardContext' instance using the
+     * controller's properties file. The properties file is located using a
+     * 'PolicySession' instance, but the way this mapping is done isn't
+     * perfect -- it may use the wrong properties file if there is another
+     * 'PolicyContainer' instance using the same 'artifactId' and 'groupId'.
+     *
+     * @param session the 'PolicySession' instance used to locate the associated
+     *     'Properties' instance
+     */
+    public GuardContext(PolicySession session) {
+        this(session, null);
+    }
+
+    /**
+     * Constructor - initialize the 'GuardContext' instance using the
+     * controller's properties file. The properties file is located using a
+     * 'PolicySession' instance, but the way this mapping is done isn't
+     * perfect -- it may use the wrong properties file if there is another
+     * 'PolicyContainer' instance using the same 'artifactId' and 'groupId'.
+     *
+     * @param session the 'PolicySession' instance used to locate the associated
+     *     'Properties' instance
+     * @param serializableName a String name unique within the Drools session
+     *     that can be used to locate the corresponding 'GuardContext' object
+     *     on the remote host
+     */
+    public GuardContext(PolicySession session, String serializableName) {
+        namedSerializable =
+            (serializableName == null ? this :
+             new DroolsSessionCommonSerializable(serializableName, this));
+
+        // At present, there is no simple way to get the properties based
+        // upon a 'PolicyContainer'. Instead, we search through all of the
+        // 'PolicyController' instances looking for one with a matching
+        // 'artifactId' and 'groupId'. Note that this may not work correctly
+        // if there is more than one controller using the same or different
+        // version of the same artifact.
+
+        PolicyContainer container = session.getPolicyContainer();
+        String artifactId = container.getArtifactId();
+        String groupId = container.getGroupId();
+
+        Properties properties =
+            PolicyControllerConstants.getFactory().get(groupId, artifactId).getProperties();
+        init(properties);
+    }
+
+    /**
+     * Constructor - initialize the 'GuardContext' instance using the
+     * specified properties.
+     *
+     * @param properties configuration data used to initialize the 'GuardContext' instance
+     */
+    public GuardContext(Properties properties) {
+        init(properties);
+    }
+
+    /**
+     * Common initialization routine code used by both constructors.
+     *
+     * @param properties configuration data used to initialize the 'GuardContext' instance
+     */
+    private void init(Properties properties) {
+        // used to store error messages
+        StringBuilder sb = new StringBuilder();
+
+        // fetch these parameters, if they exist
+        String disabledString =
+            PolicyEngineConstants.getManager().getEnvironmentProperty(Util.PROP_GUARD_DISABLED);
+
+        if (disabledString != null) {
+            // decode optional 'guard.disabled' parameter
+            disabled = Boolean.valueOf(disabledString);
+            if (disabled) {
+                // skip everything else
+                return;
+            }
+        }
+
+        // extract 'guard.java.persistence.jdbc.*' parameters,
+        // which are all mandatory
+        dbProperties = new Properties();
+        setProperty(dbProperties, Util.ONAP_KEY_URL, Util.ECLIPSE_LINK_KEY_URL, sb);
+        setProperty(dbProperties, Util.ONAP_KEY_USER, Util.ECLIPSE_LINK_KEY_USER, sb);
+        setProperty(dbProperties, Util.ONAP_KEY_PASS, Util.ECLIPSE_LINK_KEY_PASS, sb);
+        String driver = properties.getProperty("guard." + ECLIPSE_LINK_KEY_DRIVER);
+        if (driver != null) {
+            dbProperties.setProperty(ECLIPSE_LINK_KEY_DRIVER, driver);
+        }
+
+        // if there are any errors, update 'errorMessage' & disable guard queries
+        if (sb.length() != 0) {
+            // remove the terminating ", ", and extract resulting error message
+            sb.setLength(sb.length() - 2);
+            errorMessage = sb.toString();
+            disabled = true;
+            logger.error("Initialization failure: " + errorMessage);
+        }
+    }
+
+    /**
+     * Fetch a property from the PolicyEngine environment, and store it in
+     * a corresponding property in 'properties'.
+     *
+     * @param properties the location to store the properties
+     * @param srcName source environment property name
+     * @param destName destination property name
+     * @param log a 'StringBuilder' used to construct an error message, if needed
+     */
+    private void setProperty(Properties properties, String srcName, String destName, StringBuilder log) {
+        String value =
+            PolicyEngineConstants.getManager().getEnvironmentProperty(srcName);
+        if (value == null) {
+            log.append("'").append(srcName).append("' is not defined, ");
+        } else {
+            properties.setProperty(destName, value);
+        }
+    }
+
+    /**
+     * Do an asynchronous (non-blocking) HTTP REST query to see if this
+     * operation is permitted by 'guard'. The response is returned by
+     * inserting a 'PolicyGuardResponse' instance into Drools memory.
+     *
+     * @param workingMemory the Drools response is inserted here
+     * @param actor the processor being acted upon (e.g. "APPC")
+     * @param recipe otherwise known as "operation" (e.g. "Restart")
+     * @param target a further qualifier on 'actor'? (e.g. "VM")
+     * @param requestId the UUID string identifying the overall request
+     */
+    public void asyncQuery(
+        WorkingMemory workingMemory,
+        String actor, String recipe, String target,
+        String requestId) {
+
+        asyncQuery(workingMemory, actor, recipe, target, requestId, null);
+    }
+
+    /**
+     * Do an asynchronous (non-blocking) HTTP REST query to see if this
+     * operation is permitted by 'guard'. The response is returned by
+     * inserting a 'PolicyGuardResponse' instance into Drools memory.
+     *
+     * @param workingMemory the Drools response is inserted here
+     * @param actor the processor being acted upon (e.g. "APPC")
+     * @param recipe otherwise known as "operation" (e.g. "Restart")
+     * @param target a further qualifier on 'actor'? (e.g. "VM")
+     * @param requestId the UUID string identifying the overall request
+     * @param controlLoopName the 'controlLoopName' value or 'null'
+     *     (if 'null', it is ommitted from the query to 'guard')
+     */
+    public void asyncQuery(
+        final WorkingMemory workingMemory,
+        final String actor, final String recipe, final String target,
+        final String requestId, final String controlLoopName) {
+
+        if (disabled) {
+            logger.error("query skipped: {}", errorMessage);
+            workingMemory.insert(
+                new PolicyGuardResponse("Deny", UUID.fromString(requestId), recipe));
+            return;
+        }
+
+        CallGuardTask cgt = new CallGuardTask(workingMemory, controlLoopName,
+            actor, recipe, target, requestId, () -> null);
+
+        PolicyEngineConstants.getManager().getExecutorService().execute(cgt);
+    }
+
+    /**
+     * Create an 'EntityManagerFactory', if needed, and then create a new
+     * 'EntityManager' instance.
+     *
+     * @return a new 'EntityManager' instance
+     */
+    private EntityManager createEntityManager() {
+        if (emf == null) {
+            // 'EntityManagerFactory' does not exist yet -- create one
+
+            // copy database properties to a 'HashMap'
+            HashMap<Object,Object> propertiesMap = new HashMap<>(dbProperties);
+
+            // use 'ClassLoader' from Drools session
+            propertiesMap.put("eclipselink.classloader",
+                              GuardContext.class.getClassLoader());
+
+            // create DB tables, if needed
+            propertiesMap.put("eclipselink.ddl-generation", "create-tables");
+
+            // create entity manager factory
+            emf = Persistence.createEntityManagerFactory("OperationsHistoryPU", propertiesMap);
+        }
+
+        // create and return the 'EntityManager'
+        return emf.createEntityManager();
+    }
+
+    /**
+     * This is a synchronous (blocking) method, which creates a database entity
+     * for an in-progress request.
+     *
+     * @param starttime this is used as the 'starttime' timestamp in the record
+     * @param endtime this is used as the 'endtime' timestamp in the record
+     * @param closedLoopControlName uniquely identifies the Drools rules
+     * @param actor the processor being acted upon (e.g. "APPC")
+     * @param recipe otherwise known as "operation" (e.g. "Restart")
+     * @param target a further qualifier on 'actor'? (e.g. "VM")
+     * @param requestId the UUID string identifying the overall request
+     * @param subRequestId further qualifier on 'requestId'
+     * @param message indicates success status, or reason for failure
+     * @param outcome 'PolicyResult' enumeration string
+     * @return 'true' if the operation was successful, and 'false' if not
+     */
+    public boolean createDbEntry(
+        Instant starttime, Instant endtime, String closedLoopControlName,
+        String actor, String recipe, String target,
+        String requestId, String subRequestId, String message, String outcome) {
+
+        if (disabled) {
+            if (errorMessage != null) {
+                logger.error("Database update skipped: " + errorMessage);
+            }
+            return false;
+        }
+
+        EntityManager em = null;
+        boolean rval = false;
+
+        try {
+            em = createEntityManager();
+
+            // create the new DB table entry
+            Dbao newEntry = new Dbao();
+
+            // populate the new DB table entry
+            newEntry.setClosedLoopName(closedLoopControlName);
+            newEntry.setRequestId(requestId);
+            newEntry.setActor(actor);
+            newEntry.setOperation(recipe);
+            newEntry.setTarget(target);
+            newEntry.setStarttime(new Timestamp(starttime.toEpochMilli()));
+            newEntry.setSubrequestId(subRequestId);
+
+            newEntry.setEndtime(new Timestamp(endtime.toEpochMilli()));
+            newEntry.setMessage(message);
+            newEntry.setOutcome(outcome);
+
+            // store the new entry in the DB
+            em.getTransaction().begin();
+            em.persist(newEntry);
+            em.getTransaction().commit();
+
+            rval = true;
+        } finally {
+            // free EntityManager
+            if (em != null) {
+                em.close();
+            }
+        }
+        return rval;
+    }
+
+    /**
+     * This is an asynchronous (non-blocking) method, which creates a database
+     * entity for an in-progress request.
+     *
+     * @param starttime this is used as the 'starttime' timestamp in the record
+     * @param endtime this is used as the 'endtime' timestamp in the record
+     * @param closedLoopControlName uniquely identifies the Drools rules
+     * @param actor the processor being acted upon (e.g. "APPC")
+     * @param recipe otherwise known as "operation" (e.g. "Restart")
+     * @param target a further qualifier on 'actor'? (e.g. "VM")
+     * @param requestId the UUID string identifying the overall request
+     * @param subRequestId further qualifier on 'requestId'
+     * @param message indicates success status, or reason for failure
+     * @param outcome 'PolicyResult' enumeration string
+     */
+    public void asyncCreateDbEntry(
+        final Instant starttime, final Instant endtime,
+        final String closedLoopControlName,
+        final String actor, final String recipe, final String target,
+        final String requestId, final String subRequestId,
+        final String message, final String outcome) {
+        if (disabled) {
+            if (errorMessage != null) {
+                logger.error("Database update skipped: " + errorMessage);
+            }
+            return;
+        }
+
+        PolicyEngineConstants.getManager().getExecutorService().execute(() -> {
+            try {
+                // using a separate thread, call the synchronous 'createDbEntry'
+                // method
+                createDbEntry(starttime, endtime, closedLoopControlName,
+                              actor, recipe, target, requestId, subRequestId,
+                              message, outcome);
+            } catch (Exception e) {
+                logger.error("GuardContext.asyncCreateDbEntry", e);
+            }
+        });
+    }
+
+    /**
+     * This method is used as part of serialization -- 'namedSerializable'
+     * is serialized instead of 'this'.
+     *
+     * @return the object to be serialized
+     */
+    private Object writeReplace() throws ObjectStreamException {
+        return namedSerializable;
+    }
+}
diff --git a/controlloop/m2/guard/src/test/java/org/onap/policy/guard/GuardContextTest.java b/controlloop/m2/guard/src/test/java/org/onap/policy/guard/GuardContextTest.java
new file mode 100644 (file)
index 0000000..1a61d90
--- /dev/null
@@ -0,0 +1,139 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * guard
+ * ================================================================================
+ * 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.guard;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.isNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.drools.core.WorkingMemory;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+import org.onap.policy.drools.system.PolicyEngineConstants;
+
+public class GuardContextTest {
+
+    private static Properties prop;
+    private static GuardContext guardContext;
+    private static WorkingMemory workingMemory;
+    private static LinkedBlockingQueue<Object> queue = new LinkedBlockingQueue<>();
+
+    /**
+     * Class-level initialization.
+     */
+    @BeforeClass
+    public static void setup() throws IOException {
+        PolicyEngineConstants.getManager().configure(new Properties());
+        PolicyEngineConstants.getManager().start();
+
+        prop = new Properties();
+        prop.setProperty("guard.pdp.rest.url", "http://www.google.com/");
+        prop.setProperty("guard.pdp.rest.client.user", "testuser");
+        prop.setProperty("guard.pdp.rest.client.password", "testpassword");
+        prop.setProperty("guard.pdp.rest.timeout", "1000");
+        prop.setProperty("guard.pdp.rest.environment", "dev");
+
+        workingMemory = mock(WorkingMemory.class);
+        when(workingMemory.insert(isNotNull())).thenAnswer(
+            invocation -> {
+                queue.add(invocation.getArgument(0));
+                return null;
+            });
+    }
+
+    @AfterClass
+    public static void stop() {
+        PolicyEngineConstants.getManager().stop();
+    }
+
+    @Test
+    public void guardDbResponseTest() throws InterruptedException {
+        Properties props = new Properties(prop);
+        props.setProperty("guard.disabled", "false");
+        props.setProperty("guard.javax.persistence.jdbc.user", "user");
+        props.setProperty("guard.javax.persistence.jdbc.password", "secret");
+        props.setProperty("guard.javax.persistence.jdbc.driver", "org.h2.Driver");
+        props.setProperty("guard.javax.persistence.jdbc.url", "jdbc:h2:file:./H2DB");
+
+        guardContext = new GuardContext(props);
+        assertNotNull(guardContext);
+
+        guardContext.asyncCreateDbEntry(Instant.now().minusSeconds(1), Instant.now(),
+            "testCLName", "testActor", "testRecipe", "testTarget",
+            UUID.randomUUID().toString(), "1", "testMessage", "testOutcome");
+
+        queue.clear();
+        guardContext.asyncQuery(workingMemory, "testActor", "testRecipe",
+            "testTarget", UUID.randomUUID().toString(), "testCLName");
+        Object response = queue.poll(10, TimeUnit.SECONDS);
+        assertNotNull(response);
+    }
+
+    @Test
+    public void badValuesTest() throws InterruptedException {
+        Properties props = new Properties(prop);
+        props.setProperty("guard.disabled", "true");
+        props.setProperty("guard.pdp.rest.client.user", "");
+        props.setProperty("guard.pdp.rest.client.password", "");
+        props.setProperty("guard.pdp.rest.url", "bad,testuser,testpassword");
+
+        guardContext = new GuardContext(props);
+
+        guardContext.asyncCreateDbEntry(Instant.now().minusSeconds(1), Instant.now(),
+            "testCLName", "testActor", "testRecipe", "testTarget",
+            UUID.randomUUID().toString(), "1", "testMessage", "testOutcome");
+
+        queue.clear();
+        guardContext.asyncQuery(workingMemory, "testActor", "testRecipe",
+            "testTarget", UUID.randomUUID().toString());
+        Object response = queue.poll(10, TimeUnit.SECONDS);
+        assertNotNull(response);
+    }
+
+    @Test
+    public void policyGuardResponseTest() {
+        UUID requestId = UUID.randomUUID();
+        PolicyGuardResponse emptyResponse1 = new PolicyGuardResponse(null, null, null);
+
+        assertNotNull(emptyResponse1);
+
+        PolicyGuardResponse response = new PolicyGuardResponse("Some Result", requestId, "Some Details");
+
+        response.setRequestId(requestId);
+        assertEquals(requestId, response.getRequestId());
+
+        response.setResult("Some Result");
+        assertEquals("Some Result", response.getResult());
+
+        assertEquals("PolicyGuardResponse [requestId=", response.toString().substring(0, 31));
+    }
+}
diff --git a/controlloop/m2/lock/pom.xml b/controlloop/m2/lock/pom.xml
new file mode 100644 (file)
index 0000000..00ea2a8
--- /dev/null
@@ -0,0 +1,55 @@
+<?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>
+    <artifactId>m2</artifactId>
+    <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+    <version>1.6.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>lock</artifactId>
+  <name>lock</name>
+  <description>Generic Lock used for Locking Target Entities</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+      <artifactId>base</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onap.policy.drools-pdp</groupId>
+      <artifactId>policy-management</artifactId>
+      <version>${version.policy.drools-pdp}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/controlloop/m2/lock/src/main/java/org/onap/policy/drools/m2/lock/LockAdjunct.java b/controlloop/m2/lock/src/main/java/org/onap/policy/drools/m2/lock/LockAdjunct.java
new file mode 100644 (file)
index 0000000..e81ab62
--- /dev/null
@@ -0,0 +1,164 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/lock
+ * ================================================================================
+ * 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.drools.m2.lock;
+
+import java.io.Serializable;
+import java.util.HashMap;
+
+import org.onap.policy.drools.core.lock.Lock;
+import org.onap.policy.drools.core.lock.LockCallback;
+import org.onap.policy.drools.system.PolicyEngineConstants;
+import org.onap.policy.m2.base.Transaction;
+
+/*
+ * Adjunct data placed within the transaction, to contain locks
+ * on control loop use case basis. This same instance is shared
+ * among all actor operation instances within the transaction
+ * regardless of what actor is in the policy chain. As a result,
+ * the lock gets passed from one to the next, and is only freed
+ * when the transaction completes.
+ */
+public class LockAdjunct implements Transaction.Adjunct, LockCallback,
+    Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * This is the callback interface, which is only used if the lock is
+     * initially not available, and we end up waiting for it.
+     */
+    public interface Requestor {
+        /**
+         * This method is called once the lock has been acquired.
+         */
+        void lockAvailable();
+
+        /**
+         * This method is called to notify the requestor that the lock could not
+         * be obtained.
+         */
+        void lockUnavailable();
+    }
+
+    // lock associated with all of the AOTS and SDNR operations
+    // within the transaction
+    private Lock lock = null;
+
+    // the initial requestor of the lock
+    // (only set if we don't immediately acquire the lock)
+    private Requestor requestor = null;
+
+    /**
+     * allocate a lock on behalf of the requestor.
+     *
+     * @param requestor
+     *            the AOTS or SDNR operation requesting the lock
+     * @param key
+     *            string key identifying the lock
+     * @param ownerKey
+     *            string key identifying the owner
+     * @param waitForLock
+     *            used to determine if an operation should wait for a lock to
+     *            become available or fail immediately
+     * @return 'true' if we immediately acquired the lock, 'false' if the lock
+     *         is currently in use by another transaction, and we need to wait
+     */
+    public boolean getLock(Requestor requestor, String key, String ownerKey,
+                           boolean waitForLock) {
+        if (lock != null) {
+            if (lock.isActive()) {
+                // we already have an active lock
+                return true;
+            }
+
+            // We may have timed out earlier waiting for the lock, or
+            // we may have lost the lock after persistent restore. In
+            // any case, free the lock we have now, and allocate a new one.
+            lock.free();
+        }
+
+        // register the requestor in case the lockUnavailable() callback runs
+        // immediately. Previously the requestor would be set after the
+        // lock constructor ran but now the lockUnavailable() callback
+        // could be executed while the constructor is still on the stack which
+        // would result in a null requestor, thus the callback never
+        // notifying the requestor that the lock was unavailable.
+        this.requestor = requestor;
+
+        // try to allocate a new lock
+        if ((lock = PolicyEngineConstants.getManager().createLock(
+            key, ownerKey, 600, this, waitForLock)).isActive()) {
+            // the lock is good
+            return true;
+        }
+
+        // we need to wait for the lock -- return false,
+        return false;
+    }
+
+    /*=================================*/
+    /* 'Transaction.Adjunct' interface */
+    /*=================================*/
+
+    /**
+     * Notification that the transaction has completed.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public void cleanup(Transaction transaction) {
+        if (lock != null) {
+            // free the lock or cancel the reservation
+            lock.free();
+        }
+    }
+
+    /*==========================*/
+    /* 'LockCallback' interface */
+    /*==========================*/
+
+    /**
+     * Notification that the lock is available.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public void lockAvailable(Lock lock) {
+        this.lock = lock;
+        if (requestor != null) {
+            // notify requestor
+            requestor.lockAvailable();
+        }
+    }
+
+    /**
+     * Notification that the lock is not available.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public void lockUnavailable(Lock lock) {
+        this.lock = lock;
+        if (requestor != null) {
+            // notify requestor
+            requestor.lockUnavailable();
+        }
+    }
+}
diff --git a/controlloop/m2/lock/src/test/java/org/onap/policy/drools/m2/lock/LockAdjunctTest.java b/controlloop/m2/lock/src/test/java/org/onap/policy/drools/m2/lock/LockAdjunctTest.java
new file mode 100644 (file)
index 0000000..4c5770f
--- /dev/null
@@ -0,0 +1,97 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * m2/lock
+ * ================================================================================
+ * 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.drools.m2.lock;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+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.core.lock.Lock;
+import org.onap.policy.drools.core.lock.LockCallback;
+import org.onap.policy.drools.system.PolicyEngineConstants;
+import org.onap.policy.m2.base.Transaction;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LockAdjunctTest {
+
+    private static Logger logger = LoggerFactory.getLogger(LockAdjunctTest.class);
+
+    public class TestOwner implements LockCallback {
+
+        @Override
+        public void lockAvailable(Lock arg0) {
+            return;
+        }
+
+        @Override
+        public void lockUnavailable(Lock arg0) {
+            return;
+        }
+    }
+
+    private LockCallback owner;
+    private Lock lock;
+
+    @BeforeClass
+    public static void start() {
+        PolicyEngineConstants.getManager().configure(new Properties());
+        PolicyEngineConstants.getManager().start();
+    }
+
+    @AfterClass
+    public static void stop() {
+        PolicyEngineConstants.getManager().stop();
+    }
+
+    @Test
+    public void lockAdjunctTest() {
+        owner = new TestOwner();
+        lock = PolicyEngineConstants.getManager().createLock("key", "ownerKey", 60, owner, false);
+        LockAdjunct lockA = new LockAdjunct();
+
+        assertNotNull(lockA);
+
+        lockA.lockUnavailable(lock);
+        assertTrue(lock.isActive());
+        assertTrue(lockA.getLock(null, "key", "ownerKey", false));
+        LockAdjunct lockB = new LockAdjunct();
+        assertFalse(lockB.getLock(null, "key", "ownerKey", false));
+        assertTrue(lock.free());
+
+        lockB.lockAvailable(lock);
+        assertFalse(lock.isActive());
+        assertTrue(lockB.getLock(null, "key", "ownerKey", false));
+        assertFalse(lock.free());
+
+        UUID uuid = UUID.randomUUID();
+        Transaction transaction = new Transaction(null, "1111", uuid, null);
+        lockA.cleanup(transaction);
+    }
+}
diff --git a/controlloop/m2/pom.xml b/controlloop/m2/pom.xml
new file mode 100644 (file)
index 0000000..8a0a400
--- /dev/null
@@ -0,0 +1,49 @@
+<?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</groupId>
+    <artifactId>controlloop</artifactId>
+    <version>1.6.0-SNAPSHOT</version>
+  </parent>
+
+  <groupId>org.onap.policy.drools-applications.controlloop.m2</groupId>
+  <artifactId>m2</artifactId>
+
+  <name>M2 Control Loop Model</name>
+  <packaging>pom</packaging>
+
+  <modules>
+    <module>util</module>
+    <module>guard</module>
+    <module>base</module>
+    <module>appclcm</module>
+    <module>lock</module>
+    <module>adapters</module>
+    <module>test</module>
+    <module>feature-controlloop-m2</module>
+  </modules>
+
+</project>
diff --git a/controlloop/m2/test/pom.xml b/controlloop/m2/test/pom.xml
new file mode 100644 (file)
index 0000000..c389be5
--- /dev/null
@@ -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 (file)
index 0000000..3a8cffb
--- /dev/null
@@ -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 (file)
index 0000000..3a80f95
--- /dev/null
@@ -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 (file)
index 0000000..578dd6d
--- /dev/null
@@ -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 (file)
index 0000000..393a030
--- /dev/null
@@ -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 (file)
index 0000000..ee25b03
--- /dev/null
@@ -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 (file)
index 0000000..2a24f13
--- /dev/null
@@ -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 (file)
index 0000000..e4f1a8c
--- /dev/null
@@ -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 (file)
index 0000000..fc9b66f
--- /dev/null
@@ -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 (file)
index 0000000..0cf8b7b
--- /dev/null
@@ -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>
diff --git a/controlloop/m2/util/pom.xml b/controlloop/m2/util/pom.xml
new file mode 100644 (file)
index 0000000..b453cc5
--- /dev/null
@@ -0,0 +1,47 @@
+<?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>util</artifactId>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.onap.policy.drools-pdp</groupId>
+      <artifactId>policy-core</artifactId>
+      <version>${version.policy.drools-pdp}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/controlloop/m2/util/src/main/java/org/onap/policy/util/DroolsSessionCommonSerializable.java b/controlloop/m2/util/src/main/java/org/onap/policy/util/DroolsSessionCommonSerializable.java
new file mode 100644 (file)
index 0000000..d8501fe
--- /dev/null
@@ -0,0 +1,136 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * util
+ * ================================================================================
+ * 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.util;
+
+import java.io.InvalidObjectException;
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.util.HashMap;
+
+import org.onap.policy.drools.core.PolicySession;
+
+/**
+ * This class provides a way to serialize/deserialize objects by locating
+ * an already existing object on the remote end, and using that instead. It
+ * is useful for objects that may be shared by multiple transactions.
+ */
+public class DroolsSessionCommonSerializable implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    // identifies an object within a Drools session
+    private String name;
+
+    // the object is serialized, but is only used if the corresponding object
+    // on the remote end can't be located for some reason
+    private Object object;
+
+    /**
+     * Constructor - initialize instance, and also store a reference to
+     * the object in a 'PolicySession' adjunct.
+     *
+     * @param name identifies the object within the Drools session
+     * @param object the shared object
+     */
+    public DroolsSessionCommonSerializable(String name, Object object) {
+        this.name = name;
+        this.object = object;
+
+        // store a reference to the object within the adjunct
+        // (only works if we can locate the Drools session)
+        Adjunct adjunct = getAdjunct();
+        if (adjunct != null) {
+            adjunct.put(name, object);
+        }
+    }
+
+    /**
+     * Return this object as a String.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return "DroolsSessionCommonSerializable[" + name + "]";
+    }
+
+    /**
+     * This method runs as part of deserialization. If we are able to locate
+     * the 'PolicySession' and adjunct, and fetch the replacement object from
+     * the adjunct, that is used. Otherwise, the deserialized object is used
+     * (which is likely a duplicate).
+     *
+     * @return the local named object (if available), or the deserialized
+     *     object
+     */
+    private Object readResolve() throws ObjectStreamException {
+        Adjunct adjunct = getAdjunct();
+        Object replacementObject;
+
+        if (adjunct != null && (replacementObject = adjunct.get(name)) != null) {
+            // we found the adjunct, as well as the replacement object -- use
+            // the replacement object
+            return replacementObject;
+        }
+
+        // either we couldn't find the adjunct, or couldn't locate the object
+        // within the adjunct
+        return object;
+    }
+
+    /**
+     * This method will:
+     * 1) Locate the 'PolicySession' (only works from within the Drools thread),
+     * 2) Find or create the adjunct.
+     *
+     * @return the adjunct, or 'null' if we aren't running within a
+     *     Drools session
+     */
+    private Adjunct getAdjunct() {
+        // PolicySession - this only works from within the Drools thread
+        PolicySession session = PolicySession.getCurrentSession();
+        Adjunct adjunct = null;
+        if (session != null) {
+            // we found the 'PolicySession' -- now, look for the adjunct
+            Object adj = session.getAdjunct(Adjunct.class);
+            if (adj == null || !(adj instanceof Adjunct)) {
+                // adjunct does not exist, or has the wrong type -- create it
+                adjunct = new Adjunct();
+                session.setAdjunct(Adjunct.class, adjunct);
+            } else {
+                // found the adjunct -- return it
+                adjunct = (Adjunct)adj;
+                //adjunct = Adjunct.class.cast(adj);
+            }
+        }
+        return adjunct;
+    }
+
+    /* ============================================================ */
+
+    /**
+     * While 'HashMap&lt;String, Object&gt;' could be used directly instead of defining
+     * a subclass, you can't do run-time type checking of a parameterized type.
+     * As a result, the 'getAdjunct' method (above) would get compile-time
+     * warnings.
+     */
+    private static class Adjunct extends HashMap<String, Object> {
+    }
+}
diff --git a/controlloop/m2/util/src/test/java/org/onap/policy/util/DroolsSessionCommonSerializableTest.java b/controlloop/m2/util/src/test/java/org/onap/policy/util/DroolsSessionCommonSerializableTest.java
new file mode 100644 (file)
index 0000000..520158b
--- /dev/null
@@ -0,0 +1,36 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * util
+ * ================================================================================
+ * 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.util;
+
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Test;
+
+public class DroolsSessionCommonSerializableTest {
+
+    @Test
+    public void test() {
+        Object object = new Object();
+        DroolsSessionCommonSerializable droolsSessionCommonSerializable =
+            new DroolsSessionCommonSerializable("drools", object);
+        assertNotNull(droolsSessionCommonSerializable.toString());
+    }
+}
index 734d79d..b2a73b9 100644 (file)
@@ -31,6 +31,7 @@
   <modules>
     <module>common</module>
     <module>templates</module>
+    <module>m2</module>
     <module>packages</module>
   </modules>
 </project>