Merge changes Ib795f077,I723e4262
authorGuillaume Lambert <guillaume.lambert@orange.com>
Thu, 29 Feb 2024 13:24:43 +0000 (13:24 +0000)
committerGerrit Code Review <gerrit@opendaylight.org>
Thu, 29 Feb 2024 13:24:43 +0000 (13:24 +0000)
* changes:
  Refactoring rollback when service create fails
  New Package dealing with device rollback

23 files changed:
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/DeviceRendererService.java
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/DeviceRendererServiceImpl.java
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/RendererServiceOperationsImpl.java
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/tasks/DeviceRenderingTask.java
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/tasks/NetworkDeviceRenderingRollbackTask.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/tasks/ResultMessage.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/tasks/RollbackResultMessage.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/Connection.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/DeviceInterface.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/Transaction.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/Delete.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/DeleteService.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/DeleteSubscriber.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/FailedRollbackResult.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/Result.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/Subscriber.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/History.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/NonStickHistoryMemory.java [new file with mode: 0644]
renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/TransactionHistory.java [new file with mode: 0644]
renderer/src/test/java/org/opendaylight/transportpce/renderer/provisiondevice/RendererServiceOperationsImplTest.java
renderer/src/test/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/ConnectionTest.java [new file with mode: 0644]
renderer/src/test/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/DeviceInterfaceTest.java [new file with mode: 0644]
renderer/src/test/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/TransactionHistoryTest.java [new file with mode: 0644]

index a242147bdcdfffe2e4cb4d15a8fa7addc3e097d6..049247b45dc17f8e0d8a541c92a26f5de9a6c88d 100644 (file)
@@ -10,6 +10,7 @@ package org.opendaylight.transportpce.renderer.provisiondevice;
 
 import org.opendaylight.transportpce.common.openroadminterfaces.OpenRoadmInterfaceException;
 import org.opendaylight.transportpce.renderer.provisiondevice.servicepath.ServicePathDirection;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.history.History;
 import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.CreateOtsOmsInput;
 import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.CreateOtsOmsOutput;
 import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.RendererRollbackInput;
