ONOS-6613 Non-disruptive intent reallocation

Change-Id: I5d051c20402a226ad540b8bc08695b602ff75273
diff --git a/core/net/src/main/java/org/onosproject/net/intent/impl/installer/FlowRuleIntentInstaller.java b/core/net/src/main/java/org/onosproject/net/intent/impl/installer/FlowRuleIntentInstaller.java
index 8217255..d96de2f 100644
--- a/core/net/src/main/java/org/onosproject/net/intent/impl/installer/FlowRuleIntentInstaller.java
+++ b/core/net/src/main/java/org/onosproject/net/intent/impl/installer/FlowRuleIntentInstaller.java
@@ -16,34 +16,57 @@
 
 package org.onosproject.net.intent.impl.installer;
 
+import com.google.common.collect.Lists;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
+import org.apache.felix.scr.annotations.Property;
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.core.DefaultApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.NetworkResource;
+import org.onosproject.net.flow.DefaultFlowRule;
 import org.onosproject.net.flow.FlowRule;
 import org.onosproject.net.flow.FlowRuleOperations;
 import org.onosproject.net.flow.FlowRuleOperationsContext;
 import org.onosproject.net.flow.FlowRuleService;
 import org.onosproject.net.intent.FlowRuleIntent;
-import org.onosproject.net.intent.IntentInstallCoordinator;
+import org.onosproject.net.intent.Intent;
 import org.onosproject.net.intent.IntentData;
 import org.onosproject.net.intent.IntentExtensionService;
-import org.onosproject.net.intent.IntentOperationContext;
+import org.onosproject.net.intent.IntentInstallCoordinator;
 import org.onosproject.net.intent.IntentInstaller;
-import org.onosproject.net.intent.impl.IntentManager;
+import org.onosproject.net.intent.IntentOperationContext;
+import org.onosproject.net.intent.IntentStore;
 import org.onosproject.net.intent.ObjectiveTrackerService;
+import org.onosproject.net.intent.impl.IntentManager;
+import org.osgi.service.component.ComponentContext;
 import org.slf4j.Logger;
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
+import static org.onlab.util.Tools.groupedThreads;
 import static org.onosproject.net.intent.IntentInstaller.Direction.ADD;
 import static org.onosproject.net.intent.IntentInstaller.Direction.REMOVE;
