Add class to manage policy deployment records 74/116874/1
authorJim Hahn <jrh3@att.com>
Thu, 14 Jan 2021 13:32:40 +0000 (08:32 -0500)
committerJim Hahn <jrh3@att.com>
Thu, 14 Jan 2021 13:37:09 +0000 (08:37 -0500)
Issue-ID: POLICY-2648
Change-Id: Icbb9545e3df6942e6f9cf98689607c461f6c8cdd
Signed-off-by: Jim Hahn <jrh3@att.com>
main/src/main/java/org/onap/policy/pap/main/notification/DeploymentStatus.java [new file with mode: 0644]
main/src/main/java/org/onap/policy/pap/main/notification/StatusAction.java
main/src/main/java/org/onap/policy/pap/main/notification/StatusKey.java
main/src/test/java/org/onap/policy/pap/main/notification/DeploymentStatusTest.java [new file with mode: 0644]

diff --git a/main/src/main/java/org/onap/policy/pap/main/notification/DeploymentStatus.java b/main/src/main/java/org/onap/policy/pap/main/notification/DeploymentStatus.java
new file mode 100644 (file)
index 0000000..ef97fae
--- /dev/null
@@ -0,0 +1,320 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2021 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.pap.main.notification;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.BiPredicate;
+import java.util.stream.Collectors;
+import lombok.AccessLevel;
+import lombok.Getter;
+import org.onap.policy.models.base.PfModelException;
+import org.onap.policy.models.pap.concepts.PolicyNotification;
+import org.onap.policy.models.pdp.concepts.PdpPolicyStatus;
+import org.onap.policy.models.pdp.concepts.PdpPolicyStatus.State;
+import org.onap.policy.models.provider.PolicyModelsProvider;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifier;
+import org.onap.policy.pap.main.notification.StatusAction.Action;
+
+/**
+ * Collection of Policy Deployment Status records.
+ */
+public class DeploymentStatus {
+    /**
+     * Tracks the groups that have been loaded.
+     */
+    private final Set<String> pdpGroupLoaded = new HashSet<>();
+
+    /**
+     * Records, mapped by PDP/Policy pair.
+     */
+    @Getter(AccessLevel.PROTECTED)
+    private final Map<StatusKey, StatusAction> recordMap = new HashMap<>();
+
+    /**
+     * Records the policy status so that notifications can be generated. When
+     * {@link #loadByGroup(String)} is invoked, records are added to this. Other than
+     * that, this is not updated until {@link #addNotifications(PolicyNotification)} is
+     * invoked.
+     */
+    private DeploymentTracker tracker = new DeploymentTracker();
+
+    private PolicyModelsProvider provider;
+
+
+    /**
+     * Constructs the object.
+     *
+     * @param provider the provider to use to access the DB
+     */
+    public DeploymentStatus(PolicyModelsProvider provider) {
+        this.provider = provider;
+    }
+
+    /**
+     * Adds new policy status to a notification.
+     *
+     * @param notif notification to which to add policy status
+     */
+    protected void addNotifications(PolicyNotification notif) {
+        DeploymentTracker newTracker = new DeploymentTracker();
+        recordMap.values().forEach(newTracker::add);
+
+        tracker.addNotifications(notif, newTracker);
+
+        tracker = newTracker;
+    }
+
+    /**
+     * Loads policy deployment status associated with the given PDP group.
+     *
+     * @param pdpGroup group whose records are to be loaded
+     * @throws PfModelException if an error occurs
+     */
+    public void loadByGroup(String pdpGroup) throws PfModelException {
+        if (pdpGroupLoaded.contains(pdpGroup)) {
+            return;
+        }
+
+        pdpGroupLoaded.add(pdpGroup);
+
+        for (PdpPolicyStatus status : provider.getGroupPolicyStatus(pdpGroup)) {
+            StatusAction status2 = new StatusAction(Action.UNCHANGED, status);
+            recordMap.put(new StatusKey(status), status2);
+            tracker.add(status2);
+        }
+    }
+
+    /**
+     * Flushes changes to the DB, adding policy status to the notification.
+     *
+     * @param notif notification to which to add policy status
+     */
+    public void flush(PolicyNotification notif) {
+        addNotifications(notif);
+        deleteUndeployments();
+        flush();
+    }
+
+    /**
+     * Flushes changes to the DB.
+     */
+    protected void flush() {
+        // categorize the records
+        List<PdpPolicyStatus> created = new ArrayList<>();
+        List<PdpPolicyStatus> updated = new ArrayList<>();
+        List<PdpPolicyStatus> deleted = new ArrayList<>();
+
+        for (StatusAction status : recordMap.values()) {
+            switch (status.getAction()) {
+                case CREATED:
+                    created.add(status.getStatus());
+                    break;
+                case UPDATED:
+                    updated.add(status.getStatus());
+                    break;
+                case DELETED:
+                    deleted.add(status.getStatus());
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        provider.cudPolicyStatus(created, updated, deleted);
+
+        /*
+         * update the records to indicate everything is now unchanged (i.e., matches what
+         * is in the DB)
+         */
+
+        Iterator<StatusAction> iter = recordMap.values().iterator();
+        while (iter.hasNext()) {
+            StatusAction status = iter.next();
+
+            if (status.getAction() == Action.DELETED) {
+                iter.remove();
+            } else {
+                status.setAction(Action.UNCHANGED);
+            }
+        }
+    }
+
+    /**
+     * Deletes records for any policies that have been completely undeployed.
+     */
+    protected void deleteUndeployments() {
+        // identify the incomplete policies
+
+        // @formatter:off
+        Set<ToscaConceptIdentifier> incomplete = recordMap.values().stream()
+            .filter(status -> status.getAction() != Action.DELETED)
+            .map(StatusAction::getStatus)
+            .filter(status -> status.getState() == State.WAITING)
+            .map(PdpPolicyStatus::getPolicy)
+            .collect(Collectors.toSet());
+        // @formatter:on
+
+        // delete if UNDEPLOYED and not incomplete
+        deleteDeployment((key, status) -> !status.getStatus().isDeploy() && !incomplete.contains(key.getPolicy()));
+    }
+
+    /**
+     * Delete deployment records for a PDP.
+     *
+     * @param pdpId PDP whose records are to be deleted
+     */
+    public void deleteDeployment(String pdpId) {
+        deleteDeployment((key, status) -> key.getPdpId().equals(pdpId));
+    }
+
+    /**
+     * Delete deployment records for a policy.
+     *
+     * @param policy policy whose records are to be deleted
+     * @param deploy {@code true} to delete deployment records, {@code false} to delete
+     *        undeployment records
+     */
+    public void deleteDeployment(ToscaConceptIdentifier policy, boolean deploy) {
+        deleteDeployment((key, status) -> status.getStatus().isDeploy() == deploy && key.getPolicy().equals(policy));
+    }
+
+    /**
+     * Delete deployment records for a policy.
+     *
+     * @param filter filter to identify records to be deleted
+     */
+    private void deleteDeployment(BiPredicate<StatusKey, StatusAction> filter) {
+        Iterator<Entry<StatusKey, StatusAction>> iter = recordMap.entrySet().iterator();
+        while (iter.hasNext()) {
+            Entry<StatusKey, StatusAction> entry = iter.next();
+            StatusKey key = entry.getKey();
+            StatusAction value = entry.getValue();
+
+            if (filter.test(key, value)) {
+                if (value.getAction() == Action.CREATED) {
+                    // it's a new record - just remove it
+                    iter.remove();
+                } else {
+                    // it's an existing record - mark it for deletion
+                    value.setAction(Action.DELETED);
+                }
+            }
+        }
+    }
+
+    /**
+     * Deploys/undeploys a policy to a PDP. Assumes that
+     * {@link #deleteDeployment(ToscaConceptIdentifier, boolean)} has already been invoked
+     * to delete any records having the wrong "deploy" value.
+     *
+     * @param pdpId PDP to which the policy is to be deployed
+     * @param policy policy to be deployed
+     * @param policyType policy's type
+     * @param pdpGroup PdpGroup containing the PDP of interest
+     * @param pdpType PDP type (i.e., PdpSubGroup) containing the PDP of interest
+     * @param deploy {@code true} if the policy is being deployed, {@code false} if
+     *        undeployed
+     */
+    public void deploy(String pdpId, ToscaConceptIdentifier policy, ToscaConceptIdentifier policyType, String pdpGroup,
+                    String pdpType, boolean deploy) {
+
+        recordMap.compute(new StatusKey(pdpId, policy), (key, status) -> {
+
+            if (status == null) {
+                // no record yet - create one
+
+                // @formatter:off
+                return new StatusAction(Action.CREATED, PdpPolicyStatus.builder()
+                                    .pdpGroup(pdpGroup)
+                                    .pdpId(pdpId)
+                                    .pdpType(pdpType)
+                                    .policy(policy)
+                                    .policyType(policyType)
+                                    .deploy(deploy)
+                                    .state(State.WAITING)
+                                    .build());
+                // @formatter:on
+            }
+
+            PdpPolicyStatus status2 = status.getStatus();
+
+            // record already exists - see if the deployment flag should be changed
+
+            if (status2.isDeploy() != deploy) {
+                // deployment flag has changed
+                status.setChanged();
+                status2.setDeploy(deploy);
+                status2.setState(State.WAITING);
+
+
+            } else if (status.getAction() == Action.DELETED) {
+                // deployment flag is unchanged
+                status.setAction(Action.UPDATED);
+            }
+
+            return status;
+        });
+    }
+
+    /**
+     * Indicates the deployment/undeployment of a set of policies to a PDP has completed.
+     *
+     * @param pdpId PDP of interest
+     * @param expectedPolicies policies that we expected to be deployed to the PDP
+     * @param actualPolicies policies that were actually deployed to the PDP
+     */
+    public void completeDeploy(String pdpId, Set<ToscaConceptIdentifier> expectedPolicies,
+                    Set<ToscaConceptIdentifier> actualPolicies) {
+
+        for (StatusAction status : recordMap.values()) {
+            PdpPolicyStatus status2 = status.getStatus();
+
+            if (!status.getStatus().getPdpId().equals(pdpId)
+                            || expectedPolicies.contains(status2.getPolicy()) != status2.isDeploy()) {
+                /*
+                 * The policy is "expected" to be deployed, but the record is not marked
+                 * for deployment (or vice versa), which means the expected policy is out
+                 * of date with the DB, thus we'll ignore this policy for now.
+                 */
+                continue;
+            }
+
+            State state;
+            if (actualPolicies.contains(status2.getPolicy())) {
+                state = (status.getStatus().isDeploy() ? State.SUCCESS : State.FAILURE);
+            } else {
+                state = (status.getStatus().isDeploy() ? State.FAILURE : State.SUCCESS);
+            }
+
+            if (status2.getState() != state) {
+                status.setChanged();
+                status2.setState(state);
+            }
+        }
+    }
+}
index c2f1edc..0548226 100644 (file)
 package org.onap.policy.pap.main.notification;
 
 import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Setter;
+import lombok.ToString;
 import org.onap.policy.models.pdp.concepts.PdpPolicyStatus;
 
 @Getter
 @AllArgsConstructor
+@EqualsAndHashCode
+@ToString
 public class StatusAction {
     public enum Action {
         // existing record; matches what is in the DB
index 27a33b0..d99e363 100644 (file)
@@ -23,6 +23,7 @@ package org.onap.policy.pap.main.notification;
 import lombok.AllArgsConstructor;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
+import lombok.ToString;
 import org.onap.policy.models.pdp.concepts.PdpPolicyStatus;
 import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifier;
 
@@ -32,6 +33,7 @@ import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifier;
 @Getter
 @EqualsAndHashCode
 @AllArgsConstructor
+@ToString
 public class StatusKey {
     private String pdpId;
     private ToscaConceptIdentifier policy;
diff --git a/main/src/test/java/org/onap/policy/pap/main/notification/DeploymentStatusTest.java b/main/src/test/java/org/onap/policy/pap/main/notification/DeploymentStatusTest.java
new file mode 100644 (file)
index 0000000..e191e02
--- /dev/null
@@ -0,0 +1,529 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2021 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.pap.main.notification;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import lombok.NonNull;
+import org.apache.commons.lang3.builder.CompareToBuilder;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.models.base.PfModelException;
+import org.onap.policy.models.pap.concepts.PolicyNotification;
+import org.onap.policy.models.pap.concepts.PolicyStatus;
+import org.onap.policy.models.pdp.concepts.PdpPolicyStatus;
+import org.onap.policy.models.pdp.concepts.PdpPolicyStatus.PdpPolicyStatusBuilder;
+import org.onap.policy.models.pdp.concepts.PdpPolicyStatus.State;
+import org.onap.policy.models.provider.PolicyModelsProvider;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifier;
+import org.onap.policy.pap.main.notification.StatusAction.Action;
+
+public class DeploymentStatusTest {
+
+    private static final String VERSION = "1.2.3";
+    private static final @NonNull String GROUP_A = "groupA";
+    private static final String PDP_A = "pdpA";
+    private static final String PDP_B = "pdpB";
+    private static final String PDP_C = "pdpC";
+    private static final String PDP_D = "pdpD";
+    private static final String PDP_TYPE = "MyPdpType";
+    private static final ToscaConceptIdentifier POLICY_A = new ToscaConceptIdentifier("MyPolicyA", VERSION);
+    private static final ToscaConceptIdentifier POLICY_B = new ToscaConceptIdentifier("MyPolicyB", VERSION);
+    private static final ToscaConceptIdentifier POLICY_C = new ToscaConceptIdentifier("MyPolicyC", VERSION);
+    private static final ToscaConceptIdentifier POLICY_D = new ToscaConceptIdentifier("MyPolicyD", VERSION);
+    private static final ToscaConceptIdentifier POLICY_TYPE = new ToscaConceptIdentifier("MyPolicyType", VERSION);
+
+    private PdpPolicyStatusBuilder builder;
+
+    @Captor
+    private ArgumentCaptor<List<PdpPolicyStatus>> created;
+    @Captor
+    private ArgumentCaptor<List<PdpPolicyStatus>> updated;
+    @Captor
+    private ArgumentCaptor<List<PdpPolicyStatus>> deleted;
+
+    @Mock
+    private PolicyModelsProvider provider;
+
+    private DeploymentStatus tracker;
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        tracker = new DeploymentStatus(provider);
+
+        // @formatter:off
+        builder = PdpPolicyStatus.builder()
+                        .pdpGroup(GROUP_A)
+                        .pdpId(PDP_A)
+                        .pdpType(PDP_TYPE)
+                        .policy(POLICY_A)
+                        .policyType(POLICY_TYPE)
+                        .deploy(true)
+                        .state(State.SUCCESS);
+        // @formatter:on
+    }
+
+    @Test
+    public void testAddNotifications() {
+        PdpPolicyStatus create = builder.pdpId("created").state(State.FAILURE).build();
+        PdpPolicyStatus update = builder.pdpId("updated").state(State.SUCCESS).build();
+        PdpPolicyStatus delete = builder.pdpId("deleted").state(State.SUCCESS).build();
+        PdpPolicyStatus unchange = builder.pdpId("unchanged").state(State.FAILURE).build();
+
+        // @formatter:off
+        tracker.getRecordMap().putAll(makeMap(
+                        Action.CREATED, create,
+                        Action.UPDATED, update,
+                        Action.DELETED, delete,
+                        Action.UNCHANGED, unchange
+                        ));
+        // @formatter:on
+
+        PolicyNotification notif = new PolicyNotification();
+
+        tracker.addNotifications(notif);
+        assertThat(notif.getAdded()).hasSize(1);
+        assertThat(notif.getDeleted()).isEmpty();
+
+        PolicyStatus status = notif.getAdded().get(0);
+        assertThat(status.getFailureCount()).isEqualTo(2);
+        assertThat(status.getIncompleteCount()).isZero();
+        assertThat(status.getSuccessCount()).isEqualTo(1);
+        assertThat(status.getPolicy()).isEqualTo(POLICY_A);
+        assertThat(status.getPolicyType()).isEqualTo(POLICY_TYPE);
+
+        /*
+         * repeat - should be no notifications
+         */
+        notif = new PolicyNotification();
+        tracker.addNotifications(notif);
+        assertThat(notif.getAdded()).isEmpty();
+        assertThat(notif.getDeleted()).isEmpty();
+    }
+
+    @Test
+    public void testLoadByGroup() throws PfModelException {
+        PdpPolicyStatus status1 = builder.build();
+        PdpPolicyStatus status2 = builder.policy(POLICY_B).build();
+        PdpPolicyStatus status3 = builder.policy(POLICY_A).pdpId(PDP_B).build();
+
+        when(provider.getGroupPolicyStatus(GROUP_A)).thenReturn(List.of(status1, status2, status3));
+
+        tracker.loadByGroup(GROUP_A);
+
+        // @formatter:off
+        assertThat(tracker.getRecordMap()).isEqualTo(makeMap(
+            Action.UNCHANGED, status1,
+            Action.UNCHANGED, status2,
+            Action.UNCHANGED, status3
+            ));
+        // @formatter:on
+
+        // try again - should not reload
+        tracker.loadByGroup(GROUP_A);
+        verify(provider).getGroupPolicyStatus(anyString());
+    }
+
+    @Test
+    public void testFlushPdpNotification() {
+        PdpPolicyStatus create = builder.pdpId("created").state(State.FAILURE).build();
+        tracker.getRecordMap().putAll(makeMap(Action.CREATED, create));
+
+        PolicyNotification notif = new PolicyNotification();
+
+        tracker.flush(notif);
+
+        assertThat(notif.getAdded()).hasSize(1);
+        assertThat(notif.getDeleted()).isEmpty();
+    }
+
+    @Test
+    public void testFlush() throws PfModelException {
+        PdpPolicyStatus create1 = builder.pdpId("createA").build();
+        PdpPolicyStatus create2 = builder.pdpId("createB").build();
+        PdpPolicyStatus update1 = builder.pdpId("updateA").build();
+        PdpPolicyStatus update2 = builder.pdpId("updateB").build();
+        PdpPolicyStatus delete1 = builder.pdpId("deleteA").build();
+        PdpPolicyStatus delete2 = builder.pdpId("deleteB").build();
+        PdpPolicyStatus unchange1 = builder.pdpId("unchangeA").build();
+        PdpPolicyStatus unchange2 = builder.pdpId("unchangeB").build();
+
+        // @formatter:off
+        tracker.getRecordMap().putAll(makeMap(
+                        Action.CREATED, create1,
+                        Action.CREATED, create2,
+                        Action.UPDATED, update1,
+                        Action.UPDATED, update2,
+                        Action.DELETED, delete1,
+                        Action.DELETED, delete2,
+                        Action.UNCHANGED, unchange1,
+                        Action.UNCHANGED, unchange2
+                        ));
+        // @formatter:on
+
+        tracker.flush();
+
+        verify(provider).cudPolicyStatus(created.capture(), updated.capture(), deleted.capture());
+
+        assertThat(sort(created.getValue())).isEqualTo(List.of(create1, create2));
+        assertThat(sort(updated.getValue())).isEqualTo(List.of(update1, update2));
+        assertThat(sort(deleted.getValue())).isEqualTo(List.of(delete1, delete2));
+
+        // @formatter:off
+        assertThat(tracker.getRecordMap()).isEqualTo(makeMap(
+                        Action.UNCHANGED, create1,
+                        Action.UNCHANGED, create2,
+                        Action.UNCHANGED, update1,
+                        Action.UNCHANGED, update2,
+                        Action.UNCHANGED, unchange1,
+                        Action.UNCHANGED, unchange2
+                        ));
+        // @formatter:on
+    }
+
+    @Test
+    public void testDeleteUndeployments() {
+        builder.deploy(true);
+        PdpPolicyStatus delete = builder.policy(POLICY_A).build();
+        PdpPolicyStatus deployedComplete = builder.policy(POLICY_B).build();
+
+        builder.deploy(false);
+        PdpPolicyStatus undepComplete1 = builder.policy(POLICY_C).build();
+        PdpPolicyStatus undepIncomplete1 = builder.policy(POLICY_D).build();
+
+        builder.pdpId(PDP_B);
+        PdpPolicyStatus undepComplete2 = builder.policy(POLICY_C).build();
+        PdpPolicyStatus undepIncomplete2 = builder.policy(POLICY_D).state(State.WAITING).build();
+
+        // @formatter:off
+        Map<StatusKey, StatusAction> map = makeMap(
+                        Action.DELETED, delete,
+                        Action.UNCHANGED, deployedComplete,
+                        Action.UNCHANGED, undepComplete1,
+                        Action.UNCHANGED, undepComplete2,
+                        Action.UNCHANGED, undepIncomplete1,
+                        Action.UNCHANGED, undepIncomplete2
+                        );
+        // @formatter:on
+
+        tracker.getRecordMap().putAll(map);
+
+        tracker.deleteUndeployments();
+
+        // the completed undeployments should now be marked DELETED
+
+        // @formatter:off
+        assertThat(tracker.getRecordMap()).isEqualTo(makeMap(
+                        Action.DELETED, delete,
+                        Action.UNCHANGED, deployedComplete,
+                        Action.DELETED, undepComplete1,
+                        Action.DELETED, undepComplete2,
+                        Action.UNCHANGED, undepIncomplete1,
+                        Action.UNCHANGED, undepIncomplete2
+                        ));
+        // @formatter:on
+    }
+
+    @Test
+    public void testDeleteDeploymentString() {
+        PdpPolicyStatus statusaa = builder.pdpId(PDP_A).policy(POLICY_A).build();
+        PdpPolicyStatus statusab = builder.pdpId(PDP_A).policy(POLICY_B).build();
+        PdpPolicyStatus statusba = builder.pdpId(PDP_B).policy(POLICY_A).build();
+        PdpPolicyStatus statuscb = builder.pdpId(PDP_C).policy(POLICY_B).build();
+
+        // @formatter:off
+        tracker.getRecordMap().putAll(makeMap(
+                        Action.UNCHANGED, statusaa,
+                        Action.UNCHANGED, statusab,
+                        Action.UNCHANGED, statusba,
+                        Action.UNCHANGED, statuscb
+                        ));
+        // @formatter:on
+
+        tracker.deleteDeployment(PDP_A);
+
+        // @formatter:off
+        assertThat(tracker.getRecordMap()).isEqualTo(makeMap(
+                        Action.DELETED, statusaa,
+                        Action.DELETED, statusab,
+                        Action.UNCHANGED, statusba,
+                        Action.UNCHANGED, statuscb
+                        ));
+        // @formatter:on
+    }
+
+    @Test
+    public void testDeleteDeploymentToscaConceptIdentifierBoolean() {
+        PdpPolicyStatus deploy1A = builder.policy(POLICY_A).build();
+        PdpPolicyStatus deploy2A = builder.policy(POLICY_A).pdpId(PDP_B).build();
+        PdpPolicyStatus deployB = builder.policy(POLICY_B).pdpId(PDP_A).build();
+
+        builder.deploy(false);
+        PdpPolicyStatus undeployA = builder.policy(POLICY_A).build();
+        PdpPolicyStatus undeployB = builder.policy(POLICY_B).build();
+
+        // @formatter:off
+        tracker.getRecordMap().putAll(makeMap(
+                        Action.UNCHANGED, deploy1A,
+                        Action.UNCHANGED, deploy2A,
+                        Action.UNCHANGED, deployB,
+                        Action.UNCHANGED, undeployA,
+                        Action.UNCHANGED, undeployB
+                        ));
+        // @formatter:on
+
+        tracker.deleteDeployment(POLICY_A, true);
+
+        // @formatter:off
+        assertThat(tracker.getRecordMap()).isEqualTo(makeMap(
+                        Action.DELETED, deploy1A,
+                        Action.DELETED, deploy2A,
+                        Action.UNCHANGED, deployB,
+                        Action.UNCHANGED, undeployA,
+                        Action.UNCHANGED, undeployB
+                        ));
+        // @formatter:on
+
+        tracker.deleteDeployment(POLICY_B, false);
+
+        // @formatter:off
+        assertThat(tracker.getRecordMap()).isEqualTo(makeMap(
+                        Action.DELETED, deploy1A,
+                        Action.DELETED, deploy2A,
+                        Action.UNCHANGED, deployB,
+                        Action.UNCHANGED, undeployA,
+                        Action.DELETED, undeployB
+                        ));
+        // @formatter:on
+    }
+
+    @Test
+    public void testDeleteDeploymentBiPredicateOfStatusKeyStatusAction() {
+        PdpPolicyStatus create1 = builder.pdpId(PDP_A).build();
+        PdpPolicyStatus delete = builder.pdpId(PDP_B).build();
+        PdpPolicyStatus update = builder.pdpId(PDP_C).build();
+        PdpPolicyStatus unchange = builder.pdpId(PDP_D).build();
+
+        PdpPolicyStatus create2 = builder.pdpId(PDP_B).build();
+
+        // @formatter:off
+        tracker.getRecordMap().putAll(makeMap(
+                        Action.CREATED, create1,
+                        Action.CREATED, create2,
+                        Action.DELETED, delete,
+                        Action.UPDATED, update,
+                        Action.UNCHANGED, unchange
+                        ));
+        // @formatter:on
+
+        tracker.deleteDeployment(POLICY_A, true);
+
+        // @formatter:off
+        assertThat(tracker.getRecordMap()).isEqualTo(makeMap(
+                        Action.DELETED, delete,
+                        Action.DELETED, update,
+                        Action.DELETED, unchange
+                        ));
+        // @formatter:on
+    }
+
+    @Test
+    public void testDeploy() {
+        tracker.deploy(PDP_A, POLICY_A, POLICY_TYPE, GROUP_A, PDP_TYPE, true);
+
+        assertThat(tracker.getRecordMap()).hasSize(1);
+
+        StatusAction status2 = tracker.getRecordMap().values().iterator().next();
+
+        assertThat(status2.getAction()).isEqualTo(Action.CREATED);
+        assertThat(status2.getStatus().getState()).isEqualTo(State.WAITING);
+        assertThat(status2.getStatus().isDeploy()).isTrue();
+
+        /*
+         * repeat - should be the same status
+         */
+        tracker.deploy(PDP_A, POLICY_A, POLICY_TYPE, GROUP_A, PDP_TYPE, true);
+
+        assertThat(tracker.getRecordMap()).hasSize(1);
+        assertThat(tracker.getRecordMap().values().iterator().next()).isSameAs(status2);
+        assertThat(status2.getAction()).isEqualTo(Action.CREATED);
+        assertThat(status2.getStatus().getState()).isEqualTo(State.WAITING);
+        assertThat(status2.getStatus().isDeploy()).isTrue();
+
+        /*
+         * repeat, with different values - should be unchanged
+         */
+        status2.setAction(Action.UNCHANGED);
+        status2.getStatus().setDeploy(true);
+        status2.getStatus().setState(State.SUCCESS);
+
+        tracker.deploy(PDP_A, POLICY_A, POLICY_TYPE, GROUP_A, PDP_TYPE, true);
+
+        assertThat(tracker.getRecordMap()).hasSize(1);
+        assertThat(tracker.getRecordMap().values().iterator().next()).isSameAs(status2);
+        assertThat(status2.getAction()).isEqualTo(Action.UNCHANGED);
+        assertThat(status2.getStatus().getState()).isEqualTo(State.SUCCESS);
+        assertThat(status2.getStatus().isDeploy()).isTrue();
+
+        /*
+         * incorrect "deploy" value - should update it
+         */
+        status2.setAction(Action.UNCHANGED);
+        status2.getStatus().setDeploy(true);
+
+        tracker.deploy(PDP_A, POLICY_A, POLICY_TYPE, GROUP_A, PDP_TYPE, false);
+
+        assertThat(status2.getAction()).isEqualTo(Action.UPDATED);
+        assertThat(status2.getStatus().getState()).isEqualTo(State.WAITING);
+        assertThat(status2.getStatus().isDeploy()).isFalse();
+
+        /*
+         * marked for deletion - should reinstate it
+         */
+        status2.setAction(Action.DELETED);
+        status2.getStatus().setState(State.FAILURE);
+        status2.getStatus().setDeploy(false);
+
+        tracker.deploy(PDP_A, POLICY_A, POLICY_TYPE, GROUP_A, PDP_TYPE, false);
+
+        assertThat(status2.getAction()).isEqualTo(Action.UPDATED);
+        assertThat(status2.getStatus().getState()).isEqualTo(State.FAILURE);
+        assertThat(status2.getStatus().isDeploy()).isFalse();
+    }
+
+    @Test
+    public void testCompleteDeploy() {
+        tracker.deploy(PDP_A, POLICY_A, POLICY_TYPE, GROUP_A, PDP_TYPE, true);
+        assertThat(tracker.getRecordMap()).hasSize(1);
+
+        // deployed, but not expected to be deployed - record should be left as is
+        checkCompleteDeploy(true, Set.of(), Set.of(), Action.UNCHANGED, State.WAITING);
+        checkCompleteDeploy(true, Set.of(), Set.of(POLICY_A), Action.UNCHANGED, State.WAITING);
+
+        // expected, but not actually deployed - failure
+        checkCompleteDeploy(true, Set.of(POLICY_A), Set.of(), Action.UPDATED, State.FAILURE);
+
+        // expected and actually deployed - success
+        checkCompleteDeploy(true, Set.of(POLICY_A), Set.of(POLICY_A), Action.UPDATED, State.SUCCESS);
+        checkCompleteDeploy(true, Set.of(POLICY_A, POLICY_B), Set.of(POLICY_A), Action.UPDATED, State.SUCCESS);
+
+        // not expected and not actually deployed - success
+        checkCompleteDeploy(false, Set.of(), Set.of(), Action.UPDATED, State.SUCCESS);
+
+        // not expected, but actually deployed - failure
+        checkCompleteDeploy(false, Set.of(), Set.of(POLICY_A), Action.UPDATED, State.FAILURE);
+
+        // undeployed, but expected to be deployed - record should be left as is
+        checkCompleteDeploy(false, Set.of(POLICY_A), Set.of(), Action.UNCHANGED, State.WAITING);
+        checkCompleteDeploy(false, Set.of(POLICY_A), Set.of(POLICY_A), Action.UNCHANGED, State.WAITING);
+        checkCompleteDeploy(false, Set.of(POLICY_A, POLICY_B), Set.of(POLICY_A), Action.UNCHANGED, State.WAITING);
+
+        /*
+         * Try a case where the state is already correct.
+         */
+        StatusAction status = tracker.getRecordMap().values().iterator().next();
+        status.getStatus().setDeploy(false);
+        status.setAction(Action.UNCHANGED);
+        status.getStatus().setState(State.SUCCESS);
+
+        tracker.completeDeploy(PDP_A, Set.of(), Set.of());
+
+        assertThat(status.getAction()).isEqualTo(Action.UNCHANGED);
+        assertThat(status.getStatus().getState()).isEqualTo(State.SUCCESS);
+
+        /*
+         * Try a case where the PDP does not match the record.
+         */
+        status.getStatus().setDeploy(false);
+        status.setAction(Action.UNCHANGED);
+        status.getStatus().setState(State.WAITING);
+
+        tracker.completeDeploy(PDP_B, Set.of(), Set.of());
+
+        assertThat(status.getAction()).isEqualTo(Action.UNCHANGED);
+        assertThat(status.getStatus().getState()).isEqualTo(State.WAITING);
+    }
+
+    private void checkCompleteDeploy(boolean deploy, Set<ToscaConceptIdentifier> expected,
+                    Set<ToscaConceptIdentifier> actual, Action action, State state) {
+
+        StatusAction status = tracker.getRecordMap().values().iterator().next();
+        status.getStatus().setDeploy(deploy);
+        status.setAction(Action.UNCHANGED);
+        status.getStatus().setState(State.WAITING);
+
+        tracker.completeDeploy(PDP_A, expected, actual);
+
+        assertThat(status.getAction()).isEqualTo(action);
+        assertThat(status.getStatus().getState()).isEqualTo(state);
+    }
+
+    private List<PdpPolicyStatus> sort(List<PdpPolicyStatus> list) {
+
+        Collections.sort(list, (rec1, rec2) -> {
+
+            // @formatter:off
+            return new CompareToBuilder()
+                            .append(rec1.getPdpId(), rec2.getPdpId())
+                            .append(rec1.getPolicy(), rec2.getPolicy())
+                            .toComparison();
+            // @formatter:on
+        });
+
+        return list;
+    }
+
+    /**
+     * Makes a map.
+     *
+     * @param data pairs of (Action, PdpPolicyStatus)
+     * @return a new map containing the given data
+     */
+    private Map<StatusKey, StatusAction> makeMap(Object... data) {
+        Map<StatusKey, StatusAction> map = new HashMap<>();
+
+        assert (data.length % 2 == 0);
+
+        for (int idata = 0; idata < data.length; idata += 2) {
+            Action action = (Action) data[idata];
+            PdpPolicyStatus status = (PdpPolicyStatus) data[idata + 1];
+            map.put(new StatusKey(status), new StatusAction(action, status));
+        }
+
+        return map;
+    }
+}