@@ -44,6 +45,35 @@ public interface DeviceRendererService {
      */
     ServicePathOutput setupServicePath(ServicePathInput input, ServicePathDirection direction);
 
+    /**
+     * This method set's wavelength path based on following steps.
+     *
+     * <p>
+     * For each node:
+     * 1. Create Och interface on source termination point.
+     * 2. Create Och interface on destination termination point.
+     * 3. Create cross connect between source and destination tps created in step 1
+     *    and 2.
+     *
+     * Naming convention used for OCH interfaces name : tp-wavenumber Naming
+     * convention used for cross connect name : src-dest-wavenumber
+     * </p>
+     *
+     * @param input
+     *            Input parameter from the service-path yang model
+     * @param direction
+     *            Service Path direction
+     * @param transactionHistory
+     *            Object tracking created interface(s) and connection(s).
+     *
+     * @return Result list of all nodes if request successful otherwise specific
+     *         reason of failure.
+     */
+    ServicePathOutput setupServicePath(
+            ServicePathInput input,
+            ServicePathDirection direction,
+            History transactionHistory);
+
     /**
      * This method removes wavelength path based on following steps.
      *
@@ -72,6 +102,14 @@ public interface DeviceRendererService {
      */
     RendererRollbackOutput rendererRollback(RendererRollbackInput input);
 
+    /**
+     * Rollback created interfaces and cross connects specified by transaction history.
+     *
+     * @param transactionHistory The transaction history in need of rollback.
+     * @return Success flag and nodes which failed to rollback
+     */
+    RendererRollbackOutput rendererRollback(History transactionHistory);
+
     /**
      * This method creates the basis of ots and oms interfaces on a specific ROADM degree.
      *
index 836282c44a8ae4fab4c9c9827c007c728b7b11e2..a38b848b414f1888ada468153a571145659cb762 100644 (file)
@@ -49,6 +49,15 @@ import org.opendaylight.transportpce.common.openroadminterfaces.OpenRoadmInterfa
 import org.opendaylight.transportpce.renderer.openroadminterface.OpenRoadmInterfaceFactory;
 import org.opendaylight.transportpce.renderer.provisiondevice.servicepath.ServiceListTopology;
 import org.opendaylight.transportpce.renderer.provisiondevice.servicepath.ServicePathDirection;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.Connection;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.DeviceInterface;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.DeleteService;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.DeleteSubscriber;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.FailedRollbackResult;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.Result;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.Subscriber;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.history.History;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.history.NonStickHistoryMemory;
 import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.alarmsuppression.rev171102.ServiceNodelist;
 import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.alarmsuppression.rev171102.service.nodelist.NodelistBuilder;
 import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.alarmsuppression.rev171102.service.nodelist.NodelistKey;
@@ -113,12 +122,21 @@ public class DeviceRendererServiceImpl implements DeviceRendererService {
         this.openRoadmInterfaceFactory = new OpenRoadmInterfaceFactory(mappingUtils, portMapping, openRoadmInterfaces);
     }
 
+    @Override
+    public ServicePathOutput setupServicePath(ServicePathInput input, ServicePathDirection direction) {
+        return setupServicePath(input, direction, new NonStickHistoryMemory());
+    }
+
     @SuppressWarnings("rawtypes")
     // FIXME check if the ForkJoinTask raw type can be avoided
     // Raw types use are discouraged since they lack type safety.
     // Resulting Problems are observed at run time and not at compile time
     @Override
-    public ServicePathOutput setupServicePath(ServicePathInput input, ServicePathDirection direction) {
+    public ServicePathOutput setupServicePath(
+            ServicePathInput input,
+            ServicePathDirection direction,
+            History transactionHistory
+    ) {
         LOG.info("setup service path for input {} and direction {}", input, direction);
         List<Nodes> nodes = new ArrayList<>();
         if (input.getNodes() != null) {
@@ -164,10 +182,14 @@ public class DeviceRendererServiceImpl implements DeviceRendererService {
                         crossConnectFlag++;
                         String supportingOchInterface = this.openRoadmInterfaceFactory.createOpenRoadmOchInterface(
                                 nodeId, destTp, spectrumInformation);
+                        transactionHistory.add(new DeviceInterface(nodeId, supportingOchInterface));
+
                         // Split the string based on # pass the last element as the supported Interface
                         // This is needed for 7.1 device models with B100G, we have OTSI, OTSI-group combined as OCH
                         String[] listOfSuppOchInf = supportingOchInterface.split("#");
                         List<String> createdOchInf = Arrays.asList(listOfSuppOchInf);
+                        transactionHistory.addInterfaces(nodeId, listOfSuppOchInf);
+
                         createdOchInterfaces.addAll(createdOchInf);
                         LOG.info("DEST all otsi interfaces {}", createdOchInterfaces);
                         // Taking the last element
@@ -176,6 +198,8 @@ public class DeviceRendererServiceImpl implements DeviceRendererService {
                                 .createOpenRoadmOtu4Interface(nodeId, destTp, supportingOchInterface, apiInfoA,
                                         apiInfoZ);
                         createdOtuInterfaces.add(supportingOtuInterface);
+                        transactionHistory.add(new DeviceInterface(nodeId, supportingOtuInterface));
+
                         LOG.info("all dest otu interfaces {}", createdOtuInterfaces);
                         if (srcTp == null) {
                             otnLinkTps.add(new LinkTpBuilder().setNodeId(nodeId).setTpId(destTp).build());
@@ -183,23 +207,32 @@ public class DeviceRendererServiceImpl implements DeviceRendererService {
                             // If src and dest tp contains the network token, then it is regenerator
                             LOG.info("Create the ODUCn for regen on the dest-tp");
                             // Here we first create ODUCn interface for the Regen
-                            createdOduInterfaces.add(this.openRoadmInterfaceFactory
-                                    .createOpenRoadmOducn(nodeId, destTp));
+                            String openRoadmOducn = this.openRoadmInterfaceFactory
+                                    .createOpenRoadmOducn(nodeId, destTp);
+                            createdOduInterfaces.add(openRoadmOducn);
+                            transactionHistory.addInterfaces(nodeId, openRoadmOducn);
+
                             LOG.info("all dest odu interfaces {}", createdOduInterfaces);
                         } else {
                             // This is needed for 7.1 device models for 400GE, since we have ODUC4 and ODUflex
                             // are combined
-                            createdOduInterfaces = Set.of(this.openRoadmInterfaceFactory
-                                .createOpenRoadmOdu4HOInterface(
-                                    nodeId, destTp, false, apiInfoA, apiInfoZ, PT_07).split("#"));
+                            String[] oduInterfaces = this.openRoadmInterfaceFactory
+                                    .createOpenRoadmOdu4HOInterface(
+                                            nodeId, destTp, false, apiInfoA, apiInfoZ, PT_07).split("#");
+                            createdOduInterfaces.addAll(Arrays.asList(oduInterfaces));
+                            transactionHistory.addInterfaces(nodeId, oduInterfaces);
+
                         }
                     }
                     if ((srcTp != null) && srcTp.contains(StringConstants.CLIENT_TOKEN)) {
                         LOG.info("Adding supporting EThernet interface for node {}, src tp {}", nodeId, srcTp);
                         crossConnectFlag++;
                         // create OpenRoadm Xponder Client Interfaces
-                        createdEthInterfaces.add(this.openRoadmInterfaceFactory.createOpenRoadmEthInterface(
-                                nodeId, srcTp));
+                        String openRoadmEthInterface = this.openRoadmInterfaceFactory.createOpenRoadmEthInterface(
+                                nodeId, srcTp);
+                        createdEthInterfaces.add(openRoadmEthInterface);
+                        transactionHistory.add(new DeviceInterface(nodeId, openRoadmEthInterface));
+
                     }
                     if ((srcTp != null) && srcTp.contains(StringConstants.NETWORK_TOKEN)) {
                         LOG.info("Adding supporting OCH interface for node {}, src tp {}, spectrumInformation {}",
@@ -208,10 +241,14 @@ public class DeviceRendererServiceImpl implements DeviceRendererService {
                         // create OpenRoadm Xponder Line Interfaces
                         String supportingOchInterface = this.openRoadmInterfaceFactory.createOpenRoadmOchInterface(
                                 nodeId, srcTp, spectrumInformation);
+                        transactionHistory.add(new DeviceInterface(nodeId, supportingOchInterface));
+
                         // createdOchInterfaces.add(supportingOchInterface);
                         // Split the string based on # pass the last element as the supported Interface
                         // This is needed for 7.1 device models with B100G, we have OTSI, OTSI-group combined as OCH
                         String[] listOfSuppOchInf = supportingOchInterface.split("#");
+                        transactionHistory.addInterfaces(nodeId, listOfSuppOchInf);
+
                         List<String> tmpCreatedOchInterfaces = Arrays.asList(listOfSuppOchInf);
                         createdOchInterfaces.addAll(tmpCreatedOchInterfaces);
                         // Taking the last element
@@ -219,40 +256,53 @@ public class DeviceRendererServiceImpl implements DeviceRendererService {
                         String supportingOtuInterface = this.openRoadmInterfaceFactory.createOpenRoadmOtu4Interface(
                                 nodeId, srcTp, supportingOchInterface, apiInfoA, apiInfoZ);
                         createdOtuInterfaces.add(supportingOtuInterface);
+                        transactionHistory.add(new DeviceInterface(nodeId, supportingOtuInterface));
+
                         if (destTp == null) {
                             otnLinkTps.add(new LinkTpBuilder().setNodeId(nodeId).setTpId(srcTp).build());
                         } else if (destTp.contains(StringConstants.NETWORK_TOKEN)) {
                             // If the src and dest tp have network-token, then it is a regen
                             LOG.info("Create the regen-interfaces on the src-tp");
                             // Here we first create ODUCn interface for the Regen
-                            createdOduInterfaces.add(this.openRoadmInterfaceFactory.createOpenRoadmOducn(nodeId,
-                                    srcTp));
+                            String openRoadmOducn = this.openRoadmInterfaceFactory.createOpenRoadmOducn(nodeId,
+                                    srcTp);
+                            createdOduInterfaces.add(openRoadmOducn);
+                            transactionHistory.add(new DeviceInterface(nodeId, openRoadmOducn));
+
                             LOG.info("all src odu interfaces {}", createdOduInterfaces);
                         } else {
-                            createdOduInterfaces.add(this.openRoadmInterfaceFactory.createOpenRoadmOdu4HOInterface(
-                                    nodeId, srcTp, false, apiInfoA, apiInfoZ, PT_07));
+                            String openRoadmOdu4HOInterface = this.openRoadmInterfaceFactory
+                                    .createOpenRoadmOdu4HOInterface(nodeId, srcTp, false, apiInfoA, apiInfoZ, PT_07);
+                            createdOduInterfaces.add(openRoadmOdu4HOInterface);
+                            transactionHistory.add(new DeviceInterface(nodeId, openRoadmOdu4HOInterface));
                         }
                     }
                     if ((destTp != null) && destTp.contains(StringConstants.CLIENT_TOKEN)) {
                         LOG.info("Adding supporting EThernet interface for node {}, dest tp {}", nodeId, destTp);
                         crossConnectFlag++;
                         // create OpenRoadm Xponder Client Interfaces
-                        createdEthInterfaces.add(this.openRoadmInterfaceFactory.createOpenRoadmEthInterface(
-                                nodeId, destTp));
+                        String openRoadmEthInterface = this.openRoadmInterfaceFactory.createOpenRoadmEthInterface(
+                                nodeId, destTp);
+                        createdEthInterfaces.add(openRoadmEthInterface);
+                        transactionHistory.add(new DeviceInterface(nodeId, openRoadmEthInterface));
                     }
                     if ((srcTp != null) && (srcTp.contains(StringConstants.TTP_TOKEN)
                             || srcTp.contains(StringConstants.PP_TOKEN))) {
                         LOG.info("Adding supporting OCH interface for node {}, src tp {}, spectrumInformation {}",
                                 nodeId, srcTp, spectrumInformation);
-                        createdOchInterfaces.addAll(this.openRoadmInterfaceFactory.createOpenRoadmOchInterfaces(
-                                nodeId, srcTp, spectrumInformation));
+                        List<String> openRoadmOchInterfaces = this.openRoadmInterfaceFactory
+                                .createOpenRoadmOchInterfaces(nodeId, srcTp, spectrumInformation);
+                        createdOchInterfaces.addAll(openRoadmOchInterfaces);
+                        transactionHistory.addInterfaces(nodeId, openRoadmOchInterfaces);
                     }
                     if ((destTp != null) && (destTp.contains(StringConstants.TTP_TOKEN)
                             || destTp.contains(StringConstants.PP_TOKEN))) {
                         LOG.info("Adding supporting OCH interface for node {}, dest tp {}, spectrumInformation {}",
                                 nodeId, destTp, spectrumInformation);
-                        createdOchInterfaces.addAll(this.openRoadmInterfaceFactory.createOpenRoadmOchInterfaces(
-                                nodeId, destTp, spectrumInformation));
+                        List<String> openRoadmOchInterfaces = this.openRoadmInterfaceFactory
+                                .createOpenRoadmOchInterfaces(nodeId, destTp, spectrumInformation);
+                        createdOchInterfaces.addAll(openRoadmOchInterfaces);
+                        transactionHistory.addInterfaces(nodeId, openRoadmOchInterfaces);
                     }
                     if (crossConnectFlag < 1) {
                         LOG.info("Creating cross connect between source {} and destination {} for node {}", srcTp,
@@ -261,7 +311,9 @@ public class DeviceRendererServiceImpl implements DeviceRendererService {
                                 this.crossConnect.postCrossConnect(nodeId, srcTp, destTp, spectrumInformation);
                         if (connectionNameOpt.isPresent()) {
                             nodesProvisioned.add(nodeId);
-                            createdConnections.add(connectionNameOpt.orElseThrow());
+                            String connectionName = connectionNameOpt.orElseThrow();
+                            createdConnections.add(connectionName);
+                            transactionHistory.add(new Connection(nodeId, connectionName, false));
                         } else {
                             processErrorMessage("Unable to post Roadm-connection for node " + nodeId, forkJoinPool,
                                     results);
@@ -558,6 +610,26 @@ public class DeviceRendererServiceImpl implements DeviceRendererService {
                 .build();
     }
 
+    @Override
+    public RendererRollbackOutput rendererRollback(History transactionHistory) {
+        LOG.info("Rolling back...");
+
+        Result rollbackResult = new FailedRollbackResult();
+        Subscriber deleteSubscriber = new DeleteSubscriber(rollbackResult);
+
+        transactionHistory.rollback(
+                new DeleteService(
+                        crossConnect,
+                        openRoadmInterfaces,
+                        deleteSubscriber
+                )
+        );
+
+        LOG.info("Rollback done!");
+
+        return rollbackResult.renderRollbackOutput();
+    }
+
     private boolean alarmSuppressionNodeRegistration(ServicePathInput input) {
         Map<org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.alarmsuppression.rev171102.service
                 .nodelist.nodelist.NodesKey,
index b051192ad21e54bcef1920f024a38ab371cdcda0..cca076fa691a1650fea5b79820bfeba20706afea 100644 (file)
@@ -36,10 +36,14 @@ import org.opendaylight.transportpce.renderer.ServicePathInputData;
 import org.opendaylight.transportpce.renderer.provisiondevice.servicepath.ServicePathDirection;
 import org.opendaylight.transportpce.renderer.provisiondevice.tasks.DeviceRenderingRollbackTask;
 import org.opendaylight.transportpce.renderer.provisiondevice.tasks.DeviceRenderingTask;
+import org.opendaylight.transportpce.renderer.provisiondevice.tasks.NetworkDeviceRenderingRollbackTask;
 import org.opendaylight.transportpce.renderer.provisiondevice.tasks.OlmPowerSetupRollbackTask;
 import org.opendaylight.transportpce.renderer.provisiondevice.tasks.OlmPowerSetupTask;
 import org.opendaylight.transportpce.renderer.provisiondevice.tasks.OtnDeviceRenderingTask;
 import org.opendaylight.transportpce.renderer.provisiondevice.tasks.RollbackProcessor;
+import org.opendaylight.transportpce.renderer.provisiondevice.tasks.RollbackResultMessage;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.history.History;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.history.TransactionHistory;
 import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.Action;
 import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.OtnServicePathInput;
 import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.networkutils.rev220630.OtnLinkType;
@@ -370,9 +374,12 @@ public class RendererServiceOperationsImpl implements RendererServiceOperations
             servicePathDataAtoZ.getServicePathInput().getServiceName(),
             RpcStatusEx.Pending,
             RENDERING_DEVICES_A_Z_MSG);
+
+        History transactionHistory = new TransactionHistory();
         ListenableFuture<DeviceRenderingResult> atozrenderingFuture =
             this.executor.submit(
-                new DeviceRenderingTask(this.deviceRenderer, servicePathDataAtoZ, ServicePathDirection.A_TO_Z));
+                new DeviceRenderingTask(this.deviceRenderer, servicePathDataAtoZ, ServicePathDirection.A_TO_Z,
+                        transactionHistory));
 
         LOG.info(RENDERING_DEVICES_Z_A_MSG);
         sendNotifications(
@@ -382,7 +389,8 @@ public class RendererServiceOperationsImpl implements RendererServiceOperations
             RENDERING_DEVICES_Z_A_MSG);
         ListenableFuture<DeviceRenderingResult> ztoarenderingFuture =
             this.executor.submit(
-                new DeviceRenderingTask(this.deviceRenderer, servicePathDataZtoA, ServicePathDirection.Z_TO_A));
+                new DeviceRenderingTask(this.deviceRenderer, servicePathDataZtoA, ServicePathDirection.Z_TO_A,
+                        transactionHistory));
 
         ListenableFuture<List<DeviceRenderingResult>> renderingCombinedFuture =
             Futures.allAsList(atozrenderingFuture, ztoarenderingFuture);
@@ -403,16 +411,15 @@ public class RendererServiceOperationsImpl implements RendererServiceOperations
         }
 
         rollbackProcessor.addTask(
-            new DeviceRenderingRollbackTask(
-                "AtoZDeviceTask",
-                ! renderingResults.get(0).isSuccess(),
-                renderingResults.get(0).getRenderedNodeInterfaces(),
-                this.deviceRenderer));
-        rollbackProcessor.addTask(
-                new DeviceRenderingRollbackTask("ZtoADeviceTask",
-                ! renderingResults.get(1).isSuccess(),
-                renderingResults.get(1).getRenderedNodeInterfaces(),
-                this.deviceRenderer));
+            new NetworkDeviceRenderingRollbackTask(
+                "RollbackTransactionHistoryTask",
+                transactionHistory,
+                ! (renderingResults.get(0).isSuccess() && renderingResults.get(1).isSuccess()),
+                deviceRenderer,
+                new RollbackResultMessage()
+            )
+        );
+
         return renderingResults;
     }
 
index 6a30f950d7f00efc5d24eb19c638e32a57b364b5..4b2d7fbed1364662aacf5d3b0a11d54ef4f3adf7 100644 (file)
@@ -14,6 +14,7 @@ import org.opendaylight.transportpce.renderer.ServicePathInputData;
 import org.opendaylight.transportpce.renderer.provisiondevice.DeviceRendererService;
 import org.opendaylight.transportpce.renderer.provisiondevice.DeviceRenderingResult;
 import org.opendaylight.transportpce.renderer.provisiondevice.servicepath.ServicePathDirection;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.history.History;
 import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.ServicePathOutput;
 import org.opendaylight.yang.gen.v1.http.org.transportpce.common.types.rev220926.optical.renderer.nodes.Nodes;
 import org.slf4j.Logger;
@@ -26,12 +27,14 @@ public class DeviceRenderingTask implements Callable<DeviceRenderingResult> {
     private final DeviceRendererService deviceRenderer;
     private final ServicePathInputData servicePathInputData;
     private final ServicePathDirection direction;
+    private final History transactionHistory;
 
     public DeviceRenderingTask(DeviceRendererService deviceRenderer, ServicePathInputData servicePathInputData,
-            ServicePathDirection direction) {
+            ServicePathDirection direction, History transactionHistory) {
         this.deviceRenderer = deviceRenderer;
         this.servicePathInputData = servicePathInputData;
         this.direction = direction;
+        this.transactionHistory = transactionHistory;
     }
 
     @Override
@@ -43,7 +46,7 @@ public class DeviceRenderingTask implements Callable<DeviceRenderingResult> {
             case Create:
                 operation = "setup";
                 output = this.deviceRenderer.setupServicePath(this.servicePathInputData.getServicePathInput(),
-                    this.direction);
+                    this.direction, transactionHistory);
                 olmList = this.servicePathInputData.getNodeLists().getOlmNodeList();
                 break;
             case Delete:
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/tasks/NetworkDeviceRenderingRollbackTask.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/tasks/NetworkDeviceRenderingRollbackTask.java
new file mode 100644 (file)
index 0000000..4c1faa1
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.tasks;
+
+import org.opendaylight.transportpce.renderer.provisiondevice.DeviceRendererService;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.history.History;
+import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.RendererRollbackOutput;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public class NetworkDeviceRenderingRollbackTask extends RollbackTask {
+
+    private final History transactionHistory;
+
+    private final boolean isRollbackNecessary;
+
+    private final DeviceRendererService deviceRendererService;
+
+    private final ResultMessage message;
+
+    private static final Logger LOG = LoggerFactory.getLogger(NetworkDeviceRenderingRollbackTask.class);
+
+    public NetworkDeviceRenderingRollbackTask(String id, History transactionHistory,
+                                              boolean isRollbackNecessary,
+                                              DeviceRendererService deviceRendererService, ResultMessage message) {
+        super(id);
+        this.transactionHistory = transactionHistory;
+        this.isRollbackNecessary = isRollbackNecessary;
+        this.deviceRendererService = deviceRendererService;
+        this.message = message;
+    }
+
+    @Override
+    public boolean isRollbackNecessary() {
+        return isRollbackNecessary;
+    }
+
+    @Override
+    public Void call() throws Exception {
+
+        RendererRollbackOutput rollbackOutput = deviceRendererService.rendererRollback(transactionHistory);
+
+        if (! rollbackOutput.getSuccess()) {
+            LOG.warn("Device rendering rollback of {} was not successful! Failed rollback on {}.", this.getId(),
+                    message.createErrorMessage(rollbackOutput.nonnullFailedToRollback().values()));
+        } else {
+            LOG.info("Device rollback of {} successful.", this.getId());
+        }
+
+        return null;
+    }
+}
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/tasks/ResultMessage.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/tasks/ResultMessage.java
new file mode 100644 (file)
index 0000000..d1540ca
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.tasks;
+
+import java.util.Collection;
+import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.renderer.rollback.output.FailedToRollback;
+
+public interface ResultMessage {
+
+    /**
+     * Build an error message for a failed rollback.
+     */
+    String createErrorMessage(Collection<FailedToRollback> failedRollbacks);
+
+}
\ No newline at end of file
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/tasks/RollbackResultMessage.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/tasks/RollbackResultMessage.java
new file mode 100644 (file)
index 0000000..7fae4c9
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.tasks;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.renderer.rollback.output.FailedToRollback;
+
+public class RollbackResultMessage implements ResultMessage {
+
+    @Override
+    public String createErrorMessage(Collection<FailedToRollback> failedRollbacks) {
+        List<String> failedRollbackNodes = new ArrayList<>();
+
+        failedRollbacks.forEach(failedRollback -> {
+            var intf = failedRollback.getInterface();
+
+            failedRollbackNodes.add(
+                failedRollback.getNodeId()
+                    + ": "
+                    + intf == null ? "" : String.join(", ", intf)
+            );
+        });
+
+        return String.join(System.lineSeparator(), failedRollbackNodes);
+    }
+
+}
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/Connection.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/Connection.java
new file mode 100644 (file)
index 0000000..ad04ae7
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction;
+
+import java.util.List;
+import java.util.Objects;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.Delete;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Connection transaction.
+ *
+ * <p>
+ * i.e. a class tracking a connection.
+ */
+public class Connection implements Transaction {
+
+    private static final Logger LOG = LoggerFactory.getLogger(Connection.class);
+    private final String deviceId;
+    private final String connectionNumber;
+    private final boolean isOtn;
+
+    public Connection(String deviceId, String connectionNumber, boolean isOtn) {
+        this.deviceId = deviceId;
+        this.connectionNumber = connectionNumber;
+        this.isOtn = isOtn;
+    }
+
+    @Override
+    public boolean rollback(Delete delete) {
+        List<String> supportingInterfaces = delete.deleteCrossConnect(deviceId, connectionNumber, isOtn);
+
+        if (supportingInterfaces == null || supportingInterfaces.size() == 0) {
+            return false;
+        }
+
+        LOG.info("Supporting interfaces {} affected by rollback on {} {}",
+                String.join(", ", supportingInterfaces), deviceId, connectionNumber);
+
+        return true;
+
+    }
+
+    @Override
+    public String description() {
+        return String.format("Connection %s connection number %s isOtn %s", deviceId,
+                connectionNumber, isOtn);
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (this == object) {
+            return true;
+        }
+        if (!(object instanceof Connection)) {
+            return false;
+        }
+        Connection that = (Connection) object;
+        return isOtn == that.isOtn && Objects.equals(deviceId, that.deviceId)
+                && Objects.equals(connectionNumber, that.connectionNumber);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(deviceId, connectionNumber, isOtn);
+    }
+}
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/DeviceInterface.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/DeviceInterface.java
new file mode 100644 (file)
index 0000000..8bcc2c1
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction;
+
+import java.util.Objects;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.Delete;
+
+public class DeviceInterface implements Transaction {
+
+    private final String nodeId;
+
+    private final String interfaceId;
+
+    public DeviceInterface(String nodeId, String interfaceId) {
+        this.nodeId = nodeId;
+        this.interfaceId = interfaceId;
+    }
+
+    @Override
+    public boolean rollback(Delete delete) {
+        return delete.deleteInterface(nodeId, interfaceId);
+    }
+
+    @Override
+    public String description() {
+        return String.format("Node: %s interface id: %s", nodeId, interfaceId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(nodeId, interfaceId);
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (this == object) {
+            return true;
+        }
+        if (!(object instanceof DeviceInterface)) {
+            return false;
+        }
+        DeviceInterface that = (DeviceInterface) object;
+        return Objects.equals(nodeId, that.nodeId) && Objects.equals(interfaceId,
+                that.interfaceId);
+    }
+}
\ No newline at end of file
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/Transaction.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/Transaction.java
new file mode 100644 (file)
index 0000000..d1300e3
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction;
+
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.Delete;
+
+/**
+ * Any class wishing to keep track of transactions
+ * may implement this interface.
+ */
+public interface Transaction {
+
+    /**
+     * Rollback this transaction.
+     */
+    boolean rollback(Delete delete);
+
+    String description();
+
+    int hashCode();
+
+    boolean equals(Object object);
+}
\ No newline at end of file
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/Delete.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/Delete.java
new file mode 100644 (file)
index 0000000..95fcdff
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete;
+
+import java.util.List;
+
+/**
+ * A class capable of deleting service connections/interfaces
+ * may implement this interface.
+ */
+public interface Delete {
+
+    /**
+     * Delete cross connection.
+     * Typically, deleted before interfaces.
+     */
+    List<String> deleteCrossConnect(String deviceId, String connectionNumber, boolean isOtn);
+
+    /**
+     * Delete an interface.
+     * Typically, deleted after the cross connection.
+     */
+    boolean deleteInterface(String nodeId, String interfaceId);
+}
\ No newline at end of file
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/DeleteService.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/DeleteService.java
new file mode 100644 (file)
index 0000000..62af4bf
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.transportpce.common.crossconnect.CrossConnect;
+import org.opendaylight.transportpce.common.openroadminterfaces.OpenRoadmInterfaceException;
+import org.opendaylight.transportpce.common.openroadminterfaces.OpenRoadmInterfaces;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DeleteService implements Delete {
+
+    private final CrossConnect crossConnect;
+    private final OpenRoadmInterfaces openRoadmInterfaces;
+
+    private final Subscriber subscriber;
+
+    private static final Logger LOG = LoggerFactory.getLogger(DeleteService.class);
+
+    public DeleteService(
+            CrossConnect crossConnect,
+            OpenRoadmInterfaces openRoadmInterfaces,
+            Subscriber subscriber) {
+        this.crossConnect = crossConnect;
+        this.openRoadmInterfaces = openRoadmInterfaces;
+        this.subscriber = subscriber;
+    }
+
+    @Override
+    public @NonNull List<String> deleteCrossConnect(String deviceId, String connectionNumber,
+                                                    boolean isOtn) {
+        List<String> result = crossConnect.deleteCrossConnect(deviceId, connectionNumber, isOtn);
+
+        if (result == null) {
+            subscriber.result(false, deviceId, connectionNumber);
+            return new ArrayList<>();
+        }
+
+        subscriber.result(true, deviceId, connectionNumber);
+
+        return result;
+    }
+
+    @Override
+    public boolean deleteInterface(String nodeId, String interfaceId) {
+        try {
+            openRoadmInterfaces.deleteInterface(nodeId, interfaceId);
+
+            subscriber.result(true, nodeId, interfaceId);
+            return true;
+        } catch (OpenRoadmInterfaceException e) {
+            LOG.error("Failed rolling back {} {}", nodeId, interfaceId);
+            subscriber.result(false, nodeId, interfaceId);
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/DeleteSubscriber.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/DeleteSubscriber.java
new file mode 100644 (file)
index 0000000..886b459
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete;
+
+public class DeleteSubscriber implements Subscriber {
+
+    private final Result result;
+
+    public DeleteSubscriber(Result result) {
+        this.result = result;
+    }
+
+    @Override
+    public void result(Boolean success, String nodeId, String interfaceId) {
+
+        result.add(success, nodeId, interfaceId);
+
+    }
+
+}
\ No newline at end of file
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/FailedRollbackResult.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/FailedRollbackResult.java
new file mode 100644 (file)
index 0000000..5637879
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.RendererRollbackOutput;
+import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.RendererRollbackOutputBuilder;
+import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.renderer.rollback.output.FailedToRollback;
+import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.renderer.rollback.output.FailedToRollbackBuilder;
+import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.renderer.rollback.output.FailedToRollbackKey;
+
+public class FailedRollbackResult implements Result {
+
+    private final Map<String, Set<String>> failedRollback = Collections.synchronizedMap(
+            new HashMap<>());
+
+    @Override
+    public boolean add(boolean success, String nodeId, String interfaceId) {
+
+        if (success) {
+            return false;
+        }
+
+        if (!failedRollback.containsKey(nodeId)) {
+            failedRollback.put(nodeId, new LinkedHashSet<>());
+        }
+
+        return failedRollback.get(nodeId).add(interfaceId);
+    }
+
+    @Override
+    public RendererRollbackOutput renderRollbackOutput() {
+
+        Map<FailedToRollbackKey, FailedToRollback> failedToRollbackList = new HashMap<>();
+
+        for (Entry<String, Set<String>> entry : failedRollback.entrySet()) {
+
+            FailedToRollback failedToRollack = new FailedToRollbackBuilder()
+                    .withKey(new FailedToRollbackKey(entry.getKey()))
+                    .setNodeId(entry.getKey())
+                    .setInterface(entry.getValue())
+                    .build();
+
+            failedToRollbackList.put(failedToRollack.key(), failedToRollack);
+
+        }
+
+        return new RendererRollbackOutputBuilder()
+                .setSuccess(failedRollback.isEmpty())
+                .setFailedToRollback(failedToRollbackList)
+                .build();
+
+    }
+
+}
\ No newline at end of file
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/Result.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/Result.java
new file mode 100644 (file)
index 0000000..fb77027
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete;
+
+import org.opendaylight.yang.gen.v1.http.org.opendaylight.transportpce.device.renderer.rev211004.RendererRollbackOutput;
+
+public interface Result {
+
+    boolean add(boolean success, String nodeId, String interfaceId);
+
+    RendererRollbackOutput renderRollbackOutput();
+
+}
\ No newline at end of file
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/Subscriber.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/delete/Subscriber.java
new file mode 100644 (file)
index 0000000..8e83d16
--- /dev/null
@@ -0,0 +1,15 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete;
+
+public interface Subscriber {
+
+    void result(Boolean success, String nodeId, String interfaceId);
+
+}
\ No newline at end of file
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/History.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/History.java
new file mode 100644 (file)
index 0000000..fd2646a
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction.history;
+
+import java.util.List;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.Transaction;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.Delete;
+
+public interface History {
+
+    /**
+     * Add transaction.
+     *
+     * <p>
+     * Only accepts the transaction if this History
+     * object doesn't already contain the object.
+     *
+     * @return true if the transaction was added.
+     */
+    boolean add(Transaction transaction);
+
+    /**
+     * A list of transactions.
+     *
+     * <p>
+     * Will only accept unique transactions.
+     * @return true if all transactions was added. false if one or more transactions was rejected.
+     */
+    boolean add(List<Transaction> transactions);
+
+    /**
+     * Add an array of interface transactions.
+     *
+     * <p>
+     * Duplicate interface ids, null or empty strings
+     * are silently ignored.
+     * @return may return false
+     */
+    boolean addInterfaces(String nodeId, String interfaceId);
+
+    /**
+     * Add an array of interface transactions.
+     *
+     * <p>
+     * Duplicate interface ids, null or empty strings
+     * are silently ignored.
+     * @return may return false
+     */
+    boolean addInterfaces(String nodeId, String[] interfaceIds);
+
+    /**
+     * Add a list of interface transactions.
+     *
+     * <p>
+     * Duplicate interface ids, null or empty strings
+     * are silently ignored.
+     */
+    boolean addInterfaces(String nodeId, List<String> interfaceIds);
+
+    /**
+     * Rollback all transactions.
+     *
+     * <p>
+     * Typically, the transactions are rolled back in reverse
+     * order, but the implementing class may choose a different
+     * logic.
+     */
+    boolean rollback(Delete delete);
+
+}
\ No newline at end of file
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/NonStickHistoryMemory.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/NonStickHistoryMemory.java
new file mode 100644 (file)
index 0000000..71d8f74
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction.history;
+
+import java.util.List;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.Transaction;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.Delete;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Goldfish implementation of the History interface.
+ *
+ * <p>
+ * This implementation simply doesn't track anything.
+ * Most useful for backwards compatibility reasons.
+ */
+public class NonStickHistoryMemory implements History {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NonStickHistoryMemory.class);
+
+    @Override
+    public boolean add(Transaction transaction) {
+        LOG.warn("Transaction history disabled. Ignoring '{}'.", transaction.description());
+        return false;
+    }
+
+    @Override
+    public boolean add(List<Transaction> transactions) {
+        LOG.warn("Transaction history disabled. No rollback executed.");
+        return false;
+    }
+
+    @Override
+    public boolean addInterfaces(String nodeId, String interfaceId) {
+        LOG.warn("Transaction history disabled.");
+        return false;
+    }
+
+    @Override
+    public boolean addInterfaces(String nodeId, String[] interfaceIds) {
+        LOG.warn("Transaction history disabled.");
+        return false;
+    }
+
+    @Override
+    public boolean addInterfaces(String nodeId, List<String> interfaceIds) {
+        LOG.warn("Transaction history disabled.");
+        return false;
+    }
+
+    @Override
+    public boolean rollback(Delete delete) {
+        LOG.warn("Transaction history disabled. No rollback executed.");
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/TransactionHistory.java b/renderer/src/main/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/TransactionHistory.java
new file mode 100644 (file)
index 0000000..a1565fc
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction.history;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.DeviceInterface;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.Transaction;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.Delete;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A class keeping track of transaction history.
+ *
+ * <p>
+ * A transaction can be something like an interface or a roadm connection, that may need to be
+ * rolled back in the future.
+ */
+public class TransactionHistory implements History {
+
+    private static final Logger LOG = LoggerFactory.getLogger(TransactionHistory.class);
+    Set<Transaction> transactionHistory = Collections.synchronizedSet(new LinkedHashSet<>());
+
+    @Override
+    public boolean add(Transaction transaction) {
+
+        boolean result = transactionHistory.add(transaction);
+
+        if (result) {
+            LOG.info("Adding {}", transaction.description());
+        } else {
+            LOG.warn("Transaction {} not added.", transaction.description());
+        }
+
+        return result;
+    }
+
+    @Override
+    public boolean add(List<Transaction> transactions) {
+        Set<Boolean> results = new HashSet<>(transactions.size());
+
+        for (Transaction transaction : transactions) {
+            results.add(add(transaction));
+        }
+
+        return results.stream().allMatch(i -> (i.equals(Boolean.TRUE)));
+    }
+
+    @Override
+    public boolean addInterfaces(String nodeId, String interfaceId) {
+        return addInterfaces(nodeId, Collections.singletonList(interfaceId));
+    }
+
+    @Override
+    public boolean addInterfaces(String nodeId, String[] interfaceIds) {
+
+        return addInterfaces(nodeId, Arrays.asList(interfaceIds));
+
+    }
+
+    @Override
+    public boolean addInterfaces(String nodeId, List<String> interfaceIds) {
+
+        Set<Boolean> results = new HashSet<>();
+        Set<String> unique = new LinkedHashSet<>();
+
+        for (String interfaceId : interfaceIds) {
+            if (interfaceId != null && !interfaceId.trim().isEmpty()) {
+                unique.add(interfaceId.trim());
+            }
+        }
+
+        for (String interfaceId : unique) {
+            results.add(this.add(new DeviceInterface(nodeId, interfaceId)));
+        }
+
+        return results.stream().allMatch(i -> (i.equals(Boolean.TRUE)));
+
+    }
+
+    @Override
+    public boolean rollback(Delete delete) {
+
+        LOG.info("History contains {} items. Rolling them back in reverse order.",
+                transactionHistory.size());
+
+        List<Transaction> reverse = new ArrayList<>(transactionHistory);
+
+        Collections.reverse(reverse);
+
+        boolean success = true;
+
+        for (Transaction transaction : reverse) {
+            LOG.info("Rolling back {}", transaction.description());
+            if (!transaction.rollback(delete)) {
+                success = false;
+            }
+        }
+
+        return success;
+
+    }
+}
\ No newline at end of file
index 63e347868f6cf6dd6ae3fb91136a1007e0772fbc..fe1973618c1953c37a4386b1d65ac46f92c51d9b 100644 (file)
@@ -101,7 +101,7 @@ public class RendererServiceOperationsImplTest extends AbstractTest {
             .buildServiceImplementationRequestInputTerminationPointResource(StringConstants.TTP_TOKEN);
         ServicePathOutputBuilder mockOutputBuilder = new ServicePathOutputBuilder().setResult("success")
             .setSuccess(true);
-        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any());
+        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any(), any());
         ServiceImplementationRequestOutput result =
                 this.rendererServiceOperations.serviceImplementation(input, false).get();
         assertEquals(ResponseCodes.RESPONSE_OK, result.getConfigurationResponseCommon().getResponseCode());
@@ -113,7 +113,7 @@ public class RendererServiceOperationsImplTest extends AbstractTest {
             .buildServiceImplementationRequestInputTerminationPointResource(StringConstants.TTP_TOKEN);
         ServicePathOutputBuilder mockOutputBuilder = new ServicePathOutputBuilder().setResult("success")
             .setSuccess(true);
-        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any());
+        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any(), any());
         doReturn(RpcResultBuilder.failed().buildFuture()).when(this.olmService).servicePowerSetup(any());
         ServiceImplementationRequestOutput result =
                 this.rendererServiceOperations.serviceImplementation(input, false).get();
@@ -126,7 +126,7 @@ public class RendererServiceOperationsImplTest extends AbstractTest {
             .buildServiceImplementationRequestInputTerminationPointResource(StringConstants.PP_TOKEN);
         ServicePathOutputBuilder mockOutputBuilder = new ServicePathOutputBuilder().setResult("success")
             .setSuccess(true);
-        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any());
+        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any(), any());
         ServiceImplementationRequestOutput result = this.rendererServiceOperations.serviceImplementation(input,
                 false).get();
         assertEquals(ResponseCodes.RESPONSE_OK, result.getConfigurationResponseCommon().getResponseCode());
@@ -138,7 +138,7 @@ public class RendererServiceOperationsImplTest extends AbstractTest {
             .buildServiceImplementationRequestInputTerminationPointResource(StringConstants.NETWORK_TOKEN);
         ServicePathOutputBuilder mockOutputBuilder = new ServicePathOutputBuilder().setResult("success")
             .setSuccess(true);
-        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any());
+        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any(), any());
         ServiceImplementationRequestOutput result = this.rendererServiceOperations.serviceImplementation(input,
                 false).get();
         assertEquals(ResponseCodes.RESPONSE_OK, result.getConfigurationResponseCommon().getResponseCode());
@@ -150,7 +150,7 @@ public class RendererServiceOperationsImplTest extends AbstractTest {
             .buildServiceImplementationRequestInputTerminationPointResource(StringConstants.CLIENT_TOKEN);
         ServicePathOutputBuilder mockOutputBuilder = new ServicePathOutputBuilder().setResult("success")
             .setSuccess(true);
-        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any());
+        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any(), any());
         ServiceImplementationRequestOutput result = this.rendererServiceOperations.serviceImplementation(input,
                 false).get();
         assertEquals(ResponseCodes.RESPONSE_OK, result.getConfigurationResponseCommon().getResponseCode());
