From 06f51633e19d6f7bb7302b45f00a830254ec574f Mon Sep 17 00:00:00 2001 From: Yuliia Miroshnychenko Date: Thu, 21 Nov 2024 17:26:22 +0100 Subject: [PATCH] [TEST]: Isl/Switch maintenance mode: Y-Flow evacuation --- .../spec/links/LinkMaintenanceSpec.groovy | 100 +++++++++++++- .../switches/SwitchMaintenanceSpec.groovy | 124 +++++++++++++++--- .../model/topology/TopologyDefinition.java | 11 +- 3 files changed, 208 insertions(+), 27 deletions(-) diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/links/LinkMaintenanceSpec.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/links/LinkMaintenanceSpec.groovy index 2445d103ba..92dccb736a 100644 --- a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/links/LinkMaintenanceSpec.groovy +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/links/LinkMaintenanceSpec.groovy @@ -2,6 +2,7 @@ package org.openkilda.functionaltests.spec.links import static org.openkilda.functionaltests.extension.tags.Tag.ISL_RECOVER_ON_FAIL import static org.openkilda.functionaltests.extension.tags.Tag.SMOKE +import static org.openkilda.functionaltests.helpers.Wrappers.timedLoop import static org.openkilda.testing.Constants.DEFAULT_COST import static org.openkilda.testing.Constants.PATH_INSTALLATION_TIME import static org.openkilda.testing.Constants.WAIT_OFFSET @@ -11,6 +12,8 @@ import org.openkilda.functionaltests.extension.tags.Tags import org.openkilda.functionaltests.helpers.Wrappers import org.openkilda.functionaltests.helpers.factory.FlowFactory import org.openkilda.functionaltests.helpers.model.FlowEntityPath +import org.openkilda.functionaltests.helpers.model.FlowWithSubFlowsEntityPath +import org.openkilda.functionaltests.helpers.factory.YFlowFactory import org.openkilda.messaging.payload.flow.FlowState import org.springframework.beans.factory.annotation.Autowired @@ -22,6 +25,10 @@ class LinkMaintenanceSpec extends HealthCheckSpecification { @Shared FlowFactory flowFactory + @Autowired + @Shared + YFlowFactory yFlowFactory + @Tags(SMOKE) def "Maintenance mode can be set/unset for a particular link"() { given: "An active link" @@ -65,8 +72,10 @@ class LinkMaintenanceSpec extends HealthCheckSpecification { islHelper.setLinkMaintenance(isl, true, false) then: "Flows are not evacuated (rerouted) and have the same paths" - flow1.retrieveAllEntityPaths() == flow1Path - flow2.retrieveAllEntityPaths() == flow2Path + timedLoop(3) { + assert flow1.retrieveAllEntityPaths() == flow1Path + assert flow2.retrieveAllEntityPaths() == flow2Path + } when: "Set maintenance mode again with flows evacuation flag for the same link" northbound.setLinkMaintenance(islUtils.toLinkUnderMaintenance(isl, true, true)) @@ -88,6 +97,91 @@ class LinkMaintenanceSpec extends HealthCheckSpecification { !flow2PathUpdated.getInvolvedIsls().contains(isl) } + def "Y-Flows can be evacuated (rerouted) from a particular link when setting maintenance mode for it"() { + given: "Switch triplet with two possible paths at least for non-neighbouring switches" + def swTriplet = switchTriplets.all().nonNeighbouring().withAtLeastNNonOverlappingPaths(2).random() + + and: "Create Y-Flows going through selected switch triplet" + def yFlow1 = yFlowFactory.getRandom(swTriplet, false) + def yFlow1Path = yFlow1.retrieveAllEntityPaths() + + def yFlow2 = yFlowFactory.getRandom(swTriplet, false, yFlow1.occupiedEndpoints()) + def yFlow2Path = yFlow2.retrieveAllEntityPaths() + assert yFlow1Path.getInvolvedIsls().sort() == yFlow2Path.getInvolvedIsls().sort() + + when: "Set maintenance mode without flows evacuation flag for the first link involved in flow paths" + def isl = yFlow1Path.getInvolvedIsls().first() + islHelper.setLinkMaintenance(isl, true, false) + + then: "Y-Flows are not evacuated (rerouted) and have the same paths" + timedLoop(3) { + assert yFlow1.retrieveAllEntityPaths() == yFlow1Path + assert yFlow2.retrieveAllEntityPaths() == yFlow2Path + } + + when: "Set maintenance mode again with flows evacuation flag for the same link" + northbound.setLinkMaintenance(islUtils.toLinkUnderMaintenance(isl, true, true)) + + then: "Y-Flows are evacuated (rerouted) and link under maintenance is not involved in new flow paths" + FlowWithSubFlowsEntityPath yFlow1PathUpdated, yFlow2PathUpdated + Wrappers.wait(PATH_INSTALLATION_TIME + WAIT_OFFSET) { + [yFlow1, yFlow2].each { flow -> assert flow.retrieveDetails().status == FlowState.UP } + yFlow1PathUpdated = yFlow1.retrieveAllEntityPaths() + yFlow2PathUpdated = yFlow2.retrieveAllEntityPaths() + + assert yFlow1PathUpdated != yFlow1Path + assert yFlow2PathUpdated != yFlow2Path + } + + and: "Link under maintenance is not involved in new Y-Flow paths" + !yFlow1PathUpdated.getInvolvedIsls().contains(isl) + !yFlow2PathUpdated.getInvolvedIsls().contains(isl) + } + + @Tags(SMOKE) + def "Both Y-Flow and Flow can be evacuated (rerouted) from a particular link when setting maintenance mode for it"() { + given: "Switch triplet with active switches" + def swTriplet = switchTriplets.all().withSharedEpEp1Ep2InChain().random() + + and: "Create Y-Flows going through selected switch triplet" + def yFlow = yFlowFactory.getRandom(swTriplet, false) + def yFlowPath = yFlow.retrieveAllEntityPaths() + def isl = yFlowPath.getInvolvedIsls().first() + + and: "Switch pair has been selected based on Y-Flow used Isl" + def switchPair = switchPairs.all().specificPair(isl.srcSwitch, isl.dstSwitch) + + and: "Create Flow going through selected switch pair" + def flow = flowFactory.getRandom(switchPair) + def flowPath = flow.retrieveAllEntityPaths() + assert flowPath.getInvolvedIsls().contains(isl) + + when: "Set maintenance mode without flows evacuation flag for the first link involved in flow paths" + islHelper.setLinkMaintenance(isl, true, false) + + then: "Both Y-Flow and Flow are not evacuated (rerouted) and have the same paths" + timedLoop(3) { + assert flow.retrieveAllEntityPaths() == flowPath + assert yFlow.retrieveAllEntityPaths() == yFlowPath + } + + when: "Set maintenance mode again with flows evacuation flag for the same link" + northbound.setLinkMaintenance(islUtils.toLinkUnderMaintenance(isl, true, true)) + + then: "Both Y-Flow and Flow are evacuated (rerouted)" + Wrappers.wait(PATH_INSTALLATION_TIME + WAIT_OFFSET) { + assert flow.retrieveFlowStatus().status == FlowState.UP + assert yFlow.retrieveDetails().status == FlowState.UP + + assert flow.retrieveAllEntityPaths() != flowPath + assert yFlow.retrieveAllEntityPaths() != yFlowPath + } + + and: "Link under maintenance is not involved in new flow paths" + !flow.retrieveAllEntityPaths().getInvolvedIsls().contains(isl) + !yFlow.retrieveAllEntityPaths().getInvolvedIsls().contains(isl) + } + @Tags(ISL_RECOVER_ON_FAIL) def "Flows are rerouted to a path with link under maintenance when there are no other paths available"() { given: "Two active not neighboring switches with two possible paths at least" @@ -118,7 +212,7 @@ class LinkMaintenanceSpec extends HealthCheckSpecification { then: "Flows are rerouted to alternative path with link under maintenance" Wrappers.wait(rerouteDelay + WAIT_OFFSET * 2) { - [flow1, flow2].each { flow -> assert flow.retrieveFlowStatus().status == FlowState.UP } + [flow1, flow2].each { flow -> assert flow.retrieveFlowStatus().status == FlowState.UP } def flow1PathUpdated = flow1.retrieveAllEntityPaths() def flow2PathUpdated = flow2.retrieveAllEntityPaths() diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/SwitchMaintenanceSpec.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/SwitchMaintenanceSpec.groovy index df7b26082a..9ccf2854f5 100644 --- a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/SwitchMaintenanceSpec.groovy +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/SwitchMaintenanceSpec.groovy @@ -3,6 +3,7 @@ package org.openkilda.functionaltests.spec.switches import static org.junit.jupiter.api.Assumptions.assumeTrue import static org.openkilda.functionaltests.extension.tags.Tag.ISL_RECOVER_ON_FAIL import static org.openkilda.functionaltests.extension.tags.Tag.SMOKE +import static org.openkilda.functionaltests.helpers.Wrappers.timedLoop import static org.openkilda.testing.Constants.DEFAULT_COST import static org.openkilda.testing.Constants.PATH_INSTALLATION_TIME import static org.openkilda.testing.Constants.WAIT_OFFSET @@ -11,8 +12,8 @@ import org.openkilda.functionaltests.HealthCheckSpecification import org.openkilda.functionaltests.extension.tags.Tags import org.openkilda.functionaltests.helpers.Wrappers import org.openkilda.functionaltests.helpers.factory.FlowFactory -import org.openkilda.functionaltests.helpers.model.FlowEntityPath import org.openkilda.functionaltests.helpers.model.Path +import org.openkilda.functionaltests.helpers.factory.YFlowFactory import org.openkilda.messaging.info.event.IslChangeType import org.openkilda.messaging.payload.flow.FlowState import org.openkilda.model.SwitchId @@ -26,6 +27,9 @@ class SwitchMaintenanceSpec extends HealthCheckSpecification { @Shared @Autowired FlowFactory flowFactory + @Shared + @Autowired + YFlowFactory yFlowFactory @Tags(SMOKE) def "Maintenance mode can be set/unset for a particular switch"() { @@ -69,54 +73,132 @@ class SwitchMaintenanceSpec extends HealthCheckSpecification { } } - @Tags(SMOKE) - def "Flows can be evacuated (rerouted) from a particular switch when setting maintenance mode for it"() { + def "Flows can be evacuated (rerouted) from a particular switch(several Isls are in use) when setting maintenance mode for it"() { given: "Two active not neighboring switches and a switch to be maintained" SwitchId swId - List pathIsls + List intermediateSwIsls + List flow1PathIsls def switchPair = switchPairs.all().nonNeighbouring().getSwitchPairs().find { List availablePath = it.retrieveAvailablePaths() - pathIsls = availablePath.find { Path aPath -> + flow1PathIsls = availablePath.find { Path aPath -> swId = aPath.getInvolvedSwitches().find { aSw -> - availablePath.findAll { it != aPath }.find { !it.getInvolvedSwitches().contains(aSw) } + intermediateSwIsls = topology.getRelatedIsls(aSw) + availablePath.findAll { it != aPath }.find { !it.getInvolvedSwitches().contains(aSw) && intermediateSwIsls.size() > 2 } } }.getInvolvedIsls() } ?: assumeTrue(false, "No suiting switches found. Need a switch pair with at least 2 paths and one of the " + "paths should not use the maintenance switch") - switchPair.retrieveAvailablePaths().collect { it.getInvolvedIsls() }.findAll { it != pathIsls } - .each { islHelper.makePathIslsMorePreferable(pathIsls, it) } + switchPair.retrieveAvailablePaths().collect { it.getInvolvedIsls() }.findAll { it != flow1PathIsls } + .each { islHelper.makePathIslsMorePreferable(flow1PathIsls, it) } - and: "Create a couple of flows going through these switches" + and: "Create a Flow going through these switches" def flow1 = flowFactory.getRandom(switchPair) - def flow2 = flowFactory.getRandom(switchPair, false, FlowState.UP, flow1.occupiedEndpoints()) - assert flow1.retrieveAllEntityPaths().getInvolvedIsls() == pathIsls - assert flow2.retrieveAllEntityPaths().getInvolvedIsls()== pathIsls + def flow1Path = flow1.retrieveAllEntityPaths() + def flow1IntermediateSwIsl = flow1PathIsls.findAll { it in intermediateSwIsls || it.reversed in intermediateSwIsls } + assert flow1Path.getInvolvedIsls().containsAll(flow1IntermediateSwIsl) + + and: "Create an additional Flow that has the same intermediate switch, but other ISLs are involved into a path" + def additionalSwitchPair = switchPairs.all().nonNeighbouring() + .excludeSwitches([switchPair.src, topology.switches.find { it.dpId == swId }]).random() + + def flow2PathIsls = additionalSwitchPair.retrieveAvailablePaths() + .find { it.getInvolvedSwitches().contains(swId) && it.getInvolvedIsls().intersect(flow1PathIsls).isEmpty() }.getInvolvedIsls() + + additionalSwitchPair.retrieveAvailablePaths().collect { it.getInvolvedIsls() }.findAll { it != flow2PathIsls } + .each { islHelper.makePathIslsMorePreferable(flow2PathIsls, it) } + + def flow2 = flowFactory.getRandom(additionalSwitchPair, false) + def flow2Path = flow2.retrieveAllEntityPaths() + def flow2IntermediateSwIsl = flow2PathIsls.findAll { it in intermediateSwIsls || it.reversed in intermediateSwIsls } + assert flow2Path.getInvolvedIsls().containsAll(flow2IntermediateSwIsl) + assert flow2IntermediateSwIsl.intersect(flow1IntermediateSwIsl).isEmpty() when: "Set maintenance mode without flows evacuation flag for some intermediate switch involved in flow paths" switchHelper.setSwitchMaintenance(swId, true, false) then: "Flows are not evacuated (rerouted) and have the same paths" - flow1.retrieveAllEntityPaths().getInvolvedIsls() == pathIsls - flow2.retrieveAllEntityPaths().getInvolvedIsls() == pathIsls + timedLoop(3) { + assert flow1.retrieveAllEntityPaths() == flow1Path + assert flow2.retrieveAllEntityPaths() == flow2Path + } when: "Set maintenance mode again with flows evacuation flag for the same switch" northbound.setSwitchMaintenance(swId, true, true) then: "Flows are evacuated (rerouted)" - FlowEntityPath flow1PathUpdated, flow2PathUpdated Wrappers.wait(PATH_INSTALLATION_TIME + WAIT_OFFSET) { [flow1, flow2].each { assert it.retrieveFlowStatus().status == FlowState.UP } + assert flow1.retrieveAllEntityPaths() != flow1Path + assert flow2.retrieveAllEntityPaths() != flow2Path + } + + and: "Switch under maintenance is not involved in new flow paths" + !flow1.retrieveAllEntityPaths().getInvolvedSwitches().contains(swId) + !flow2.retrieveAllEntityPaths().getInvolvedSwitches().contains(swId) + + } - flow1PathUpdated = flow1.retrieveAllEntityPaths() - flow2PathUpdated = flow2.retrieveAllEntityPaths() + @Tags(SMOKE) + def "Both Y-Flow and Flow can be evacuated (rerouted) from a particular switch when setting maintenance mode for it"() { + given: "Switch triplet has been selected" + def swTriplet = profile == "hardware" ? switchTriplets.all().withSharedEpEp1Ep2InChain().random() + : switchTriplets.all().nonNeighbouring().random() ?: assumeTrue(false, "No suiting switches found.") + + and: "Create a Y-Flow going through selected switches and has intermediate switch in a path" + if (profile == "hardware") { + //additional steps due to the HW limitation + def availablePaths = swTriplet.pathsEp1[0].size() == 2 ? + swTriplet.retrieveAvailablePathsEp1().collect { it.getInvolvedIsls() } : + swTriplet.retrieveAvailablePathsEp2().collect { it.getInvolvedIsls() } + // 1 isl is equal to 2 pathNodes + def preferablePath = availablePaths.find { it.size() > 1 } + availablePaths.findAll { it != preferablePath }.each { + islHelper.makePathIslsMorePreferable(preferablePath, it) + } + } + def yFlow = yFlowFactory.getRandom(swTriplet, false) + def yFlowPath = yFlow.retrieveAllEntityPaths() + def intermediateSwId = yFlowPath.getInvolvedSwitches() + .find { !(it in [swTriplet.shared.dpId, swTriplet.ep1.dpId, swTriplet.ep2.dpId]) } + assert intermediateSwId + + and: "Two active not neighboring switches and preferable path with intermediate switch" + def dstSw = swTriplet.pathsEp1[0].size() == 4 ? swTriplet.ep1 : swTriplet.ep2 + def swPair = switchPairs.all().specificPair(swTriplet.shared, dstSw) + def availablePaths = swPair.retrieveAvailablePaths() + List pathIsls = availablePaths.find { path -> intermediateSwId in path.getInvolvedSwitches() }.getInvolvedIsls() + availablePaths.collect { it.getInvolvedIsls() }.findAll { it != pathIsls } + .each { islHelper.makePathIslsMorePreferable(pathIsls, it) } + + and: "Create a Flow going through these switches" + def flow = flowFactory.getRandom(swPair) + def flowPath = flow.retrieveAllEntityPaths() + assert flowPath.getInvolvedSwitches().contains(intermediateSwId) + + when: "Set maintenance mode without flows evacuation flag for some intermediate switch involved in flow paths" + switchHelper.setSwitchMaintenance(intermediateSwId, true, false) + + then: "Both Flow and Y-Flow are not evacuated (rerouted) and have the same paths" + timedLoop(3) { + assert flow.retrieveAllEntityPaths() == flowPath + assert yFlow.retrieveAllEntityPaths() == yFlowPath + } + + when: "Set maintenance mode again with flows evacuation flag for the same switch" + northbound.setSwitchMaintenance(intermediateSwId, true, true) + + then: "Both Flow and Y-Flow are evacuated (rerouted)" + Wrappers.wait(PATH_INSTALLATION_TIME + WAIT_OFFSET) { + assert flow.retrieveFlowStatus().status == FlowState.UP + assert yFlow.retrieveDetails().status == FlowState.UP - assert flow1PathUpdated.getInvolvedIsls() != pathIsls - assert flow2PathUpdated.getInvolvedIsls()!= pathIsls + assert flow.retrieveAllEntityPaths() != flowPath + assert yFlow.retrieveAllEntityPaths() != yFlowPath } and: "Switch under maintenance is not involved in new flow paths" - !flow1PathUpdated.getInvolvedSwitches().contains(swId) - !flow2PathUpdated.getInvolvedSwitches().contains(swId) + !flow.retrieveAllEntityPaths().getInvolvedSwitches().contains(intermediateSwId) + !yFlow.retrieveAllEntityPaths().getInvolvedSwitches().contains(intermediateSwId) } @Tags(ISL_RECOVER_ON_FAIL) diff --git a/src-java/testing/test-library/src/main/java/org/openkilda/testing/model/topology/TopologyDefinition.java b/src-java/testing/test-library/src/main/java/org/openkilda/testing/model/topology/TopologyDefinition.java index 0f36785228..0bea44c548 100644 --- a/src-java/testing/test-library/src/main/java/org/openkilda/testing/model/topology/TopologyDefinition.java +++ b/src-java/testing/test-library/src/main/java/org/openkilda/testing/model/topology/TopologyDefinition.java @@ -229,18 +229,23 @@ public List getBusyPortsForSwitch(SwitchId swId) { * actual ISLs. */ @JsonIgnore - public List getRelatedIsls(Switch sw) { + public List getRelatedIsls(SwitchId swId) { List isls = getIslsForActiveSwitches().stream().filter(isl -> - isl.getSrcSwitch().getDpId().equals(sw.getDpId()) || isl.getDstSwitch().getDpId().equals(sw.getDpId())) + isl.getSrcSwitch().getDpId().equals(swId) || isl.getDstSwitch().getDpId().equals(swId)) .collect(toList()); for (Isl isl : isls) { - if (isl.getDstSwitch().getDpId().equals(sw.getDpId())) { + if (isl.getDstSwitch().getDpId().equals(swId)) { isls.set(isls.indexOf(isl), isl.getReversed()); } } return isls; } + @JsonIgnore + public List getRelatedIsls(Switch sw) { + return getRelatedIsls(sw.getDpId()); + } + /** * Get random ISLs between two switches. * Returns only outgoing from the first switch ISL.