+import static org.onosproject.net.intent.IntentState.INSTALLED;
+import static org.onosproject.net.intent.IntentState.REALLOCATING;
+import static org.onosproject.net.intent.constraint.NonDisruptiveConstraint.requireNonDisruptive;
 import static org.slf4j.LoggerFactory.getLogger;
 
 /**
@@ -63,23 +86,71 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected FlowRuleService flowRuleService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ComponentConfigService configService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected IntentStore store;
+
+    private ScheduledExecutorService nonDisruptiveIntentInstaller;
+
+    private static final int DEFAULT_NON_DISRUPTIVE_INSTALLATION_WAITING_TIME = 1;
+    @Property(name = "nonDisruptiveInstallationWaitingTime",
+            intValue = DEFAULT_NON_DISRUPTIVE_INSTALLATION_WAITING_TIME,
+            label = "Number of seconds to wait during the non-disruptive installation phases")
+    private int nonDisruptiveInstallationWaitingTime = DEFAULT_NON_DISRUPTIVE_INSTALLATION_WAITING_TIME;
+
+    protected final Logger log = getLogger(IntentManager.class);
+
+    private boolean isReallocationStageFailed = false;
+
+    private static final LinkComparator LINK_COMPARATOR = new LinkComparator();
+
     @Activate
     public void activate() {
         intentExtensionService.registerInstaller(FlowRuleIntent.class, this);
+        nonDisruptiveIntentInstaller =
+                newSingleThreadScheduledExecutor(groupedThreads("onos/intent", "non-disruptive-installer", log));
+        configService.registerProperties(getClass());
     }
 
     @Deactivate
     public void deactivated() {
         intentExtensionService.unregisterInstaller(FlowRuleIntent.class);
+        configService.unregisterProperties(getClass(), false);
     }
 
-    protected final Logger log = getLogger(IntentManager.class);
+    @Modified
+    public void modified(ComponentContext context) {
+
+        if (context == null) {
+            nonDisruptiveInstallationWaitingTime = DEFAULT_NON_DISRUPTIVE_INSTALLATION_WAITING_TIME;
+            log.info("Restored default installation time for non-disruptive reallocation (1 sec.)");
+            return;
+        }
+
+        String s = Tools.get(context.getProperties(), "nonDisruptiveInstallationWaitingTime");
+        int nonDisruptiveTime = isNullOrEmpty(s) ? nonDisruptiveInstallationWaitingTime : Integer.parseInt(s);
+        if (nonDisruptiveTime != nonDisruptiveInstallationWaitingTime) {
+            nonDisruptiveInstallationWaitingTime = nonDisruptiveTime;
+            log.info("Reconfigured non-disruptive reallocation with installation delay {} sec.",
+                     nonDisruptiveInstallationWaitingTime);
+        }
+    }
 
     @Override
     public void apply(IntentOperationContext<FlowRuleIntent> context) {
         Optional<IntentData> toUninstall = context.toUninstall();
         Optional<IntentData> toInstall = context.toInstall();
 
+        if (toInstall.isPresent() && toUninstall.isPresent()) {
+            Intent intentToInstall = toInstall.get().intent();
+            if (requireNonDisruptive(intentToInstall) && INSTALLED.equals(toUninstall.get().state())) {
+                reallocate(context);
+                return;
+            }
+        }
+
         if (!toInstall.isPresent() && !toUninstall.isPresent()) {
             // Nothing to do.
             intentInstallCoordinator.intentInstallSuccess(context);
@@ -176,6 +247,307 @@
         flowRuleService.apply(operations);
     }
 
+    private void reallocate(IntentOperationContext<FlowRuleIntent> context) {
+
+        Optional<IntentData> toUninstall = context.toUninstall();
+        Optional<IntentData> toInstall = context.toInstall();
+
+        //TODO: Update the Intent store with this information
+        toInstall.get().setState(REALLOCATING);
+
+        store.write(toInstall.get());
+
+        List<FlowRuleIntent> uninstallIntents = Lists.newArrayList(context.intentsToUninstall());
+        List<FlowRuleIntent> installIntents = Lists.newArrayList(context.intentsToInstall());
+        FlowRuleOperations.Builder firstStageOperationsBuilder = FlowRuleOperations.builder();
+        List<FlowRule> secondStageFlowRules = Lists.newArrayList();
+        FlowRuleOperations.Builder thirdStageOperationsBuilder = FlowRuleOperations.builder();
+        FlowRuleOperations.Builder finalStageOperationsBuilder = FlowRuleOperations.builder();
+
+        prepareReallocation(uninstallIntents, installIntents,
+                            firstStageOperationsBuilder, secondStageFlowRules,
+                            thirdStageOperationsBuilder, finalStageOperationsBuilder);
+
+        trackIntentResources(toUninstall.get(), uninstallIntents, REMOVE);
+        trackIntentResources(toInstall.get(), installIntents, ADD);
+
+        CountDownLatch stageCompleteLatch = new CountDownLatch(1);
+
+        FlowRuleOperations firstStageOperations = firstStageOperationsBuilder
+                .build(new StageOperation(context, stageCompleteLatch));
+
+        flowRuleService.apply(firstStageOperations);
+
+        try {
+            stageCompleteLatch.await(nonDisruptiveInstallationWaitingTime, TimeUnit.SECONDS);
+            if (isReallocationStageFailed) {
+                log.error("Reallocation FAILED in stage one: the following FlowRuleOperations are not executed {}",
+                          firstStageOperations);
+                return;
+            } else {
+                log.debug("Reallocation stage one completed");
+            }
+        } catch (Exception e) {
+            log.warn("Latch exception in the reallocation stage one");
+        }
+
+        for (FlowRule flowRule : secondStageFlowRules) {
+            stageCompleteLatch = new CountDownLatch(1);
+            FlowRuleOperations operations = FlowRuleOperations.builder()
+                    .newStage()
+                    .remove(flowRule)
+                    .build(new StageOperation(context, stageCompleteLatch));
+            nonDisruptiveIntentInstaller.schedule(new NonDisruptiveInstallation(operations),
+                                                  nonDisruptiveInstallationWaitingTime,
+                                                  TimeUnit.SECONDS);
+            try {
+                stageCompleteLatch.await(nonDisruptiveInstallationWaitingTime, TimeUnit.SECONDS);
+                if (isReallocationStageFailed) {
+                    log.error("Reallocation FAILED in stage two: " +
+                                      "the following FlowRuleOperations are not executed {}",
+                              operations);
+                    return;
+                } else {
+                    log.debug("Reallocation stage two completed");
+                }
+            } catch (Exception e) {
+                log.warn("Latch exception in the reallocation stage two");
+            }
+        }
+
+        stageCompleteLatch = new CountDownLatch(1);
+        FlowRuleOperations thirdStageOperations = thirdStageOperationsBuilder
+                .build(new StageOperation(context, stageCompleteLatch));
+
+        nonDisruptiveIntentInstaller.schedule(new NonDisruptiveInstallation(thirdStageOperations),
+                                              nonDisruptiveInstallationWaitingTime,
+                                              TimeUnit.SECONDS);
+        try {
+            stageCompleteLatch.await(nonDisruptiveInstallationWaitingTime, TimeUnit.SECONDS);
+            if (isReallocationStageFailed) {
+                log.error("Reallocation FAILED in stage three: " +
+                                  "the following FlowRuleOperations are not executed {}",
+                          thirdStageOperations);
+                return;
+            } else {
+                log.debug("Reallocation stage three completed");
+            }
+        } catch (Exception e) {
+            log.warn("Latch exception in the reallocation stage three");
+        }
+
+        FlowRuleOperationsContext flowRuleOperationsContext = new FlowRuleOperationsContext() {
+            @Override
+            public void onSuccess(FlowRuleOperations ops) {
+                intentInstallCoordinator.intentInstallSuccess(context);
+                log.info("Non-disruptive reallocation completed for intent {}", toInstall.get().key());
+            }
+
+            @Override
+            public void onError(FlowRuleOperations ops) {
+                intentInstallCoordinator.intentInstallFailed(context);
+            }
+        };
+
+        FlowRuleOperations finalStageOperations = finalStageOperationsBuilder.build(flowRuleOperationsContext);
+        flowRuleService.apply(finalStageOperations);
+    }
+
+    /**
+     * This method prepares the {@link FlowRule} required for every reallocation stage.
+     *     <p>Stage 1: the FlowRules of the new path are installed,
+     *     with a lower priority only on the devices shared with the old path;</p>
+     *     <p>Stage 2: the FlowRules of the old path are removed from the ingress to the egress points,
+     *     only in the shared devices;</p>
+     *     <p>Stage 3: the FlowRules with a lower priority are restored to the original one;</p>
+     *     <p>Stage 4: the remaining FlowRules of the old path are deleted.</p>
+     *
+     * @param uninstallIntents the previous FlowRuleIntent
+     * @param installIntents the new FlowRuleIntent to be installed
+     * @param firstStageBuilder the first stage operation builder
+     * @param secondStageFlowRules the second stage FlowRules
+     * @param thirdStageBuilder the third stage operation builder
+     * @param finalStageBuilder the last stage operation builder
+     */
+    private void prepareReallocation(List<FlowRuleIntent> uninstallIntents, List<FlowRuleIntent> installIntents,
+                                     FlowRuleOperations.Builder firstStageBuilder,
+                                     List<FlowRule> secondStageFlowRules,
+                                     FlowRuleOperations.Builder thirdStageBuilder,
+                                     FlowRuleOperations.Builder finalStageBuilder) {
+
+
+        // Filter out same intents and intents with same flow rules
+        installIntents.forEach(installIntent -> {
+            uninstallIntents.forEach(uninstallIntent -> {
+
+                List<FlowRule> uninstallFlowRules = Lists.newArrayList(uninstallIntent.flowRules());
+                List<FlowRule> installFlowRules = Lists.newArrayList(installIntent.flowRules());
+
+                List<FlowRule> secondStageRules = Lists.newArrayList();
+                List<FlowRule> thirdStageRules = Lists.newArrayList();
+
+                List<DeviceId> orderedDeviceList = createIngressToEgressDeviceList(installIntent.resources());
+
+                uninstallIntent.flowRules().forEach(flowRuleToUnistall -> {
+                    installIntent.flowRules().forEach(flowRuleToInstall -> {
+
+                        if (flowRuleToInstall.exactMatch(flowRuleToUnistall)) {
+                            //The FlowRules are in common (i.e., we are sharing the path)
+                            uninstallFlowRules.remove(flowRuleToInstall);
+                            installFlowRules.remove(flowRuleToInstall);
+                        } else if (flowRuleToInstall.deviceId().equals(flowRuleToUnistall.deviceId())) {
+                            //FlowRules that have a device in common but
+                            // different treatment/selector (i.e., overlapping path)
+                            FlowRule flowRuleWithLowerPriority = DefaultFlowRule.builder()
+                                    .withPriority(flowRuleToInstall.priority() - 1)
+                                    .withSelector(flowRuleToInstall.selector())
+                                    .forDevice(flowRuleToInstall.deviceId())
+                                    .makePermanent()
+                                    .withTreatment(flowRuleToInstall.treatment())
+                                    .fromApp(new DefaultApplicationId(flowRuleToInstall.appId(),
+                                                                      "org.onosproject.net.intent"))
+                                    .build();
+
+                            //Update the FlowRule to be installed with one with a lower priority
+                            installFlowRules.remove(flowRuleToInstall);
+                            installFlowRules.add(flowRuleWithLowerPriority);
+
+                            //Add the FlowRule to be uninstalled to the second stage of non-disruptive update
+                            secondStageRules.add(flowRuleToUnistall);
+                            uninstallFlowRules.remove(flowRuleToUnistall);
+
+                            thirdStageRules.add(flowRuleToInstall);
+                            uninstallFlowRules.add(flowRuleWithLowerPriority);
+                        }
+                    });
+                });
+
+                firstStageBuilder.newStage();
+                installFlowRules.forEach(firstStageBuilder::add);
+
+                Collections.sort(secondStageRules, new SecondStageComparator(orderedDeviceList));
+                secondStageFlowRules.addAll(secondStageRules);
+
+                thirdStageBuilder.newStage();
+                thirdStageRules.forEach(thirdStageBuilder::add);
+
+                finalStageBuilder.newStage();
+                uninstallFlowRules.forEach(finalStageBuilder::remove);
+            });
+        });
+
+    }
+
+    private class StageOperation implements FlowRuleOperationsContext {
+
+        private IntentOperationContext<FlowRuleIntent> context;
+        private CountDownLatch stageCompleteLatch;
+
+        public StageOperation(IntentOperationContext<FlowRuleIntent> context, CountDownLatch stageCompleteLatch) {
+            this.context = context;
+            this.stageCompleteLatch = stageCompleteLatch;
+            isReallocationStageFailed = false;
+        }
+
+        @Override
+        public void onSuccess(FlowRuleOperations ops) {
+            stageCompleteLatch.countDown();
+            log.debug("FlowRuleOperations correctly completed");
+        }
+
+        @Override
+        public void onError(FlowRuleOperations ops) {
+            intentInstallCoordinator.intentInstallFailed(context);
+            isReallocationStageFailed = true;
+            stageCompleteLatch.countDown();
+            log.debug("Installation error for {}", ops);
+        }
+    }
+
+    private final class SecondStageComparator implements Comparator<FlowRule> {
+
+        private List<DeviceId> deviceIds;
+
+        private SecondStageComparator(List<DeviceId> deviceIds) {
+            this.deviceIds = deviceIds;
+        }
+
+        @Override
+        public int compare(FlowRule o1, FlowRule o2) {
+            Integer index1 = deviceIds.indexOf(o1.deviceId());
+            Integer index2 = deviceIds.indexOf(o2.deviceId());
+            return index1.compareTo(index2);
+        }
+    }
+
+    /**
+     * Create a list of devices ordered from the ingress to the egress of a path.
+     * @param resources the resources of the intent
+     * @return a list of devices
+     */
+    private List<DeviceId> createIngressToEgressDeviceList(Collection<NetworkResource> resources) {
+        List<DeviceId> deviceIds = Lists.newArrayList();
+        List<Link> links = Lists.newArrayList();
+
+        for (NetworkResource resource : resources) {
+            if (resource instanceof Link) {
+                Link linkToAdd = (Link) resource;
+                if (linkToAdd.type() != Link.Type.EDGE) {
+                    links.add(linkToAdd);
+                }
+            }
+        }
+
+        Collections.sort(links, LINK_COMPARATOR);
+
+        int i = 0;
+        for (Link orderedLink : links) {
+            deviceIds.add(orderedLink.src().deviceId());
+            if (i == resources.size() - 1) {
+                deviceIds.add(orderedLink.dst().deviceId());
+            }
+            i++;
+        }
+
+        return deviceIds;
+    }
+
+    /**
+     * Compares two links in order to find which one is before or after the other.
+     */
+    private static class LinkComparator implements Comparator<Link> {
+
+        @Override
+        public int compare(Link l1, Link l2) {
+
+            //l1 is before l2
+            if (l1.dst().deviceId() == l2.src().deviceId()) {
+                return -1;
+            }
+
+            //l1 is after l2
+            if (l1.src().deviceId() == l2.dst().deviceId()) {
+                return 1;
+            }
+
+            //l2 and l1 are not connected to a common device
+            return 0;
+        }
+    }
+
+    private final class NonDisruptiveInstallation implements Runnable {
+
+        private FlowRuleOperations op;
+
+        private NonDisruptiveInstallation(FlowRuleOperations op) {
+            this.op = op;
+        }
+        @Override
+        public void run() {
+            flowRuleService.apply(this.op);
+        }
+    }
+
     /**
      * Track or un-track network resource of a Intent and it's installable
      * Intents.