@@ -167,7 +167,7 @@ public class RendererServiceOperationsImplTest extends AbstractTest {
 
         ServicePathOutputBuilder mockOutputBuilder = new ServicePathOutputBuilder().setResult("failed")
             .setSuccess(false);
-        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any());
+        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any(), any());
 
         for (String tpToken : interfaceTokens) {
             ServiceImplementationRequestInput input = ServiceDataUtils
@@ -247,7 +247,7 @@ public class RendererServiceOperationsImplTest extends AbstractTest {
         when(this.olmService.getPm(eq(getPmInputA))).thenReturn(RpcResultBuilder.success(getPmOutput).buildFuture());
         ServicePathOutputBuilder mockOutputBuilder = new ServicePathOutputBuilder().setResult("success")
             .setSuccess(true);
-        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any());
+        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any(), any());
         ServiceImplementationRequestInput input = ServiceDataUtils
             .buildServiceImplementationRequestInputTerminationPointResource(StringConstants.NETWORK_TOKEN);
         ServiceImplementationRequestOutput result = this.rendererServiceOperations.serviceImplementation(input,
@@ -263,7 +263,7 @@ public class RendererServiceOperationsImplTest extends AbstractTest {
         when(this.olmService.getPm(any())).thenReturn(RpcResultBuilder.success(getPmOutput1).buildFuture());
         ServicePathOutputBuilder mockOutputBuilder = new ServicePathOutputBuilder().setResult("success")
             .setSuccess(true);
-        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any());
+        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any(), any());
         ServiceImplementationRequestOutput result = this.rendererServiceOperations.serviceImplementation(input,
                 false).get();
         assertEquals(ResponseCodes.RESPONSE_OK, result.getConfigurationResponseCommon().getResponseCode());
@@ -277,7 +277,7 @@ public class RendererServiceOperationsImplTest extends AbstractTest {
         when(this.olmService.getPm(any())).thenReturn(RpcResultBuilder.success(getPmOutput).buildFuture());
         ServicePathOutputBuilder mockOutputBuilder = new ServicePathOutputBuilder().setResult("success")
             .setSuccess(true);
-        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any());
+        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any(), any());
         ServiceImplementationRequestOutput result = this.rendererServiceOperations.serviceImplementation(input,
                 false).get();
         assertEquals(ResponseCodes.RESPONSE_OK, result.getConfigurationResponseCommon().getResponseCode());
@@ -298,7 +298,7 @@ public class RendererServiceOperationsImplTest extends AbstractTest {
         doReturn(RpcResultBuilder.success(getPmOutput).buildFuture()).when(this.olmService).getPm(any());
         ServicePathOutputBuilder mockOutputBuilder = new ServicePathOutputBuilder().setResult("success")
             .setSuccess(true);
-        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any());
+        doReturn(mockOutputBuilder.build()).when(this.deviceRenderer).setupServicePath(any(), any(), any());
         ServiceImplementationRequestInput input = ServiceDataUtils
             .buildServiceImplementationRequestInputTerminationPointResource(StringConstants.NETWORK_TOKEN);
         ServiceImplementationRequestOutput result = this.rendererServiceOperations.serviceImplementation(input,
diff --git a/renderer/src/test/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/ConnectionTest.java b/renderer/src/test/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/ConnectionTest.java
new file mode 100644 (file)
index 0000000..a3ba1e7
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.Delete;
+
+class ConnectionTest {
+
+    @Test
+    void rollback() {
+        Delete delete = Mockito.mock(Delete.class);
+        Mockito.when(delete.deleteCrossConnect("ROADM-A", "DEG1", false))
+                .thenReturn(List.of("Interface1"));
+
+        Connection n1 = new Connection("ROADM-A", "DEG1", false);
+
+        Assert.assertTrue(n1.rollback(delete));
+
+        Mockito.verify(delete, Mockito.times(1))
+                .deleteCrossConnect("ROADM-A", "DEG1", false);
+    }
+
+    @Test
+    void testTwoObjectsWithSameInformationIsEqual() {
+        Connection n1 = new Connection("ROADM-A", "DEG1", false);
+        Connection n2 = new Connection("ROADM-A", "DEG1", false);
+
+        Assert.assertTrue(n1.equals(n2));
+    }
+
+    @Test
+    void testTwoObjectsWithDifferentInformationIsNotEqual() {
+        Connection n1 = new Connection("ROADM-A", "DEG1", true);
+        Connection n2 = new Connection("ROADM-A", "DEG1", false);
+
+        Assert.assertFalse(n1.equals(n2));
+    }
+
+    @Test
+    void testTwoDifferentRoadmNodesAreNotEqual() {
+        Connection n1 = new Connection("ROADM-A", "DEG1", false);
+        Connection n2 = new Connection("ROADM-B", "DEG1", false);
+
+        Assert.assertFalse(n1.equals(n2));
+    }
+
+
+    @Test
+    void deleteReturnNull() {
+        Delete delete = Mockito.mock(Delete.class);
+        Mockito.when(delete.deleteCrossConnect("ROADM-A", "DEG1", false))
+                .thenReturn(null);
+
+        Connection n1 = new Connection("ROADM-A", "DEG1", false);
+
+        Assert.assertFalse(n1.rollback(delete));
+
+        Mockito.verify(delete, Mockito.times(1))
+                .deleteCrossConnect("ROADM-A", "DEG1", false);
+    }
+
+    @Test
+    void deleteReturnEmptyList() {
+        Delete delete = Mockito.mock(Delete.class);
+        Mockito.when(delete.deleteCrossConnect("ROADM-A", "DEG1", false))
+                .thenReturn(new ArrayList<>());
+
+        Connection n1 = new Connection("ROADM-A", "DEG1", false);
+
+        Assert.assertFalse(n1.rollback(delete));
+
+        Mockito.verify(delete, Mockito.times(1))
+                .deleteCrossConnect("ROADM-A", "DEG1", false);
+    }
+}
\ No newline at end of file
diff --git a/renderer/src/test/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/DeviceInterfaceTest.java b/renderer/src/test/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/DeviceInterfaceTest.java
new file mode 100644 (file)
index 0000000..d0481ae
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction;
+
+import org.junit.Assert;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.Delete;
+
+class DeviceInterfaceTest {
+
+    @Test
+    void rollback() {
+        Delete delete = Mockito.mock(Delete.class);
+        Mockito.when(delete.deleteInterface("ROADM-A", "DEG1")).thenReturn(true);
+
+        DeviceInterface n1 = new DeviceInterface("ROADM-A", "DEG1");
+        Assert.assertTrue(n1.rollback(delete));
+
+        Mockito.verify(delete, Mockito.times(1)).deleteInterface("ROADM-A", "DEG1");
+    }
+
+    @Test
+    void testTwoInterfacesAreEqual() {
+        DeviceInterface n1 = new DeviceInterface("ROADM-A", "DEG1");
+        DeviceInterface n2 = new DeviceInterface("ROADM-A", "DEG1");
+
+        Assert.assertTrue(n1.equals(n2));
+    }
+
+    @Test
+    void testTwoInterfacesAreNotEqual() {
+        DeviceInterface n1 = new DeviceInterface("ROADM-A", "DEG1");
+        DeviceInterface n2 = new DeviceInterface("ROADM-B", "DEG1");
+
+        Assert.assertFalse(n1.equals(n2));
+    }
+}
\ No newline at end of file
diff --git a/renderer/src/test/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/TransactionHistoryTest.java b/renderer/src/test/java/org/opendaylight/transportpce/renderer/provisiondevice/transaction/history/TransactionHistoryTest.java
new file mode 100644 (file)
index 0000000..0adf418
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Copyright © 2024 Smartoptics and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.transportpce.renderer.provisiondevice.transaction.history;
+
+import java.util.List;
+import org.junit.Assert;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.DeviceInterface;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.Transaction;
+import org.opendaylight.transportpce.renderer.provisiondevice.transaction.delete.Delete;
+
+class TransactionHistoryTest {
+
+    @Test
+    void add() {
+        Transaction transaction = Mockito.mock(Transaction.class);
+        History history = new TransactionHistory();
+
+        Assert.assertTrue(history.add(transaction));
+    }
+
+    @Test
+    void testDuplicateTransactionIsIgnored() {
+
+        Transaction t1 = new DeviceInterface("ROADM-A", "DEG1");
+        Transaction t2 = new DeviceInterface("ROADM-A", "DEG1");
+
+        History history = new TransactionHistory();
+
+        history.add(t1);
+        Assert.assertFalse(history.add(t2));
+    }
+
+    @Test
+    void testAddCollectionOfUniqueTransactions() {
+        Transaction t1 = new DeviceInterface("ROADM-A", "DEG1");
+        Transaction t2 = new DeviceInterface("ROADM-A", "DEG2");
+
+        List<Transaction> transactions = List.of(t1, t2);
+
+        History history = new TransactionHistory();
+
+        Assert.assertTrue(history.add(transactions));
+    }
+
+    @Test
+    void testAddCollectionOfDuplicateTransactions() {
+        Transaction t1 = new DeviceInterface("ROADM-A", "DEG1");
+        Transaction t2 = new DeviceInterface("ROADM-A", "DEG1");
+
+        List<Transaction> transactions = List.of(t1, t2);
+
+        History history = new TransactionHistory();
+
+        Assert.assertFalse(history.add(transactions));
+    }
+
+    @Test
+    void testAddUniqueStringOfInterfaceIds() {
+        String nodeId = "ROADM-A";
+        String[] interfaces = new String[]{"DEG1", "DEG2"};
+
+        History history = new TransactionHistory();
+
+        Assert.assertTrue(history.addInterfaces(nodeId, interfaces));
+    }
+
+    @Test
+    void testAddDuplicateStringOfInterfaceIds() {
+        String nodeId = "ROADM-A";
+        String[] interfaces = new String[]{"DEG1", "DEG1"};
+
+        History history = new TransactionHistory();
+
+        Assert.assertTrue(history.addInterfaces(nodeId, interfaces));
+
+    }
+
+    @Test
+    void testAddDuplicateListOfInterfaceIds() {
+        String nodeId = "ROADM-A";
+        List<String> interfaces = List.of("DEG1", "DEG1");
+
+        History history = new TransactionHistory();
+
+        Assert.assertTrue(history.addInterfaces(nodeId, interfaces));
+
+    }
+
+    @Test
+    void rollbackOneInterface() {
+
+        String nodeId = "ROADM-A";
+        List<String> interfaces = List.of("DEG1", "DEG1");
+
+        History history = new TransactionHistory();
+        history.addInterfaces(nodeId, interfaces);
+
+        Delete delete = Mockito.mock(Delete.class);
+        Mockito.when(delete.deleteInterface("ROADM-A", "DEG1")).thenReturn(true);
+
+        Assert.assertTrue(history.rollback(delete));
+
+        //Although the same interface was added twice, we only rollback once.
+        Mockito.verify(delete, Mockito.times(1))
+                .deleteInterface("ROADM-A", "DEG1");
+    }
+
+    @Test
+    void rollbackTwoInterfacesInReverseOrderTheyWereAdded() {
+
+        String nodeId = "ROADM-A";
+
+        //Note DEG1 is added before DEG2
+        List<String> interfaces = List.of("DEG1", "DEG2");
+
+        History history = new TransactionHistory();
+        history.addInterfaces(nodeId, interfaces);
+
+        Delete delete = Mockito.mock(Delete.class);
+        Mockito.when(delete.deleteInterface("ROADM-A", "DEG1")).thenReturn(true);
+        Mockito.when(delete.deleteInterface("ROADM-A", "DEG2")).thenReturn(true);
+
+        Assert.assertTrue(history.rollback(delete));
+
+        //The rollback occurs in the reverse order.
+        // i.e. DEG2 before DEG1.
+        InOrder inOrder = Mockito.inOrder(delete);
+        inOrder.verify(delete, Mockito.times(1))
+                .deleteInterface("ROADM-A", "DEG2");
+        inOrder.verify(delete, Mockito.times(1))
+                .deleteInterface("ROADM-A", "DEG1");
+
+    }
+}
\ No newline at end of file