From eb71782e0e556ddf3f67c7ceebc52cd496c725df Mon Sep 17 00:00:00 2001 From: Dmitrii Beliakov Date: Thu, 2 Feb 2023 15:29:52 +0000 Subject: [PATCH 01/45] Create the Request side: controller and service handling, payloads, and errors. --- .../info/network/PathValidateResponse.java | 25 ++++++++ .../command/flow/PathValidateRequest.java | 36 +++++++++++ .../topology/nbworker/bolts/PathsBolt.java | 5 +- .../nbworker/services/PathsService.java | 27 ++++++-- .../nbworker/services/PathsServiceTest.java | 2 +- .../dto/v2/flows/PathCheckErrorPayload.java | 30 +++++++++ .../dto/v2/flows/PathValidateResponse.java | 33 ++++++++++ .../controller/v1/NetworkController.java | 6 +- .../controller/v2/NetworkControllerV2.java | 64 +++++++++++++++++++ .../northbound/service/NetworkService.java | 13 +++- .../service/impl/NetworkServiceImpl.java | 18 ++++++ 11 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateResponse.java create mode 100644 src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java create mode 100644 src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathCheckErrorPayload.java create mode 100644 src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java create mode 100644 src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateResponse.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateResponse.java new file mode 100644 index 00000000000..af5cacf3cd5 --- /dev/null +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateResponse.java @@ -0,0 +1,25 @@ +/* Copyright 2018 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.messaging.info.network; + +import org.openkilda.messaging.info.InfoData; + +import java.util.List; + +public class PathValidateResponse extends InfoData { + boolean isValid; + List errors; +} diff --git a/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java b/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java new file mode 100644 index 00000000000..057595ed3b1 --- /dev/null +++ b/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java @@ -0,0 +1,36 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.messaging.command.flow; + +import org.openkilda.messaging.nbtopology.request.BaseRequest; +import org.openkilda.messaging.payload.network.PathDto; + +import lombok.EqualsAndHashCode; +import lombok.Value; + + +/** + * Request to validate that the given path is possible to create with the given constraints and resources availability. + */ +@Value +@EqualsAndHashCode(callSuper = false) +public class PathValidateRequest extends BaseRequest { + PathDto pathDto; + + public PathValidateRequest(PathDto pathDto) { + this.pathDto = pathDto; + } +} diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java index f87cfa2f05b..a0faaf540ca 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java @@ -15,6 +15,7 @@ package org.openkilda.wfm.topology.nbworker.bolts; +import org.openkilda.messaging.command.flow.PathValidateRequest; import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.error.MessageException; import org.openkilda.messaging.info.InfoData; @@ -46,7 +47,7 @@ public PathsBolt(PersistenceManager persistenceManager, PathComputerConfig pathC public void init() { super.init(); - pathService = new PathsService(repositoryFactory, pathComputerConfig); + pathService = new PathsService(repositoryFactory, pathComputerConfig, persistenceManager); } @Override @@ -55,6 +56,8 @@ List processRequest(Tuple tuple, BaseRequest request) { List result = null; if (request instanceof GetPathsRequest) { result = getPaths((GetPathsRequest) request); + } else if (request instanceof PathValidateRequest) { + result = pathService.validatePath((PathValidateRequest) request); } else { unhandledInput(tuple); } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java index 2c122bb1f10..90728ae329e 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java @@ -15,6 +15,8 @@ package org.openkilda.wfm.topology.nbworker.services; +import org.openkilda.messaging.command.flow.PathValidateRequest; +import org.openkilda.messaging.info.network.PathValidateResponse; import org.openkilda.messaging.info.network.PathsInfoData; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.KildaConfiguration; @@ -28,6 +30,7 @@ import org.openkilda.pce.PathComputerFactory; import org.openkilda.pce.exception.RecoverableException; import org.openkilda.pce.exception.UnroutableFlowException; +import org.openkilda.persistence.PersistenceManager; import org.openkilda.persistence.repositories.KildaConfigurationRepository; import org.openkilda.persistence.repositories.RepositoryFactory; import org.openkilda.persistence.repositories.SwitchPropertiesRepository; @@ -47,12 +50,14 @@ @Slf4j public class PathsService { private final int defaultMaxPathCount; - private PathComputer pathComputer; - private SwitchRepository switchRepository; - private SwitchPropertiesRepository switchPropertiesRepository; - private KildaConfigurationRepository kildaConfigurationRepository; + private final PathComputer pathComputer; + private final SwitchRepository switchRepository; + private final SwitchPropertiesRepository switchPropertiesRepository; + private final KildaConfigurationRepository kildaConfigurationRepository; + private final AvailableNetworkFactory availableNetworkFactory; - public PathsService(RepositoryFactory repositoryFactory, PathComputerConfig pathComputerConfig) { + public PathsService(RepositoryFactory repositoryFactory, PathComputerConfig pathComputerConfig, + PersistenceManager persistenceManager) { switchRepository = repositoryFactory.createSwitchRepository(); switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); kildaConfigurationRepository = repositoryFactory.createKildaConfigurationRepository(); @@ -60,6 +65,9 @@ public PathsService(RepositoryFactory repositoryFactory, PathComputerConfig path pathComputerConfig, new AvailableNetworkFactory(pathComputerConfig, repositoryFactory)); pathComputer = pathComputerFactory.getPathComputer(); defaultMaxPathCount = pathComputerConfig.getMaxPathCount(); + + availableNetworkFactory = + new AvailableNetworkFactory(pathComputerConfig, persistenceManager.getRepositoryFactory()); } /** @@ -87,7 +95,7 @@ public List getPaths( SwitchProperties srcProperties = switchPropertiesRepository.findBySwitchId(srcSwitchId).orElseThrow( () -> new SwitchPropertiesNotFoundException(srcSwitchId)); if (!srcProperties.getSupportedTransitEncapsulation().contains(flowEncapsulationType)) { - throw new IllegalArgumentException(String.format("Switch %s doesn't support %s encapslation type. Choose " + throw new IllegalArgumentException(String.format("Switch %s doesn't support %s encapsulation type. Choose " + "one of the supported encapsulation types %s or update switch properties and add needed " + "encapsulation type.", srcSwitchId, flowEncapsulationType, srcProperties.getSupportedTransitEncapsulation())); @@ -96,7 +104,7 @@ public List getPaths( SwitchProperties dstProperties = switchPropertiesRepository.findBySwitchId(dstSwitchId).orElseThrow( () -> new SwitchPropertiesNotFoundException(dstSwitchId)); if (!dstProperties.getSupportedTransitEncapsulation().contains(flowEncapsulationType)) { - throw new IllegalArgumentException(String.format("Switch %s doesn't support %s encapslation type. Choose " + throw new IllegalArgumentException(String.format("Switch %s doesn't support %s encapsulation type. Choose " + "one of the supported encapsulation types %s or update switch properties and add needed " + "encapsulation type.", dstSwitchId, requestEncapsulationType, dstProperties.getSupportedTransitEncapsulation())); @@ -120,4 +128,9 @@ public List getPaths( .map(path -> PathsInfoData.builder().path(path).build()) .collect(Collectors.toList()); } + + public List validatePath(PathValidateRequest request) { + //TODO implement path validation + throw new UnsupportedOperationException(); + } } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java index 3f2fe5de6a0..dfddace4c5f 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java @@ -84,7 +84,7 @@ public static void setUpOnce() { kildaConfigurationRepository = repositoryFactory.createKildaConfigurationRepository(); PathComputerConfig pathComputerConfig = new PropertiesBasedConfigurationProvider() .getConfiguration(PathComputerConfig.class); - pathsService = new PathsService(repositoryFactory, pathComputerConfig); + pathsService = new PathsService(repositoryFactory, pathComputerConfig, persistenceManager); } @Before diff --git a/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathCheckErrorPayload.java b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathCheckErrorPayload.java new file mode 100644 index 00000000000..358cf8d56fb --- /dev/null +++ b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathCheckErrorPayload.java @@ -0,0 +1,30 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.northbound.dto.v2.flows; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@AllArgsConstructor +@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) +public class PathCheckErrorPayload { + String errorDescription; +} diff --git a/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java new file mode 100644 index 00000000000..f0f98ef9411 --- /dev/null +++ b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java @@ -0,0 +1,33 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.northbound.dto.v2.flows; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) +public class PathValidateResponse { + boolean isValid; + List errors; +} diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/NetworkController.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/NetworkController.java index f70bb105245..f4af192c029 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/NetworkController.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/NetworkController.java @@ -59,10 +59,10 @@ public class NetworkController extends BaseController { public CompletableFuture getPaths( @RequestParam("src_switch") SwitchId srcSwitchId, @RequestParam("dst_switch") SwitchId dstSwitchId, @ApiParam(value = "Valid values are: TRANSIT_VLAN, VXLAN. If encapsulation type is not specified, default " - + "value from Kilda Configuration will be used") + + "value from OpenKilda Configuration will be used") @RequestParam(value = "encapsulation_type", required = false) FlowEncapsulationType encapsulationType, @ApiParam(value = "Valid values are: COST, LATENCY, MAX_LATENCY, COST_AND_AVAILABLE_BANDWIDTH. If path " - + "computation strategy is not specified, default value from Kilda Configuration will be used") + + "computation strategy is not specified, default value from OpenKilda Configuration will be used") @RequestParam(value = "path_computation_strategy", required = false) PathComputationStrategy pathComputationStrategy, @ApiParam(value = "Maximum latency of flow path in milliseconds. Required for MAX_LATENCY strategy. " @@ -74,7 +74,7 @@ public CompletableFuture getPaths( + "Other strategies will ignore this parameter.") @RequestParam(value = "max_latency_tier2", required = false) Long maxLatencyTier2Ms, @ApiParam(value = "Maximum count of paths which will be calculated. " - + "If maximum path count is not specified, default value from Kilda Configuration will be used") + + "If maximum path count is not specified, default value from OpenKilda Configuration will be used") @RequestParam(value = "max_path_count", required = false) Integer maxPathCount) { Duration maxLatency = maxLatencyMs != null ? Duration.ofMillis(maxLatencyMs) : null; diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java new file mode 100644 index 00000000000..2be844470ca --- /dev/null +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java @@ -0,0 +1,64 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.northbound.controller.v2; + +import org.openkilda.messaging.error.ErrorType; +import org.openkilda.messaging.error.MessageException; +import org.openkilda.messaging.payload.network.PathDto; +import org.openkilda.northbound.service.NetworkService; + +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.concurrent.CompletableFuture; + +@RestController +@RequestMapping("/v2/network") +public class NetworkControllerV2 { + + @Autowired + private NetworkService networkService; + + /** + * Validates that a given path complies with the chosen strategy and the network availability. + * It is required that the input contains path nodes. Other parameters are optional. + * TODO: order of nodes might be important, maybe there is a need to require a certain structure for that list. + * @param pathDto a payload with a path and additional flow parameters provided by a user + * @return either a successful response or the list of errors + */ + @GetMapping(path = "/path/check") + @ApiOperation(value = "Validates that a given path complies with the chosen strategy and the network availability") + @ResponseStatus(HttpStatus.OK) + public CompletableFuture validateCustomFlowPath(@RequestBody PathDto pathDto) { + if (isFlowPathRequestInvalid(pathDto)) { + throw new MessageException(ErrorType.DATA_INVALID, "Invalid Request Body", + "Invalid 'nodes' value in the request body"); + } + + return networkService.validateFlowPath(pathDto); + } + + private boolean isFlowPathRequestInvalid(PathDto pathDto) { + return pathDto == null || pathDto.getNodes() == null || pathDto.getNodes().isEmpty(); + } + +} diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java index ccda5b49503..00e799608ca 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java @@ -15,10 +15,12 @@ package org.openkilda.northbound.service; +import org.openkilda.messaging.payload.network.PathDto; import org.openkilda.messaging.payload.network.PathsDto; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.PathComputationStrategy; import org.openkilda.model.SwitchId; +import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; import java.time.Duration; import java.util.concurrent.CompletableFuture; @@ -29,10 +31,19 @@ public interface NetworkService { /** - * Gets pathes between two switches. + * Gets paths between two switches. */ CompletableFuture getPaths( SwitchId srcSwitch, SwitchId dstSwitch, FlowEncapsulationType encapsulationType, PathComputationStrategy pathComputationStrategy, Duration maxLatencyMs, Duration maxLatencyTier2, Integer maxPathCount); + + /** + * Validates that a flow with the given path can possibly be created. If it is not possible, + * it responds with the reasons, such as: not enough bandwidth, requested latency it too low, there is no + * links between the selected switches, and so on. + * @param path a path provided by a user + * @return either a successful response or the list of errors + */ + CompletableFuture validateFlowPath(PathDto path); } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java index bff33bb193c..762a15b5f57 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java @@ -16,6 +16,7 @@ package org.openkilda.northbound.service.impl; import org.openkilda.messaging.command.CommandMessage; +import org.openkilda.messaging.command.flow.PathValidateRequest; import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.error.MessageException; import org.openkilda.messaging.info.network.PathsInfoData; @@ -26,6 +27,7 @@ import org.openkilda.model.PathComputationStrategy; import org.openkilda.model.SwitchId; import org.openkilda.northbound.converter.PathMapper; +import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; import org.openkilda.northbound.messaging.MessagingChannel; import org.openkilda.northbound.service.NetworkService; import org.openkilda.northbound.utils.RequestCorrelationId; @@ -92,4 +94,20 @@ public CompletableFuture getPaths( return new PathsDto(pathsDtoList); }); } + + /** + * Validates that a flow with the given path can possibly be created. If it is not possible, + * it responds with the reasons, such as: not enough bandwidth, requested latency it too low, there is no + * links between the selected switches, and so on. + * @param path a path provided by a user + * @return either a successful response or the list of errors + */ + @Override + public CompletableFuture validateFlowPath(PathDto path) { + PathValidateRequest request = new PathValidateRequest(path); + + CommandMessage message = new CommandMessage(request, System.currentTimeMillis(), RequestCorrelationId.getId()); + return messagingChannel.sendAndGetChunked(nbworkerTopic, message) + .thenApply(PathValidateResponse.class::cast); + } } From 810cfa9d152929a3cdcedbee1102225673647734 Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Mon, 6 Feb 2023 21:29:51 +0100 Subject: [PATCH 02/45] Path validation design proposal. --- .../path-validation/path-validation.md | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/design/solutions/path-validation/path-validation.md diff --git a/docs/design/solutions/path-validation/path-validation.md b/docs/design/solutions/path-validation/path-validation.md new file mode 100644 index 00000000000..b393234ed5a --- /dev/null +++ b/docs/design/solutions/path-validation/path-validation.md @@ -0,0 +1,80 @@ +# Path Validation + +## Motivation +The main use case is to allow users to validate if some arbitrary flow path is possible to create with the given +constraints without actually creating this flow. A path here is a sequence of nodes, that is a sequence of switches +with in and out ports. Constraints are various parameters such as max latency, bandwidth, encapsulation type, +path computation strategy, and other. + +## Implementation proposal +Reuse PCE components (specifically extend AvailableNetwork) for the path validation in the same manner as it is used for +the path computation. Use nb worker topology, PathService, and PathsBolt because the path validation is read-only. + +_An alternative approach is to use FlowValidationHubBolt and the resource manager. This might help to mitigate concurrency +issues related to changes in repositories during a path validation process._ + +There is no locking of resources for this path and, therefore, no guarantee that it will be possible to create this flow +after validation in the future. + +## Northbound API + +REST URL: ```/v2/network/validate-path```, method: ```GET``` + +A user is required to provide a path represented by a list of nodes. Nodes must be ordered from start to end, +each next element is the next hop. A user can add optional parameters. + +### Request Body +```json +{ + "encapsulation_type": "TRANSIT_VLAN|VXLAN", + "max_latency": 0, + "max_latency_tier2": 0, + "path_computation_strategy": "COST|LATENCY|MAX_LATENCY|COST_AND_AVAILABLE_BANDWIDTH", + "nodes": [ + { + "switch_id": "00:00:00:00:00:00:00:01", + "output_port": 0 + }, + { + "switch_id": "00:00:00:00:00:00:00:02", + "input_port": 0, + "output_port": 1 + }, + { + "switch_id": "00:00:00:00:00:00:00:03", + "input_port": 0 + } + ] +} +``` + +### Responses + +Code: 200 + +Content: +- valid: True if the path is valid and conform to strategy, false otherwise +- errors: List of strings with error descriptions if any +- correlation_id: Correlation ID from correlation_id Header +```json +{ + "correlation_id": "string", + "valid": "bool", + "errors": [ + "error0", + "errorN" + ] +} +``` + +Codes: 4XX,5XX +```json +{ + "correlation_id": "string", + "error-description": "string", + "error-message": "string", + "error-type": "string", + "timestamp": 0 +} +``` + From b1a18d650630efd409a64d15e44b755467de58e1 Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Tue, 7 Feb 2023 17:51:07 +0100 Subject: [PATCH 03/45] Add bandwidth and reuse flow parameters. --- .../solutions/path-validation/path-validation.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/design/solutions/path-validation/path-validation.md b/docs/design/solutions/path-validation/path-validation.md index b393234ed5a..a11ea1bfec6 100644 --- a/docs/design/solutions/path-validation/path-validation.md +++ b/docs/design/solutions/path-validation/path-validation.md @@ -21,15 +21,24 @@ after validation in the future. REST URL: ```/v2/network/validate-path```, method: ```GET``` A user is required to provide a path represented by a list of nodes. Nodes must be ordered from start to end, -each next element is the next hop. A user can add optional parameters. +each next element is the next hop. A user can add optional parameters: +- encapsulation_type: enum value "TRANSIT_VLAN" or "VXLAN". +- max_bandwidth: bandwidth required for this path. +- max_latency: the first tier latency value. +- max_latency_tier2: the second tier latency value. +- path_computation_strategy: "COST|LATENCY|MAX_LATENCY|COST_AND_AVAILABLE_BANDWIDTH", +- reuse_flow_resources: a flow ID. Verify the given path as if it is created instead of the existing flow, that is as if +the resources of some flow are released before validation. ### Request Body ```json { "encapsulation_type": "TRANSIT_VLAN|VXLAN", + "max_bandwidth": 0, "max_latency": 0, "max_latency_tier2": 0, "path_computation_strategy": "COST|LATENCY|MAX_LATENCY|COST_AND_AVAILABLE_BANDWIDTH", + "reuse_flow_resources": 0, "nodes": [ { "switch_id": "00:00:00:00:00:00:00:01", From ee5d6270e99d4fd4e18700503d4d1e2aeb1faf8c Mon Sep 17 00:00:00 2001 From: Fedir Kantur Date: Wed, 8 Feb 2023 11:46:37 +0000 Subject: [PATCH 04/45] Ability to add single switch flow into diverse group Closes #2072 --- .../flowhs/validation/FlowValidator.java | 11 +- .../flowhs/validation/FlowValidatorTest.java | 146 ++++++++++++------ .../pce/AvailableNetworkFactory.java | 3 +- .../openkilda/pce/impl/AvailableNetwork.java | 12 +- .../pce/finder/DiversityPathFinderTest.java | 93 ++++++++++- .../pce/impl/AvailableNetworkTest.java | 108 +++++++------ 6 files changed, 263 insertions(+), 110 deletions(-) diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidator.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidator.java index 6bb83e18d9f..96db2e5ce79 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidator.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidator.java @@ -1,4 +1,4 @@ -/* Copyright 2021 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -399,7 +399,7 @@ private void checkOneSwitchFlowConflict(FlowEndpoint source, FlowEndpoint destin @VisibleForTesting void checkDiverseFlow(RequestedFlow targetFlow) throws InvalidFlowException { - if (targetFlow.getSrcSwitch().equals(targetFlow.getDestSwitch())) { + if (targetFlow.isOneSwitchFlow()) { throw new InvalidFlowException("Couldn't add one-switch flow into diverse group", ErrorType.PARAMETERS_INVALID); } @@ -420,7 +420,6 @@ void checkDiverseFlow(RequestedFlow targetFlow) throws InvalidFlowException { targetFlow.getDiverseFlowId()), ErrorType.INTERNAL_ERROR)); } - if (StringUtils.isNotBlank(diverseFlow.getAffinityGroupId())) { String diverseFlowId = diverseFlow.getAffinityGroupId(); diverseFlow = flowRepository.findById(diverseFlowId) @@ -428,7 +427,6 @@ void checkDiverseFlow(RequestedFlow targetFlow) throws InvalidFlowException { new InvalidFlowException(format("Failed to find diverse flow id %s", diverseFlowId), ErrorType.PARAMETERS_INVALID)); - Collection affinityFlowIds = flowRepository .findFlowsIdByAffinityGroupId(diverseFlow.getAffinityGroupId()).stream() .filter(Objects::nonNull) @@ -438,11 +436,6 @@ void checkDiverseFlow(RequestedFlow targetFlow) throws InvalidFlowException { ErrorType.PARAMETERS_INVALID); } } - - if (diverseFlow.isOneSwitchFlow()) { - throw new InvalidFlowException("Couldn't create diverse group with one-switch flow", - ErrorType.PARAMETERS_INVALID); - } } @VisibleForTesting diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java index 47f0980edd6..f097bb2a922 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java @@ -1,4 +1,4 @@ -/* Copyright 2021 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,31 +16,28 @@ package org.openkilda.wfm.topology.flowhs.validation; import static com.google.common.collect.Sets.newHashSet; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.openkilda.model.FlowEncapsulationType.TRANSIT_VLAN; import static org.openkilda.model.FlowEncapsulationType.VXLAN; -import org.openkilda.model.FlowEndpoint; -import org.openkilda.model.SwitchId; -import org.openkilda.model.SwitchProperties; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.openkilda.model.*; import org.openkilda.persistence.PersistenceManager; -import org.openkilda.persistence.repositories.FlowMirrorPathRepository; -import org.openkilda.persistence.repositories.FlowRepository; -import org.openkilda.persistence.repositories.IslRepository; -import org.openkilda.persistence.repositories.RepositoryFactory; -import org.openkilda.persistence.repositories.SwitchPropertiesRepository; -import org.openkilda.persistence.repositories.SwitchRepository; +import org.openkilda.persistence.repositories.*; import org.openkilda.wfm.topology.flowhs.model.DetectConnectedDevices; import org.openkilda.wfm.topology.flowhs.model.RequestedFlow; import org.openkilda.wfm.topology.flowhs.validation.FlowValidator.EndpointDescriptor; -import org.junit.BeforeClass; import org.junit.Test; import java.util.HashSet; +import java.util.Optional; import java.util.Set; +@RunWith(MockitoJUnitRunner.class) public class FlowValidatorTest { public static final SwitchId SWITCH_ID_1 = new SwitchId(1); public static final SwitchId SWITCH_ID_2 = new SwitchId(2); @@ -50,13 +47,17 @@ public class FlowValidatorTest { public static final EndpointDescriptor SRC_ENDPOINT = EndpointDescriptor.makeSource( FlowEndpoint.builder().switchId(SWITCH_ID_1).portNumber(PORT_1).build()); public static final String FLOW_1 = "firstFlow"; + public static final String FLOW_2 = "secondFlow"; + public static final String DIVERSE_FLOW_ID = "diverseFlowId"; - public static FlowValidator flowValidator; + @Mock + private FlowRepository flowRepository; + private FlowValidator flowValidator; - @BeforeClass - public static void setup() { + @Before + public void setup() { RepositoryFactory repositoryFactory = mock(RepositoryFactory.class); - when(repositoryFactory.createFlowRepository()).thenReturn(mock(FlowRepository.class)); + when(repositoryFactory.createFlowRepository()).thenReturn(flowRepository); when(repositoryFactory.createSwitchRepository()).thenReturn(mock(SwitchRepository.class)); when(repositoryFactory.createIslRepository()).thenReturn(mock(IslRepository.class)); when(repositoryFactory.createSwitchPropertiesRepository()).thenReturn(mock(SwitchPropertiesRepository.class)); @@ -67,7 +68,7 @@ public static void setup() { } @Test(expected = InvalidFlowException.class) - public void shouldFailOnSwapWhenEqualsEndpointsOnFirstFlow() throws InvalidFlowException { + public void failOnSwapWhenEqualsEndpointsOnFirstFlowTest() throws InvalidFlowException { RequestedFlow firstFlow = RequestedFlow.builder() .flowId(FLOW_1) .srcSwitch(SWITCH_ID_1) @@ -80,7 +81,7 @@ public void shouldFailOnSwapWhenEqualsEndpointsOnFirstFlow() throws InvalidFlowE .build(); RequestedFlow secondFlow = RequestedFlow.builder() - .flowId("secondFlow") + .flowId(FLOW_2) .srcSwitch(SWITCH_ID_2) .destSwitch(SWITCH_ID_2) .detectConnectedDevices(new DetectConnectedDevices()) @@ -90,7 +91,7 @@ public void shouldFailOnSwapWhenEqualsEndpointsOnFirstFlow() throws InvalidFlowE } @Test(expected = InvalidFlowException.class) - public void shouldFailOnSwapWhenEqualsEndpointsOnSecondFlow() throws InvalidFlowException { + public void failOnSwapWhenEqualsEndpointsOnSecondFlowTest() throws InvalidFlowException { RequestedFlow firstFlow = RequestedFlow.builder() .flowId(FLOW_1) .srcSwitch(SWITCH_ID_2) @@ -102,7 +103,7 @@ public void shouldFailOnSwapWhenEqualsEndpointsOnSecondFlow() throws InvalidFlow .build(); RequestedFlow secondFlow = RequestedFlow.builder() - .flowId("secondFlow") + .flowId(FLOW_2) .srcSwitch(SWITCH_ID_1) .srcPort(10) .srcVlan(11) @@ -116,7 +117,7 @@ public void shouldFailOnSwapWhenEqualsEndpointsOnSecondFlow() throws InvalidFlow } @Test(expected = InvalidFlowException.class) - public void shouldFailOnSwapWhenEqualsEndpointsOnFirstAndSecondFlow() throws InvalidFlowException { + public void failOnSwapWhenEqualsEndpointsOnFirstAndSecondFlowTest() throws InvalidFlowException { RequestedFlow firstFlow = RequestedFlow.builder() .flowId(FLOW_1) .srcSwitch(SWITCH_ID_1) @@ -129,7 +130,7 @@ public void shouldFailOnSwapWhenEqualsEndpointsOnFirstAndSecondFlow() throws Inv .build(); RequestedFlow secondFlow = RequestedFlow.builder() - .flowId("secondFlow") + .flowId(FLOW_2) .srcSwitch(SWITCH_ID_1) .srcPort(10) .srcVlan(11) @@ -143,7 +144,7 @@ public void shouldFailOnSwapWhenEqualsEndpointsOnFirstAndSecondFlow() throws Inv } @Test - public void shouldNotFailOnSwapWhenDifferentEndpointsOnFirstAndSecondFlow() throws InvalidFlowException { + public void doesntFailOnSwapWhenDifferentEndpointsOnFirstAndSecondFlowTest() throws InvalidFlowException { RequestedFlow firstFlow = RequestedFlow.builder() .flowId(FLOW_1) .srcSwitch(SWITCH_ID_1) @@ -156,7 +157,7 @@ public void shouldNotFailOnSwapWhenDifferentEndpointsOnFirstAndSecondFlow() thro .build(); RequestedFlow secondFlow = RequestedFlow.builder() - .flowId("secondFlow") + .flowId(FLOW_2) .srcSwitch(SWITCH_ID_1) .srcPort(14) .srcVlan(15) @@ -170,31 +171,31 @@ public void shouldNotFailOnSwapWhenDifferentEndpointsOnFirstAndSecondFlow() thro } @Test - public void shouldNotFailOnSpecifiedDestOuterVlansAndVlanStatistics() throws InvalidFlowException { + public void doesntFailOnSpecifiedDestOuterVlansAndVlanStatisticsTest() throws InvalidFlowException { RequestedFlow flow = buildFlow(0, VLAN_1, newHashSet(235)); flowValidator.checkFlowForCorrectOuterVlansWithVlanStatistics(flow); } @Test - public void shouldNotFailOnSpecifiedSrcOuterVlansAndVlanStatistics() throws InvalidFlowException { + public void doesntFailOnSpecifiedSrcOuterVlansAndVlanStatisticsTest() throws InvalidFlowException { RequestedFlow flow = buildFlow(VLAN_1, 0, newHashSet(235)); flowValidator.checkFlowForCorrectOuterVlansWithVlanStatistics(flow); } @Test - public void shouldNotFailOnSpecifiedBothOuterVlansAndEmptyVlanStatistics() throws InvalidFlowException { + public void doesntFailOnSpecifiedBothOuterVlansAndEmptyVlanStatisticsTest() throws InvalidFlowException { RequestedFlow flow = buildFlow(VLAN_1, VLAN_2, new HashSet<>()); flowValidator.checkFlowForCorrectOuterVlansWithVlanStatistics(flow); } @Test - public void shouldNotFailOnSpecifiedBothOuterVlansAndNullVlanStatistics() throws InvalidFlowException { + public void doesntFailOnSpecifiedBothOuterVlansAndNullVlanStatisticsTest() throws InvalidFlowException { RequestedFlow flow = buildFlow(VLAN_1, VLAN_2, null); flowValidator.checkFlowForCorrectOuterVlansWithVlanStatistics(flow); } @Test(expected = InvalidFlowException.class) - public void shouldFailOnSpecifiedBothOuterVlansAndVlanStatistics() throws InvalidFlowException { + public void failOnSpecifiedBothOuterVlansAndVlanStatisticsTest() throws InvalidFlowException { RequestedFlow flow = buildFlow(VLAN_1, VLAN_2, newHashSet(235)); flowValidator.checkFlowForCorrectOuterVlansWithVlanStatistics(flow); } @@ -231,46 +232,79 @@ public void checkForEncapsulationTypeRequirementCorrectTypeTest() throws Invalid flowValidator.checkForEncapsulationTypeRequirement(SRC_ENDPOINT, properties, TRANSIT_VLAN); } - private RequestedFlow getTestRequestWithMaxLatencyAndMaxLatencyTier2(Long maxLatency, Long maxLatencyTier2) { - return RequestedFlow.builder() - .flowId(FLOW_1) - .maxLatency(maxLatency) - .maxLatencyTier2(maxLatencyTier2) - .srcSwitch(SWITCH_ID_1) - .srcPort(10) - .srcVlan(11) - .destSwitch(SWITCH_ID_2) - .destPort(12) - .destVlan(13) - .detectConnectedDevices(new DetectConnectedDevices()) - .build(); - } - @Test(expected = InvalidFlowException.class) - public void shouldFailIfMaxLatencyTier2HigherThanMaxLatency() throws InvalidFlowException { + public void failIfMaxLatencyTier2HigherThanMaxLatencyTest() throws InvalidFlowException { RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2((long) 1000, (long) 500); flowValidator.checkMaxLatency(flow); } @Test(expected = InvalidFlowException.class) - public void shouldFailIfMaxLatencyTier2butMaxLatencyIsNull() throws InvalidFlowException { + public void failIfMaxLatencyTier2butMaxLatencyIsNullTest() throws InvalidFlowException { RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2(null, (long) 500); flowValidator.checkMaxLatency(flow); } @Test - public void shouldNotFailIfMaxLatencyTier2andMaxLatencyAreNull() throws InvalidFlowException { + public void doesntFailIfMaxLatencyTier2andMaxLatencyAreNullTest() throws InvalidFlowException { RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2(null, null); flowValidator.checkMaxLatency(flow); } @Test - public void shouldNotFailIfMaxLatencyTier2andMaxLatencyAreEqual() throws InvalidFlowException { + public void doesntFailIfMaxLatencyTier2andMaxLatencyAreEqualTest() throws InvalidFlowException { RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2(500L, 500L); flowValidator.checkMaxLatency(flow); } - private static RequestedFlow buildFlow(int srcVlan, int dstVlan, Set statVlans) { + @Test(expected = InvalidFlowException.class) + public void failOnAddingOneSwitchFlowToDiverseGroupWithExistingFlowTest() throws InvalidFlowException { + RequestedFlow oneSwitchFlow = buildOneSwitchFlow(); + flowValidator.checkDiverseFlow(oneSwitchFlow); + } + + @Test + public void doesntFailOnAddingFlowToDiverseGroupWithExistingOneSwitchFlowTest() throws InvalidFlowException { + RequestedFlow flow = RequestedFlow.builder() + .flowId(FLOW_1) + .srcSwitch(SWITCH_ID_1) + .srcPort(PORT_1) + .destSwitch(SWITCH_ID_2) + .destPort(PORT_1) + .detectConnectedDevices(new DetectConnectedDevices()) + .diverseFlowId(DIVERSE_FLOW_ID) + .build(); + + Switch singleSwitch = Switch.builder().switchId(SWITCH_ID_1).build(); + + Flow oneSwitchFlow = Flow.builder() + .flowId("oneSwitchFlow") + .srcSwitch(singleSwitch) + .srcPort(PORT_1) + .destSwitch(singleSwitch) + .destPort(11) + .diverseGroupId(DIVERSE_FLOW_ID) + .build(); + + when(flowRepository.findById(DIVERSE_FLOW_ID)).thenReturn(Optional.of(oneSwitchFlow)); + flowValidator.checkDiverseFlow(flow); + } + + private RequestedFlow getTestRequestWithMaxLatencyAndMaxLatencyTier2(Long maxLatency, Long maxLatencyTier2) { + return RequestedFlow.builder() + .flowId(FLOW_1) + .maxLatency(maxLatency) + .maxLatencyTier2(maxLatencyTier2) + .srcSwitch(SWITCH_ID_1) + .srcPort(10) + .srcVlan(11) + .destSwitch(SWITCH_ID_2) + .destPort(12) + .destVlan(13) + .detectConnectedDevices(new DetectConnectedDevices()) + .build(); + } + + private RequestedFlow buildFlow(int srcVlan, int dstVlan, Set statVlans) { return RequestedFlow.builder() .flowId(FLOW_1) .srcSwitch(SWITCH_ID_1) @@ -283,4 +317,16 @@ private static RequestedFlow buildFlow(int srcVlan, int dstVlan, Set st .vlanStatistics(statVlans) .build(); } + + private RequestedFlow buildOneSwitchFlow() { + return RequestedFlow.builder() + .flowId(FLOW_1) + .srcSwitch(SWITCH_ID_1) + .srcPort(PORT_1) + .destSwitch(SWITCH_ID_1) + .destPort(11) + .detectConnectedDevices(new DetectConnectedDevices()) + .diverseFlowId(DIVERSE_FLOW_ID) + .build(); + } } diff --git a/src-java/kilda-pce/src/main/java/org/openkilda/pce/AvailableNetworkFactory.java b/src-java/kilda-pce/src/main/java/org/openkilda/pce/AvailableNetworkFactory.java index e7a1632ceba..c9dfa2b8bf3 100644 --- a/src-java/kilda-pce/src/main/java/org/openkilda/pce/AvailableNetworkFactory.java +++ b/src-java/kilda-pce/src/main/java/org/openkilda/pce/AvailableNetworkFactory.java @@ -1,4 +1,4 @@ -/* Copyright 2022 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -94,6 +94,7 @@ public AvailableNetwork getAvailableNetwork(Flow flow, Collection reuseP .filter(flowPath -> !affinityPathIds.contains(flowPath.getPathId()) || flowPath.getFlowId().equals(flow.getFlowId())) .ifPresent(flowPath -> { + network.processDiversityGroupForSingleSwitchFlow(flowPath); network.processDiversitySegments(flowPath.getSegments(), flow); network.processDiversitySegmentsWithPop(flowPath.getSegments()); })); diff --git a/src-java/kilda-pce/src/main/java/org/openkilda/pce/impl/AvailableNetwork.java b/src-java/kilda-pce/src/main/java/org/openkilda/pce/impl/AvailableNetwork.java index 3b2b5992ad9..bb20c3797ad 100644 --- a/src-java/kilda-pce/src/main/java/org/openkilda/pce/impl/AvailableNetwork.java +++ b/src-java/kilda-pce/src/main/java/org/openkilda/pce/impl/AvailableNetwork.java @@ -1,4 +1,4 @@ -/* Copyright 2021 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import static com.google.common.collect.Sets.newHashSet; import org.openkilda.model.Flow; +import org.openkilda.model.FlowPath; import org.openkilda.model.PathSegment; import org.openkilda.model.SwitchId; import org.openkilda.pce.model.Edge; @@ -167,6 +168,15 @@ public void processDiversitySegmentsWithPop(List segments) { } } + /** + * Adds diversity weights into {@link AvailableNetwork} based on passed flow path. + */ + public void processDiversityGroupForSingleSwitchFlow(FlowPath flowPath) { + if (flowPath.isOneSwitchFlow()) { + getSwitch(flowPath.getDestSwitchId()).increaseDiversityGroupUseCounter(); + } + } + /** * Adds affinity weights into {@link AvailableNetwork} based on passed path segments and configuration. */ diff --git a/src-java/kilda-pce/src/test/java/org/openkilda/pce/finder/DiversityPathFinderTest.java b/src-java/kilda-pce/src/test/java/org/openkilda/pce/finder/DiversityPathFinderTest.java index 28eaf9a6cb7..a7cdc463f76 100644 --- a/src-java/kilda-pce/src/test/java/org/openkilda/pce/finder/DiversityPathFinderTest.java +++ b/src-java/kilda-pce/src/test/java/org/openkilda/pce/finder/DiversityPathFinderTest.java @@ -1,4 +1,4 @@ -/* Copyright 2022 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -108,7 +108,7 @@ public void setup() { } @Test - public void shouldChooseSamePath() throws RecoverableException, UnroutableFlowException { + public void chooseSamePathTest() throws RecoverableException, UnroutableFlowException { // Topology: // A----B----C Already created flow: A-B-C // Requested flow: A-B-C @@ -199,7 +199,7 @@ public void shouldChooseSamePath() throws RecoverableException, UnroutableFlowEx } @Test - public void shouldUseSameSwitch() throws RecoverableException, UnroutableFlowException { + public void useSameSwitchTest() throws RecoverableException, UnroutableFlowException { // Topology: // D // | @@ -296,7 +296,7 @@ public void shouldUseSameSwitch() throws RecoverableException, UnroutableFlowExc } @Test - public void shouldChooseLongerPath() throws RecoverableException, UnroutableFlowException { + public void chooseLongerPathTest() throws RecoverableException, UnroutableFlowException { // Topology: // D----E // | | @@ -393,6 +393,91 @@ public void shouldChooseLongerPath() throws RecoverableException, UnroutableFlow ); } + @Test + public void choosePathWithoutSingleSwitchFlowTest() throws RecoverableException, UnroutableFlowException { + // Topology: + // -----E----D + // | | + // A----B----C Already created single switch flow: E + // Requested flow: A-B-C-D + + List isls = new ArrayList<>(); + isls.addAll(getBidirectionalIsls(switchA, 1, switchB, 2)); + isls.addAll(getBidirectionalIsls(switchB, 3, switchC, 4)); + isls.addAll(getBidirectionalIsls(switchC, 5, switchD, 6)); + isls.addAll(getBidirectionalIsls(switchD, 7, switchE, 8)); + isls.addAll(getBidirectionalIsls(switchE, 9, switchA, 10)); + + FlowPath forwardPath = FlowPath.builder() + .srcSwitch(switchE) + .destSwitch(switchE) + .pathId(FORWARD_PATH_ID) + .segments(Collections.emptyList()) + .build(); + when(flowPathRepository.findById(FORWARD_PATH_ID)).thenReturn(java.util.Optional.of(forwardPath)); + + FlowPath reversePath = FlowPath.builder() + .srcSwitch(switchE) + .destSwitch(switchE) + .pathId(REVERSE_PATH_ID) + .segments(Collections.emptyList()) + .build(); + when(flowPathRepository.findById(REVERSE_PATH_ID)).thenReturn(java.util.Optional.of(reversePath)); + + Flow flow = getFlow(); + flow.setSrcSwitch(switchA); + flow.setDestSwitch(switchD); + flow.setDiverseGroupId(DIVERSE_GROUP_ID); + flow.setForwardPathId(new PathId("forward_path_id")); + flow.setReversePathId(new PathId("reverse_path_id")); + flow.setMaxLatency(Long.MAX_VALUE); + flow.setPathComputationStrategy(pathComputationStrategy); + + when(config.getNetworkStrategy()).thenReturn(BuildStrategy.COST.name()); + when(islRepository.findActiveByBandwidthAndEncapsulationType(flow.getBandwidth(), flow.getEncapsulationType())) + .thenReturn(isls); + + when(flowPathRepository.findPathIdsByFlowDiverseGroupId(DIVERSE_GROUP_ID)) + .thenReturn(Lists.newArrayList(FORWARD_PATH_ID, REVERSE_PATH_ID)); + + // check diversity counter + AvailableNetwork availableNetwork = availableNetworkFactory.getAvailableNetwork(flow, Collections.emptyList()); + assertEquals(2, availableNetwork.getSwitch(switchE.getSwitchId()).getDiversityGroupUseCounter()); + + // check found path + PathComputer pathComputer = new InMemoryPathComputer( + availableNetworkFactory, new BestWeightAndShortestPathFinder(200), config); + + GetPathsResult path = pathComputer.getPath(flow); + assertThat(path, is(notNullValue())); + assertThat(path.getForward(), is(notNullValue())); + assertThat(path.getForward().getSegments().size(), is(3)); + assertThat(path.getReverse(), is(notNullValue())); + assertThat(path.getReverse().getSegments().size(), is(3)); + + Segment firstSegment = path.getForward().getSegments().get(0); + assertAll( + () -> assertThat(firstSegment.getSrcSwitchId(), equalTo(switchA.getSwitchId())), + () -> assertThat(firstSegment.getDestSwitchId(), equalTo(switchB.getSwitchId())) + ); + Segment lastSegment = path.getForward().getSegments().get(2); + assertAll( + () -> assertThat(lastSegment.getSrcSwitchId(), equalTo(switchC.getSwitchId())), + () -> assertThat(lastSegment.getDestSwitchId(), equalTo(switchD.getSwitchId())) + ); + + Segment firstReverseSegment = path.getReverse().getSegments().get(0); + assertAll( + () -> assertThat(firstReverseSegment.getSrcSwitchId(), equalTo(switchD.getSwitchId())), + () -> assertThat(firstReverseSegment.getDestSwitchId(), equalTo(switchC.getSwitchId())) + ); + Segment lastReverseSegment = path.getReverse().getSegments().get(2); + assertAll( + () -> assertThat(lastReverseSegment.getSrcSwitchId(), equalTo(switchB.getSwitchId())), + () -> assertThat(lastReverseSegment.getDestSwitchId(), equalTo(switchA.getSwitchId())) + ); + } + private static List getBidirectionalIsls(Switch srcSwitch, int srcPort, Switch dstSwitch, int dstPort) { return Lists.newArrayList( diff --git a/src-java/kilda-pce/src/test/java/org/openkilda/pce/impl/AvailableNetworkTest.java b/src-java/kilda-pce/src/test/java/org/openkilda/pce/impl/AvailableNetworkTest.java index 6d50e7f2174..52033c629ce 100644 --- a/src-java/kilda-pce/src/test/java/org/openkilda/pce/impl/AvailableNetworkTest.java +++ b/src-java/kilda-pce/src/test/java/org/openkilda/pce/impl/AvailableNetworkTest.java @@ -1,4 +1,4 @@ -/* Copyright 2021 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,10 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import org.openkilda.model.Flow; import org.openkilda.model.FlowPath; @@ -35,11 +37,14 @@ import org.hamcrest.Matchers; import org.junit.Test; +import java.util.Collections; import java.util.Set; import java.util.UUID; import java.util.stream.IntStream; public class AvailableNetworkTest { + private static final int cost = 700; + private static final WeightFunction WEIGHT_FUNCTION = edge -> { long total = edge.getCost(); if (edge.isUnderMaintenance()) { @@ -48,9 +53,9 @@ public class AvailableNetworkTest { if (edge.isUnstable()) { total += 10_000; } - total += edge.getDiversityGroupUseCounter() * 1000 - + edge.getDiversityGroupPerPopUseCounter() * 1000 - + edge.getDestSwitch().getDiversityGroupUseCounter() * 100; + total += edge.getDiversityGroupUseCounter() * 1000L + + edge.getDiversityGroupPerPopUseCounter() * 1000L + + edge.getDestSwitch().getDiversityGroupUseCounter() * 100L; return new PathWeight(total); }; @@ -64,29 +69,29 @@ public class AvailableNetworkTest { .build(); @Test - public void shouldNotAllowDuplicates() { + public void dontAllowDuplicatesTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SRC_SWITCH, DST_SWITCH, 7, 60, 10, 3); addLink(network, SRC_SWITCH, DST_SWITCH, 7, 60, 20, 5); - assertThat(network.getSwitch(SRC_SWITCH).getOutgoingLinks(), Matchers.hasSize(1)); - assertThat(network.getSwitch(SRC_SWITCH).getIncomingLinks(), Matchers.empty()); - assertThat(network.getSwitch(DST_SWITCH).getOutgoingLinks(), Matchers.empty()); - assertThat(network.getSwitch(DST_SWITCH).getIncomingLinks(), Matchers.hasSize(1)); + assertThat(network.getSwitch(SRC_SWITCH).getOutgoingLinks().size(), is(1)); + assertTrue(network.getSwitch(SRC_SWITCH).getIncomingLinks().isEmpty()); + assertTrue(network.getSwitch(DST_SWITCH).getOutgoingLinks().isEmpty()); + assertThat(network.getSwitch(DST_SWITCH).getIncomingLinks().size(), is(1)); } @Test - public void shouldKeepLinksWithOtherSwitchesAfterReducing() { + public void keepLinksWithOtherSwitchesAfterReducingTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SRC_SWITCH, new SwitchId("00:00"), 1, 1, 20, 5); addLink(network, SRC_SWITCH, DST_SWITCH, 2, 2, 10, 3); addLink(network, DST_SWITCH, SRC_SWITCH, 1, 1, 20, 5); addLink(network, new SwitchId("00:00"), SRC_SWITCH, 2, 2, 10, 3); - assertThat(network.getSwitch(SRC_SWITCH).getOutgoingLinks(), Matchers.hasSize(2)); - assertThat(network.getSwitch(SRC_SWITCH).getIncomingLinks(), Matchers.hasSize(2)); + assertThat(network.getSwitch(SRC_SWITCH).getOutgoingLinks().size(), is(2)); + assertThat(network.getSwitch(SRC_SWITCH).getIncomingLinks().size(), is(2)); } private static final SwitchId SWITCH_1 = new SwitchId("00:00:00:00:00:00:00:01"); @@ -100,10 +105,7 @@ public void shouldKeepLinksWithOtherSwitchesAfterReducing() { private static final String POP_4 = "pop4"; @Test - public void shouldUpdateEdgeWeightWithPopDiversityPenalty() { - int cost = 700; - final WeightFunction weightFunction = WEIGHT_FUNCTION; - + public void updateEdgeWeightWithPopDiversityPenaltyTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SWITCH_1, SWITCH_2, 1, 2, cost, 5, POP_1, POP_2); addLink(network, SWITCH_1, SWITCH_3, 2, 1, cost, 5, POP_1, POP_4); @@ -112,13 +114,12 @@ public void shouldUpdateEdgeWeightWithPopDiversityPenalty() { addLink(network, SWITCH_5, SWITCH_3, 2, 2, cost, 5, POP_3, POP_4); addLink(network, SWITCH_5, SWITCH_2, 3, 2, cost, 5, POP_3, POP_2); - network.processDiversitySegmentsWithPop( asList(buildPathWithSegment(SWITCH_1, SWITCH_3, 2, 1, POP_1, POP_4, 0), buildPathWithSegment(SWITCH_3, SWITCH_5, 2, 2, POP_4, POP_3, 1))); long expectedWeight = cost + 1000L; for (Edge edge : network.edges) { - long currentWeight = weightFunction.apply(edge).getTotalWeight(); + long currentWeight = WEIGHT_FUNCTION.apply(edge).getTotalWeight(); if (edge.getSrcSwitch().getPop().equals(POP_4) || edge.getDestSwitch().getPop().equals(POP_4)) { assertEquals(expectedWeight, currentWeight); @@ -129,10 +130,7 @@ public void shouldUpdateEdgeWeightWithPopDiversityPenalty() { } @Test - public void shouldNotUpdateWeightsWhenTransitSegmentsNotInPop() { - int cost = 700; - final WeightFunction weightFunction = WEIGHT_FUNCTION; - + public void dontUpdateWeightsWhenTransitSegmentsNotInPopTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SWITCH_1, SWITCH_2, 1, 2, cost, 5, POP_1, POP_2); addLink(network, SWITCH_1, SWITCH_3, 2, 1, cost, 5, POP_1, null); @@ -141,21 +139,17 @@ public void shouldNotUpdateWeightsWhenTransitSegmentsNotInPop() { addLink(network, SWITCH_5, SWITCH_3, 2, 2, cost, 5, POP_3, null); addLink(network, SWITCH_5, SWITCH_2, 3, 2, cost, 5, POP_3, POP_2); - network.processDiversitySegmentsWithPop( asList(buildPathWithSegment(SWITCH_1, SWITCH_3, 2, 1, POP_1, null, 0), buildPathWithSegment(SWITCH_3, SWITCH_5, 2, 2, null, POP_3, 1))); for (Edge edge : network.edges) { - long currentWeight = weightFunction.apply(edge).getTotalWeight(); + long currentWeight = WEIGHT_FUNCTION.apply(edge).getTotalWeight(); assertEquals(cost, currentWeight); } } @Test - public void shouldNotUpdateWeightsWhenTransitSegmentsOnlyInPop() { - int cost = 700; - final WeightFunction weightFunction = WEIGHT_FUNCTION; - + public void dontUpdateWeightsWhenTransitSegmentsOnlyInPopTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SWITCH_1, SWITCH_2, 1, 2, cost, 5, null, null); addLink(network, SWITCH_1, SWITCH_3, 2, 1, cost, 5, null, POP_4); @@ -164,21 +158,17 @@ public void shouldNotUpdateWeightsWhenTransitSegmentsOnlyInPop() { addLink(network, SWITCH_5, SWITCH_3, 2, 2, cost, 5, null, POP_4); addLink(network, SWITCH_5, SWITCH_2, 3, 2, cost, 5, null, null); - network.processDiversitySegmentsWithPop( asList(buildPathWithSegment(SWITCH_1, SWITCH_3, 2, 1, POP_1, null, 0), buildPathWithSegment(SWITCH_3, SWITCH_5, 2, 2, null, POP_3, 1))); for (Edge edge : network.edges) { - long currentWeight = weightFunction.apply(edge).getTotalWeight(); + long currentWeight = WEIGHT_FUNCTION.apply(edge).getTotalWeight(); assertEquals(cost, currentWeight); } } @Test - public void shouldNotUpdateEdgeWeightWithPopDiversityPenaltyIfNoPop() { - int cost = 700; - final WeightFunction weightFunction = WEIGHT_FUNCTION; - + public void dontUpdateEdgeWeightWithPopDiversityPenaltyIfNoPopTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SWITCH_1, SWITCH_2, 1, 2, cost, 5); addLink(network, SWITCH_1, SWITCH_3, 2, 1, cost, 5); @@ -187,18 +177,17 @@ public void shouldNotUpdateEdgeWeightWithPopDiversityPenaltyIfNoPop() { addLink(network, SWITCH_5, SWITCH_3, 2, 2, cost, 5); addLink(network, SWITCH_5, SWITCH_2, 3, 2, cost, 5); - network.processDiversitySegmentsWithPop( asList(buildPathSegment(SWITCH_1, SWITCH_3, 2, 1, 0), buildPathSegment(SWITCH_3, SWITCH_5, 2, 2, 1))); for (Edge edge : network.edges) { - long currentWeight = weightFunction.apply(edge).getTotalWeight(); + long currentWeight = WEIGHT_FUNCTION.apply(edge).getTotalWeight(); assertEquals(cost, currentWeight); } } @Test - public void shouldSetEqualCostForPairedLinks() { + public void setEqualCostForPairedLinksTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SRC_SWITCH, DST_SWITCH, 7, 60, 10, 3); @@ -222,7 +211,7 @@ public void shouldSetEqualCostForPairedLinks() { } @Test - public void shouldCreateSymmetricOutgoingAndIncomming() { + public void createSymmetricOutgoingAndIncomingTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SRC_SWITCH, DST_SWITCH, 7, 60, 10, 3); @@ -242,7 +231,7 @@ public void shouldCreateSymmetricOutgoingAndIncomming() { } @Test - public void shouldFillDiversityWeightsIngress() { + public void fillDiversityWeightsIngressTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SRC_SWITCH, DST_SWITCH, 7, 60, 10, 3); @@ -257,7 +246,7 @@ public void shouldFillDiversityWeightsIngress() { } @Test - public void shouldFillEmptyDiversityWeightsForTerminatingSwitch() { + public void fillEmptyDiversityWeightsForTerminatingSwitchTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SRC_SWITCH, DST_SWITCH, 7, 60, 10, 3); @@ -279,7 +268,7 @@ public void shouldFillEmptyDiversityWeightsForTerminatingSwitch() { } @Test - public void shouldFillDiversityWeightsTransit() { + public void fillDiversityWeightsTransitTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SRC_SWITCH, DST_SWITCH, 7, 60, 10, 3); @@ -294,7 +283,7 @@ public void shouldFillDiversityWeightsTransit() { } @Test - public void shouldFillAffinityWeights() { + public void fillAffinityWeightsTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SRC_SWITCH, DST_SWITCH, 7, 60, 10, 3); addLink(network, SRC_SWITCH, DST_SWITCH, 8, 61, 10, 3); @@ -317,7 +306,7 @@ public void shouldFillAffinityWeights() { A = B - C = D */ @Test - public void shouldFillDiversityWeightsPartiallyConnected() { + public void fillDiversityWeightsPartiallyConnectedTest() { SwitchId switchA = new SwitchId("A"); SwitchId switchB = new SwitchId("B"); SwitchId switchC = new SwitchId("C"); @@ -340,7 +329,7 @@ public void shouldFillDiversityWeightsPartiallyConnected() { } @Test - public void shouldProcessAbsentDiversitySegment() { + public void processAbsentDiversitySegmentTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SRC_SWITCH, DST_SWITCH, 7, 60, 10, 3); @@ -356,6 +345,35 @@ public void shouldProcessAbsentDiversitySegment() { assertEquals(1, edge.getSrcSwitch().getDiversityGroupUseCounter()); } + @Test + public void processDiversityGroupForSingleSwitchFlowTest() { + AvailableNetwork network = new AvailableNetwork(); + + Switch srcSwitch = Switch.builder().switchId(SWITCH_1).build(); + + PathId pathId = new PathId(UUID.randomUUID().toString()); + FlowPath flowPath = FlowPath.builder() + .pathId(pathId) + .srcSwitch(srcSwitch) + .destSwitch(srcSwitch) + .segments(Collections.emptyList()) + .build(); + + addLink(network, SWITCH_1, SWITCH_1, + 7, 60, 10, 3); + + network.processDiversityGroupForSingleSwitchFlow(flowPath); + + Node node = network.getSwitch(SWITCH_1); + + assertEquals(1, node.getDiversityGroupUseCounter()); + + Edge edge = node.getOutgoingLinks().iterator().next(); + assertEquals(0, edge.getDiversityGroupUseCounter()); + assertEquals(1, edge.getDestSwitch().getDiversityGroupUseCounter()); + assertEquals(1, edge.getSrcSwitch().getDiversityGroupUseCounter()); + } + private void addLink(AvailableNetwork network, SwitchId srcDpid, SwitchId dstDpid, int srcPort, int dstPort, int cost, int latency) { addLink(network, srcDpid, dstDpid, srcPort, dstPort, cost, latency, null, null); From 05bdb861c04d3855f616e9d28d833733f2d4b635 Mon Sep 17 00:00:00 2001 From: Fedir Kantur Date: Wed, 8 Feb 2023 14:14:28 +0000 Subject: [PATCH 05/45] Fix style check issue --- .../flowhs/validation/FlowValidatorTest.java | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java index f097bb2a922..1b306b21b13 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java @@ -16,22 +16,32 @@ package org.openkilda.wfm.topology.flowhs.validation; import static com.google.common.collect.Sets.newHashSet; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.openkilda.model.FlowEncapsulationType.TRANSIT_VLAN; import static org.openkilda.model.FlowEncapsulationType.VXLAN; -import org.junit.Before; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.openkilda.model.*; +import org.openkilda.model.Flow; +import org.openkilda.model.FlowEndpoint; +import org.openkilda.model.Switch; +import org.openkilda.model.SwitchId; +import org.openkilda.model.SwitchProperties; import org.openkilda.persistence.PersistenceManager; -import org.openkilda.persistence.repositories.*; +import org.openkilda.persistence.repositories.FlowMirrorPathRepository; +import org.openkilda.persistence.repositories.FlowRepository; +import org.openkilda.persistence.repositories.IslRepository; +import org.openkilda.persistence.repositories.RepositoryFactory; +import org.openkilda.persistence.repositories.SwitchPropertiesRepository; +import org.openkilda.persistence.repositories.SwitchRepository; import org.openkilda.wfm.topology.flowhs.model.DetectConnectedDevices; import org.openkilda.wfm.topology.flowhs.model.RequestedFlow; import org.openkilda.wfm.topology.flowhs.validation.FlowValidator.EndpointDescriptor; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; import java.util.HashSet; import java.util.Optional; From 416c8ff5946a324e58f1e760aaed21635d96d5c8 Mon Sep 17 00:00:00 2001 From: Fedir Kantur Date: Thu, 9 Feb 2023 12:07:17 +0000 Subject: [PATCH 06/45] Fixes after code review --- .../flowhs/validation/FlowValidator.java | 5 -- .../flowhs/validation/FlowValidatorTest.java | 33 ++++++---- .../pce/finder/DiversityPathFinderTest.java | 5 +- .../pce/impl/AvailableNetworkTest.java | 62 +++++++++---------- 4 files changed, 55 insertions(+), 50 deletions(-) diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidator.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidator.java index 96db2e5ce79..f97d8e7f8e0 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidator.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidator.java @@ -399,11 +399,6 @@ private void checkOneSwitchFlowConflict(FlowEndpoint source, FlowEndpoint destin @VisibleForTesting void checkDiverseFlow(RequestedFlow targetFlow) throws InvalidFlowException { - if (targetFlow.isOneSwitchFlow()) { - throw new InvalidFlowException("Couldn't add one-switch flow into diverse group", - ErrorType.PARAMETERS_INVALID); - } - Flow diverseFlow = flowRepository.findById(targetFlow.getDiverseFlowId()).orElse(null); if (diverseFlow == null) { YFlow diverseYFlow = yFlowRepository.findById(targetFlow.getDiverseFlowId()) diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java index 1b306b21b13..1bde0cebaa0 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java @@ -266,9 +266,13 @@ public void doesntFailIfMaxLatencyTier2andMaxLatencyAreEqualTest() throws Invali flowValidator.checkMaxLatency(flow); } - @Test(expected = InvalidFlowException.class) - public void failOnAddingOneSwitchFlowToDiverseGroupWithExistingFlowTest() throws InvalidFlowException { + @Test + public void doesntFailOnAddingOneSwitchFlowToDiverseGroupWithExistingFlowTest() throws InvalidFlowException { RequestedFlow oneSwitchFlow = buildOneSwitchFlow(); + + Flow flow = buildDiverseGroupFlow(SWITCH_ID_1, PORT_1, SWITCH_ID_2, PORT_1); + + when(flowRepository.findById(DIVERSE_FLOW_ID)).thenReturn(Optional.of(flow)); flowValidator.checkDiverseFlow(oneSwitchFlow); } @@ -284,16 +288,7 @@ public void doesntFailOnAddingFlowToDiverseGroupWithExistingOneSwitchFlowTest() .diverseFlowId(DIVERSE_FLOW_ID) .build(); - Switch singleSwitch = Switch.builder().switchId(SWITCH_ID_1).build(); - - Flow oneSwitchFlow = Flow.builder() - .flowId("oneSwitchFlow") - .srcSwitch(singleSwitch) - .srcPort(PORT_1) - .destSwitch(singleSwitch) - .destPort(11) - .diverseGroupId(DIVERSE_FLOW_ID) - .build(); + Flow oneSwitchFlow = buildDiverseGroupFlow(SWITCH_ID_1, PORT_1, SWITCH_ID_1, 11); when(flowRepository.findById(DIVERSE_FLOW_ID)).thenReturn(Optional.of(oneSwitchFlow)); flowValidator.checkDiverseFlow(flow); @@ -339,4 +334,18 @@ private RequestedFlow buildOneSwitchFlow() { .diverseFlowId(DIVERSE_FLOW_ID) .build(); } + + private Flow buildDiverseGroupFlow(SwitchId srcSwitchId, int srcPort, SwitchId destSwitchId, int destPort) { + Switch srcSwitch = Switch.builder().switchId(srcSwitchId).build(); + Switch destSwitch = Switch.builder().switchId(destSwitchId).build(); + + return Flow.builder() + .flowId(FLOW_2) + .srcSwitch(srcSwitch) + .srcPort(srcPort) + .destSwitch(destSwitch) + .destPort(destPort) + .diverseGroupId(DIVERSE_FLOW_ID) + .build(); + } } diff --git a/src-java/kilda-pce/src/test/java/org/openkilda/pce/finder/DiversityPathFinderTest.java b/src-java/kilda-pce/src/test/java/org/openkilda/pce/finder/DiversityPathFinderTest.java index a7cdc463f76..756179e125c 100644 --- a/src-java/kilda-pce/src/test/java/org/openkilda/pce/finder/DiversityPathFinderTest.java +++ b/src-java/kilda-pce/src/test/java/org/openkilda/pce/finder/DiversityPathFinderTest.java @@ -60,6 +60,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; @RunWith(Parameterized.class) public class DiversityPathFinderTest { @@ -414,7 +415,7 @@ public void choosePathWithoutSingleSwitchFlowTest() throws RecoverableException, .pathId(FORWARD_PATH_ID) .segments(Collections.emptyList()) .build(); - when(flowPathRepository.findById(FORWARD_PATH_ID)).thenReturn(java.util.Optional.of(forwardPath)); + when(flowPathRepository.findById(FORWARD_PATH_ID)).thenReturn(Optional.of(forwardPath)); FlowPath reversePath = FlowPath.builder() .srcSwitch(switchE) @@ -422,7 +423,7 @@ public void choosePathWithoutSingleSwitchFlowTest() throws RecoverableException, .pathId(REVERSE_PATH_ID) .segments(Collections.emptyList()) .build(); - when(flowPathRepository.findById(REVERSE_PATH_ID)).thenReturn(java.util.Optional.of(reversePath)); + when(flowPathRepository.findById(REVERSE_PATH_ID)).thenReturn(Optional.of(reversePath)); Flow flow = getFlow(); flow.setSrcSwitch(switchA); diff --git a/src-java/kilda-pce/src/test/java/org/openkilda/pce/impl/AvailableNetworkTest.java b/src-java/kilda-pce/src/test/java/org/openkilda/pce/impl/AvailableNetworkTest.java index 52033c629ce..c3b4f587cfc 100644 --- a/src-java/kilda-pce/src/test/java/org/openkilda/pce/impl/AvailableNetworkTest.java +++ b/src-java/kilda-pce/src/test/java/org/openkilda/pce/impl/AvailableNetworkTest.java @@ -43,7 +43,7 @@ import java.util.stream.IntStream; public class AvailableNetworkTest { - private static final int cost = 700; + private static final int COST = 700; private static final WeightFunction WEIGHT_FUNCTION = edge -> { long total = edge.getCost(); @@ -107,24 +107,24 @@ public void keepLinksWithOtherSwitchesAfterReducingTest() { @Test public void updateEdgeWeightWithPopDiversityPenaltyTest() { AvailableNetwork network = new AvailableNetwork(); - addLink(network, SWITCH_1, SWITCH_2, 1, 2, cost, 5, POP_1, POP_2); - addLink(network, SWITCH_1, SWITCH_3, 2, 1, cost, 5, POP_1, POP_4); - addLink(network, SWITCH_1, SWITCH_4, 3, 1, cost, 5, POP_1, POP_4); - addLink(network, SWITCH_5, SWITCH_4, 1, 2, cost, 5, POP_3, POP_4); - addLink(network, SWITCH_5, SWITCH_3, 2, 2, cost, 5, POP_3, POP_4); - addLink(network, SWITCH_5, SWITCH_2, 3, 2, cost, 5, POP_3, POP_2); + addLink(network, SWITCH_1, SWITCH_2, 1, 2, COST, 5, POP_1, POP_2); + addLink(network, SWITCH_1, SWITCH_3, 2, 1, COST, 5, POP_1, POP_4); + addLink(network, SWITCH_1, SWITCH_4, 3, 1, COST, 5, POP_1, POP_4); + addLink(network, SWITCH_5, SWITCH_4, 1, 2, COST, 5, POP_3, POP_4); + addLink(network, SWITCH_5, SWITCH_3, 2, 2, COST, 5, POP_3, POP_4); + addLink(network, SWITCH_5, SWITCH_2, 3, 2, COST, 5, POP_3, POP_2); network.processDiversitySegmentsWithPop( asList(buildPathWithSegment(SWITCH_1, SWITCH_3, 2, 1, POP_1, POP_4, 0), buildPathWithSegment(SWITCH_3, SWITCH_5, 2, 2, POP_4, POP_3, 1))); - long expectedWeight = cost + 1000L; + long expectedWeight = COST + 1000L; for (Edge edge : network.edges) { long currentWeight = WEIGHT_FUNCTION.apply(edge).getTotalWeight(); if (edge.getSrcSwitch().getPop().equals(POP_4) || edge.getDestSwitch().getPop().equals(POP_4)) { assertEquals(expectedWeight, currentWeight); } else { - assertEquals(cost, currentWeight); + assertEquals(COST, currentWeight); } } } @@ -132,57 +132,57 @@ public void updateEdgeWeightWithPopDiversityPenaltyTest() { @Test public void dontUpdateWeightsWhenTransitSegmentsNotInPopTest() { AvailableNetwork network = new AvailableNetwork(); - addLink(network, SWITCH_1, SWITCH_2, 1, 2, cost, 5, POP_1, POP_2); - addLink(network, SWITCH_1, SWITCH_3, 2, 1, cost, 5, POP_1, null); - addLink(network, SWITCH_1, SWITCH_4, 3, 1, cost, 5, POP_1, POP_4); - addLink(network, SWITCH_5, SWITCH_4, 1, 2, cost, 5, POP_3, POP_4); - addLink(network, SWITCH_5, SWITCH_3, 2, 2, cost, 5, POP_3, null); - addLink(network, SWITCH_5, SWITCH_2, 3, 2, cost, 5, POP_3, POP_2); + addLink(network, SWITCH_1, SWITCH_2, 1, 2, COST, 5, POP_1, POP_2); + addLink(network, SWITCH_1, SWITCH_3, 2, 1, COST, 5, POP_1, null); + addLink(network, SWITCH_1, SWITCH_4, 3, 1, COST, 5, POP_1, POP_4); + addLink(network, SWITCH_5, SWITCH_4, 1, 2, COST, 5, POP_3, POP_4); + addLink(network, SWITCH_5, SWITCH_3, 2, 2, COST, 5, POP_3, null); + addLink(network, SWITCH_5, SWITCH_2, 3, 2, COST, 5, POP_3, POP_2); network.processDiversitySegmentsWithPop( asList(buildPathWithSegment(SWITCH_1, SWITCH_3, 2, 1, POP_1, null, 0), buildPathWithSegment(SWITCH_3, SWITCH_5, 2, 2, null, POP_3, 1))); for (Edge edge : network.edges) { long currentWeight = WEIGHT_FUNCTION.apply(edge).getTotalWeight(); - assertEquals(cost, currentWeight); + assertEquals(COST, currentWeight); } } @Test public void dontUpdateWeightsWhenTransitSegmentsOnlyInPopTest() { AvailableNetwork network = new AvailableNetwork(); - addLink(network, SWITCH_1, SWITCH_2, 1, 2, cost, 5, null, null); - addLink(network, SWITCH_1, SWITCH_3, 2, 1, cost, 5, null, POP_4); - addLink(network, SWITCH_1, SWITCH_4, 3, 1, cost, 5, null, null); - addLink(network, SWITCH_5, SWITCH_4, 1, 2, cost, 5, null, null); - addLink(network, SWITCH_5, SWITCH_3, 2, 2, cost, 5, null, POP_4); - addLink(network, SWITCH_5, SWITCH_2, 3, 2, cost, 5, null, null); + addLink(network, SWITCH_1, SWITCH_2, 1, 2, COST, 5, null, null); + addLink(network, SWITCH_1, SWITCH_3, 2, 1, COST, 5, null, POP_4); + addLink(network, SWITCH_1, SWITCH_4, 3, 1, COST, 5, null, null); + addLink(network, SWITCH_5, SWITCH_4, 1, 2, COST, 5, null, null); + addLink(network, SWITCH_5, SWITCH_3, 2, 2, COST, 5, null, POP_4); + addLink(network, SWITCH_5, SWITCH_2, 3, 2, COST, 5, null, null); network.processDiversitySegmentsWithPop( asList(buildPathWithSegment(SWITCH_1, SWITCH_3, 2, 1, POP_1, null, 0), buildPathWithSegment(SWITCH_3, SWITCH_5, 2, 2, null, POP_3, 1))); for (Edge edge : network.edges) { long currentWeight = WEIGHT_FUNCTION.apply(edge).getTotalWeight(); - assertEquals(cost, currentWeight); + assertEquals(COST, currentWeight); } } @Test public void dontUpdateEdgeWeightWithPopDiversityPenaltyIfNoPopTest() { AvailableNetwork network = new AvailableNetwork(); - addLink(network, SWITCH_1, SWITCH_2, 1, 2, cost, 5); - addLink(network, SWITCH_1, SWITCH_3, 2, 1, cost, 5); - addLink(network, SWITCH_1, SWITCH_4, 3, 1, cost, 5); - addLink(network, SWITCH_5, SWITCH_4, 1, 2, cost, 5); - addLink(network, SWITCH_5, SWITCH_3, 2, 2, cost, 5); - addLink(network, SWITCH_5, SWITCH_2, 3, 2, cost, 5); + addLink(network, SWITCH_1, SWITCH_2, 1, 2, COST, 5); + addLink(network, SWITCH_1, SWITCH_3, 2, 1, COST, 5); + addLink(network, SWITCH_1, SWITCH_4, 3, 1, COST, 5); + addLink(network, SWITCH_5, SWITCH_4, 1, 2, COST, 5); + addLink(network, SWITCH_5, SWITCH_3, 2, 2, COST, 5); + addLink(network, SWITCH_5, SWITCH_2, 3, 2, COST, 5); network.processDiversitySegmentsWithPop( asList(buildPathSegment(SWITCH_1, SWITCH_3, 2, 1, 0), buildPathSegment(SWITCH_3, SWITCH_5, 2, 2, 1))); for (Edge edge : network.edges) { long currentWeight = WEIGHT_FUNCTION.apply(edge).getTotalWeight(); - assertEquals(cost, currentWeight); + assertEquals(COST, currentWeight); } } @@ -211,7 +211,7 @@ public void setEqualCostForPairedLinksTest() { } @Test - public void createSymmetricOutgoingAndIncomingTest() { + public void createSymmetricOutgoingAndIncomingLinksTest() { AvailableNetwork network = new AvailableNetwork(); addLink(network, SRC_SWITCH, DST_SWITCH, 7, 60, 10, 3); From 97c4998939f7c99be379a7efc5586cb107a3f0bf Mon Sep 17 00:00:00 2001 From: Pablo Murillo Nogales Date: Wed, 8 Feb 2023 17:59:17 +0100 Subject: [PATCH 07/45] validate that flow_id in path and in body are the same during flow update request During the flow update request from v1 and v2 API the flow_id in the path was ignored and kilda was only updating the flow with the flow_id from the request body. This commit validates that the flow_id in the path and the flow_id in the body are the same for both, v1 and v2 APIs. If not, the controller will raise an exception so the northbound will return a bad request error. Closes #5075 --- .../controller/v1/FlowController.java | 2 +- .../controller/v2/FlowControllerV2.java | 2 +- .../northbound/service/FlowService.java | 4 +- .../service/impl/FlowServiceImpl.java | 18 +++-- .../northbound/controller/TestConfig.java | 2 +- .../{v1 => mock}/TestMessageMock.java | 71 ++++++++++++++----- .../controller/v1/FlowControllerTest.java | 39 +++++++--- .../controller/v1/SwitchControllerTest.java | 4 +- .../controller/v2/FlowControllerTest.java | 19 ++++- 9 files changed, 119 insertions(+), 42 deletions(-) rename src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/{v1 => mock}/TestMessageMock.java (76%) diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/FlowController.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/FlowController.java index 003911b8fcc..ad05eef1d2a 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/FlowController.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/FlowController.java @@ -137,7 +137,7 @@ public CompletableFuture deleteFlow(@PathVariable(name = "f @ResponseStatus(HttpStatus.OK) public CompletableFuture updateFlow(@PathVariable(name = "flow-id") String flowId, @RequestBody FlowUpdatePayload flow) { - return flowService.updateFlow(flow); + return flowService.updateFlow(flowId, flow); } /** diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/FlowControllerV2.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/FlowControllerV2.java index a641919104d..3b47273456e 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/FlowControllerV2.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/FlowControllerV2.java @@ -80,7 +80,7 @@ public CompletableFuture createFlow(@RequestBody FlowRequestV2 f public CompletableFuture updateFlow(@PathVariable(name = "flow_id") String flowId, @RequestBody FlowRequestV2 flow) { verifyRequest(flow); - return flowService.updateFlow(flow); + return flowService.updateFlow(flowId, flow); } /** diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/FlowService.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/FlowService.java index 863d756109b..6418894c3fe 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/FlowService.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/FlowService.java @@ -87,7 +87,7 @@ public interface FlowService { * @param flow flow * @return updated flow */ - CompletableFuture updateFlow(final FlowUpdatePayload flow); + CompletableFuture updateFlow(final String flowId, final FlowUpdatePayload flow); /** * Updates flow. @@ -95,7 +95,7 @@ public interface FlowService { * @param flow flow * @return updated flow */ - CompletableFuture updateFlow(FlowRequestV2 flow); + CompletableFuture updateFlow(final String flowId, FlowRequestV2 flow); /** * Patch flow. diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/FlowServiceImpl.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/FlowServiceImpl.java index 7bb83e6b75f..93ba668b46f 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/FlowServiceImpl.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/FlowServiceImpl.java @@ -239,8 +239,8 @@ public CompletableFuture getFlowV2(final String id) { * {@inheritDoc} */ @Override - public CompletableFuture updateFlow(final FlowUpdatePayload request) { - log.info("API request: Update flow request for flow {}", request); + public CompletableFuture updateFlow(final String flowId, final FlowUpdatePayload request) { + log.info("API request: Update flow request for flow {}: {}", flowId, request); final String correlationId = RequestCorrelationId.getId(); FlowRequest updateRequest; @@ -252,6 +252,7 @@ public CompletableFuture updateFlow(final FlowUpdatePayload throw new MessageException(correlationId, System.currentTimeMillis(), ErrorType.DATA_INVALID, e.getMessage(), "Can not parse arguments of the update flow request"); } + validateFlowId(updateRequest.getFlowId(), flowId, correlationId); CommandMessage command = new CommandMessage(updateRequest, System.currentTimeMillis(), correlationId, Destination.WFM); @@ -263,8 +264,8 @@ public CompletableFuture updateFlow(final FlowUpdatePayload } @Override - public CompletableFuture updateFlow(FlowRequestV2 request) { - log.info("API request: Processing flow update: {}", request); + public CompletableFuture updateFlow(final String flowId, FlowRequestV2 request) { + log.info("API request: Update flow request for flow {}: {}", flowId, request); final String correlationId = RequestCorrelationId.getId(); FlowRequest updateRequest; @@ -276,6 +277,7 @@ public CompletableFuture updateFlow(FlowRequestV2 request) { throw new MessageException(correlationId, System.currentTimeMillis(), ErrorType.DATA_INVALID, e.getMessage(), "Can not parse arguments of the update flow request"); } + validateFlowId(updateRequest.getFlowId(), flowId, correlationId); CommandMessage command = new CommandMessage(updateRequest, System.currentTimeMillis(), correlationId, Destination.WFM); @@ -861,4 +863,12 @@ private CompletableFuture> handleGetAllFlowsRequest( .map(encoder) .collect(Collectors.toList())); } + + private void validateFlowId(String requestFlowId, String pathFlowId, String correlationId) { + if (!requestFlowId.equals(pathFlowId)) { + throw new MessageException(correlationId, System.currentTimeMillis(), ErrorType.DATA_INVALID, + "flow_id from body and from path are different", + format("Body flow_id: %s, path flow_id: %s", requestFlowId, pathFlowId)); + } + } } diff --git a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/TestConfig.java b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/TestConfig.java index d957ca40a53..af53189977c 100644 --- a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/TestConfig.java +++ b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/TestConfig.java @@ -18,7 +18,7 @@ import org.openkilda.northbound.config.KafkaConfig; import org.openkilda.northbound.config.SecurityConfig; import org.openkilda.northbound.config.WebConfig; -import org.openkilda.northbound.controller.v1.TestMessageMock; +import org.openkilda.northbound.controller.mock.TestMessageMock; import org.openkilda.northbound.messaging.MessagingChannel; import org.openkilda.northbound.utils.CorrelationIdFactory; import org.openkilda.northbound.utils.TestCorrelationIdFactory; diff --git a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v1/TestMessageMock.java b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/mock/TestMessageMock.java similarity index 76% rename from src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v1/TestMessageMock.java rename to src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/mock/TestMessageMock.java index d68c638c24f..c65edb3e497 100644 --- a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v1/TestMessageMock.java +++ b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/mock/TestMessageMock.java @@ -13,10 +13,12 @@ * limitations under the License. */ -package org.openkilda.northbound.controller.v1; +package org.openkilda.northbound.controller.mock; +import static java.lang.String.format; import static java.util.Collections.singletonList; import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.openkilda.messaging.Utils.DEFAULT_CORRELATION_ID; import static org.openkilda.messaging.error.ErrorType.OPERATION_TIMED_OUT; import org.openkilda.messaging.Destination; @@ -30,6 +32,7 @@ import org.openkilda.messaging.error.ErrorData; import org.openkilda.messaging.error.ErrorMessage; import org.openkilda.messaging.error.ErrorType; +import org.openkilda.messaging.error.MessageError; import org.openkilda.messaging.error.MessageException; import org.openkilda.messaging.info.InfoData; import org.openkilda.messaging.info.event.PathInfoData; @@ -53,6 +56,7 @@ import org.openkilda.model.SwitchId; import org.openkilda.northbound.dto.v2.flows.DetectConnectedDevicesV2; import org.openkilda.northbound.dto.v2.flows.FlowEndpointV2; +import org.openkilda.northbound.dto.v2.flows.FlowRequestV2; import org.openkilda.northbound.dto.v2.flows.SwapFlowEndpointPayload; import org.openkilda.northbound.dto.v2.flows.SwapFlowPayload; import org.openkilda.northbound.messaging.MessagingChannel; @@ -71,33 +75,42 @@ */ @Component public class TestMessageMock implements MessagingChannel { - static final String FLOW_ID = "ff:00"; + public static final String FLOW_ID = "flow_id_1"; static final String SECOND_FLOW_ID = "second_flow"; - static final SwitchId SWITCH_ID = new SwitchId(FLOW_ID); + public static final String FLOW_ID_FROM_PATH = "different_flow_id"; + static final SwitchId SWITCH_ID = new SwitchId("ff:00"); static final SwitchId SECOND_SWITCH_ID = new SwitchId("ff:01"); - static final String ERROR_FLOW_ID = "error-flow"; - static final String TEST_SWITCH_ID = "ff:01"; - static final long TEST_SWITCH_RULE_COOKIE = 1L; - static final FlowEndpointPayload flowEndpoint = new FlowEndpointPayload(SWITCH_ID, 1, 1, + public static final String ERROR_FLOW_ID = "error-flow"; + public static final String TEST_SWITCH_ID = "ff:01"; + public static final long TEST_SWITCH_RULE_COOKIE = 1L; + public static final FlowEndpointPayload FLOW_ENDPOINT = new FlowEndpointPayload(SWITCH_ID, 1, 1, new DetectConnectedDevicesPayload(false, false)); static final FlowEndpointPayload secondFlowEndpoint = new FlowEndpointPayload(SECOND_SWITCH_ID, 2, 2, new DetectConnectedDevicesPayload(false, false)); - static final FlowEndpointV2 flowPayloadEndpoint = new FlowEndpointV2(SWITCH_ID, 1, 1, + public static final FlowEndpointV2 FLOW_PAYLOAD_ENDPOINT = new FlowEndpointV2(SWITCH_ID, 1, 1, new DetectConnectedDevicesV2(false, false)); static final FlowEndpointV2 secondFlowPayloadEndpoint = new FlowEndpointV2(SECOND_SWITCH_ID, 2, 2, new DetectConnectedDevicesV2(false, false)); - public static final FlowPayload flow = FlowPayload.builder() + + public static final FlowPayload FLOW = FlowPayload.builder() .id(FLOW_ID) - .source(flowEndpoint) - .destination(flowEndpoint) + .source(FLOW_ENDPOINT) + .destination(FLOW_ENDPOINT) .maximumBandwidth(10000) .description(FLOW_ID) .status(FlowState.UP.getState()) .build(); - public static final FlowResponsePayload flowResponsePayload = FlowResponsePayload.flowResponsePayloadBuilder() + + public static final FlowRequestV2 FLOW_REQUEST_V2 = FlowRequestV2.builder() + .flowId(FLOW_ID) + .source(FLOW_PAYLOAD_ENDPOINT) + .destination(FLOW_PAYLOAD_ENDPOINT) + .build(); + + public static final FlowResponsePayload FLOW_RESPONSE_PAYLOAD = FlowResponsePayload.flowResponsePayloadBuilder() .id(FLOW_ID) - .source(flowEndpoint) - .destination(flowEndpoint) + .source(FLOW_ENDPOINT) + .destination(FLOW_ENDPOINT) .maximumBandwidth(10000) .description(FLOW_ID) .status(FlowState.UP.getState()) @@ -105,8 +118,8 @@ public class TestMessageMock implements MessagingChannel { public static final SwapFlowPayload firstSwapFlow = SwapFlowPayload.builder() .flowId(FLOW_ID) - .source(flowPayloadEndpoint) - .destination(flowPayloadEndpoint) + .source(FLOW_PAYLOAD_ENDPOINT) + .destination(FLOW_PAYLOAD_ENDPOINT) .build(); public static final SwapFlowPayload secondSwapFlow = SwapFlowPayload.builder() @@ -116,10 +129,10 @@ public class TestMessageMock implements MessagingChannel { .build(); public static final SwapFlowEndpointPayload bulkFlow = new SwapFlowEndpointPayload(firstSwapFlow, secondSwapFlow); - static final FlowIdStatusPayload flowStatus = new FlowIdStatusPayload(FLOW_ID, FlowState.UP); + public static final FlowIdStatusPayload FLOW_STATUS = new FlowIdStatusPayload(FLOW_ID, FlowState.UP); static final PathInfoData path = new PathInfoData(0L, Collections.emptyList()); static final List pathPayloadsList = singletonList(new PathNodePayload(SWITCH_ID, 1, 1)); - static final FlowPathPayload flowPath = FlowPathPayload.builder() + public static final FlowPathPayload FLOW_PATH = FlowPathPayload.builder() .id(FLOW_ID) .forwardPath(pathPayloadsList) .reversePath(pathPayloadsList) @@ -151,6 +164,10 @@ public class TestMessageMock implements MessagingChannel { new SwitchRulesResponse(singletonList(TEST_SWITCH_RULE_COOKIE)); private static final Map messages = new ConcurrentHashMap<>(); + public static final MessageError DIFFERENT_FLOW_ID_ERROR = new MessageError(DEFAULT_CORRELATION_ID, 0, + ErrorType.DATA_INVALID.toString(), "flow_id from body and from path are different", + format("Body flow_id: %s, path flow_id: %s", FLOW_ID, FLOW_ID_FROM_PATH)); + /** * Chooses response by request. * @@ -160,7 +177,7 @@ public class TestMessageMock implements MessagingChannel { private CompletableFuture formatResponse(final String correlationId, final CommandData data) { CompletableFuture result = new CompletableFuture<>(); if (data instanceof FlowRequest) { - result.complete(flowResponse); + result.complete(buildFlowResponse((FlowRequest) data)); } else if (data instanceof FlowDeleteRequest) { result.complete(flowResponse); } else if (data instanceof FlowReadRequest) { @@ -176,6 +193,22 @@ private CompletableFuture formatResponse(final String correlationId, f return result; } + private FlowResponse buildFlowResponse(FlowRequest flowRequest) { + return new FlowResponse(FlowDto.builder() + .flowId(flowRequest.getFlowId()) + .bandwidth(flowRequest.getBandwidth()) + .description(flowRequest.getDescription()) + .sourceSwitch(flowRequest.getSource().getSwitchId()) + .destinationSwitch(flowRequest.getDestination().getSwitchId()) + .sourcePort(flowRequest.getSource().getPortNumber()) + .destinationPort(flowRequest.getDestination().getPortNumber()) + .sourceVlan(flowRequest.getSource().getOuterVlanId()) + .destinationVlan(flowRequest.getDestination().getOuterVlanId()) + .meterId(1) + .state(FlowState.UP) + .build()); + } + @Override public CompletableFuture sendAndGet(String topic, Message message) { if ("error-topic".equals(topic)) { diff --git a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v1/FlowControllerTest.java b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v1/FlowControllerTest.java index 1f3d8ae0b2f..72c644f10cd 100644 --- a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v1/FlowControllerTest.java +++ b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v1/FlowControllerTest.java @@ -20,7 +20,7 @@ import static org.openkilda.messaging.Utils.DEFAULT_CORRELATION_ID; import static org.openkilda.messaging.Utils.EXTRA_AUTH; import static org.openkilda.messaging.Utils.MAPPER; -import static org.openkilda.northbound.controller.v1.TestMessageMock.ERROR_FLOW_ID; +import static org.openkilda.northbound.controller.mock.TestMessageMock.ERROR_FLOW_ID; import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; @@ -37,6 +37,7 @@ import org.openkilda.messaging.payload.flow.FlowPathPayload; import org.openkilda.messaging.payload.flow.FlowResponsePayload; import org.openkilda.northbound.controller.TestConfig; +import org.openkilda.northbound.controller.mock.TestMessageMock; import org.openkilda.northbound.utils.RequestCorrelationId; import com.fasterxml.jackson.core.type.TypeReference; @@ -90,7 +91,7 @@ public void createFlow() throws Exception { MvcResult mvcResult = mockMvc.perform(put("/v1/flows") .header(CORRELATION_ID, testCorrelationId()) .contentType(APPLICATION_JSON_VALUE) - .content(MAPPER.writeValueAsString(TestMessageMock.flow))) + .content(MAPPER.writeValueAsString(TestMessageMock.FLOW))) .andReturn(); MvcResult result = mockMvc.perform(asyncDispatch(mvcResult)) @@ -100,7 +101,7 @@ public void createFlow() throws Exception { System.out.println("RESPONSE: " + result.getResponse().getContentAsString()); FlowResponsePayload response = MAPPER.readValue(result.getResponse().getContentAsString(), FlowResponsePayload.class); - assertEquals(TestMessageMock.flowResponsePayload, response); + assertEquals(TestMessageMock.FLOW_RESPONSE_PAYLOAD, response); } @Test @@ -118,7 +119,7 @@ public void getFlow() throws Exception { .andReturn(); FlowResponsePayload response = MAPPER.readValue(result.getResponse().getContentAsString(), FlowResponsePayload.class); - assertEquals(TestMessageMock.flowResponsePayload, response); + assertEquals(TestMessageMock.FLOW_RESPONSE_PAYLOAD, response); } @Test @@ -135,7 +136,7 @@ public void deleteFlow() throws Exception { .andReturn(); FlowResponsePayload response = MAPPER.readValue(result.getResponse().getContentAsString(), FlowResponsePayload.class); - assertEquals(TestMessageMock.flowResponsePayload, response); + assertEquals(TestMessageMock.FLOW_RESPONSE_PAYLOAD, response); } @Test @@ -153,7 +154,7 @@ public void deleteFlows() throws Exception { .andReturn(); FlowResponsePayload[] response = MAPPER.readValue(result.getResponse().getContentAsString(), FlowResponsePayload[].class); - assertEquals(TestMessageMock.flowResponsePayload, response[0]); + assertEquals(TestMessageMock.FLOW_RESPONSE_PAYLOAD, response[0]); } @Test @@ -171,7 +172,7 @@ public void updateFlow() throws Exception { MvcResult mvcResult = mockMvc.perform(put("/v1/flows/{flow-id}", TestMessageMock.FLOW_ID) .header(CORRELATION_ID, testCorrelationId()) .contentType(APPLICATION_JSON_VALUE) - .content(MAPPER.writeValueAsString(TestMessageMock.flow))) + .content(MAPPER.writeValueAsString(TestMessageMock.FLOW))) .andReturn(); MvcResult result = mockMvc.perform(asyncDispatch(mvcResult)) @@ -180,9 +181,25 @@ public void updateFlow() throws Exception { .andReturn(); FlowResponsePayload response = MAPPER.readValue(result.getResponse().getContentAsString(), FlowResponsePayload.class); - assertEquals(TestMessageMock.flowResponsePayload, response); + assertEquals(TestMessageMock.FLOW_RESPONSE_PAYLOAD, response); } + @Test + @WithMockUser(username = USERNAME, password = PASSWORD, roles = ROLE) + public void updateFlowDifferentFlowIdInPathFails() throws Exception { + MvcResult result = mockMvc.perform(put("/v1/flows/{flow-id}", TestMessageMock.FLOW_ID_FROM_PATH) + .header(CORRELATION_ID, testCorrelationId()) + .contentType(APPLICATION_JSON_VALUE) + .content(MAPPER.writeValueAsString(TestMessageMock.FLOW))) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(APPLICATION_JSON_VALUE)) + .andReturn(); + + MessageError response = MAPPER.readValue(result.getResponse().getContentAsString(), MessageError.class); + assertEquals(TestMessageMock.DIFFERENT_FLOW_ID_ERROR, response); + } + + @Test @WithMockUser(username = USERNAME, password = PASSWORD, roles = ROLE) public void getFlows() throws Exception { @@ -198,7 +215,7 @@ public void getFlows() throws Exception { List response = MAPPER.readValue( result.getResponse().getContentAsString(), new TypeReference>() {}); - assertEquals(Collections.singletonList(TestMessageMock.flowResponsePayload), response); + assertEquals(Collections.singletonList(TestMessageMock.FLOW_RESPONSE_PAYLOAD), response); } @Test @@ -215,7 +232,7 @@ public void statusFlow() throws Exception { .andReturn(); FlowIdStatusPayload response = MAPPER.readValue(result.getResponse().getContentAsString(), FlowIdStatusPayload.class); - assertEquals(TestMessageMock.flowStatus, response); + assertEquals(TestMessageMock.FLOW_STATUS, response); } @Test @@ -231,7 +248,7 @@ public void pathFlow() throws Exception { .andExpect(content().contentType(APPLICATION_JSON_VALUE)) .andReturn(); FlowPathPayload response = MAPPER.readValue(result.getResponse().getContentAsString(), FlowPathPayload.class); - assertEquals(TestMessageMock.flowPath, response); + assertEquals(TestMessageMock.FLOW_PATH, response); } @Test diff --git a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v1/SwitchControllerTest.java b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v1/SwitchControllerTest.java index e76fad6c09a..f659527ab5b 100644 --- a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v1/SwitchControllerTest.java +++ b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v1/SwitchControllerTest.java @@ -19,8 +19,8 @@ import static org.openkilda.messaging.Utils.CORRELATION_ID; import static org.openkilda.messaging.Utils.EXTRA_AUTH; import static org.openkilda.messaging.Utils.MAPPER; -import static org.openkilda.northbound.controller.v1.TestMessageMock.TEST_SWITCH_ID; -import static org.openkilda.northbound.controller.v1.TestMessageMock.TEST_SWITCH_RULE_COOKIE; +import static org.openkilda.northbound.controller.mock.TestMessageMock.TEST_SWITCH_ID; +import static org.openkilda.northbound.controller.mock.TestMessageMock.TEST_SWITCH_RULE_COOKIE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; diff --git a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v2/FlowControllerTest.java b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v2/FlowControllerTest.java index 94a27c2a44f..e1190d14272 100644 --- a/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v2/FlowControllerTest.java +++ b/src-java/northbound-service/northbound/src/test/java/org/openkilda/northbound/controller/v2/FlowControllerTest.java @@ -23,11 +23,13 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.openkilda.messaging.error.MessageError; import org.openkilda.northbound.controller.TestConfig; -import org.openkilda.northbound.controller.v1.TestMessageMock; +import org.openkilda.northbound.controller.mock.TestMessageMock; import org.openkilda.northbound.dto.v2.flows.SwapFlowEndpointPayload; import org.openkilda.northbound.utils.RequestCorrelationId; @@ -87,6 +89,21 @@ public void bulkUpdateFlow() throws Exception { assertEquals(TestMessageMock.bulkFlow.getSecondFlow(), response.getSecondFlow()); } + @Test + @WithMockUser(username = USERNAME, password = PASSWORD, roles = ROLE) + public void updateFlowDifferentFlowIdInPathFails() throws Exception { + MvcResult result = mockMvc.perform(put("/v2/flows/{flow-id}", TestMessageMock.FLOW_ID_FROM_PATH) + .header(CORRELATION_ID, testCorrelationId()) + .contentType(APPLICATION_JSON_VALUE) + .content(MAPPER.writeValueAsString(TestMessageMock.FLOW_REQUEST_V2))) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(APPLICATION_JSON_VALUE)) + .andReturn(); + + MessageError response = MAPPER.readValue(result.getResponse().getContentAsString(), MessageError.class); + assertEquals(TestMessageMock.DIFFERENT_FLOW_ID_ERROR, response); + } + private static String testCorrelationId() { return UUID.randomUUID().toString(); } From 3119893a7b1e474b9b1b8e90307040364f26b30d Mon Sep 17 00:00:00 2001 From: Dmitrii Beliakov Date: Sat, 11 Feb 2023 21:12:48 +0000 Subject: [PATCH 08/45] Implement path validation for each segment using bandwidth, latency, and link availability functions. Add necessary request and response handling. --- .../info/network/PathValidateData.java} | 32 +++- .../info/network/PathValidateResponse.java | 25 --- .../messaging/payload/network/PathDto.java | 2 + .../command/flow/PathValidateRequest.java | 11 +- .../java/org/openkilda/pce/PathComputer.java | 13 +- .../org/openkilda/pce/finder/PathFinder.java | 17 +- .../topology/nbworker/bolts/PathsBolt.java | 2 +- .../topology/nbworker/bolts/RouterBolt.java | 3 + .../nbworker/services/PathsService.java | 52 ++++-- .../nbworker/validators/PathValidator.java | 167 ++++++++++++++++++ .../nbworker/services/PathsServiceTest.java | 111 +++++++++++- .../dto/v2/flows/PathValidateResponse.java | 4 +- .../controller/v2/NetworkControllerV2.java | 13 +- .../northbound/converter/PathMapper.java | 4 + .../service/impl/NetworkServiceImpl.java | 11 +- 15 files changed, 394 insertions(+), 73 deletions(-) rename src-java/{northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathCheckErrorPayload.java => base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateData.java} (51%) delete mode 100644 src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateResponse.java create mode 100644 src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java diff --git a/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathCheckErrorPayload.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateData.java similarity index 51% rename from src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathCheckErrorPayload.java rename to src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateData.java index 358cf8d56fb..d1bba87efca 100644 --- a/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathCheckErrorPayload.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateData.java @@ -1,4 +1,4 @@ -/* Copyright 2023 Telstra Open Source +/* Copyright 2018 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,18 +13,34 @@ * limitations under the License. */ -package org.openkilda.northbound.dto.v2.flows; +package org.openkilda.messaging.info.network; +import org.openkilda.messaging.info.InfoData; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Value; + +import java.util.List; -@Data +@Value @Builder -@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) @JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) -public class PathCheckErrorPayload { - String errorDescription; +public class PathValidateData extends InfoData { + @JsonProperty("is_valid") + Boolean isValid; + @JsonProperty("errors") + List errors; + + @JsonCreator + public PathValidateData(@JsonProperty("is_valid") Boolean isValid, + @JsonProperty("errors") List errors) { + this.isValid = isValid; + this.errors = errors; + } } diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateResponse.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateResponse.java deleted file mode 100644 index af5cacf3cd5..00000000000 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -/* Copyright 2018 Telstra Open Source - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.openkilda.messaging.info.network; - -import org.openkilda.messaging.info.InfoData; - -import java.util.List; - -public class PathValidateResponse extends InfoData { - boolean isValid; - List errors; -} diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathDto.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathDto.java index d6084efa10b..d40c9878e43 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathDto.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathDto.java @@ -20,12 +20,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; import lombok.Value; import java.time.Duration; import java.util.List; @Value +@Builder @JsonInclude(JsonInclude.Include.NON_NULL) public class PathDto { @JsonProperty("bandwidth") diff --git a/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java b/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java index 057595ed3b1..df10d1595aa 100644 --- a/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java +++ b/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java @@ -15,22 +15,27 @@ package org.openkilda.messaging.command.flow; +import org.openkilda.messaging.nbtopology.annotations.ReadRequest; import org.openkilda.messaging.nbtopology.request.BaseRequest; import org.openkilda.messaging.payload.network.PathDto; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.EqualsAndHashCode; import lombok.Value; - /** * Request to validate that the given path is possible to create with the given constraints and resources availability. */ @Value -@EqualsAndHashCode(callSuper = false) +@ReadRequest +@EqualsAndHashCode(callSuper = true) public class PathValidateRequest extends BaseRequest { + @JsonProperty("path") PathDto pathDto; - public PathValidateRequest(PathDto pathDto) { + @JsonCreator + public PathValidateRequest(@JsonProperty("path") PathDto pathDto) { this.pathDto = pathDto; } } diff --git a/src-java/kilda-pce/src/main/java/org/openkilda/pce/PathComputer.java b/src-java/kilda-pce/src/main/java/org/openkilda/pce/PathComputer.java index 373cbbac34a..0e3080fdecd 100644 --- a/src-java/kilda-pce/src/main/java/org/openkilda/pce/PathComputer.java +++ b/src-java/kilda-pce/src/main/java/org/openkilda/pce/PathComputer.java @@ -30,12 +30,12 @@ import java.util.List; /** - * Represents computation operations on flow path. + * Represents computation operations on flow paths. */ public interface PathComputer { /** - * Gets path between source and destination switches for specified flow. The path is built over available ISLs + * Gets a path between source and destination switches for a specified flow. The path is built over available ISLs * only. * * @param flow the {@link Flow} instance @@ -46,7 +46,7 @@ default GetPathsResult getPath(Flow flow) throws UnroutableFlowException, Recove } /** - * Gets path between source and destination switch for specified flow. + * Gets a path between source and destination switches for a specified flow. * * @param flow the {@link Flow} instance. * @param reusePathsResources allow already allocated path resources (bandwidth) @@ -57,14 +57,17 @@ GetPathsResult getPath(Flow flow, Collection reusePathsResources, boolea throws UnroutableFlowException, RecoverableException; /** - * Gets N best paths. + * Gets the best N paths. N is a number, not greater than the count param, of all paths that can be found. * * @param srcSwitch source switchId * @param dstSwitch destination switchId + * @param count calculates no more than this number of paths * @param flowEncapsulationType target encapsulation type + * @param pathComputationStrategy depending on this strategy, different weight functions are used + * to determine the best path * @param maxLatency max latency * @param maxLatencyTier2 max latency tier2 - * @return an list of N (or less) best paths ordered from best to worst. + * @return a list of the best N paths ordered from best to worst. */ List getNPaths(SwitchId srcSwitch, SwitchId dstSwitch, int count, FlowEncapsulationType flowEncapsulationType, PathComputationStrategy pathComputationStrategy, diff --git a/src-java/kilda-pce/src/main/java/org/openkilda/pce/finder/PathFinder.java b/src-java/kilda-pce/src/main/java/org/openkilda/pce/finder/PathFinder.java index 8e818adb64f..188087c8564 100644 --- a/src-java/kilda-pce/src/main/java/org/openkilda/pce/finder/PathFinder.java +++ b/src-java/kilda-pce/src/main/java/org/openkilda/pce/finder/PathFinder.java @@ -43,7 +43,7 @@ FindPathResult findPathWithMinWeight(AvailableNetwork network, * * @return a pair of ordered lists that represents the path from start to end, or an empty list if no path found. * Returns backUpPathComputationWayUsed = true if found path has latency greater than maxLatency. - * Returns empty path if found path has latency greater than latencyLimit. + * Returns an empty path if the found path has latency greater than latencyLimit. */ FindPathResult findPathWithMinWeightAndLatencyLimits(AvailableNetwork network, SwitchId startSwitchId, SwitchId endSwitchId, @@ -64,18 +64,25 @@ FindPathResult findPathWithWeightCloseToMaxWeight(AvailableNetwork network, throws UnroutableFlowException; /** - * Find N (or less) best paths. + * Find the best N paths. + * N is a number, not greater than count, of all paths that can be found. * - * @return an list of N (or less) best paths. + * @param startSwitchId source switchId + * @param endSwitchId destination switchId + * @param network available network + * @param count find no more than this number of paths + * @param weightFunction use this weight function for the path computation + * @return a list of the best N paths. */ List findNPathsBetweenSwitches( AvailableNetwork network, SwitchId startSwitchId, SwitchId endSwitchId, int count, WeightFunction weightFunction) throws UnroutableFlowException; /** - * Find N (or less) best paths wih max weight restrictions. + * Find the best N paths with max weight restrictions. + * N is a number, not greater than count, of all paths that can be found. * - * @return an list of N (or less) best paths. + * @return a list of the best N paths. */ List findNPathsBetweenSwitches( AvailableNetwork network, SwitchId startSwitchId, SwitchId endSwitchId, int count, diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java index a0faaf540ca..222bec523bd 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java @@ -47,7 +47,7 @@ public PathsBolt(PersistenceManager persistenceManager, PathComputerConfig pathC public void init() { super.init(); - pathService = new PathsService(repositoryFactory, pathComputerConfig, persistenceManager); + pathService = new PathsService(repositoryFactory, pathComputerConfig, repositoryFactory.createIslRepository()); } @Override diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java index ecf83aecf44..d62f2d7ce2a 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java @@ -20,6 +20,7 @@ import org.openkilda.messaging.Message; import org.openkilda.messaging.command.CommandData; import org.openkilda.messaging.command.CommandMessage; +import org.openkilda.messaging.command.flow.PathValidateRequest; import org.openkilda.messaging.error.ErrorMessage; import org.openkilda.messaging.info.InfoMessage; import org.openkilda.messaging.nbtopology.request.BaseRequest; @@ -90,6 +91,8 @@ private void processRequest(Tuple input, String key, BaseRequest request) { emitWithContext(StreamType.KILDA_CONFIG.toString(), input, new Values(request)); } else if (request instanceof GetPathsRequest) { emitWithContext(StreamType.PATHS.toString(), input, new Values(request)); + } else if (request instanceof PathValidateRequest) { + emitWithContext(StreamType.PATHS.toString(), input, new Values(request)); } else if (request instanceof HistoryRequest) { emitWithContext(StreamType.HISTORY.toString(), input, new Values(request)); } else if (request instanceof MeterModifyRequest) { diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java index 90728ae329e..d8a2b32843a 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java @@ -16,8 +16,9 @@ package org.openkilda.wfm.topology.nbworker.services; import org.openkilda.messaging.command.flow.PathValidateRequest; -import org.openkilda.messaging.info.network.PathValidateResponse; +import org.openkilda.messaging.info.network.PathValidateData; import org.openkilda.messaging.info.network.PathsInfoData; +import org.openkilda.messaging.payload.network.PathDto; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.KildaConfiguration; import org.openkilda.model.PathComputationStrategy; @@ -30,7 +31,7 @@ import org.openkilda.pce.PathComputerFactory; import org.openkilda.pce.exception.RecoverableException; import org.openkilda.pce.exception.UnroutableFlowException; -import org.openkilda.persistence.PersistenceManager; +import org.openkilda.persistence.repositories.IslRepository; import org.openkilda.persistence.repositories.KildaConfigurationRepository; import org.openkilda.persistence.repositories.RepositoryFactory; import org.openkilda.persistence.repositories.SwitchPropertiesRepository; @@ -38,10 +39,13 @@ import org.openkilda.wfm.error.SwitchNotFoundException; import org.openkilda.wfm.error.SwitchPropertiesNotFoundException; import org.openkilda.wfm.share.mappers.PathMapper; +import org.openkilda.wfm.topology.nbworker.validators.PathValidator; import lombok.extern.slf4j.Slf4j; import java.time.Duration; +import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -54,10 +58,10 @@ public class PathsService { private final SwitchRepository switchRepository; private final SwitchPropertiesRepository switchPropertiesRepository; private final KildaConfigurationRepository kildaConfigurationRepository; - private final AvailableNetworkFactory availableNetworkFactory; + private final IslRepository islRepository; public PathsService(RepositoryFactory repositoryFactory, PathComputerConfig pathComputerConfig, - PersistenceManager persistenceManager) { + IslRepository islRepository) { switchRepository = repositoryFactory.createSwitchRepository(); switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); kildaConfigurationRepository = repositoryFactory.createKildaConfigurationRepository(); @@ -65,9 +69,7 @@ public PathsService(RepositoryFactory repositoryFactory, PathComputerConfig path pathComputerConfig, new AvailableNetworkFactory(pathComputerConfig, repositoryFactory)); pathComputer = pathComputerFactory.getPathComputer(); defaultMaxPathCount = pathComputerConfig.getMaxPathCount(); - - availableNetworkFactory = - new AvailableNetworkFactory(pathComputerConfig, persistenceManager.getRepositoryFactory()); + this.islRepository = islRepository; } /** @@ -129,8 +131,38 @@ public List getPaths( .collect(Collectors.toList()); } - public List validatePath(PathValidateRequest request) { - //TODO implement path validation - throw new UnsupportedOperationException(); + /** + * This method validates a path and collects errors if any. Validations depend on the information in the request. + * For example, if the request doesn't contain latency, the path will not be validated using max latency strategy. + * @param request request containing the path and parameters to validate + * @return a response with the success or the list of errors + */ + public List validatePath(PathValidateRequest request) { + PathValidator pathValidator = new PathValidator(islRepository); + + return Collections.singletonList(pathValidator.validatePath(pathDtoToPath(request.getPathDto()))); + } + + private Path pathDtoToPath(PathDto pathDto) { + List segments = new LinkedList<>(); + + for (int i = 0; i < pathDto.getNodes().size() - 1; i++) { + segments.add(Path.Segment.builder() + .srcSwitchId(pathDto.getNodes().get(i).getSwitchId()) + .srcPort(pathDto.getNodes().get(i).getOutputPort()) + .destSwitchId(pathDto.getNodes().get(i + 1).getSwitchId()) + .destPort(pathDto.getNodes().get(i + 1).getInputPort()) + .build()); + } + + return Path.builder() + .srcSwitchId(pathDto.getNodes().get(0).getSwitchId()) + .destSwitchId(pathDto.getNodes().get(pathDto.getNodes().size() - 1).getSwitchId()) + .isBackupPath(pathDto.getIsBackupPath()) + .minAvailableBandwidth(pathDto.getBandwidth()) + .latency(pathDto.getLatency()) + .segments(segments) + .build(); } + } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java new file mode 100644 index 00000000000..b170c8446f3 --- /dev/null +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java @@ -0,0 +1,167 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.wfm.topology.nbworker.validators; + +import org.openkilda.messaging.info.network.PathValidateData; +import org.openkilda.model.Isl; +import org.openkilda.pce.Path; +import org.openkilda.persistence.repositories.IslRepository; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + + +public class PathValidator { + + private final IslRepository islRepository; + + public PathValidator(IslRepository islRepository) { + this.islRepository = islRepository; + } + + /** + * Validates whether it is possible to create a path with the given parameters. When there obstacles, this validator + * returns all errors found on each segment. For example, when there is a path 1-2-3-4 and there is no link between + * 1 and 2, no sufficient latency between 2 and 3, and not enough bandwidth between 3-4, then this validator returns + * all 3 errors. + * @param path path parameters to validate. + * @return a response object containing the validation result and errors if any + */ + public PathValidateData validatePath(Path path) { + Set result = path.getSegments().stream() + .map(segment -> executeValidations(new PathSegment(path, segment), islRepository, getValidations(path))) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + + return PathValidateData.builder() + .isValid(result.isEmpty()) + .errors(new LinkedList<>(result)) + .build(); + } + + private Set executeValidations(PathSegment segment, IslRepository islRepository, + List>> validations) { + + return validations.stream() + .map(f -> f.apply(segment, islRepository)) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + } + + private List>> getValidations(Path path) { + List>> validationFunctions = new LinkedList<>(); + + validationFunctions.add(getValidateLinkBiFunction()); + + if (path.getLatency() != 0) { + validationFunctions.add(getValidateLatencyFunction()); + } + if (path.getMinAvailableBandwidth() != 0) { + validationFunctions.add(getValidateBandwidthFunction()); + } + return validationFunctions; + } + + private BiFunction> getValidateLinkBiFunction() { + return (pathSegment, islRepository) -> { + + if (!islRepository.findByEndpoints(pathSegment.getSegment().getSrcSwitchId(), + pathSegment.getSegment().getSrcPort(), + pathSegment.getSegment().getDestSwitchId(), + pathSegment.getSegment().getDestPort()).isPresent()) { + return Collections.singleton(getNoLinkError(pathSegment)); + } + + return Collections.emptySet(); + }; + } + + private BiFunction> getValidateBandwidthFunction() { + return (pathSegment, islRepository) -> { + Optional isl = islRepository.findByEndpoints( + pathSegment.getSegment().getSrcSwitchId(), pathSegment.getSegment().getSrcPort(), + pathSegment.getSegment().getDestSwitchId(), pathSegment.getSegment().getDestPort()); + if (!isl.isPresent()) { + return Collections.singleton(getNoLinkError(pathSegment)); + } + + if (isl.get().getAvailableBandwidth() < pathSegment.path.getMinAvailableBandwidth()) { + return Collections.singleton(getBandwidthErrorMessage(pathSegment, + isl.get().getAvailableBandwidth(), pathSegment.path.getMinAvailableBandwidth())); + } + + return Collections.emptySet(); + }; + } + + private BiFunction> getValidateLatencyFunction() { + return (pathSegment, islRepository) -> { + Optional isl = islRepository.findByEndpoints( + pathSegment.getSegment().getSrcSwitchId(), pathSegment.getSegment().getSrcPort(), + pathSegment.getSegment().getDestSwitchId(), pathSegment.getSegment().getDestPort()); + if (!isl.isPresent()) { + return Collections.singleton(getNoLinkError(pathSegment)); + } + + if (isl.get().getLatency() > pathSegment.getPath().getLatency()) { + return Collections.singleton(getLatencyErrorMessage(pathSegment, + pathSegment.getPath().getLatency(), isl.get().getLatency())); + } + + return Collections.emptySet(); + }; + } + + private String getLatencyErrorMessage(PathSegment pathSegment, long requestedLatency, long actualLatency) { + return String.format( + "Requested latency is too low between source switch %s port %d and destination switch %s port %d." + + " Requested %d, but the link supports %d", + pathSegment.getSegment().getSrcSwitchId(), pathSegment.getSegment().getSrcPort(), + pathSegment.getSegment().getDestSwitchId(), pathSegment.getSegment().getDestPort(), + requestedLatency, actualLatency); + } + + private String getBandwidthErrorMessage(PathSegment pathSegment, Long requestedBandwidth, long actualBandwidth) { + return String.format( + "There is not enough Bandwidth between source switch %s port %d and destination switch %s port %d." + + " Requested bandwidth %d, but the link supports %d", + pathSegment.getSegment().getSrcSwitchId(), pathSegment.getSegment().getSrcPort(), + pathSegment.getSegment().getDestSwitchId(), pathSegment.getSegment().getDestPort(), + requestedBandwidth, actualBandwidth); + } + + private String getNoLinkError(PathSegment pathSegment) { + return String.format( + "There is no ISL between source switch %s port %d and destination switch %s port %d", + pathSegment.getSegment().getSrcSwitchId(), pathSegment.getSegment().getSrcPort(), + pathSegment.getSegment().getDestSwitchId(), pathSegment.getSegment().getDestPort()); + } + + @Getter + @AllArgsConstructor + private static class PathSegment { + Path path; + Path.Segment segment; + } +} diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java index dfddace4c5f..1800a656afa 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java @@ -16,6 +16,7 @@ package org.openkilda.wfm.topology.nbworker.services; import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -26,7 +27,11 @@ import static org.openkilda.model.PathComputationStrategy.MAX_LATENCY; import org.openkilda.config.provider.PropertiesBasedConfigurationProvider; +import org.openkilda.messaging.command.flow.PathValidateRequest; +import org.openkilda.messaging.info.network.PathValidateData; import org.openkilda.messaging.info.network.PathsInfoData; +import org.openkilda.messaging.payload.flow.PathNodePayload; +import org.openkilda.messaging.payload.network.PathDto; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.Isl; import org.openkilda.model.IslStatus; @@ -53,6 +58,7 @@ import org.junit.Test; import java.time.Duration; +import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -64,6 +70,7 @@ public class PathsServiceTest extends InMemoryGraphBasedTest { private static final SwitchId SWITCH_ID_1 = new SwitchId(1); private static final SwitchId SWITCH_ID_2 = new SwitchId(2); private static final SwitchId SWITCH_ID_3 = new SwitchId(3); + private static final SwitchId SWITCH_ID_4 = new SwitchId(4); public static final long BASE_LATENCY = 10000; public static final long MIN_LATENCY = BASE_LATENCY - SWITCH_COUNT; @@ -84,7 +91,7 @@ public static void setUpOnce() { kildaConfigurationRepository = repositoryFactory.createKildaConfigurationRepository(); PathComputerConfig pathComputerConfig = new PropertiesBasedConfigurationProvider() .getConfiguration(PathComputerConfig.class); - pathsService = new PathsService(repositoryFactory, pathComputerConfig, persistenceManager); + pathsService = new PathsService(repositoryFactory, pathComputerConfig, islRepository); } @Before @@ -290,6 +297,108 @@ public void findNPathsByDefaultEncapsulationAndCost() assertVxlanAndCostPathes(paths); } + @Test + public void whenValidPath_validatePathReturnsValidResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathDto.builder() + .nodes(nodes) + .bandwidth(0L) + .isBackupPath(false) + .latency(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertTrue(responses.get(0).getIsValid()); + } + + @Test + public void whenValidPathButNotEnoughBandwidth_validateReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathDto.builder() + .nodes(nodes) + .bandwidth(1000000000L) + .isBackupPath(false) + .latency(10L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertTrue(responses.get(0).getErrors().get(0) + .contains("There is not enough Bandwidth between source switch")); + } + + @Test + public void whenValidPathButTooLowLatency_validateReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathDto.builder() + .nodes(nodes) + .bandwidth(0L) + .isBackupPath(false) + .latency(10L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertTrue(responses.get(0).getErrors().get(0) + .contains("Requested latency is too low between source switch")); + } + + @Test + public void whenNoLinkBetweenSwitches_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 1)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 0, null)); + PathValidateRequest request = new PathValidateRequest(PathDto.builder() + .nodes(nodes) + .bandwidth(0L) + .isBackupPath(false) + .latency(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertTrue(responses.get(0).getErrors().get(0) + .startsWith("There is no ISL between source switch")); + } + + @Test + public void whenMultipleProblemsOnPath_validatePathReturnsAllErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_4, 7, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathDto.builder() + .nodes(nodes) + .bandwidth(1000000L) + .isBackupPath(false) + .latency(10L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertEquals("There must be 4 errors in total. bandwidth, latency, 2 links are not present", + 4, responses.get(0).getErrors().size()); + } + private void assertMaxLatencyPaths(List paths, Duration maxLatency, long expectedCount, FlowEncapsulationType encapsulationType) { assertEquals(expectedCount, paths.size()); diff --git a/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java index f0f98ef9411..4bdf1ba8223 100644 --- a/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java +++ b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java @@ -28,6 +28,6 @@ @AllArgsConstructor @JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) public class PathValidateResponse { - boolean isValid; - List errors; + Boolean isValid; + List errors; } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java index 2be844470ca..ab1af188cb9 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java @@ -18,6 +18,7 @@ import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.error.MessageException; import org.openkilda.messaging.payload.network.PathDto; +import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; import org.openkilda.northbound.service.NetworkService; import io.swagger.annotations.ApiOperation; @@ -40,25 +41,19 @@ public class NetworkControllerV2 { /** * Validates that a given path complies with the chosen strategy and the network availability. - * It is required that the input contains path nodes. Other parameters are optional. - * TODO: order of nodes might be important, maybe there is a need to require a certain structure for that list. + * It is required that the input contains path nodes. Other parameters are opti * @param pathDto a payload with a path and additional flow parameters provided by a user * @return either a successful response or the list of errors */ @GetMapping(path = "/path/check") @ApiOperation(value = "Validates that a given path complies with the chosen strategy and the network availability") @ResponseStatus(HttpStatus.OK) - public CompletableFuture validateCustomFlowPath(@RequestBody PathDto pathDto) { - if (isFlowPathRequestInvalid(pathDto)) { + public CompletableFuture validateCustomFlowPath(@RequestBody PathDto pathDto) { + if (pathDto == null || pathDto.getNodes() == null || pathDto.getNodes().size() < 2) { throw new MessageException(ErrorType.DATA_INVALID, "Invalid Request Body", "Invalid 'nodes' value in the request body"); } return networkService.validateFlowPath(pathDto); } - - private boolean isFlowPathRequestInvalid(PathDto pathDto) { - return pathDto == null || pathDto.getNodes() == null || pathDto.getNodes().isEmpty(); - } - } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java index 54051c47cb4..48aac3b1b96 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java @@ -16,10 +16,12 @@ package org.openkilda.northbound.converter; import org.openkilda.messaging.info.network.Path; +import org.openkilda.messaging.info.network.PathValidateData; import org.openkilda.messaging.model.FlowPathDto; import org.openkilda.messaging.payload.flow.GroupFlowPathPayload; import org.openkilda.messaging.payload.flow.GroupFlowPathPayload.FlowProtectedPathsPayload; import org.openkilda.messaging.payload.network.PathDto; +import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; import org.mapstruct.Mapper; @@ -33,4 +35,6 @@ default PathDto mapToPath(Path data) { GroupFlowPathPayload mapGroupFlowPathPayload(FlowPathDto data); FlowProtectedPathsPayload mapFlowProtectedPathPayload(FlowPathDto.FlowProtectedPathDto data); + + PathValidateResponse toPathValidateResponse(PathValidateData data); } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java index 762a15b5f57..54855150e97 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java @@ -19,6 +19,7 @@ import org.openkilda.messaging.command.flow.PathValidateRequest; import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.error.MessageException; +import org.openkilda.messaging.info.network.PathValidateData; import org.openkilda.messaging.info.network.PathsInfoData; import org.openkilda.messaging.nbtopology.request.GetPathsRequest; import org.openkilda.messaging.payload.network.PathDto; @@ -100,14 +101,16 @@ public CompletableFuture getPaths( * it responds with the reasons, such as: not enough bandwidth, requested latency it too low, there is no * links between the selected switches, and so on. * @param path a path provided by a user - * @return either a successful response or the list of errors + * @return either a successful response or a list of errors */ @Override public CompletableFuture validateFlowPath(PathDto path) { PathValidateRequest request = new PathValidateRequest(path); - CommandMessage message = new CommandMessage(request, System.currentTimeMillis(), RequestCorrelationId.getId()); - return messagingChannel.sendAndGetChunked(nbworkerTopic, message) - .thenApply(PathValidateResponse.class::cast); + CommandMessage message = new CommandMessage(request, System.currentTimeMillis(), + RequestCorrelationId.getId()); + return messagingChannel.sendAndGet(nbworkerTopic, message) + .thenApply(PathValidateData.class::cast) + .thenApply(pathMapper::toPathValidateResponse); } } From 3e633b43c148c32c4de85f2643fe1b4cec017749 Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Thu, 16 Feb 2023 22:12:34 +0100 Subject: [PATCH 09/45] Fix wording, adjust URL, adjust response body. --- .../path-validation/path-validation.md | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/docs/design/solutions/path-validation/path-validation.md b/docs/design/solutions/path-validation/path-validation.md index a11ea1bfec6..8d8fff9d764 100644 --- a/docs/design/solutions/path-validation/path-validation.md +++ b/docs/design/solutions/path-validation/path-validation.md @@ -1,24 +1,22 @@ # Path Validation ## Motivation -The main use case is to allow users to validate if some arbitrary flow path is possible to create with the given +The main use case is to allow users to verify whether some arbitrary flow path is possible to create with the given constraints without actually creating this flow. A path here is a sequence of nodes, that is a sequence of switches with in and out ports. Constraints are various parameters such as max latency, bandwidth, encapsulation type, path computation strategy, and other. -## Implementation proposal -Reuse PCE components (specifically extend AvailableNetwork) for the path validation in the same manner as it is used for -the path computation. Use nb worker topology, PathService, and PathsBolt because the path validation is read-only. - -_An alternative approach is to use FlowValidationHubBolt and the resource manager. This might help to mitigate concurrency -issues related to changes in repositories during a path validation process._ +## Implementation details +The validation of a path is done for each segment and each validation type individually. This way, the validation +collects all errors on the path and returns them all in a single response. The response is concise amd formed +in human-readable format. There is no locking of resources for this path and, therefore, no guarantee that it will be possible to create this flow after validation in the future. ## Northbound API -REST URL: ```/v2/network/validate-path```, method: ```GET``` +REST URL: ```/v2/network/path/check```, method: ```GET``` A user is required to provide a path represented by a list of nodes. Nodes must be ordered from start to end, each next element is the next hop. A user can add optional parameters: @@ -26,7 +24,7 @@ each next element is the next hop. A user can add optional parameters: - max_bandwidth: bandwidth required for this path. - max_latency: the first tier latency value. - max_latency_tier2: the second tier latency value. -- path_computation_strategy: "COST|LATENCY|MAX_LATENCY|COST_AND_AVAILABLE_BANDWIDTH", +- path_computation_strategy: "COST|LATENCY|MAX_LATENCY|COST_AND_AVAILABLE_BANDWIDTH". - reuse_flow_resources: a flow ID. Verify the given path as if it is created instead of the existing flow, that is as if the resources of some flow are released before validation. @@ -62,13 +60,13 @@ the resources of some flow are released before validation. Code: 200 Content: -- valid: True if the path is valid and conform to strategy, false otherwise +- is_valid: True if the path is valid and conform to the strategy, false otherwise - errors: List of strings with error descriptions if any - correlation_id: Correlation ID from correlation_id Header ```json { "correlation_id": "string", - "valid": "bool", + "is_valid": "bool", "errors": [ "error0", "errorN" From e33cabb5baef96d6b8faf68dad8b70bfb91f96f5 Mon Sep 17 00:00:00 2001 From: Dmitrii Beliakov Date: Sat, 18 Feb 2023 11:05:07 +0000 Subject: [PATCH 10/45] Add new DTOs and mappers to handle validation parameters. Add validators for ISL and bandwidth for forward and reverse paths, latency tier 2, diverse flow intersection, and reusable resources. --- ...ateData.java => PathValidationResult.java} | 6 +- .../payload/network/PathValidationDto.java | 75 ++++ .../command/flow/PathValidateRequest.java | 9 +- .../openkilda/model/PathValidationData.java | 48 +++ .../mappers/FlowEncapsulationTypeMapper.java | 31 ++ .../mappers/PathValidationDataMapper.java | 64 ++++ .../topology/nbworker/bolts/PathsBolt.java | 3 +- .../nbworker/services/PathsService.java | 42 +-- .../nbworker/validators/PathValidator.java | 328 ++++++++++++++---- .../nbworker/services/PathsServiceTest.java | 142 ++++++-- .../controller/v2/NetworkControllerV2.java | 21 +- .../northbound/converter/PathMapper.java | 4 +- .../northbound/service/NetworkService.java | 6 +- .../service/impl/NetworkServiceImpl.java | 11 +- 14 files changed, 639 insertions(+), 151 deletions(-) rename src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/{PathValidateData.java => PathValidationResult.java} (86%) create mode 100644 src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java create mode 100644 src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java create mode 100644 src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/FlowEncapsulationTypeMapper.java create mode 100644 src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateData.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidationResult.java similarity index 86% rename from src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateData.java rename to src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidationResult.java index d1bba87efca..adb80d520c8 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidateData.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidationResult.java @@ -31,15 +31,15 @@ @Builder @EqualsAndHashCode(callSuper = false) @JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) -public class PathValidateData extends InfoData { +public class PathValidationResult extends InfoData { @JsonProperty("is_valid") Boolean isValid; @JsonProperty("errors") List errors; @JsonCreator - public PathValidateData(@JsonProperty("is_valid") Boolean isValid, - @JsonProperty("errors") List errors) { + public PathValidationResult(@JsonProperty("is_valid") Boolean isValid, + @JsonProperty("errors") List errors) { this.isValid = isValid; this.errors = errors; } diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java new file mode 100644 index 00000000000..770234499cf --- /dev/null +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java @@ -0,0 +1,75 @@ +/* Copyright 2019 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.messaging.payload.network; + +import org.openkilda.messaging.payload.flow.FlowEncapsulationType; +import org.openkilda.messaging.payload.flow.PathNodePayload; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Value +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PathValidationDto { + @JsonProperty("bandwidth") + Long bandwidth; + + @JsonProperty("max_latency") + Long latencyMs; + + @JsonProperty("max_latency_tier2") + Long latencyTier2ms; + + @JsonProperty("nodes") + List nodes; + + @JsonProperty("is_backup_path") + Boolean isBackupPath; + + @JsonProperty("diverse_with_flow") + String diverseWithFlow; + + @JsonProperty("reuse_flow_resources") + String reuseFlowResources; + + @JsonProperty("flow_encapsulation_type") + FlowEncapsulationType flowEncapsulationType; + + @JsonCreator + public PathValidationDto(@JsonProperty("bandwidth") Long bandwidth, + @JsonProperty("latency_ms") Long latencyMs, + @JsonProperty("max_latency_tier2") Long latencyTier2ms, + @JsonProperty("nodes") List nodes, + @JsonProperty("is_backup_path") Boolean isBackupPath, + @JsonProperty("diverse_with_flow") String diverseWithFlow, + @JsonProperty("reuse_flow_resources") String reuseFlowResources, + @JsonProperty("flow_encapsulation_type") FlowEncapsulationType flowEncapsulationType) { + this.bandwidth = bandwidth; + this.latencyMs = latencyMs; + this.latencyTier2ms = latencyTier2ms; + this.nodes = nodes; + this.isBackupPath = isBackupPath; + this.diverseWithFlow = diverseWithFlow; + this.reuseFlowResources = reuseFlowResources; + this.flowEncapsulationType = flowEncapsulationType; + } +} diff --git a/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java b/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java index df10d1595aa..3979a0ad324 100644 --- a/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java +++ b/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java @@ -17,7 +17,7 @@ import org.openkilda.messaging.nbtopology.annotations.ReadRequest; import org.openkilda.messaging.nbtopology.request.BaseRequest; -import org.openkilda.messaging.payload.network.PathDto; +import org.openkilda.messaging.payload.network.PathValidationDto; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -31,11 +31,12 @@ @ReadRequest @EqualsAndHashCode(callSuper = true) public class PathValidateRequest extends BaseRequest { + @JsonProperty("path") - PathDto pathDto; + PathValidationDto pathValidationDto; @JsonCreator - public PathValidateRequest(@JsonProperty("path") PathDto pathDto) { - this.pathDto = pathDto; + public PathValidateRequest(@JsonProperty("path") PathValidationDto pathValidationDtoDto) { + this.pathValidationDto = pathValidationDtoDto; } } diff --git a/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java b/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java new file mode 100644 index 00000000000..0dadf423ffb --- /dev/null +++ b/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java @@ -0,0 +1,48 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Value +@Builder +public class PathValidationData { + + Long bandwidth; + Long latencyMs; + Long latencyTier2ms; + List pathSegments; + Boolean isBackupPath; + String diverseWithFlow; + String reuseFlowResources; + FlowEncapsulationType flowEncapsulationType; + SwitchId srcSwitchId; + Integer srcPort; + SwitchId destSwitchId; + Integer destPort; + + @Value + @Builder + public static class PathSegmentValidationData { + SwitchId srcSwitchId; + Integer srcPort; + SwitchId destSwitchId; + Integer destPort; + } +} diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/FlowEncapsulationTypeMapper.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/FlowEncapsulationTypeMapper.java new file mode 100644 index 00000000000..8f5d28839a1 --- /dev/null +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/FlowEncapsulationTypeMapper.java @@ -0,0 +1,31 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.wfm.share.mappers; + +import org.openkilda.model.FlowEncapsulationType; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface FlowEncapsulationTypeMapper { + + FlowEncapsulationTypeMapper INSTANCE = Mappers.getMapper(FlowEncapsulationTypeMapper.class); + + FlowEncapsulationType toOpenKildaModel( + org.openkilda.messaging.payload.flow.FlowEncapsulationType flowEncapsulationType); + +} diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java new file mode 100644 index 00000000000..204a8397813 --- /dev/null +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java @@ -0,0 +1,64 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.wfm.share.mappers; + +import org.openkilda.messaging.payload.network.PathValidationDto; +import org.openkilda.model.PathValidationData; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.LinkedList; +import java.util.List; + +@Mapper +public abstract class PathValidationDataMapper { + + public static PathValidationDataMapper INSTANCE = Mappers.getMapper(PathValidationDataMapper.class); + + /** + * Converts NB PathValidationDto to messaging PathValidationData. + * @param pathValidationDto NB representation of a path validation data + * @return the messaging representation of a path validation data + */ + public PathValidationData toPathValidationData(PathValidationDto pathValidationDto) { + List segments = new LinkedList<>(); + + for (int i = 0; i < pathValidationDto.getNodes().size() - 1; i++) { + segments.add(PathValidationData.PathSegmentValidationData.builder() + .srcSwitchId(pathValidationDto.getNodes().get(i).getSwitchId()) + .srcPort(pathValidationDto.getNodes().get(i).getOutputPort()) + .destSwitchId(pathValidationDto.getNodes().get(i + 1).getSwitchId()) + .destPort(pathValidationDto.getNodes().get(i + 1).getInputPort()) + .build()); + } + + return PathValidationData.builder() + .srcSwitchId(pathValidationDto.getNodes().get(0).getSwitchId()) + .destSwitchId(pathValidationDto.getNodes().get(pathValidationDto.getNodes().size() - 1).getSwitchId()) + .isBackupPath(pathValidationDto.getIsBackupPath()) + .bandwidth(pathValidationDto.getBandwidth()) + .latencyMs(pathValidationDto.getLatencyMs()) + .latencyTier2ms(pathValidationDto.getLatencyTier2ms()) + .diverseWithFlow(pathValidationDto.getDiverseWithFlow()) + .reuseFlowResources(pathValidationDto.getReuseFlowResources()) + .flowEncapsulationType(FlowEncapsulationTypeMapper.INSTANCE.toOpenKildaModel( + pathValidationDto.getFlowEncapsulationType())) + .pathSegments(segments) + .build(); + } + +} diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java index 222bec523bd..c544ea9f751 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java @@ -47,7 +47,8 @@ public PathsBolt(PersistenceManager persistenceManager, PathComputerConfig pathC public void init() { super.init(); - pathService = new PathsService(repositoryFactory, pathComputerConfig, repositoryFactory.createIslRepository()); + pathService = new PathsService(repositoryFactory, pathComputerConfig, repositoryFactory.createIslRepository(), + repositoryFactory.createFlowRepository()); } @Override diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java index d8a2b32843a..8ccd6cff698 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java @@ -16,9 +16,8 @@ package org.openkilda.wfm.topology.nbworker.services; import org.openkilda.messaging.command.flow.PathValidateRequest; -import org.openkilda.messaging.info.network.PathValidateData; +import org.openkilda.messaging.info.network.PathValidationResult; import org.openkilda.messaging.info.network.PathsInfoData; -import org.openkilda.messaging.payload.network.PathDto; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.KildaConfiguration; import org.openkilda.model.PathComputationStrategy; @@ -31,6 +30,7 @@ import org.openkilda.pce.PathComputerFactory; import org.openkilda.pce.exception.RecoverableException; import org.openkilda.pce.exception.UnroutableFlowException; +import org.openkilda.persistence.repositories.FlowRepository; import org.openkilda.persistence.repositories.IslRepository; import org.openkilda.persistence.repositories.KildaConfigurationRepository; import org.openkilda.persistence.repositories.RepositoryFactory; @@ -39,13 +39,13 @@ import org.openkilda.wfm.error.SwitchNotFoundException; import org.openkilda.wfm.error.SwitchPropertiesNotFoundException; import org.openkilda.wfm.share.mappers.PathMapper; +import org.openkilda.wfm.share.mappers.PathValidationDataMapper; import org.openkilda.wfm.topology.nbworker.validators.PathValidator; import lombok.extern.slf4j.Slf4j; import java.time.Duration; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -59,17 +59,19 @@ public class PathsService { private final SwitchPropertiesRepository switchPropertiesRepository; private final KildaConfigurationRepository kildaConfigurationRepository; private final IslRepository islRepository; + private final FlowRepository flowRepository; public PathsService(RepositoryFactory repositoryFactory, PathComputerConfig pathComputerConfig, - IslRepository islRepository) { + IslRepository islRepository, FlowRepository flowRepository) { switchRepository = repositoryFactory.createSwitchRepository(); switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); kildaConfigurationRepository = repositoryFactory.createKildaConfigurationRepository(); + this.islRepository = islRepository; + this.flowRepository = flowRepository; PathComputerFactory pathComputerFactory = new PathComputerFactory( pathComputerConfig, new AvailableNetworkFactory(pathComputerConfig, repositoryFactory)); pathComputer = pathComputerFactory.getPathComputer(); defaultMaxPathCount = pathComputerConfig.getMaxPathCount(); - this.islRepository = islRepository; } /** @@ -137,32 +139,12 @@ public List getPaths( * @param request request containing the path and parameters to validate * @return a response with the success or the list of errors */ - public List validatePath(PathValidateRequest request) { - PathValidator pathValidator = new PathValidator(islRepository); - - return Collections.singletonList(pathValidator.validatePath(pathDtoToPath(request.getPathDto()))); - } - - private Path pathDtoToPath(PathDto pathDto) { - List segments = new LinkedList<>(); - - for (int i = 0; i < pathDto.getNodes().size() - 1; i++) { - segments.add(Path.Segment.builder() - .srcSwitchId(pathDto.getNodes().get(i).getSwitchId()) - .srcPort(pathDto.getNodes().get(i).getOutputPort()) - .destSwitchId(pathDto.getNodes().get(i + 1).getSwitchId()) - .destPort(pathDto.getNodes().get(i + 1).getInputPort()) - .build()); - } + public List validatePath(PathValidateRequest request) { + PathValidator pathValidator = new PathValidator(islRepository, flowRepository); - return Path.builder() - .srcSwitchId(pathDto.getNodes().get(0).getSwitchId()) - .destSwitchId(pathDto.getNodes().get(pathDto.getNodes().size() - 1).getSwitchId()) - .isBackupPath(pathDto.getIsBackupPath()) - .minAvailableBandwidth(pathDto.getBandwidth()) - .latency(pathDto.getLatency()) - .segments(segments) - .build(); + return Collections.singletonList(pathValidator.validatePath( + PathValidationDataMapper.INSTANCE.toPathValidationData(request.getPathValidationDto()) + )); } } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java index b170c8446f3..fd614083a81 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java @@ -15,29 +15,38 @@ package org.openkilda.wfm.topology.nbworker.validators; -import org.openkilda.messaging.info.network.PathValidateData; +import org.openkilda.messaging.info.network.PathValidationResult; +import org.openkilda.model.Flow; +import org.openkilda.model.FlowPath; import org.openkilda.model.Isl; -import org.openkilda.pce.Path; +import org.openkilda.model.IslStatus; +import org.openkilda.model.PathSegment; +import org.openkilda.model.PathValidationData; +import org.openkilda.persistence.repositories.FlowRepository; import org.openkilda.persistence.repositories.IslRepository; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Predicate; import java.util.stream.Collectors; public class PathValidator { private final IslRepository islRepository; + private final FlowRepository flowRepository; - public PathValidator(IslRepository islRepository) { + public PathValidator(IslRepository islRepository, FlowRepository flowRepository) { this.islRepository = islRepository; + this.flowRepository = flowRepository; } /** @@ -45,123 +54,296 @@ public PathValidator(IslRepository islRepository) { * returns all errors found on each segment. For example, when there is a path 1-2-3-4 and there is no link between * 1 and 2, no sufficient latency between 2 and 3, and not enough bandwidth between 3-4, then this validator returns * all 3 errors. - * @param path path parameters to validate. + * @param pathValidationData path parameters to validate. * @return a response object containing the validation result and errors if any */ - public PathValidateData validatePath(Path path) { - Set result = path.getSegments().stream() - .map(segment -> executeValidations(new PathSegment(path, segment), islRepository, getValidations(path))) + public PathValidationResult validatePath(PathValidationData pathValidationData) { + Set result = pathValidationData.getPathSegments().stream() + .map(segment -> executeValidations( + new InputData(pathValidationData, segment), + new RepositoryData(islRepository, flowRepository), + getValidations(pathValidationData))) .flatMap(Set::stream) .collect(Collectors.toSet()); - return PathValidateData.builder() + return PathValidationResult.builder() .isValid(result.isEmpty()) .errors(new LinkedList<>(result)) .build(); } - private Set executeValidations(PathSegment segment, IslRepository islRepository, - List>> validations) { + private Set executeValidations(InputData inputData, + RepositoryData repositoryData, + List>> validations) { return validations.stream() - .map(f -> f.apply(segment, islRepository)) + .map(f -> f.apply(inputData, repositoryData)) .flatMap(Set::stream) .collect(Collectors.toSet()); } - private List>> getValidations(Path path) { - List>> validationFunctions = new LinkedList<>(); + private List>> getValidations( + PathValidationData inputData) { + List>> validationFunctions = new LinkedList<>(); - validationFunctions.add(getValidateLinkBiFunction()); + validationFunctions.add(this::validateLink); - if (path.getLatency() != 0) { - validationFunctions.add(getValidateLatencyFunction()); + if (inputData.getLatencyMs() != 0) { + validationFunctions.add(this::validateLatency); } - if (path.getMinAvailableBandwidth() != 0) { - validationFunctions.add(getValidateBandwidthFunction()); + if (inputData.getLatencyTier2ms() != 0) { + validationFunctions.add(this::validateLatencyTier2); } + if (inputData.getBandwidth() != 0) { + validationFunctions.add(this::validateBandwidth); + } + if (inputData.getDiverseWithFlow() != null && !inputData.getDiverseWithFlow().isEmpty()) { + validationFunctions.add(this::validateDiverseWithFlow); + } + return validationFunctions; } - private BiFunction> getValidateLinkBiFunction() { - return (pathSegment, islRepository) -> { + private Set validateLink(InputData inputData, RepositoryData repositoryData) { + Optional forward = repositoryData.getIslRepository().findByEndpoints( + inputData.getSegment().getSrcSwitchId(), + inputData.getSegment().getSrcPort(), + inputData.getSegment().getDestSwitchId(), + inputData.getSegment().getDestPort()); + if (!forward.isPresent()) { + return Collections.singleton(getNoForwardIslError(inputData)); + } - if (!islRepository.findByEndpoints(pathSegment.getSegment().getSrcSwitchId(), - pathSegment.getSegment().getSrcPort(), - pathSegment.getSegment().getDestSwitchId(), - pathSegment.getSegment().getDestPort()).isPresent()) { - return Collections.singleton(getNoLinkError(pathSegment)); - } + if (forward.get().getStatus() != IslStatus.ACTIVE) { + return Collections.singleton(getForwardIslNotActiveError(inputData)); + } + + Optional reverse = repositoryData.getIslRepository().findByEndpoints( + inputData.getSegment().getDestSwitchId(), + inputData.getSegment().getDestPort(), + inputData.getSegment().getSrcSwitchId(), + inputData.getSegment().getSrcPort()); + if (!reverse.isPresent()) { + return Collections.singleton(getNoReverseIslError(inputData)); + } + + if (reverse.get().getStatus() != IslStatus.ACTIVE) { + return Collections.singleton(getReverseIslNotActiveError(inputData)); + } - return Collections.emptySet(); - }; + return Collections.emptySet(); } - private BiFunction> getValidateBandwidthFunction() { - return (pathSegment, islRepository) -> { - Optional isl = islRepository.findByEndpoints( - pathSegment.getSegment().getSrcSwitchId(), pathSegment.getSegment().getSrcPort(), - pathSegment.getSegment().getDestSwitchId(), pathSegment.getSegment().getDestPort()); - if (!isl.isPresent()) { - return Collections.singleton(getNoLinkError(pathSegment)); - } + private Set validateBandwidth(InputData inputData, RepositoryData repositoryData) { + Optional forward = repositoryData.getIslRepository().findByEndpoints( + inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort(), + inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort()); + if (!forward.isPresent()) { + return Collections.singleton(getNoForwardIslError(inputData)); + } + + Set errors = new HashSet<>(); + if (getForwardBandwidthWithReusableResources(forward.get(), inputData) < inputData.getPath().getBandwidth()) { + errors.add(getForwardBandwidthErrorMessage(inputData, forward.get().getAvailableBandwidth())); + } + + Optional reverse = repositoryData.getIslRepository().findByEndpoints( + inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort(), + inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort()); + if (!reverse.isPresent()) { + return Collections.singleton(getNoForwardIslError(inputData)); + } + if (getReverseBandwidthWithReusableResources(reverse.get(), inputData) < inputData.getPath().getBandwidth()) { + errors.add(getReverseBandwidthErrorMessage(inputData, reverse.get().getAvailableBandwidth())); + } - if (isl.get().getAvailableBandwidth() < pathSegment.path.getMinAvailableBandwidth()) { - return Collections.singleton(getBandwidthErrorMessage(pathSegment, - isl.get().getAvailableBandwidth(), pathSegment.path.getMinAvailableBandwidth())); - } + return errors; + } - return Collections.emptySet(); - }; + private long getForwardBandwidthWithReusableResources(Isl isl, InputData inputData) { + return getBandwidthWithReusableResources(inputData, isl, + pathSegment -> inputData.getSegment().getSrcSwitchId().equals(pathSegment.getSrcSwitchId()) + && inputData.getSegment().getDestSwitchId().equals(pathSegment.getDestSwitchId()) + && inputData.getSegment().getSrcPort().equals(pathSegment.getSrcPort()) + && inputData.getSegment().getDestPort().equals(pathSegment.getDestPort())); } - private BiFunction> getValidateLatencyFunction() { - return (pathSegment, islRepository) -> { - Optional isl = islRepository.findByEndpoints( - pathSegment.getSegment().getSrcSwitchId(), pathSegment.getSegment().getSrcPort(), - pathSegment.getSegment().getDestSwitchId(), pathSegment.getSegment().getDestPort()); - if (!isl.isPresent()) { - return Collections.singleton(getNoLinkError(pathSegment)); - } + private long getReverseBandwidthWithReusableResources(Isl isl, InputData inputData) { + return getBandwidthWithReusableResources(inputData, isl, + pathSegment -> inputData.getSegment().getSrcSwitchId().equals(pathSegment.getDestSwitchId()) + && inputData.getSegment().getDestSwitchId().equals(pathSegment.getSrcSwitchId()) + && inputData.getSegment().getSrcPort().equals(pathSegment.getDestPort()) + && inputData.getSegment().getDestPort().equals(pathSegment.getSrcPort())); + } - if (isl.get().getLatency() > pathSegment.getPath().getLatency()) { - return Collections.singleton(getLatencyErrorMessage(pathSegment, - pathSegment.getPath().getLatency(), isl.get().getLatency())); - } + private long getBandwidthWithReusableResources(InputData inputData, Isl isl, + Predicate pathSegmentPredicate) { + if (inputData.getPath().getReuseFlowResources() != null + && !inputData.getPath().getReuseFlowResources().isEmpty()) { - return Collections.emptySet(); - }; + Optional flow = flowRepository.findById(inputData.getPath().getReuseFlowResources()); + + Optional segment = flow.flatMap(value -> value.getData().getPaths().stream() + .map(FlowPath::getSegments) + .flatMap(List::stream) + .filter(pathSegmentPredicate) + .findAny()); + + return segment.map(s -> isl.getAvailableBandwidth() + s.getBandwidth()) + .orElseGet(isl::getAvailableBandwidth); + } + + return isl.getAvailableBandwidth(); + } + + private Set validateEncapsulationType(InputData inputData, RepositoryData repositoryData) { + // TODO what does it actually mean to validate the encapsulation type? + throw new UnsupportedOperationException(); + } + + private Set validateDiverseWithFlow(InputData inputData, RepositoryData repositoryData) { + if (!repositoryData.getIslRepository().findByEndpoints(inputData.getSegment().getSrcSwitchId(), + inputData.getSegment().getSrcPort(), + inputData.getSegment().getDestSwitchId(), + inputData.getSegment().getDestPort()).isPresent()) { + return Collections.singleton(getNoForwardIslError(inputData)); + } + + Optional diverseFlow = flowRepository.findById(inputData.getPath().getDiverseWithFlow()); + if (!diverseFlow.isPresent()) { + return Collections.singleton(getNoDiverseFlowFoundError(inputData)); + } + + if (diverseFlow.get().getData().getPaths().stream() + .map(FlowPath::getSegments) + .flatMap(List::stream) + .anyMatch(pathSegment -> inputData.getSegment().getSrcSwitchId().equals(pathSegment.getSrcSwitchId()) + && inputData.getSegment().getDestSwitchId().equals(pathSegment.getDestSwitchId()) + && inputData.getSegment().getSrcPort().equals(pathSegment.getSrcPort()) + && inputData.getSegment().getDestPort().equals(pathSegment.getDestPort()))) { + return Collections.singleton(getNotDiverseSegmentError(inputData)); + } + + return Collections.emptySet(); } - private String getLatencyErrorMessage(PathSegment pathSegment, long requestedLatency, long actualLatency) { + private Set validateLatency(InputData inputData, RepositoryData repositoryData) { + Optional isl = repositoryData.getIslRepository().findByEndpoints( + inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort(), + inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort()); + if (!isl.isPresent()) { + return Collections.singleton(getNoForwardIslError(inputData)); + } + // TODO make sure that comparing numbers of the same order + if (isl.get().getLatency() > inputData.getPath().getLatencyMs()) { + return Collections.singleton(getLatencyErrorMessage(inputData, isl.get().getLatency())); + } + + return Collections.emptySet(); + } + + private Set validateLatencyTier2(InputData inputData, RepositoryData repositoryData) { + Optional isl = repositoryData.getIslRepository().findByEndpoints( + inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort(), + inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort()); + if (!isl.isPresent()) { + return Collections.singleton(getNoForwardIslError(inputData)); + } + // TODO make sure that comparing numbers of the same orders + if (isl.get().getLatency() > inputData.getPath().getLatencyTier2ms()) { + return Collections.singleton(getLatencyTier2ErrorMessage(inputData, isl.get().getLatency())); + } + + return Collections.emptySet(); + } + + private String getLatencyTier2ErrorMessage(InputData data, long actualLatency) { + return String.format( + "Requested latency tier 2 is too low between source switch %s port %d and destination switch" + + " %s port %d. Requested %d, but the link supports %d", + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), + data.getPath().getLatencyTier2ms(), actualLatency); + } + + private String getLatencyErrorMessage(InputData data, long actualLatency) { return String.format( "Requested latency is too low between source switch %s port %d and destination switch %s port %d." - + " Requested %d, but the link supports %d", - pathSegment.getSegment().getSrcSwitchId(), pathSegment.getSegment().getSrcPort(), - pathSegment.getSegment().getDestSwitchId(), pathSegment.getSegment().getDestPort(), - requestedLatency, actualLatency); + + " Requested %d, but the link supports %d", + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), + data.getPath().getLatencyMs(), actualLatency); + } + + private String getForwardBandwidthErrorMessage(InputData data, long actualBandwidth) { + return String.format( + "There is not enough bandwidth between the source switch %s port %d and destination switch %s port %d" + + " (forward path). Requested bandwidth %d, but the link supports %d", + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), + data.getPath().getBandwidth(), actualBandwidth); + } + + private String getReverseBandwidthErrorMessage(InputData data, long actualBandwidth) { + return String.format( + "There is not enough Bandwidth between the source switch %s port %d and destination switch %s port %d" + + " (reverse path). Requested bandwidth %d, but the link supports %d", + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getPath().getBandwidth(), actualBandwidth); } - private String getBandwidthErrorMessage(PathSegment pathSegment, Long requestedBandwidth, long actualBandwidth) { + private String getNoForwardIslError(InputData data) { return String.format( - "There is not enough Bandwidth between source switch %s port %d and destination switch %s port %d." - + " Requested bandwidth %d, but the link supports %d", - pathSegment.getSegment().getSrcSwitchId(), pathSegment.getSegment().getSrcPort(), - pathSegment.getSegment().getDestSwitchId(), pathSegment.getSegment().getDestPort(), - requestedBandwidth, actualBandwidth); + "There is no ISL between source switch %s port %d and destination switch %s port %d", + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); } - private String getNoLinkError(PathSegment pathSegment) { + private String getNoReverseIslError(InputData data) { return String.format( "There is no ISL between source switch %s port %d and destination switch %s port %d", - pathSegment.getSegment().getSrcSwitchId(), pathSegment.getSegment().getSrcPort(), - pathSegment.getSegment().getDestSwitchId(), pathSegment.getSegment().getDestPort()); + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort()); + } + + private String getForwardIslNotActiveError(InputData data) { + return String.format( + "The ISL is not in ACTIVE state between source switch %s port %d and destination switch %s port %d", + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); + } + + private String getReverseIslNotActiveError(InputData data) { + return String.format( + "The ISL is not in ACTIVE state between source switch %s port %d and destination switch %s port %d", + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort()); + } + + private String getNoDiverseFlowFoundError(InputData data) { + return String.format("Could not find the diverse flow with ID %s", data.getPath().getDiverseWithFlow()); + } + + private String getNotDiverseSegmentError(InputData data) { + return String.format("The following segment intersects with the flow %s: source switch %s port %d and " + + "destination switch %s port %d", + data.getPath().getDiverseWithFlow(), + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); + } + + @Getter + @AllArgsConstructor + private static class InputData { + PathValidationData path; + PathValidationData.PathSegmentValidationData segment; } @Getter @AllArgsConstructor - private static class PathSegment { - Path path; - Path.Segment segment; + private static class RepositoryData { + IslRepository islRepository; + FlowRepository flowRepository; } } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java index 1800a656afa..4a5c80281a6 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java @@ -28,23 +28,30 @@ import org.openkilda.config.provider.PropertiesBasedConfigurationProvider; import org.openkilda.messaging.command.flow.PathValidateRequest; -import org.openkilda.messaging.info.network.PathValidateData; +import org.openkilda.messaging.info.network.PathValidationResult; import org.openkilda.messaging.info.network.PathsInfoData; import org.openkilda.messaging.payload.flow.PathNodePayload; -import org.openkilda.messaging.payload.network.PathDto; +import org.openkilda.messaging.payload.network.PathValidationDto; +import org.openkilda.model.Flow; import org.openkilda.model.FlowEncapsulationType; +import org.openkilda.model.FlowPath; +import org.openkilda.model.FlowPathDirection; import org.openkilda.model.Isl; import org.openkilda.model.IslStatus; import org.openkilda.model.KildaConfiguration; import org.openkilda.model.PathComputationStrategy; +import org.openkilda.model.PathId; +import org.openkilda.model.PathSegment; import org.openkilda.model.Switch; import org.openkilda.model.SwitchId; import org.openkilda.model.SwitchProperties; import org.openkilda.model.SwitchStatus; +import org.openkilda.model.cookie.FlowSegmentCookie; import org.openkilda.pce.PathComputerConfig; import org.openkilda.pce.exception.RecoverableException; import org.openkilda.pce.exception.UnroutableFlowException; import org.openkilda.persistence.inmemory.InMemoryGraphBasedTest; +import org.openkilda.persistence.repositories.FlowRepository; import org.openkilda.persistence.repositories.IslRepository; import org.openkilda.persistence.repositories.KildaConfigurationRepository; import org.openkilda.persistence.repositories.RepositoryFactory; @@ -58,6 +65,7 @@ import org.junit.Test; import java.time.Duration; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -78,6 +86,7 @@ public class PathsServiceTest extends InMemoryGraphBasedTest { private static SwitchRepository switchRepository; private static SwitchPropertiesRepository switchPropertiesRepository; private static IslRepository islRepository; + private static FlowRepository flowRepository; private static PathsService pathsService; @@ -88,10 +97,12 @@ public static void setUpOnce() { switchRepository = repositoryFactory.createSwitchRepository(); switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); islRepository = repositoryFactory.createIslRepository(); + flowRepository = repositoryFactory.createFlowRepository(); kildaConfigurationRepository = repositoryFactory.createKildaConfigurationRepository(); PathComputerConfig pathComputerConfig = new PropertiesBasedConfigurationProvider() .getConfiguration(PathComputerConfig.class); - pathsService = new PathsService(repositoryFactory, pathComputerConfig, islRepository); + pathsService = new PathsService(repositoryFactory, pathComputerConfig, islRepository, + repositoryFactory.createFlowRepository()); } @Before @@ -303,13 +314,14 @@ public void whenValidPath_validatePathReturnsValidResponseTest() { nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathDto.builder() + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() .nodes(nodes) .bandwidth(0L) .isBackupPath(false) - .latency(0L) + .latencyMs(0L) + .latencyTier2ms(0L) .build()); - List responses = pathsService.validatePath(request); + List responses = pathsService.validatePath(request); assertFalse(responses.isEmpty()); assertTrue(responses.get(0).getIsValid()); @@ -319,21 +331,27 @@ public void whenValidPath_validatePathReturnsValidResponseTest() { public void whenValidPathButNotEnoughBandwidth_validateReturnsErrorResponseTest() { List nodes = new LinkedList<>(); nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathDto.builder() + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() .nodes(nodes) .bandwidth(1000000000L) .isBackupPath(false) - .latency(10L) + .latencyMs(0L) + .latencyTier2ms(0L) .build()); - List responses = pathsService.validatePath(request); + List responses = pathsService.validatePath(request); assertFalse(responses.isEmpty()); assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); + assertEquals("There must be 2 errors: forward and reverse paths", + 2, responses.get(0).getErrors().size()); + Collections.sort(responses.get(0).getErrors()); assertTrue(responses.get(0).getErrors().get(0) - .contains("There is not enough Bandwidth between source switch")); + .startsWith("There is not enough Bandwidth between the source switch 00:00:00:00:00:00:00:03 port 6 " + + "and destination switch 00:00:00:00:00:00:00:01 port 6 (reverse path).")); + assertTrue(responses.get(0).getErrors().get(1) + .startsWith("There is not enough bandwidth between the source switch 00:00:00:00:00:00:00:01 port 6 and" + + " destination switch 00:00:00:00:00:00:00:03 port 6 (forward path).")); } @Test @@ -342,13 +360,14 @@ public void whenValidPathButTooLowLatency_validateReturnsErrorResponseTest() { nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathDto.builder() + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() .nodes(nodes) .bandwidth(0L) .isBackupPath(false) - .latency(10L) + .latencyMs(10L) + .latencyTier2ms(0L) .build()); - List responses = pathsService.validatePath(request); + List responses = pathsService.validatePath(request); assertFalse(responses.isEmpty()); assertFalse(responses.get(0).getIsValid()); @@ -362,13 +381,14 @@ public void whenNoLinkBetweenSwitches_validatePathReturnsErrorResponseTest() { List nodes = new LinkedList<>(); nodes.add(new PathNodePayload(SWITCH_ID_1, null, 1)); nodes.add(new PathNodePayload(SWITCH_ID_2, 0, null)); - PathValidateRequest request = new PathValidateRequest(PathDto.builder() + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() .nodes(nodes) .bandwidth(0L) .isBackupPath(false) - .latency(0L) + .latencyMs(0L) + .latencyTier2ms(0L) .build()); - List responses = pathsService.validatePath(request); + List responses = pathsService.validatePath(request); assertFalse(responses.isEmpty()); assertFalse(responses.get(0).getIsValid()); @@ -377,6 +397,34 @@ public void whenNoLinkBetweenSwitches_validatePathReturnsErrorResponseTest() { .startsWith("There is no ISL between source switch")); } + @Test + public void whenDiverseWith_andExistsIntersection_validateReturnsError() { + Switch switch1 = Switch.builder().switchId(SWITCH_ID_1).build(); + Switch switch2 = Switch.builder().switchId(SWITCH_ID_3).build(); + createFlow("flow_1", switch1, 6, switch2, 6); + + assertTrue(flowRepository.findById("flow_1").isPresent()); + assertFalse(flowRepository.findById("flow_1").get().getData().getPaths().isEmpty()); + + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .isBackupPath(false) + .latencyMs(0L) + .latencyTier2ms(0L) + .diverseWithFlow("flow_1") + .build()); + List responses = pathsService.validatePath(request); + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertEquals(1, responses.get(0).getErrors().size()); + assertTrue(responses.get(0).getErrors().get(0).startsWith("The following segment intersects with the flow")); + } + @Test public void whenMultipleProblemsOnPath_validatePathReturnsAllErrorResponseTest() { List nodes = new LinkedList<>(); @@ -384,19 +432,20 @@ public void whenMultipleProblemsOnPath_validatePathReturnsAllErrorResponseTest() nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); nodes.add(new PathNodePayload(SWITCH_ID_4, 7, 7)); nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathDto.builder() + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() .nodes(nodes) .bandwidth(1000000L) .isBackupPath(false) - .latency(10L) + .latencyMs(10L) + .latencyTier2ms(0L) .build()); - List responses = pathsService.validatePath(request); + List responses = pathsService.validatePath(request); assertFalse(responses.isEmpty()); assertFalse(responses.get(0).getIsValid()); assertFalse(responses.get(0).getErrors().isEmpty()); - assertEquals("There must be 4 errors in total. bandwidth, latency, 2 links are not present", - 4, responses.get(0).getErrors().size()); + assertEquals("There must be 5 errors in total: 2 bandwidth (forward and reverse paths), " + + "2 links are not present, and 1 latency", 5, responses.get(0).getErrors().size()); } private void assertMaxLatencyPaths(List paths, Duration maxLatency, long expectedCount, @@ -483,4 +532,49 @@ private void createSwitchProperties(Switch sw, FlowEncapsulationType... encapsul .supportedTransitEncapsulation(Sets.newHashSet(encapsulation)) .build()); } + + private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort) { + Flow flow = Flow.builder() + .flowId(flowId) + .srcSwitch(srcSwitch) + .srcPort(srcPort) + .destSwitch(destSwitch) + .destPort(destPort) + .build(); + FlowPath forwardPath = FlowPath.builder() + .pathId(new PathId("path_1")) + .srcSwitch(srcSwitch) + .destSwitch(destSwitch) + .cookie(new FlowSegmentCookie(FlowPathDirection.FORWARD, 1L)) + .bandwidth(flow.getBandwidth()) + .ignoreBandwidth(false) + .segments(Collections.singletonList(PathSegment.builder() + .pathId(new PathId("forward_segment")) + .srcSwitch(srcSwitch) + .srcPort(srcPort) + .destSwitch(destSwitch) + .destPort(destPort) + .build())) + .build(); + + flow.setForwardPath(forwardPath); + FlowPath reversePath = FlowPath.builder() + .pathId(new PathId("path_2")) + .srcSwitch(destSwitch) + .destSwitch(srcSwitch) + .cookie(new FlowSegmentCookie(FlowPathDirection.REVERSE, 1L)) + .bandwidth(flow.getBandwidth()) + .ignoreBandwidth(false) + .segments(Collections.singletonList(PathSegment.builder() + .pathId(new PathId("reverse_segment")) + .srcSwitch(destSwitch) + .srcPort(destPort) + .destSwitch(srcSwitch) + .destPort(srcPort) + .build())) + .build(); + flow.setReversePath(reversePath); + + flowRepository.add(flow); + } } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java index ab1af188cb9..9aca291ea89 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java @@ -17,7 +17,7 @@ import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.error.MessageException; -import org.openkilda.messaging.payload.network.PathDto; +import org.openkilda.messaging.payload.network.PathValidationDto; import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; import org.openkilda.northbound.service.NetworkService; @@ -42,18 +42,27 @@ public class NetworkControllerV2 { /** * Validates that a given path complies with the chosen strategy and the network availability. * It is required that the input contains path nodes. Other parameters are opti - * @param pathDto a payload with a path and additional flow parameters provided by a user + * @param pathValidationDto a payload with a path and additional flow parameters provided by a user * @return either a successful response or the list of errors */ @GetMapping(path = "/path/check") @ApiOperation(value = "Validates that a given path complies with the chosen strategy and the network availability") @ResponseStatus(HttpStatus.OK) - public CompletableFuture validateCustomFlowPath(@RequestBody PathDto pathDto) { - if (pathDto == null || pathDto.getNodes() == null || pathDto.getNodes().size() < 2) { + public CompletableFuture validateCustomFlowPath( + @RequestBody PathValidationDto pathValidationDto) { + validateInput(pathValidationDto); + + return networkService.validateFlowPath(pathValidationDto); + } + + private void validateInput(PathValidationDto pathValidationDto) { + //TODO validate all fields + + if (pathValidationDto == null + || pathValidationDto.getNodes() == null + || pathValidationDto.getNodes().size() < 2) { throw new MessageException(ErrorType.DATA_INVALID, "Invalid Request Body", "Invalid 'nodes' value in the request body"); } - - return networkService.validateFlowPath(pathDto); } } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java index 48aac3b1b96..ee45e4f1df8 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java @@ -16,7 +16,7 @@ package org.openkilda.northbound.converter; import org.openkilda.messaging.info.network.Path; -import org.openkilda.messaging.info.network.PathValidateData; +import org.openkilda.messaging.info.network.PathValidationResult; import org.openkilda.messaging.model.FlowPathDto; import org.openkilda.messaging.payload.flow.GroupFlowPathPayload; import org.openkilda.messaging.payload.flow.GroupFlowPathPayload.FlowProtectedPathsPayload; @@ -36,5 +36,5 @@ default PathDto mapToPath(Path data) { FlowProtectedPathsPayload mapFlowProtectedPathPayload(FlowPathDto.FlowProtectedPathDto data); - PathValidateResponse toPathValidateResponse(PathValidateData data); + PathValidateResponse toPathValidateResponse(PathValidationResult data); } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java index 00e799608ca..09c8a9ebb2a 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java @@ -15,7 +15,7 @@ package org.openkilda.northbound.service; -import org.openkilda.messaging.payload.network.PathDto; +import org.openkilda.messaging.payload.network.PathValidationDto; import org.openkilda.messaging.payload.network.PathsDto; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.PathComputationStrategy; @@ -42,8 +42,8 @@ CompletableFuture getPaths( * Validates that a flow with the given path can possibly be created. If it is not possible, * it responds with the reasons, such as: not enough bandwidth, requested latency it too low, there is no * links between the selected switches, and so on. - * @param path a path provided by a user + * @param pathValidationDto a path together with validation parameters provided by a user * @return either a successful response or the list of errors */ - CompletableFuture validateFlowPath(PathDto path); + CompletableFuture validateFlowPath(PathValidationDto pathValidationDto); } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java index 54855150e97..5652244ce92 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java @@ -19,10 +19,11 @@ import org.openkilda.messaging.command.flow.PathValidateRequest; import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.error.MessageException; -import org.openkilda.messaging.info.network.PathValidateData; +import org.openkilda.messaging.info.network.PathValidationResult; import org.openkilda.messaging.info.network.PathsInfoData; import org.openkilda.messaging.nbtopology.request.GetPathsRequest; import org.openkilda.messaging.payload.network.PathDto; +import org.openkilda.messaging.payload.network.PathValidationDto; import org.openkilda.messaging.payload.network.PathsDto; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.PathComputationStrategy; @@ -100,17 +101,17 @@ public CompletableFuture getPaths( * Validates that a flow with the given path can possibly be created. If it is not possible, * it responds with the reasons, such as: not enough bandwidth, requested latency it too low, there is no * links between the selected switches, and so on. - * @param path a path provided by a user + * @param pathValidationDto a path together with validation parameters provided by a user * @return either a successful response or a list of errors */ @Override - public CompletableFuture validateFlowPath(PathDto path) { - PathValidateRequest request = new PathValidateRequest(path); + public CompletableFuture validateFlowPath(PathValidationDto pathValidationDto) { + PathValidateRequest request = new PathValidateRequest(pathValidationDto); CommandMessage message = new CommandMessage(request, System.currentTimeMillis(), RequestCorrelationId.getId()); return messagingChannel.sendAndGet(nbworkerTopic, message) - .thenApply(PathValidateData.class::cast) + .thenApply(PathValidationResult.class::cast) .thenApply(pathMapper::toPathValidateResponse); } } From ec1c5359acc2ea4171953b487fc155b104bf30c8 Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Tue, 21 Feb 2023 18:34:28 +0100 Subject: [PATCH 11/45] Add support of path computation strategy (validations are used or ignored based on the strategy), add path encapsulation validation, add an error message when switch is not found, add unit tests, remove no longer needed parts --- .../payload/network/PathValidationDto.java | 13 +- .../openkilda/model/PathValidationData.java | 2 +- .../mappers/PathValidationDataMapper.java | 2 +- .../nbworker/services/PathsService.java | 4 +- .../nbworker/validators/PathValidator.java | 94 ++++++++-- .../nbworker/services/PathsServiceTest.java | 167 ++++++++++++++++-- 6 files changed, 251 insertions(+), 31 deletions(-) diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java index 770234499cf..e879b2a7058 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java @@ -17,6 +17,7 @@ import org.openkilda.messaging.payload.flow.FlowEncapsulationType; import org.openkilda.messaging.payload.flow.PathNodePayload; +import org.openkilda.model.PathComputationStrategy; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; @@ -42,9 +43,6 @@ public class PathValidationDto { @JsonProperty("nodes") List nodes; - @JsonProperty("is_backup_path") - Boolean isBackupPath; - @JsonProperty("diverse_with_flow") String diverseWithFlow; @@ -54,22 +52,25 @@ public class PathValidationDto { @JsonProperty("flow_encapsulation_type") FlowEncapsulationType flowEncapsulationType; + @JsonProperty("path_computation_strategy") + PathComputationStrategy pathComputationStrategy; + @JsonCreator public PathValidationDto(@JsonProperty("bandwidth") Long bandwidth, @JsonProperty("latency_ms") Long latencyMs, @JsonProperty("max_latency_tier2") Long latencyTier2ms, @JsonProperty("nodes") List nodes, - @JsonProperty("is_backup_path") Boolean isBackupPath, @JsonProperty("diverse_with_flow") String diverseWithFlow, @JsonProperty("reuse_flow_resources") String reuseFlowResources, - @JsonProperty("flow_encapsulation_type") FlowEncapsulationType flowEncapsulationType) { + @JsonProperty("flow_encapsulation_type") FlowEncapsulationType flowEncapsulationType, + @JsonProperty("path_computation_strategy") PathComputationStrategy computationStrategy) { this.bandwidth = bandwidth; this.latencyMs = latencyMs; this.latencyTier2ms = latencyTier2ms; this.nodes = nodes; - this.isBackupPath = isBackupPath; this.diverseWithFlow = diverseWithFlow; this.reuseFlowResources = reuseFlowResources; this.flowEncapsulationType = flowEncapsulationType; + this.pathComputationStrategy = computationStrategy; } } diff --git a/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java b/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java index 0dadf423ffb..594fb6403d3 100644 --- a/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java +++ b/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java @@ -28,7 +28,6 @@ public class PathValidationData { Long latencyMs; Long latencyTier2ms; List pathSegments; - Boolean isBackupPath; String diverseWithFlow; String reuseFlowResources; FlowEncapsulationType flowEncapsulationType; @@ -36,6 +35,7 @@ public class PathValidationData { Integer srcPort; SwitchId destSwitchId; Integer destPort; + PathComputationStrategy pathComputationStrategy; @Value @Builder diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java index 204a8397813..5578ae4e5df 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java @@ -49,7 +49,6 @@ public PathValidationData toPathValidationData(PathValidationDto pathValidationD return PathValidationData.builder() .srcSwitchId(pathValidationDto.getNodes().get(0).getSwitchId()) .destSwitchId(pathValidationDto.getNodes().get(pathValidationDto.getNodes().size() - 1).getSwitchId()) - .isBackupPath(pathValidationDto.getIsBackupPath()) .bandwidth(pathValidationDto.getBandwidth()) .latencyMs(pathValidationDto.getLatencyMs()) .latencyTier2ms(pathValidationDto.getLatencyTier2ms()) @@ -57,6 +56,7 @@ public PathValidationData toPathValidationData(PathValidationDto pathValidationD .reuseFlowResources(pathValidationDto.getReuseFlowResources()) .flowEncapsulationType(FlowEncapsulationTypeMapper.INSTANCE.toOpenKildaModel( pathValidationDto.getFlowEncapsulationType())) + .pathComputationStrategy(pathValidationDto.getPathComputationStrategy()) .pathSegments(segments) .build(); } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java index 8ccd6cff698..1b791b11941 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java @@ -140,7 +140,9 @@ public List getPaths( * @return a response with the success or the list of errors */ public List validatePath(PathValidateRequest request) { - PathValidator pathValidator = new PathValidator(islRepository, flowRepository); + PathValidator pathValidator = new PathValidator(islRepository, + flowRepository, + switchPropertiesRepository); return Collections.singletonList(pathValidator.validatePath( PathValidationDataMapper.INSTANCE.toPathValidationData(request.getPathValidationDto()) diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java index fd614083a81..12b56e02e87 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java @@ -15,6 +15,10 @@ package org.openkilda.wfm.topology.nbworker.validators; +import static org.openkilda.model.PathComputationStrategy.COST_AND_AVAILABLE_BANDWIDTH; +import static org.openkilda.model.PathComputationStrategy.LATENCY; +import static org.openkilda.model.PathComputationStrategy.MAX_LATENCY; + import org.openkilda.messaging.info.network.PathValidationResult; import org.openkilda.model.Flow; import org.openkilda.model.FlowPath; @@ -22,8 +26,10 @@ import org.openkilda.model.IslStatus; import org.openkilda.model.PathSegment; import org.openkilda.model.PathValidationData; +import org.openkilda.model.SwitchProperties; import org.openkilda.persistence.repositories.FlowRepository; import org.openkilda.persistence.repositories.IslRepository; +import org.openkilda.persistence.repositories.SwitchPropertiesRepository; import lombok.AllArgsConstructor; import lombok.Getter; @@ -43,10 +49,14 @@ public class PathValidator { private final IslRepository islRepository; private final FlowRepository flowRepository; + private final SwitchPropertiesRepository switchPropertiesRepository; - public PathValidator(IslRepository islRepository, FlowRepository flowRepository) { + public PathValidator(IslRepository islRepository, + FlowRepository flowRepository, + SwitchPropertiesRepository switchPropertiesRepository) { this.islRepository = islRepository; this.flowRepository = flowRepository; + this.switchPropertiesRepository = switchPropertiesRepository; } /** @@ -61,7 +71,7 @@ public PathValidationResult validatePath(PathValidationData pathValidationData) Set result = pathValidationData.getPathSegments().stream() .map(segment -> executeValidations( new InputData(pathValidationData, segment), - new RepositoryData(islRepository, flowRepository), + new RepositoryData(islRepository, flowRepository, switchPropertiesRepository), getValidations(pathValidationData))) .flatMap(Set::stream) .collect(Collectors.toSet()); @@ -83,24 +93,42 @@ private Set executeValidations(InputData inputData, } private List>> getValidations( - PathValidationData inputData) { + PathValidationData pathValidationData) { List>> validationFunctions = new LinkedList<>(); validationFunctions.add(this::validateLink); - if (inputData.getLatencyMs() != 0) { + if (pathValidationData.getLatencyMs() != null + && pathValidationData.getLatencyMs() != 0 + && (pathValidationData.getPathComputationStrategy() == null + || pathValidationData.getPathComputationStrategy() == LATENCY + || pathValidationData.getPathComputationStrategy() == MAX_LATENCY)) { validationFunctions.add(this::validateLatency); } - if (inputData.getLatencyTier2ms() != 0) { + + if (pathValidationData.getLatencyTier2ms() != null + && pathValidationData.getLatencyTier2ms() != 0 + && (pathValidationData.getPathComputationStrategy() == null + || pathValidationData.getPathComputationStrategy() == LATENCY + || pathValidationData.getPathComputationStrategy() == MAX_LATENCY)) { validationFunctions.add(this::validateLatencyTier2); } - if (inputData.getBandwidth() != 0) { + + if (pathValidationData.getBandwidth() != null + && pathValidationData.getBandwidth() != 0 + && (pathValidationData.getPathComputationStrategy() == null + || pathValidationData.getPathComputationStrategy() == COST_AND_AVAILABLE_BANDWIDTH)) { validationFunctions.add(this::validateBandwidth); } - if (inputData.getDiverseWithFlow() != null && !inputData.getDiverseWithFlow().isEmpty()) { + + if (pathValidationData.getDiverseWithFlow() != null && !pathValidationData.getDiverseWithFlow().isEmpty()) { validationFunctions.add(this::validateDiverseWithFlow); } + if (pathValidationData.getFlowEncapsulationType() != null) { + validationFunctions.add(this::validateEncapsulationType); + } + return validationFunctions; } @@ -183,7 +211,7 @@ private long getBandwidthWithReusableResources(InputData inputData, Isl isl, Optional flow = flowRepository.findById(inputData.getPath().getReuseFlowResources()); - Optional segment = flow.flatMap(value -> value.getData().getPaths().stream() + Optional segment = flow.flatMap(value -> value.getPaths().stream() .map(FlowPath::getSegments) .flatMap(List::stream) .filter(pathSegmentPredicate) @@ -197,8 +225,33 @@ private long getBandwidthWithReusableResources(InputData inputData, Isl isl, } private Set validateEncapsulationType(InputData inputData, RepositoryData repositoryData) { - // TODO what does it actually mean to validate the encapsulation type? - throw new UnsupportedOperationException(); + Set errors = new HashSet<>(); + Optional srcSwitchProperties = switchPropertiesRepository.findBySwitchId( + inputData.getPath().getSrcSwitchId()); + if (!srcSwitchProperties.isPresent()) { + errors.add(getSrcSwitchNotFoundError(inputData)); + } + + srcSwitchProperties.ifPresent(switchProperties -> { + if (!switchProperties.getSupportedTransitEncapsulation() + .contains(inputData.getPath().getFlowEncapsulationType())) { + errors.add(getSrcSwitchDoesNotSupportEncapsulationTypeError(inputData)); + } + }); + + Optional destSwitchProperties = switchPropertiesRepository.findBySwitchId( + inputData.getPath().getDestSwitchId()); + if (!destSwitchProperties.isPresent()) { + errors.add(getDestSwitchNotFoundError(inputData)); + } + + destSwitchProperties.ifPresent(switchProperties -> { + if (!switchProperties.getSupportedTransitEncapsulation() + .contains(inputData.getPath().getFlowEncapsulationType())) { + errors.add(getDestSwitchDoesNotSupportEncapsulationTypeError(inputData)); + } + }); + return errors; } private Set validateDiverseWithFlow(InputData inputData, RepositoryData repositoryData) { @@ -286,7 +339,7 @@ private String getForwardBandwidthErrorMessage(InputData data, long actualBandwi private String getReverseBandwidthErrorMessage(InputData data, long actualBandwidth) { return String.format( - "There is not enough Bandwidth between the source switch %s port %d and destination switch %s port %d" + "There is not enough bandwidth between the source switch %s port %d and destination switch %s port %d" + " (reverse path). Requested bandwidth %d, but the link supports %d", data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), @@ -333,6 +386,24 @@ private String getNotDiverseSegmentError(InputData data) { data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); } + private String getSrcSwitchNotFoundError(InputData data) { + return String.format("The following switch has not been found: %s", data.getSegment().getSrcSwitchId()); + } + + private String getDestSwitchNotFoundError(InputData data) { + return String.format("The following switch has not been found: %s", data.getSegment().getDestSwitchId()); + } + + private String getSrcSwitchDoesNotSupportEncapsulationTypeError(InputData data) { + return String.format("The switch %s doesn't support encapsulation type %s", + data.getSegment().getSrcSwitchId(), data.getPath().getFlowEncapsulationType()); + } + + private String getDestSwitchDoesNotSupportEncapsulationTypeError(InputData data) { + return String.format("The switch %s doesn't support encapsulation type %s", + data.getSegment().getDestSwitchId(), data.getPath().getFlowEncapsulationType()); + } + @Getter @AllArgsConstructor private static class InputData { @@ -345,5 +416,6 @@ private static class InputData { private static class RepositoryData { IslRepository islRepository; FlowRepository flowRepository; + SwitchPropertiesRepository switchPropertiesRepository; } } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java index 4a5c80281a6..6b3f1c93e25 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java @@ -23,6 +23,7 @@ import static org.openkilda.model.FlowEncapsulationType.TRANSIT_VLAN; import static org.openkilda.model.FlowEncapsulationType.VXLAN; import static org.openkilda.model.PathComputationStrategy.COST; +import static org.openkilda.model.PathComputationStrategy.COST_AND_AVAILABLE_BANDWIDTH; import static org.openkilda.model.PathComputationStrategy.LATENCY; import static org.openkilda.model.PathComputationStrategy.MAX_LATENCY; @@ -317,7 +318,6 @@ public void whenValidPath_validatePathReturnsValidResponseTest() { PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() .nodes(nodes) .bandwidth(0L) - .isBackupPath(false) .latencyMs(0L) .latencyTier2ms(0L) .build()); @@ -335,7 +335,6 @@ public void whenValidPathButNotEnoughBandwidth_validateReturnsErrorResponseTest( PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() .nodes(nodes) .bandwidth(1000000000L) - .isBackupPath(false) .latencyMs(0L) .latencyTier2ms(0L) .build()); @@ -355,7 +354,7 @@ public void whenValidPathButNotEnoughBandwidth_validateReturnsErrorResponseTest( } @Test - public void whenValidPathButTooLowLatency_validateReturnsErrorResponseTest() { + public void whenValidPathButTooLowLatency_andLatencyStrategy_validateReturnsErrorResponseTest() { List nodes = new LinkedList<>(); nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); @@ -363,9 +362,9 @@ public void whenValidPathButTooLowLatency_validateReturnsErrorResponseTest() { PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() .nodes(nodes) .bandwidth(0L) - .isBackupPath(false) .latencyMs(10L) .latencyTier2ms(0L) + .pathComputationStrategy(LATENCY) .build()); List responses = pathsService.validatePath(request); @@ -376,6 +375,70 @@ public void whenValidPathButTooLowLatency_validateReturnsErrorResponseTest() { .contains("Requested latency is too low between source switch")); } + @Test + public void whenValidPathButTooLowLatency_andMaxLatencyStrategy_validateReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10L) + .latencyTier2ms(0L) + .pathComputationStrategy(MAX_LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertTrue(responses.get(0).getErrors().get(0) + .contains("Requested latency is too low between source switch")); + } + + @Test + public void whenValidPathButTooLowLatencyTier2_andLatencyStrategy_validateReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10000000000L) + .latencyTier2ms(100L) + .pathComputationStrategy(LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertTrue(responses.get(0).getErrors().get(0) + .contains("Requested latency tier 2 is too low")); + } + + @Test + public void whenSwitchDoesNotSupportEncapsulationType_validateReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + // TODO investigate: when uncommenting the following line, switch 3 supports VXLAN. Why? + //nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .flowEncapsulationType(org.openkilda.messaging.payload.flow.FlowEncapsulationType.VXLAN) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertTrue(responses.get(0).getErrors().get(0) + .contains("doesn't support encapsulation type VXLAN")); + } + @Test public void whenNoLinkBetweenSwitches_validatePathReturnsErrorResponseTest() { List nodes = new LinkedList<>(); @@ -384,7 +447,6 @@ public void whenNoLinkBetweenSwitches_validatePathReturnsErrorResponseTest() { PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() .nodes(nodes) .bandwidth(0L) - .isBackupPath(false) .latencyMs(0L) .latencyTier2ms(0L) .build()); @@ -413,7 +475,6 @@ public void whenDiverseWith_andExistsIntersection_validateReturnsError() { PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() .nodes(nodes) .bandwidth(0L) - .isBackupPath(false) .latencyMs(0L) .latencyTier2ms(0L) .diverseWithFlow("flow_1") @@ -435,7 +496,6 @@ public void whenMultipleProblemsOnPath_validatePathReturnsAllErrorResponseTest() PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() .nodes(nodes) .bandwidth(1000000L) - .isBackupPath(false) .latencyMs(10L) .latencyTier2ms(0L) .build()); @@ -448,6 +508,84 @@ public void whenMultipleProblemsOnPath_validatePathReturnsAllErrorResponseTest() + "2 links are not present, and 1 latency", 5, responses.get(0).getErrors().size()); } + @Test + public void whenNonLatencyPathComputationStrategy_ignoreLatencyAnd_validatePathReturnsSuccessResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertTrue(responses.get(0).getIsValid()); + } + + @Test + public void whenValidPathWithExistingFlowAndReuseResources_validatePathReturnsSuccessResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(1000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) + .build()); + List responsesBefore = pathsService.validatePath(request); + + assertFalse(responsesBefore.isEmpty()); + assertTrue("The path using default segments with bandwidth 1003 must be valid", + responsesBefore.get(0).getIsValid()); + + Optional islForward = islRepository.findByEndpoints(SWITCH_ID_3, 7, SWITCH_ID_2, 7); + assertTrue(islForward.isPresent()); + islForward.get().setAvailableBandwidth(100L); + Optional islReverse = islRepository.findByEndpoints(SWITCH_ID_2, 7, SWITCH_ID_3, 7); + assertTrue(islReverse.isPresent()); + islReverse.get().setAvailableBandwidth(100L); + + String flowToReuse = "flow_3_2"; + createFlow(flowToReuse, Switch.builder().switchId(SWITCH_ID_3).build(), 2000, + Switch.builder().switchId(SWITCH_ID_2).build(), 2000, + false, 900L, islForward.get()); + + List responsesAfter = pathsService.validatePath(request); + + assertFalse(responsesAfter.isEmpty()); + assertFalse("The path must not be valid because the flow %s consumes bandwidth", + responsesAfter.get(0).getIsValid()); + assertFalse(responsesAfter.get(0).getErrors().isEmpty()); + assertEquals("There must be 2 errors in total: not enough bandwidth on forward and reverse paths", + 2, responsesAfter.get(0).getErrors().size()); + assertTrue(responsesAfter.get(0).getErrors().get(0).contains("There is not enough bandwidth")); + assertTrue(responsesAfter.get(0).getErrors().get(1).contains("There is not enough bandwidth")); + + PathValidateRequest requestWithReuseResources = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(1000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) + .reuseFlowResources(flowToReuse) + .build()); + + List responseWithReuseResources = pathsService.validatePath(requestWithReuseResources); + + assertFalse(responseWithReuseResources.isEmpty()); + assertTrue("The path must be valid because, although the flow %s consumes bandwidth, the validator" + + " includes the consumed bandwidth to available bandwidth", + responseWithReuseResources.get(0).getIsValid()); + } + private void assertMaxLatencyPaths(List paths, Duration maxLatency, long expectedCount, FlowEncapsulationType encapsulationType) { assertEquals(expectedCount, paths.size()); @@ -534,6 +672,11 @@ private void createSwitchProperties(Switch sw, FlowEncapsulationType... encapsul } private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort) { + createFlow(flowId, srcSwitch, srcPort, destSwitch, destPort, null, null, null); + } + + private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort, + Boolean ignoreBandwidth, Long bandwidth, Isl isl) { Flow flow = Flow.builder() .flowId(flowId) .srcSwitch(srcSwitch) @@ -541,6 +684,8 @@ private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch des .destSwitch(destSwitch) .destPort(destPort) .build(); + Optional.ofNullable(ignoreBandwidth).ifPresent(flow::setIgnoreBandwidth); + Optional.ofNullable(bandwidth).ifPresent(flow::setBandwidth); FlowPath forwardPath = FlowPath.builder() .pathId(new PathId("path_1")) .srcSwitch(srcSwitch) @@ -551,9 +696,9 @@ private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch des .segments(Collections.singletonList(PathSegment.builder() .pathId(new PathId("forward_segment")) .srcSwitch(srcSwitch) - .srcPort(srcPort) + .srcPort(isl == null ? srcPort : isl.getSrcPort()) .destSwitch(destSwitch) - .destPort(destPort) + .destPort(isl == null ? destPort : isl.getDestPort()) .build())) .build(); @@ -568,9 +713,9 @@ private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch des .segments(Collections.singletonList(PathSegment.builder() .pathId(new PathId("reverse_segment")) .srcSwitch(destSwitch) - .srcPort(destPort) + .srcPort(isl == null ? destPort : isl.getDestPort()) .destSwitch(srcSwitch) - .destPort(srcPort) + .destPort(isl == null ? srcPort : isl.getSrcPort()) .build())) .build(); flow.setReversePath(reversePath); From 69ed53736beaa688ac841d135d26d6639ffb75a2 Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Wed, 22 Feb 2023 13:35:28 +0100 Subject: [PATCH 12/45] Correct assertions and typo in the error message. --- .../nbworker/services/PathsServiceTest.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java index 6b3f1c93e25..5a2ebdb35cd 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java @@ -345,12 +345,14 @@ public void whenValidPathButNotEnoughBandwidth_validateReturnsErrorResponseTest( assertEquals("There must be 2 errors: forward and reverse paths", 2, responses.get(0).getErrors().size()); Collections.sort(responses.get(0).getErrors()); - assertTrue(responses.get(0).getErrors().get(0) - .startsWith("There is not enough Bandwidth between the source switch 00:00:00:00:00:00:00:03 port 6 " - + "and destination switch 00:00:00:00:00:00:00:01 port 6 (reverse path).")); - assertTrue(responses.get(0).getErrors().get(1) - .startsWith("There is not enough bandwidth between the source switch 00:00:00:00:00:00:00:01 port 6 and" - + " destination switch 00:00:00:00:00:00:00:03 port 6 (forward path).")); + assertEquals(responses.get(0).getErrors().get(0), + "There is not enough bandwidth between the source switch 00:00:00:00:00:00:00:01 port 6 and " + + "destination switch 00:00:00:00:00:00:00:03 port 6 (forward path). " + + "Requested bandwidth 1000000000, but the link supports 1003"); + assertEquals(responses.get(0).getErrors().get(1), + "There is not enough bandwidth between the source switch 00:00:00:00:00:00:00:03 port 6 and " + + "destination switch 00:00:00:00:00:00:00:01 port 6 (reverse path). " + + "Requested bandwidth 1000000000, but the link supports 1003"); } @Test From 52b6ca6bcf2021741494c5598787acd1b8f677ee Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Thu, 2 Feb 2023 21:51:24 +0100 Subject: [PATCH 13/45] Fixed bug with empty vlan stats and set vlans Closes #5063 --- .../services/FlowOperationsService.java | 10 ++- .../services/FlowOperationsServiceTest.java | 88 ++++++++++++++++--- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java index ecb92f2a25c..1404e14b858 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java @@ -577,8 +577,7 @@ private void validateFlow(FlowPatch flowPatch, Flow flow) { + "at the same time"); } - if ((flow.getVlanStatistics() != null && !flow.getVlanStatistics().isEmpty()) - || (flowPatch.getVlanStatistics() != null && !flowPatch.getVlanStatistics().isEmpty())) { + if (!isVlanStatisticsEmpty(flowPatch, flow)) { boolean zeroResultSrcVlan = isResultingVlanValueIsZero(flowPatch.getSource(), flow.getSrcVlan()); boolean zeroResultDstVlan = isResultingVlanValueIsZero(flowPatch.getDestination(), flow.getDestVlan()); @@ -734,6 +733,13 @@ private Collection getDiverseWithFlow(Flow flow) { .collect(Collectors.toSet()); } + private static boolean isVlanStatisticsEmpty(FlowPatch flowPatch, Flow flow) { + if (flowPatch.getVlanStatistics() != null) { + return flowPatch.getVlanStatistics().isEmpty(); + } + return flow.getVlanStatistics() == null || flow.getVlanStatistics().isEmpty(); + } + @Data @Builder static class UpdateFlowResult { diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsServiceTest.java index 92e37b6d464..5e0a900155b 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsServiceTest.java @@ -76,8 +76,10 @@ public class FlowOperationsServiceTest extends InMemoryGraphBasedTest { public static final SwitchId SWITCH_ID_3 = new SwitchId(3); public static final SwitchId SWITCH_ID_4 = new SwitchId(4); public static final int VLAN_1 = 1; - public static final int PORT_1 = 1; - public static final int PORT_2 = 2; + public static final int PORT_1 = 2; + public static final int PORT_2 = 3; + public static final int VLAN_2 = 4; + public static final int VLAN_3 = 5; private static FlowOperationsService flowOperationsService; private static FlowRepository flowRepository; @@ -250,29 +252,60 @@ public void updateVlanStatisticsTest() throws FlowNotFoundException { } @Test - public void updateVlanStatisticsToZeroDstVlanIsZeroTest() throws FlowNotFoundException { - runUpdateVlanStatisticsToZero(VLAN_1, 0); + public void updateVlanStatisticsToZeroOldSrcAndDstVlanAreZeroTest() throws FlowNotFoundException { + runUpdateVlanStatisticsToZeroTest(0, 0, VLAN_1, VLAN_2); } @Test - public void updateVlanStatisticsToZeroSrcVlanIsZeroTest() throws FlowNotFoundException { - runUpdateVlanStatisticsToZero(0, VLAN_1); + public void updateVlanStatisticsToZeroOldDstVlanAreNotZeroTest() throws FlowNotFoundException { + runUpdateVlanStatisticsToZeroTest(VLAN_3, 0, VLAN_1, VLAN_2); } @Test - public void updateVlanStatisticsToZeroSrcAndVlanAreZeroTest() throws FlowNotFoundException { - runUpdateVlanStatisticsToZero(0, 0); + public void updateVlanStatisticsToZeroOldSrcVlanAreNotZeroTest() throws FlowNotFoundException { + runUpdateVlanStatisticsToZeroTest(0, VLAN_3, VLAN_1, VLAN_2); } - private void runUpdateVlanStatisticsToZero(int srcVLan, int dstVlan) + @Test + public void updateVlanStatisticsToZeroDstVlanIsZeroNewVlansNullTest() throws FlowNotFoundException { + runUpdateVlanStatisticsToZeroTest(VLAN_1, 0, null, null); + } + + @Test + public void updateVlanStatisticsToZeroSrcVlanIsZeroNewVlansNullTest() throws FlowNotFoundException { + runUpdateVlanStatisticsToZeroTest(0, VLAN_1, null, null); + } + + @Test + public void updateVlanStatisticsToZeroSrcAndDstVlanAreZeroNewVlansNullTest() throws FlowNotFoundException { + runUpdateVlanStatisticsToZeroTest(0, 0, null, null); + } + + @Test + public void updateVlanStatisticsToZeroDstVlanIsZeroNewVlansZerosTest() throws FlowNotFoundException { + runUpdateVlanStatisticsToZeroTest(VLAN_1, 0, 0, 0); + } + + @Test + public void updateVlanStatisticsToZeroSrcVlanIsZeroNewVlansZerosTest() throws FlowNotFoundException { + runUpdateVlanStatisticsToZeroTest(0, VLAN_1, 0, 0); + } + + @Test + public void updateVlanStatisticsToZeroSrcAndDstVlanAreZeroNewVlansZerosTest() throws FlowNotFoundException { + runUpdateVlanStatisticsToZeroTest(0, 0, 0, 0); + } + + private void runUpdateVlanStatisticsToZeroTest( + int oldSrcVlan, int oldDstVlan, Integer newSrcVlan, Integer newDstVLan) throws FlowNotFoundException { Set originalVlanStatistics = Sets.newHashSet(1, 2, 3); Flow flow = new TestFlowBuilder() .flowId(FLOW_ID_1) .srcSwitch(switchA) - .srcVlan(srcVLan) + .srcVlan(oldSrcVlan) .destSwitch(switchB) - .destVlan(dstVlan) + .destVlan(oldDstVlan) .vlanStatistics(originalVlanStatistics) .build(); flowRepository.add(flow); @@ -280,6 +313,8 @@ private void runUpdateVlanStatisticsToZero(int srcVLan, int dstVlan) FlowPatch receivedFlow = FlowPatch.builder() .flowId(FLOW_ID_1) .vlanStatistics(new HashSet<>()) + .source(buildPathEndpoint(newSrcVlan)) + .destination(buildPathEndpoint(newDstVLan)) .build(); Flow updatedFlow = flowOperationsService.updateFlow(new FlowCarrierImpl(), receivedFlow); @@ -287,13 +322,29 @@ private void runUpdateVlanStatisticsToZero(int srcVLan, int dstVlan) } @Test(expected = IllegalArgumentException.class) - public void unableToUpdateVlanStatisticsTest() throws FlowNotFoundException { + public void unableToUpdateVlanStatisticsOldVlansSetNewVlansNullTest() throws FlowNotFoundException { + rununableToUpdateVlanStatisticsTest(VLAN_1, VLAN_2, null, null); + } + + @Test(expected = IllegalArgumentException.class) + public void unableToUpdateVlanStatisticsOldVlansSetNewVlansSetTest() throws FlowNotFoundException { + rununableToUpdateVlanStatisticsTest(VLAN_1, VLAN_2, VLAN_2, VLAN_3); + } + + @Test(expected = IllegalArgumentException.class) + public void unableToUpdateVlanStatisticsOldVlansZeroNewVlansSetTest() throws FlowNotFoundException { + rununableToUpdateVlanStatisticsTest(0, 0, VLAN_1, VLAN_2); + } + + private void rununableToUpdateVlanStatisticsTest( + int oldSrcVlan, int oldDstVlan, Integer newSrcVlan, Integer newDstVLan) + throws FlowNotFoundException { Flow flow = new TestFlowBuilder() .flowId(FLOW_ID_1) .srcSwitch(switchA) - .srcVlan(VLAN_1) + .srcVlan(oldSrcVlan) .destSwitch(switchB) - .destVlan(VLAN_1) + .destVlan(oldDstVlan) .vlanStatistics(new HashSet<>()) .build(); flowRepository.add(flow); @@ -301,6 +352,8 @@ public void unableToUpdateVlanStatisticsTest() throws FlowNotFoundException { FlowPatch receivedFlow = FlowPatch.builder() .flowId(FLOW_ID_1) .vlanStatistics(Sets.newHashSet(1, 2, 3)) + .source(PatchEndpoint.builder().vlanId(newSrcVlan).build()) + .destination(PatchEndpoint.builder().vlanId(newDstVLan).build()) .build(); flowOperationsService.updateFlow(new FlowCarrierImpl(), receivedFlow); @@ -836,6 +889,13 @@ private PathSegment createPathSegment(PathId pathId, Switch srcSwitch, int srcPo return pathSegment; } + private static PatchEndpoint buildPathEndpoint(Integer vlan) { + if (vlan == null) { + return null; + } + return PatchEndpoint.builder().vlanId(vlan).build(); + } + private class FlowCarrierImpl implements FlowOperationsCarrier { @Override public void emitPeriodicPingUpdate(String flowId, boolean enabled) { From 76b62ee163fc08707e757f1cf1fe09f5b93f84cb Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 24 Feb 2023 11:17:43 +0000 Subject: [PATCH 14/45] Removed isl Max Bandwidth From Topology Yaml file #5013 --- .../src/test/resources/topology.yaml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src-java/testing/functional-tests/src/test/resources/topology.yaml b/src-java/testing/functional-tests/src/test/resources/topology.yaml index 02a06563722..03235aa4b2c 100644 --- a/src-java/testing/functional-tests/src/test/resources/topology.yaml +++ b/src-java/testing/functional-tests/src/test/resources/topology.yaml @@ -84,13 +84,11 @@ isls: src_port: 1 dst_switch: ofsw2 dst_port: 4 - max_bandwidth: 40000000 - src_switch: ofsw1 src_port: 2 dst_switch: ofsw7 dst_port: 15 - max_bandwidth: 40000000 - src_switch: ofsw3 src_port: 2 @@ -99,49 +97,41 @@ isls: a_switch: in_port: 54 out_port: 50 - max_bandwidth: 40000000 - src_switch: ofsw3 src_port: 1 dst_switch: ofsw2 dst_port: 3 - max_bandwidth: 36000000 - src_switch: ofsw4 src_port: 1 dst_switch: ofsw2 dst_port: 2 - max_bandwidth: 40000000 - src_switch: ofsw8 src_port: 1 dst_switch: ofsw2 dst_port: 5 - max_bandwidth: 40000000 - src_switch: ofsw4 src_port: 4 dst_switch: ofsw3 dst_port: 4 - max_bandwidth: 40000000 - src_switch: ofsw7 src_port: 19 dst_switch: ofsw2 dst_port: 6 - max_bandwidth: 900000 - src_switch: ofsw3 src_port: 5 dst_switch: ofsw9 dst_port: 32 - max_bandwidth: 9000000 - src_switch: ofsw7 src_port: 49 dst_switch: ofsw9 dst_port: 48 - max_bandwidth: 9000000 a_switch: in_port: 7 out_port: 8 @@ -150,56 +140,47 @@ isls: src_port: 51 dst_switch: ofsw8 dst_port: 47 - max_bandwidth: 9000000 - src_switch: ofsw7 src_port: 50 dst_switch: ofsw8 dst_port: 7 - max_bandwidth: 9000000 - src_switch: ofsw3 src_port: 6 dst_switch: ofsw8 dst_port: 16 - max_bandwidth: 9000000 - src_switch: ofsw8 src_port: 49 dst_switch: ofsw9 dst_port: 52 - max_bandwidth: 36000000 a_switch: in_port: 51 out_port: 52 - src_switch: ofsw8 src_port: 40 - max_bandwidth: 36000000 a_switch: in_port: 40 - src_switch: ofsw9 src_port: 47 - max_bandwidth: 36000000 a_switch: in_port: 47 - src_switch: ofsw9 src_port: 38 - max_bandwidth: 36000000 a_switch: in_port: 34 - src_switch: ofsw9 src_port: 37 - max_bandwidth: 36000000 a_switch: in_port: 33 - src_switch: ofsw9 src_port: 15 - max_bandwidth: 36000000 a_switch: in_port: 20 From de4debfc8380af73204f4752f916168033a6d730 Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Wed, 22 Feb 2023 15:17:37 +0100 Subject: [PATCH 15/45] Add error messages when switch is not found to the link validation. Add unit tests. Add details about parameters to the feature description. Add input parameters validation --- .../path-validation/path-validation.md | 28 +- .../base-topology/base-messaging/build.gradle | 1 + .../payload/network/PathValidationDto.java | 3 + .../openkilda/model/PathValidationData.java | 5 +- .../mappers/PathValidationDataMapper.java | 7 +- .../nbworker/services/PathsService.java | 3 +- .../nbworker/validators/PathValidator.java | 97 ++- .../nbworker/services/PathsServiceTest.java | 348 ----------- .../validators/PathValidatorTest.java | 569 ++++++++++++++++++ .../controller/v2/NetworkControllerV2.java | 17 +- 10 files changed, 676 insertions(+), 402 deletions(-) create mode 100644 src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java diff --git a/docs/design/solutions/path-validation/path-validation.md b/docs/design/solutions/path-validation/path-validation.md index 8d8fff9d764..228461f3485 100644 --- a/docs/design/solutions/path-validation/path-validation.md +++ b/docs/design/solutions/path-validation/path-validation.md @@ -18,15 +18,24 @@ after validation in the future. REST URL: ```/v2/network/path/check```, method: ```GET``` -A user is required to provide a path represented by a list of nodes. Nodes must be ordered from start to end, +A user is required to provide a path represented by a list of nodes. Nodes must be ordered from start to end; each next element is the next hop. A user can add optional parameters: -- encapsulation_type: enum value "TRANSIT_VLAN" or "VXLAN". -- max_bandwidth: bandwidth required for this path. -- max_latency: the first tier latency value. -- max_latency_tier2: the second tier latency value. -- path_computation_strategy: "COST|LATENCY|MAX_LATENCY|COST_AND_AVAILABLE_BANDWIDTH". -- reuse_flow_resources: a flow ID. Verify the given path as if it is created instead of the existing flow, that is as if -the resources of some flow are released before validation. +- `encapsulation_type`: enum value "TRANSIT_VLAN" or "VXLAN". API returns an error for every switch in the list if it + doesn't support the given encapsulation type. +- `max_bandwidth`: bandwidth required for this path. API returns an error for each segment of the given path when the + available bandwidth on the segment is less than the given value. When used in combination with `reuse_flow_resources`, + available bandwidth as if the given flow doesn't consume any bandwidth. +- `max_latency`: the first tier latency value in milliseconds. API returns an error for each segment of the given path, + which max latency is greater than the given value. +- `max_latency_tier2`: the second tier latency value in milliseconds. API returns an error for each segment of the given + path, which max latency is greater than the given value. +- `path_computation_strategy`: an enum value PathComputationStrategy. API will return different set of errors depending + on the selected strategy. For example, when COST is selected API ignores available bandwidth and latency parameters. + If none is selected, all validations are executed. +- `reuse_flow_resources`: a flow ID. Verify the given path as if it is created instead of the existing flow, that is as + if the resources of some flow are released before validation. Returns an error if this flow doesn't exist. +- `diverse_with_flow`: a flow ID. Verify whether the given path intersects with the given flow. API returns an error for + each common segment. Returns an error if this flow doesn't exist. ### Request Body ```json @@ -36,7 +45,8 @@ the resources of some flow are released before validation. "max_latency": 0, "max_latency_tier2": 0, "path_computation_strategy": "COST|LATENCY|MAX_LATENCY|COST_AND_AVAILABLE_BANDWIDTH", - "reuse_flow_resources": 0, + "reuse_flow_resources": "flow_id", + "diverse_with_flow": "diverse_flow_id", "nodes": [ { "switch_id": "00:00:00:00:00:00:00:01", diff --git a/src-java/base-topology/base-messaging/build.gradle b/src-java/base-topology/base-messaging/build.gradle index 0488bb67a9b..1ccedc0f36a 100644 --- a/src-java/base-topology/base-messaging/build.gradle +++ b/src-java/base-topology/base-messaging/build.gradle @@ -15,6 +15,7 @@ dependencies { implementation 'com.google.guava:guava' implementation 'org.apache.commons:commons-lang3' implementation 'org.slf4j:slf4j-api' + implementation 'javax.validation:validation-api' testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.vintage:junit-vintage-engine' diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java index e879b2a7058..62ca9ea377c 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java @@ -26,6 +26,7 @@ import lombok.Value; import java.util.List; +import javax.validation.constraints.PositiveOrZero; @Value @Builder @@ -35,9 +36,11 @@ public class PathValidationDto { Long bandwidth; @JsonProperty("max_latency") + @PositiveOrZero(message = "max_latency cannot be negative") Long latencyMs; @JsonProperty("max_latency_tier2") + @PositiveOrZero(message = "max_latency_tier2 cannot be negative") Long latencyTier2ms; @JsonProperty("nodes") diff --git a/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java b/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java index 594fb6403d3..b11717e7dca 100644 --- a/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java +++ b/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java @@ -18,6 +18,7 @@ import lombok.Builder; import lombok.Value; +import java.time.Duration; import java.util.List; @Value @@ -25,8 +26,8 @@ public class PathValidationData { Long bandwidth; - Long latencyMs; - Long latencyTier2ms; + Duration latency; + Duration latencyTier2; List pathSegments; String diverseWithFlow; String reuseFlowResources; diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java index 5578ae4e5df..eccc27d3317 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java @@ -21,6 +21,7 @@ import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; +import java.time.Duration; import java.util.LinkedList; import java.util.List; @@ -50,8 +51,10 @@ public PathValidationData toPathValidationData(PathValidationDto pathValidationD .srcSwitchId(pathValidationDto.getNodes().get(0).getSwitchId()) .destSwitchId(pathValidationDto.getNodes().get(pathValidationDto.getNodes().size() - 1).getSwitchId()) .bandwidth(pathValidationDto.getBandwidth()) - .latencyMs(pathValidationDto.getLatencyMs()) - .latencyTier2ms(pathValidationDto.getLatencyTier2ms()) + .latency(pathValidationDto.getLatencyMs() == null ? null : + Duration.ofMillis(pathValidationDto.getLatencyMs())) + .latencyTier2(pathValidationDto.getLatencyTier2ms() == null ? null : + Duration.ofMillis(pathValidationDto.getLatencyTier2ms())) .diverseWithFlow(pathValidationDto.getDiverseWithFlow()) .reuseFlowResources(pathValidationDto.getReuseFlowResources()) .flowEncapsulationType(FlowEncapsulationTypeMapper.INSTANCE.toOpenKildaModel( diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java index 1b791b11941..58a6a31ca18 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java @@ -142,7 +142,8 @@ public List getPaths( public List validatePath(PathValidateRequest request) { PathValidator pathValidator = new PathValidator(islRepository, flowRepository, - switchPropertiesRepository); + switchPropertiesRepository, + switchRepository); return Collections.singletonList(pathValidator.validatePath( PathValidationDataMapper.INSTANCE.toPathValidationData(request.getPathValidationDto()) diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java index 12b56e02e87..4ae01bddfc9 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java @@ -30,10 +30,12 @@ import org.openkilda.persistence.repositories.FlowRepository; import org.openkilda.persistence.repositories.IslRepository; import org.openkilda.persistence.repositories.SwitchPropertiesRepository; +import org.openkilda.persistence.repositories.SwitchRepository; import lombok.AllArgsConstructor; import lombok.Getter; +import java.time.Duration; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; @@ -44,26 +46,30 @@ import java.util.function.Predicate; import java.util.stream.Collectors; - public class PathValidator { private final IslRepository islRepository; private final FlowRepository flowRepository; private final SwitchPropertiesRepository switchPropertiesRepository; + private final SwitchRepository switchRepository; + public PathValidator(IslRepository islRepository, FlowRepository flowRepository, - SwitchPropertiesRepository switchPropertiesRepository) { + SwitchPropertiesRepository switchPropertiesRepository, + SwitchRepository switchRepository) { this.islRepository = islRepository; this.flowRepository = flowRepository; this.switchPropertiesRepository = switchPropertiesRepository; + this.switchRepository = switchRepository; } /** - * Validates whether it is possible to create a path with the given parameters. When there obstacles, this validator - * returns all errors found on each segment. For example, when there is a path 1-2-3-4 and there is no link between - * 1 and 2, no sufficient latency between 2 and 3, and not enough bandwidth between 3-4, then this validator returns - * all 3 errors. + * Validates whether it is possible to create a path with the given parameters. When there are obstacles, this + * validator returns all errors found on each segment. For example, when there is a path 1-2-3-4 and there is + * no link between 1 and 2, no sufficient latency between 2 and 3, and not enough bandwidth between 3-4, then + * this validator returns all 3 errors. + * * @param pathValidationData path parameters to validate. * @return a response object containing the validation result and errors if any */ @@ -71,7 +77,7 @@ public PathValidationResult validatePath(PathValidationData pathValidationData) Set result = pathValidationData.getPathSegments().stream() .map(segment -> executeValidations( new InputData(pathValidationData, segment), - new RepositoryData(islRepository, flowRepository, switchPropertiesRepository), + new RepositoryData(islRepository, flowRepository, switchPropertiesRepository, switchRepository), getValidations(pathValidationData))) .flatMap(Set::stream) .collect(Collectors.toSet()); @@ -98,16 +104,16 @@ private List>> getValidations( validationFunctions.add(this::validateLink); - if (pathValidationData.getLatencyMs() != null - && pathValidationData.getLatencyMs() != 0 + if (pathValidationData.getLatency() != null + && !pathValidationData.getLatency().isZero() && (pathValidationData.getPathComputationStrategy() == null || pathValidationData.getPathComputationStrategy() == LATENCY || pathValidationData.getPathComputationStrategy() == MAX_LATENCY)) { validationFunctions.add(this::validateLatency); } - if (pathValidationData.getLatencyTier2ms() != null - && pathValidationData.getLatencyTier2ms() != 0 + if (pathValidationData.getLatencyTier2() != null + && !pathValidationData.getLatencyTier2().isZero() && (pathValidationData.getPathComputationStrategy() == null || pathValidationData.getPathComputationStrategy() == LATENCY || pathValidationData.getPathComputationStrategy() == MAX_LATENCY)) { @@ -133,28 +139,54 @@ private List>> getValidations( } private Set validateLink(InputData inputData, RepositoryData repositoryData) { + Set errors = new HashSet<>(); + errors.addAll(validateForwardLink(inputData, repositoryData)); + errors.addAll(validateReverseLink(inputData, repositoryData)); + + return errors; + } + + private Set validateForwardLink(InputData inputData, RepositoryData repositoryData) { + if (!repositoryData.getSwitchRepository().findById(inputData.getSegment().getSrcSwitchId()).isPresent()) { + return Collections.singleton(getSrcSwitchNotFoundError(inputData)); + } + if (!repositoryData.getSwitchRepository().findById(inputData.getSegment().getDestSwitchId()).isPresent()) { + return Collections.singleton(getDestSwitchNotFoundError(inputData)); + } + Optional forward = repositoryData.getIslRepository().findByEndpoints( inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort(), inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort()); + if (!forward.isPresent()) { return Collections.singleton(getNoForwardIslError(inputData)); } - if (forward.get().getStatus() != IslStatus.ACTIVE) { return Collections.singleton(getForwardIslNotActiveError(inputData)); } + return Collections.emptySet(); + } + + private Set validateReverseLink(InputData inputData, RepositoryData repositoryData) { + if (!repositoryData.getSwitchRepository().findById(inputData.getSegment().getDestSwitchId()).isPresent()) { + return Collections.singleton(getDestSwitchNotFoundError(inputData)); + } + if (!repositoryData.getSwitchRepository().findById(inputData.getSegment().getSrcSwitchId()).isPresent()) { + return Collections.singleton(getSrcSwitchNotFoundError(inputData)); + } + Optional reverse = repositoryData.getIslRepository().findByEndpoints( inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort(), inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort()); + if (!reverse.isPresent()) { return Collections.singleton(getNoReverseIslError(inputData)); } - if (reverse.get().getStatus() != IslStatus.ACTIVE) { return Collections.singleton(getReverseIslNotActiveError(inputData)); } @@ -287,8 +319,9 @@ private Set validateLatency(InputData inputData, RepositoryData reposito if (!isl.isPresent()) { return Collections.singleton(getNoForwardIslError(inputData)); } - // TODO make sure that comparing numbers of the same order - if (isl.get().getLatency() > inputData.getPath().getLatencyMs()) { + + Duration actualLatency = Duration.ofNanos(isl.get().getLatency()); + if (actualLatency.compareTo(inputData.getPath().getLatency()) > 0) { return Collections.singleton(getLatencyErrorMessage(inputData, isl.get().getLatency())); } @@ -302,8 +335,9 @@ private Set validateLatencyTier2(InputData inputData, RepositoryData rep if (!isl.isPresent()) { return Collections.singleton(getNoForwardIslError(inputData)); } - // TODO make sure that comparing numbers of the same orders - if (isl.get().getLatency() > inputData.getPath().getLatencyTier2ms()) { + + Duration actualLatency = Duration.ofNanos(isl.get().getLatency()); + if (actualLatency.compareTo(inputData.getPath().getLatencyTier2()) > 0) { return Collections.singleton(getLatencyTier2ErrorMessage(inputData, isl.get().getLatency())); } @@ -312,25 +346,25 @@ private Set validateLatencyTier2(InputData inputData, RepositoryData rep private String getLatencyTier2ErrorMessage(InputData data, long actualLatency) { return String.format( - "Requested latency tier 2 is too low between source switch %s port %d and destination switch" - + " %s port %d. Requested %d, but the link supports %d", + "Requested latency tier 2 is too low between end points: switch %s port %d and switch" + + " %s port %d. Requested %d ms, but the link supports %d ms", data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), - data.getPath().getLatencyTier2ms(), actualLatency); + data.getPath().getLatencyTier2().toMillis(), Duration.ofNanos(actualLatency).toMillis()); } private String getLatencyErrorMessage(InputData data, long actualLatency) { return String.format( - "Requested latency is too low between source switch %s port %d and destination switch %s port %d." - + " Requested %d, but the link supports %d", + "Requested latency is too low between end points: switch %s port %d and switch %s port %d." + + " Requested %d ms, but the link supports %d ms", data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), - data.getPath().getLatencyMs(), actualLatency); + data.getPath().getLatency().toMillis(), Duration.ofNanos(actualLatency).toMillis()); } private String getForwardBandwidthErrorMessage(InputData data, long actualBandwidth) { return String.format( - "There is not enough bandwidth between the source switch %s port %d and destination switch %s port %d" + "There is not enough bandwidth between end points: switch %s port %d and switch %s port %d" + " (forward path). Requested bandwidth %d, but the link supports %d", data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), @@ -339,7 +373,7 @@ private String getForwardBandwidthErrorMessage(InputData data, long actualBandwi private String getReverseBandwidthErrorMessage(InputData data, long actualBandwidth) { return String.format( - "There is not enough bandwidth between the source switch %s port %d and destination switch %s port %d" + "There is not enough bandwidth between end points: switch %s port %d and switch %s port %d" + " (reverse path). Requested bandwidth %d, but the link supports %d", data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), @@ -348,28 +382,28 @@ private String getReverseBandwidthErrorMessage(InputData data, long actualBandwi private String getNoForwardIslError(InputData data) { return String.format( - "There is no ISL between source switch %s port %d and destination switch %s port %d", + "There is no ISL between end points: switch %s port %d and switch %s port %d", data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); } private String getNoReverseIslError(InputData data) { return String.format( - "There is no ISL between source switch %s port %d and destination switch %s port %d", + "There is no ISL between end points: switch %s port %d and switch %s port %d", data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort()); } private String getForwardIslNotActiveError(InputData data) { return String.format( - "The ISL is not in ACTIVE state between source switch %s port %d and destination switch %s port %d", + "The ISL is not in ACTIVE state between end points: switch %s port %d and switch %s port %d", data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); } private String getReverseIslNotActiveError(InputData data) { return String.format( - "The ISL is not in ACTIVE state between source switch %s port %d and destination switch %s port %d", + "The ISL is not in ACTIVE state between end points: switch %s port %d and switch %s port %d", data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort()); } @@ -395,12 +429,12 @@ private String getDestSwitchNotFoundError(InputData data) { } private String getSrcSwitchDoesNotSupportEncapsulationTypeError(InputData data) { - return String.format("The switch %s doesn't support encapsulation type %s", + return String.format("The switch %s doesn't support the encapsulation type %s", data.getSegment().getSrcSwitchId(), data.getPath().getFlowEncapsulationType()); } private String getDestSwitchDoesNotSupportEncapsulationTypeError(InputData data) { - return String.format("The switch %s doesn't support encapsulation type %s", + return String.format("The switch %s doesn't support the encapsulation type %s", data.getSegment().getDestSwitchId(), data.getPath().getFlowEncapsulationType()); } @@ -417,5 +451,6 @@ private static class RepositoryData { IslRepository islRepository; FlowRepository flowRepository; SwitchPropertiesRepository switchPropertiesRepository; + SwitchRepository switchRepository; } } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java index 5a2ebdb35cd..aa71cabbc7c 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java @@ -16,43 +16,30 @@ package org.openkilda.wfm.topology.nbworker.services; import static junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.openkilda.model.FlowEncapsulationType.TRANSIT_VLAN; import static org.openkilda.model.FlowEncapsulationType.VXLAN; import static org.openkilda.model.PathComputationStrategy.COST; -import static org.openkilda.model.PathComputationStrategy.COST_AND_AVAILABLE_BANDWIDTH; import static org.openkilda.model.PathComputationStrategy.LATENCY; import static org.openkilda.model.PathComputationStrategy.MAX_LATENCY; import org.openkilda.config.provider.PropertiesBasedConfigurationProvider; -import org.openkilda.messaging.command.flow.PathValidateRequest; -import org.openkilda.messaging.info.network.PathValidationResult; import org.openkilda.messaging.info.network.PathsInfoData; -import org.openkilda.messaging.payload.flow.PathNodePayload; -import org.openkilda.messaging.payload.network.PathValidationDto; -import org.openkilda.model.Flow; import org.openkilda.model.FlowEncapsulationType; -import org.openkilda.model.FlowPath; -import org.openkilda.model.FlowPathDirection; import org.openkilda.model.Isl; import org.openkilda.model.IslStatus; import org.openkilda.model.KildaConfiguration; import org.openkilda.model.PathComputationStrategy; -import org.openkilda.model.PathId; -import org.openkilda.model.PathSegment; import org.openkilda.model.Switch; import org.openkilda.model.SwitchId; import org.openkilda.model.SwitchProperties; import org.openkilda.model.SwitchStatus; -import org.openkilda.model.cookie.FlowSegmentCookie; import org.openkilda.pce.PathComputerConfig; import org.openkilda.pce.exception.RecoverableException; import org.openkilda.pce.exception.UnroutableFlowException; import org.openkilda.persistence.inmemory.InMemoryGraphBasedTest; -import org.openkilda.persistence.repositories.FlowRepository; import org.openkilda.persistence.repositories.IslRepository; import org.openkilda.persistence.repositories.KildaConfigurationRepository; import org.openkilda.persistence.repositories.RepositoryFactory; @@ -66,8 +53,6 @@ import org.junit.Test; import java.time.Duration; -import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -79,7 +64,6 @@ public class PathsServiceTest extends InMemoryGraphBasedTest { private static final SwitchId SWITCH_ID_1 = new SwitchId(1); private static final SwitchId SWITCH_ID_2 = new SwitchId(2); private static final SwitchId SWITCH_ID_3 = new SwitchId(3); - private static final SwitchId SWITCH_ID_4 = new SwitchId(4); public static final long BASE_LATENCY = 10000; public static final long MIN_LATENCY = BASE_LATENCY - SWITCH_COUNT; @@ -87,7 +71,6 @@ public class PathsServiceTest extends InMemoryGraphBasedTest { private static SwitchRepository switchRepository; private static SwitchPropertiesRepository switchPropertiesRepository; private static IslRepository islRepository; - private static FlowRepository flowRepository; private static PathsService pathsService; @@ -98,7 +81,6 @@ public static void setUpOnce() { switchRepository = repositoryFactory.createSwitchRepository(); switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); islRepository = repositoryFactory.createIslRepository(); - flowRepository = repositoryFactory.createFlowRepository(); kildaConfigurationRepository = repositoryFactory.createKildaConfigurationRepository(); PathComputerConfig pathComputerConfig = new PropertiesBasedConfigurationProvider() .getConfiguration(PathComputerConfig.class); @@ -309,285 +291,6 @@ public void findNPathsByDefaultEncapsulationAndCost() assertVxlanAndCostPathes(paths); } - @Test - public void whenValidPath_validatePathReturnsValidResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(0L) - .latencyTier2ms(0L) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertTrue(responses.get(0).getIsValid()); - } - - @Test - public void whenValidPathButNotEnoughBandwidth_validateReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(1000000000L) - .latencyMs(0L) - .latencyTier2ms(0L) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertEquals("There must be 2 errors: forward and reverse paths", - 2, responses.get(0).getErrors().size()); - Collections.sort(responses.get(0).getErrors()); - assertEquals(responses.get(0).getErrors().get(0), - "There is not enough bandwidth between the source switch 00:00:00:00:00:00:00:01 port 6 and " - + "destination switch 00:00:00:00:00:00:00:03 port 6 (forward path). " - + "Requested bandwidth 1000000000, but the link supports 1003"); - assertEquals(responses.get(0).getErrors().get(1), - "There is not enough bandwidth between the source switch 00:00:00:00:00:00:00:03 port 6 and " - + "destination switch 00:00:00:00:00:00:00:01 port 6 (reverse path). " - + "Requested bandwidth 1000000000, but the link supports 1003"); - } - - @Test - public void whenValidPathButTooLowLatency_andLatencyStrategy_validateReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(10L) - .latencyTier2ms(0L) - .pathComputationStrategy(LATENCY) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertTrue(responses.get(0).getErrors().get(0) - .contains("Requested latency is too low between source switch")); - } - - @Test - public void whenValidPathButTooLowLatency_andMaxLatencyStrategy_validateReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(10L) - .latencyTier2ms(0L) - .pathComputationStrategy(MAX_LATENCY) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertTrue(responses.get(0).getErrors().get(0) - .contains("Requested latency is too low between source switch")); - } - - @Test - public void whenValidPathButTooLowLatencyTier2_andLatencyStrategy_validateReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(10000000000L) - .latencyTier2ms(100L) - .pathComputationStrategy(LATENCY) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertTrue(responses.get(0).getErrors().get(0) - .contains("Requested latency tier 2 is too low")); - } - - @Test - public void whenSwitchDoesNotSupportEncapsulationType_validateReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - // TODO investigate: when uncommenting the following line, switch 3 supports VXLAN. Why? - //nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .flowEncapsulationType(org.openkilda.messaging.payload.flow.FlowEncapsulationType.VXLAN) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertTrue(responses.get(0).getErrors().get(0) - .contains("doesn't support encapsulation type VXLAN")); - } - - @Test - public void whenNoLinkBetweenSwitches_validatePathReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 1)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 0, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(0L) - .latencyTier2ms(0L) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertTrue(responses.get(0).getErrors().get(0) - .startsWith("There is no ISL between source switch")); - } - - @Test - public void whenDiverseWith_andExistsIntersection_validateReturnsError() { - Switch switch1 = Switch.builder().switchId(SWITCH_ID_1).build(); - Switch switch2 = Switch.builder().switchId(SWITCH_ID_3).build(); - createFlow("flow_1", switch1, 6, switch2, 6); - - assertTrue(flowRepository.findById("flow_1").isPresent()); - assertFalse(flowRepository.findById("flow_1").get().getData().getPaths().isEmpty()); - - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(0L) - .latencyTier2ms(0L) - .diverseWithFlow("flow_1") - .build()); - List responses = pathsService.validatePath(request); - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertEquals(1, responses.get(0).getErrors().size()); - assertTrue(responses.get(0).getErrors().get(0).startsWith("The following segment intersects with the flow")); - } - - @Test - public void whenMultipleProblemsOnPath_validatePathReturnsAllErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_4, 7, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(1000000L) - .latencyMs(10L) - .latencyTier2ms(0L) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertEquals("There must be 5 errors in total: 2 bandwidth (forward and reverse paths), " - + "2 links are not present, and 1 latency", 5, responses.get(0).getErrors().size()); - } - - @Test - public void whenNonLatencyPathComputationStrategy_ignoreLatencyAnd_validatePathReturnsSuccessResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(10L) - .latencyTier2ms(0L) - .pathComputationStrategy(COST) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertTrue(responses.get(0).getIsValid()); - } - - @Test - public void whenValidPathWithExistingFlowAndReuseResources_validatePathReturnsSuccessResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(1000L) - .latencyMs(0L) - .latencyTier2ms(0L) - .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) - .build()); - List responsesBefore = pathsService.validatePath(request); - - assertFalse(responsesBefore.isEmpty()); - assertTrue("The path using default segments with bandwidth 1003 must be valid", - responsesBefore.get(0).getIsValid()); - - Optional islForward = islRepository.findByEndpoints(SWITCH_ID_3, 7, SWITCH_ID_2, 7); - assertTrue(islForward.isPresent()); - islForward.get().setAvailableBandwidth(100L); - Optional islReverse = islRepository.findByEndpoints(SWITCH_ID_2, 7, SWITCH_ID_3, 7); - assertTrue(islReverse.isPresent()); - islReverse.get().setAvailableBandwidth(100L); - - String flowToReuse = "flow_3_2"; - createFlow(flowToReuse, Switch.builder().switchId(SWITCH_ID_3).build(), 2000, - Switch.builder().switchId(SWITCH_ID_2).build(), 2000, - false, 900L, islForward.get()); - - List responsesAfter = pathsService.validatePath(request); - - assertFalse(responsesAfter.isEmpty()); - assertFalse("The path must not be valid because the flow %s consumes bandwidth", - responsesAfter.get(0).getIsValid()); - assertFalse(responsesAfter.get(0).getErrors().isEmpty()); - assertEquals("There must be 2 errors in total: not enough bandwidth on forward and reverse paths", - 2, responsesAfter.get(0).getErrors().size()); - assertTrue(responsesAfter.get(0).getErrors().get(0).contains("There is not enough bandwidth")); - assertTrue(responsesAfter.get(0).getErrors().get(1).contains("There is not enough bandwidth")); - - PathValidateRequest requestWithReuseResources = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(1000L) - .latencyMs(0L) - .latencyTier2ms(0L) - .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) - .reuseFlowResources(flowToReuse) - .build()); - - List responseWithReuseResources = pathsService.validatePath(requestWithReuseResources); - - assertFalse(responseWithReuseResources.isEmpty()); - assertTrue("The path must be valid because, although the flow %s consumes bandwidth, the validator" - + " includes the consumed bandwidth to available bandwidth", - responseWithReuseResources.get(0).getIsValid()); - } - private void assertMaxLatencyPaths(List paths, Duration maxLatency, long expectedCount, FlowEncapsulationType encapsulationType) { assertEquals(expectedCount, paths.size()); @@ -673,55 +376,4 @@ private void createSwitchProperties(Switch sw, FlowEncapsulationType... encapsul .build()); } - private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort) { - createFlow(flowId, srcSwitch, srcPort, destSwitch, destPort, null, null, null); - } - - private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort, - Boolean ignoreBandwidth, Long bandwidth, Isl isl) { - Flow flow = Flow.builder() - .flowId(flowId) - .srcSwitch(srcSwitch) - .srcPort(srcPort) - .destSwitch(destSwitch) - .destPort(destPort) - .build(); - Optional.ofNullable(ignoreBandwidth).ifPresent(flow::setIgnoreBandwidth); - Optional.ofNullable(bandwidth).ifPresent(flow::setBandwidth); - FlowPath forwardPath = FlowPath.builder() - .pathId(new PathId("path_1")) - .srcSwitch(srcSwitch) - .destSwitch(destSwitch) - .cookie(new FlowSegmentCookie(FlowPathDirection.FORWARD, 1L)) - .bandwidth(flow.getBandwidth()) - .ignoreBandwidth(false) - .segments(Collections.singletonList(PathSegment.builder() - .pathId(new PathId("forward_segment")) - .srcSwitch(srcSwitch) - .srcPort(isl == null ? srcPort : isl.getSrcPort()) - .destSwitch(destSwitch) - .destPort(isl == null ? destPort : isl.getDestPort()) - .build())) - .build(); - - flow.setForwardPath(forwardPath); - FlowPath reversePath = FlowPath.builder() - .pathId(new PathId("path_2")) - .srcSwitch(destSwitch) - .destSwitch(srcSwitch) - .cookie(new FlowSegmentCookie(FlowPathDirection.REVERSE, 1L)) - .bandwidth(flow.getBandwidth()) - .ignoreBandwidth(false) - .segments(Collections.singletonList(PathSegment.builder() - .pathId(new PathId("reverse_segment")) - .srcSwitch(destSwitch) - .srcPort(isl == null ? destPort : isl.getDestPort()) - .destSwitch(srcSwitch) - .destPort(isl == null ? srcPort : isl.getSrcPort()) - .build())) - .build(); - flow.setReversePath(reversePath); - - flowRepository.add(flow); - } } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java new file mode 100644 index 00000000000..8aa3fba5b71 --- /dev/null +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java @@ -0,0 +1,569 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.wfm.topology.nbworker.validators; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; +import static org.openkilda.model.FlowEncapsulationType.TRANSIT_VLAN; +import static org.openkilda.model.PathComputationStrategy.COST; +import static org.openkilda.model.PathComputationStrategy.COST_AND_AVAILABLE_BANDWIDTH; +import static org.openkilda.model.PathComputationStrategy.LATENCY; +import static org.openkilda.model.PathComputationStrategy.MAX_LATENCY; + +import org.openkilda.config.provider.PropertiesBasedConfigurationProvider; +import org.openkilda.messaging.command.flow.PathValidateRequest; +import org.openkilda.messaging.info.network.PathValidationResult; +import org.openkilda.messaging.payload.flow.PathNodePayload; +import org.openkilda.messaging.payload.network.PathValidationDto; +import org.openkilda.model.Flow; +import org.openkilda.model.FlowPath; +import org.openkilda.model.FlowPathDirection; +import org.openkilda.model.Isl; +import org.openkilda.model.IslStatus; +import org.openkilda.model.PathId; +import org.openkilda.model.PathSegment; +import org.openkilda.model.Switch; +import org.openkilda.model.SwitchId; +import org.openkilda.model.SwitchProperties; +import org.openkilda.model.SwitchStatus; +import org.openkilda.model.cookie.FlowSegmentCookie; +import org.openkilda.pce.PathComputerConfig; +import org.openkilda.persistence.inmemory.InMemoryGraphBasedTest; +import org.openkilda.persistence.repositories.FlowRepository; +import org.openkilda.persistence.repositories.IslRepository; +import org.openkilda.persistence.repositories.RepositoryFactory; +import org.openkilda.persistence.repositories.SwitchPropertiesRepository; +import org.openkilda.persistence.repositories.SwitchRepository; +import org.openkilda.wfm.topology.nbworker.services.PathsService; + +import com.google.common.collect.Sets; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class PathValidatorTest extends InMemoryGraphBasedTest { + private static final SwitchId SWITCH_ID_1 = new SwitchId(1); + private static final SwitchId SWITCH_ID_2 = new SwitchId(2); + private static final SwitchId SWITCH_ID_3 = new SwitchId(3); + private static SwitchRepository switchRepository; + private static SwitchPropertiesRepository switchPropertiesRepository; + private static IslRepository islRepository; + private static FlowRepository flowRepository; + private static PathsService pathsService; + + private boolean isSetupDone; + + @BeforeClass + public static void setUpOnce() { + RepositoryFactory repositoryFactory = persistenceManager.getRepositoryFactory(); + switchRepository = repositoryFactory.createSwitchRepository(); + switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); + islRepository = repositoryFactory.createIslRepository(); + flowRepository = repositoryFactory.createFlowRepository(); + PathComputerConfig pathComputerConfig = new PropertiesBasedConfigurationProvider() + .getConfiguration(PathComputerConfig.class); + pathsService = new PathsService(repositoryFactory, pathComputerConfig, islRepository, + repositoryFactory.createFlowRepository()); + } + + + @Before + public void createTestTopology() { + if (!isSetupDone) { + Switch switchA = Switch.builder().switchId(SWITCH_ID_1).status(SwitchStatus.ACTIVE).build(); + Switch switchB = Switch.builder().switchId(SWITCH_ID_2).status(SwitchStatus.ACTIVE).build(); + Switch switchTransit = Switch.builder().switchId(SWITCH_ID_3).status(SwitchStatus.ACTIVE).build(); + + switchRepository.add(switchA); + switchRepository.add(switchB); + switchRepository.add(switchTransit); + + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switchA) + .supportedTransitEncapsulation(Sets.newHashSet(TRANSIT_VLAN)) + .build()); + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switchB) + .supportedTransitEncapsulation(Sets.newHashSet(TRANSIT_VLAN)) + .build()); + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switchTransit) + .supportedTransitEncapsulation(Sets.newHashSet(TRANSIT_VLAN)) + .build()); + + createOneWayIsl(switchA, 6, switchTransit, 6, 10, 2_000_000, 10_000); + createOneWayIsl(switchTransit, 6, switchA, 6, 10, 2_000_000, 10_000); + + createOneWayIsl(switchB, 7, switchTransit, 7, 15, 3_000_000, 20_000); + createOneWayIsl(switchTransit, 7, switchB, 7, 15, 3_000_000, 20_000); + + isSetupDone = true; + } + } + + @Test + public void whenValidPath_validatePathReturnsValidResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertTrue(responses.get(0).getIsValid()); + } + + @Test + public void whenValidPathButNotEnoughBandwidth_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(1000000000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertEquals("There must be 2 errors: forward and reverse paths", + 2, responses.get(0).getErrors().size()); + Collections.sort(responses.get(0).getErrors()); + assertEquals(responses.get(0).getErrors().get(0), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:01 port 6 and " + + "switch 00:00:00:00:00:00:00:03 port 6 (forward path). Requested bandwidth 1000000000," + + " but the link supports 10000"); + assertEquals(responses.get(0).getErrors().get(1), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03 port 6 and " + + "switch 00:00:00:00:00:00:00:01 port 6 (reverse path). Requested bandwidth 1000000000," + + " but the link supports 10000"); + } + + @Test + public void whenNoSwitchOnPath_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(new SwitchId("01:01:01:01"), null, 6)); + nodes.add(new PathNodePayload(new SwitchId("01:01:01:02"), 6, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(1000000000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + + Set actualErrorMessages = new HashSet<>(responses.get(0).getErrors()); + Set expectedErrorMessages = new HashSet<>(Arrays.asList( + "The following switch has not been found: 00:00:00:00:01:01:01:01", + "The following switch has not been found: 00:00:00:00:01:01:01:02", + "There is no ISL between end points: switch 00:00:00:00:01:01:01:01 port 6" + + " and switch 00:00:00:00:01:01:01:02 port 6" + )); + + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenValidPathButTooLowLatency_andLatencyStrategy_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(1L) + .latencyTier2ms(0L) + .pathComputationStrategy(LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + + Set expectedErrorMessages = new HashSet<>(); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03 port" + + " 7 and switch 00:00:00:00:00:00:00:02 port 7. Requested 1 ms, but the link supports 3 ms"); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:01 port" + + " 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); + + Set actualErrorMessages = new HashSet<>(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenValidPathButTooLowLatency_andMaxLatencyStrategy_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(1L) + .latencyTier2ms(0L) + .pathComputationStrategy(MAX_LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + + Set expectedErrorMessages = new HashSet<>(); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03 port" + + " 7 and switch 00:00:00:00:00:00:00:02 port 7. Requested 1 ms, but the link supports 3 ms"); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:01 port" + + " 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); + + Set actualErrorMessages = new HashSet<>(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenValidPathButTooLowLatencyTier2_andLatencyStrategy_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10000000000L) + .latencyTier2ms(1L) + .pathComputationStrategy(LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + Set expectedErrorMessages = new HashSet<>(); + expectedErrorMessages.add("Requested latency tier 2 is too low between end points: switch 00:00:00:00:00:00:00:" + + "03 port 7 and switch 00:00:00:00:00:00:00:02 port 7. Requested 1 ms, but the link supports 3 ms"); + expectedErrorMessages.add("Requested latency tier 2 is too low between end points: switch 00:00:00:00:00:00:00:" + + "01 port 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); + + Set actualErrorMessages = new HashSet<>(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenSwitchDoesNotSupportEncapsulationType_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .flowEncapsulationType(org.openkilda.messaging.payload.flow.FlowEncapsulationType.VXLAN) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertEquals(responses.get(0).getErrors().get(0), + "The switch 00:00:00:00:00:00:00:03 doesn't support the encapsulation type VXLAN"); + } + + @Test + public void whenNoLinkBetweenSwitches_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 1)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 0, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + Set expectedErrorMessages = new HashSet<>(); + expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:02 port 0 and switch" + + " 00:00:00:00:00:00:00:01 port 1"); + expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:01 port 1 and switch" + + " 00:00:00:00:00:00:00:02 port 0"); + + Set actualErrorMessages = new HashSet<>(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenDiverseWith_andExistsIntersection_validatePathReturnsError() { + Switch switch1 = Switch.builder().switchId(SWITCH_ID_1).build(); + Switch switch2 = Switch.builder().switchId(SWITCH_ID_3).build(); + createFlow("flow_1", switch1, 6, switch2, 6); + + assertTrue(flowRepository.findById("flow_1").isPresent()); + assertFalse(flowRepository.findById("flow_1").get().getData().getPaths().isEmpty()); + + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .diverseWithFlow("flow_1") + .build()); + List responses = pathsService.validatePath(request); + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertEquals(1, responses.get(0).getErrors().size()); + assertEquals(responses.get(0).getErrors().get(0), + "The following segment intersects with the flow flow_1: source switch 00:00:00:00:00:00:00:01" + + " port 6 and destination switch 00:00:00:00:00:00:00:03 port 6"); + } + + @Test + public void whenMultipleProblemsOnPath_validatePathReturnsAllErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(new SwitchId("00"), 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(1000000L) + .latencyMs(1L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + + assertEquals("There must be 5 errors in total: 2 not enough bandwidth (forward and reverse paths), " + + "1 link is not present, 1 switch is not found, and 1 latency", + 5, responses.get(0).getErrors().size()); + Set expectedErrorMessages = new HashSet<>(); + expectedErrorMessages.add("The following switch has not been found: 00:00:00:00:00:00:00:00"); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:01" + + " port 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); + expectedErrorMessages.add("There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03" + + " port 6 and switch 00:00:00:00:00:00:00:01 port 6 (reverse path). Requested bandwidth 1000000," + + " but the link supports 10000"); + expectedErrorMessages.add("There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:01" + + " port 6 and switch 00:00:00:00:00:00:00:03 port 6 (forward path). Requested bandwidth 1000000," + + " but the link supports 10000"); + expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:03 port 7 and" + + " switch 00:00:00:00:00:00:00:00 port 7"); + assertEquals(expectedErrorMessages, new HashSet<>(responses.get(0).getErrors())); + } + + @Test + public void whenValidPathAndDiverseFlowDoesNotExist_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST) + .diverseWithFlow("non_existing_flow_id") + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertEquals("Could not find the diverse flow with ID non_existing_flow_id", + responses.get(0).getErrors().get(0)); + } + + @Test + public void whenNonLatencyPathComputationStrategy_ignoreLatencyAnd_validatePathReturnsSuccessResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertTrue(responses.get(0).getIsValid()); + } + + @Test + public void whenValidPathWithExistingFlowAndReuseResources_validatePathReturnsSuccessResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(1000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) + .build()); + List responsesBefore = pathsService.validatePath(request); + + assertFalse(responsesBefore.isEmpty()); + assertTrue("The path using default segments with bandwidth 1003 must be valid", + responsesBefore.get(0).getIsValid()); + + Optional islForward = islRepository.findByEndpoints(SWITCH_ID_3, 7, SWITCH_ID_2, 7); + assertTrue(islForward.isPresent()); + islForward.get().setAvailableBandwidth(100L); + Optional islReverse = islRepository.findByEndpoints(SWITCH_ID_2, 7, SWITCH_ID_3, 7); + assertTrue(islReverse.isPresent()); + islReverse.get().setAvailableBandwidth(100L); + + String flowToReuse = "flow_3_2"; + createFlow(flowToReuse, Switch.builder().switchId(SWITCH_ID_3).build(), 2000, + Switch.builder().switchId(SWITCH_ID_2).build(), 2000, + false, 900L, islForward.get()); + + List responsesAfter = pathsService.validatePath(request); + + assertFalse(responsesAfter.isEmpty()); + assertFalse("The path must not be valid because the flow %s consumes bandwidth", + responsesAfter.get(0).getIsValid()); + assertFalse(responsesAfter.get(0).getErrors().isEmpty()); + assertEquals("There must be 2 errors in total: not enough bandwidth on forward and reverse paths", + 2, responsesAfter.get(0).getErrors().size()); + assertEquals(responsesAfter.get(0).getErrors().get(0), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:02 port 7 and" + + " switch 00:00:00:00:00:00:00:03 port 7 (reverse path). Requested bandwidth 1000, but the" + + " link supports 100"); + assertEquals(responsesAfter.get(0).getErrors().get(1), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03 port 7 and" + + " switch 00:00:00:00:00:00:00:02 port 7 (forward path). Requested bandwidth 1000, but the" + + " link supports 100"); + + PathValidateRequest requestWithReuseResources = new PathValidateRequest(PathValidationDto.builder() + .nodes(nodes) + .bandwidth(1000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) + .reuseFlowResources(flowToReuse) + .build()); + + List responseWithReuseResources = pathsService.validatePath(requestWithReuseResources); + + assertFalse(responseWithReuseResources.isEmpty()); + assertTrue("The path must be valid because, although the flow %s consumes bandwidth, the validator" + + " includes the consumed bandwidth to available bandwidth", + responseWithReuseResources.get(0).getIsValid()); + } + + private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort) { + createFlow(flowId, srcSwitch, srcPort, destSwitch, destPort, null, null, null); + } + + private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort, + Boolean ignoreBandwidth, Long bandwidth, Isl isl) { + Flow flow = Flow.builder() + .flowId(flowId) + .srcSwitch(srcSwitch) + .srcPort(srcPort) + .destSwitch(destSwitch) + .destPort(destPort) + .build(); + Optional.ofNullable(ignoreBandwidth).ifPresent(flow::setIgnoreBandwidth); + Optional.ofNullable(bandwidth).ifPresent(flow::setBandwidth); + FlowPath forwardPath = FlowPath.builder() + .pathId(new PathId("path_1")) + .srcSwitch(srcSwitch) + .destSwitch(destSwitch) + .cookie(new FlowSegmentCookie(FlowPathDirection.FORWARD, 1L)) + .bandwidth(flow.getBandwidth()) + .ignoreBandwidth(false) + .segments(Collections.singletonList(PathSegment.builder() + .pathId(new PathId("forward_segment")) + .srcSwitch(srcSwitch) + .srcPort(isl == null ? srcPort : isl.getSrcPort()) + .destSwitch(destSwitch) + .destPort(isl == null ? destPort : isl.getDestPort()) + .build())) + .build(); + + flow.setForwardPath(forwardPath); + FlowPath reversePath = FlowPath.builder() + .pathId(new PathId("path_2")) + .srcSwitch(destSwitch) + .destSwitch(srcSwitch) + .cookie(new FlowSegmentCookie(FlowPathDirection.REVERSE, 1L)) + .bandwidth(flow.getBandwidth()) + .ignoreBandwidth(false) + .segments(Collections.singletonList(PathSegment.builder() + .pathId(new PathId("reverse_segment")) + .srcSwitch(destSwitch) + .srcPort(isl == null ? destPort : isl.getDestPort()) + .destSwitch(srcSwitch) + .destPort(isl == null ? srcPort : isl.getSrcPort()) + .build())) + .build(); + flow.setReversePath(reversePath); + + flowRepository.add(flow); + } + + private void createOneWayIsl(Switch srcSwitch, int srcPort, Switch dstSwitch, int dstPort, int cost, + long latency, int bandwidth) { + islRepository.add(Isl.builder() + .srcSwitch(srcSwitch) + .srcPort(srcPort) + .destSwitch(dstSwitch) + .destPort(dstPort) + .status(IslStatus.ACTIVE) + .actualStatus(IslStatus.ACTIVE) + .cost(cost) + .availableBandwidth(bandwidth) + .maxBandwidth(bandwidth) + .latency(latency) + .build()); + } +} diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java index 9aca291ea89..a8fbb21cc6d 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java @@ -36,12 +36,16 @@ @RequestMapping("/v2/network") public class NetworkControllerV2 { + private final NetworkService networkService; + @Autowired - private NetworkService networkService; + public NetworkControllerV2(NetworkService networkService) { + this.networkService = networkService; + } /** * Validates that a given path complies with the chosen strategy and the network availability. - * It is required that the input contains path nodes. Other parameters are opti + * It is required that the input contains path nodes. Other parameters are optional. * @param pathValidationDto a payload with a path and additional flow parameters provided by a user * @return either a successful response or the list of errors */ @@ -50,13 +54,6 @@ public class NetworkControllerV2 { @ResponseStatus(HttpStatus.OK) public CompletableFuture validateCustomFlowPath( @RequestBody PathValidationDto pathValidationDto) { - validateInput(pathValidationDto); - - return networkService.validateFlowPath(pathValidationDto); - } - - private void validateInput(PathValidationDto pathValidationDto) { - //TODO validate all fields if (pathValidationDto == null || pathValidationDto.getNodes() == null @@ -64,5 +61,7 @@ private void validateInput(PathValidationDto pathValidationDto) { throw new MessageException(ErrorType.DATA_INVALID, "Invalid Request Body", "Invalid 'nodes' value in the request body"); } + + return networkService.validateFlowPath(pathValidationDto); } } From 22aee0e9262d27988cd26bd7c87d1f7e06d655b5 Mon Sep 17 00:00:00 2001 From: pkazlenka Date: Mon, 27 Feb 2023 14:56:24 +0100 Subject: [PATCH 16/45] #5063: Can't patch flow with empty vlan stats and non zeros src/dst vlans tests * Added tests to verify bugfix #5063 --- .../spec/flows/FlowCrudSpec.groovy | 176 ++++++++++++------ 1 file changed, 115 insertions(+), 61 deletions(-) diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy index 204f0093797..b78b0295075 100644 --- a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy @@ -3,6 +3,9 @@ package org.openkilda.functionaltests.spec.flows import org.openkilda.functionaltests.exception.ExpectedHttpClientErrorException import org.openkilda.functionaltests.helpers.Wrappers import org.openkilda.model.PathComputationStrategy +import org.openkilda.northbound.dto.v2.flows.FlowPatchEndpoint +import org.openkilda.northbound.dto.v2.flows.FlowPatchV2 +import org.openkilda.northbound.dto.v2.flows.FlowStatistics import static groovyx.gpars.GParsPool.withPool import static org.junit.jupiter.api.Assumptions.assumeTrue @@ -67,7 +70,8 @@ class FlowCrudSpec extends HealthCheckSpecification { final static Integer IMPOSSIBLY_LOW_LATENCY = 1 final static Long IMPOSSIBLY_HIGH_BANDWIDTH = Long.MAX_VALUE - @Autowired @Shared + @Autowired + @Shared Provider traffExamProvider @Shared @@ -124,7 +128,9 @@ class FlowCrudSpec extends HealthCheckSpecification { } and: "Flow writes stats" - if(trafficApplicable) { statsHelper.verifyFlowWritesStats(flow.flowId, beforeTraffic, true) } + if (trafficApplicable) { + statsHelper.verifyFlowWritesStats(flow.flowId, beforeTraffic, true) + } when: "Remove the flow" flowHelperV2.deleteFlow(flow.flowId) @@ -178,7 +184,7 @@ class FlowCrudSpec extends HealthCheckSpecification { where: data << [ [ - description : "same switch-port but vlans on src and dst are swapped", + description : "same switch-port but vlans on src and dst are swapped", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch).tap { @@ -194,7 +200,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "same switch-port but vlans on src and dst are different", + description : "same switch-port but vlans on src and dst are different", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch) @@ -208,7 +214,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "vlan-port of new src = vlan-port of existing dst (+ different src)", + description : "vlan-port of new src = vlan-port of existing dst (+ different src)", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch) @@ -227,7 +233,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "vlan-port of new dst = vlan-port of existing src (but different switches)", + description : "vlan-port of new dst = vlan-port of existing src (but different switches)", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch).tap { @@ -243,7 +249,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "vlan of new dst = vlan of existing src and port of new dst = port of " + + description : "vlan of new dst = vlan of existing src and port of new dst = port of " + "existing dst", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches @@ -256,7 +262,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "default and tagged flows on the same port on dst switch", + description : "default and tagged flows on the same port on dst switch", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch) @@ -268,7 +274,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "default and tagged flows on the same port on src switch", + description : "default and tagged flows on the same port on src switch", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch) @@ -280,7 +286,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "tagged and default flows on the same port on dst switch", + description : "tagged and default flows on the same port on dst switch", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch).tap { @@ -293,7 +299,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "tagged and default flows on the same port on src switch", + description : "tagged and default flows on the same port on src switch", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch).tap { @@ -306,7 +312,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "default and tagged flows on the same ports on src and dst switches", + description : "default and tagged flows on the same ports on src and dst switches", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch) @@ -320,7 +326,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "tagged and default flows on the same ports on src and dst switches", + description : "tagged and default flows on the same ports on src and dst switches", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch).tap { @@ -572,7 +578,7 @@ class FlowCrudSpec extends HealthCheckSpecification { validation.verifyRuleSectionsAreEmpty(["excess", "missing"]) def swProps = switchHelper.getCachedSwProps(it.dpId) def amountOfMultiTableRules = swProps.multiTable ? 1 : 0 - def amountOfServer42Rules = (swProps.server42FlowRtt && it.dpId in [srcSwitch.dpId,dstSwitch.dpId]) ? 1 : 0 + def amountOfServer42Rules = (swProps.server42FlowRtt && it.dpId in [srcSwitch.dpId, dstSwitch.dpId]) ? 1 : 0 if (swProps.multiTable && swProps.server42FlowRtt) { if ((flow.destination.getSwitchId() == it.dpId && flow.destination.vlanId) || (flow.source.getSwitchId() == it.dpId && flow.source.vlanId)) amountOfServer42Rules += 1 @@ -601,17 +607,19 @@ class FlowCrudSpec extends HealthCheckSpecification { !actualException && flowHelperV2.deleteFlow(flow.flowId) where: - problem | update | expectedException - "invalid encapsulation type"| - {FlowRequestV2 flowToSpoil -> - flowToSpoil.setEncapsulationType("fake") - return flowToSpoil} | - new ExpectedHttpClientErrorException(HttpStatus.BAD_REQUEST, ~/Can not parse arguments of the create flow request/) - "unavailable latency" | - {FlowRequestV2 flowToSpoil -> - flowToSpoil.setMaxLatency(IMPOSSIBLY_LOW_LATENCY) - flowToSpoil.setPathComputationStrategy(PathComputationStrategy.MAX_LATENCY.toString()) - return flowToSpoil}| + problem | update | expectedException + "invalid encapsulation type" | + { FlowRequestV2 flowToSpoil -> + flowToSpoil.setEncapsulationType("fake") + return flowToSpoil + } | + new ExpectedHttpClientErrorException(HttpStatus.BAD_REQUEST, ~/Can not parse arguments of the create flow request/) + "unavailable latency" | + { FlowRequestV2 flowToSpoil -> + flowToSpoil.setMaxLatency(IMPOSSIBLY_LOW_LATENCY) + flowToSpoil.setPathComputationStrategy(PathComputationStrategy.MAX_LATENCY.toString()) + return flowToSpoil + } | new ExpectedHttpClientErrorException(HttpStatus.NOT_FOUND, ~/Latency limit: Requested path must have latency ${ IMPOSSIBLY_LOW_LATENCY}ms or lower, but best path has latency \d+ms/) @@ -640,7 +648,7 @@ class FlowCrudSpec extends HealthCheckSpecification { expectedException.equals(actualException) cleanup: "Remove the flow" - Wrappers.silent {flowHelperV2.deleteFlow(flow.flowId)} + Wrappers.silent { flowHelperV2.deleteFlow(flow.flowId) } } @Tidy @@ -667,22 +675,22 @@ class FlowCrudSpec extends HealthCheckSpecification { where: data << [ [ - switchType: "source", - port : "srcPort", - errorMessage : { Isl violatedIsl -> + switchType : "source", + port : "srcPort", + errorMessage : { Isl violatedIsl -> "Could not create flow" }, - errorDescription : { Isl violatedIsl -> + errorDescription: { Isl violatedIsl -> getPortViolationError("source", violatedIsl.srcPort, violatedIsl.srcSwitch.dpId) } ], [ - switchType: "destination", - port : "dstPort", - errorMessage : { Isl violatedIsl -> + switchType : "destination", + port : "dstPort", + errorMessage : { Isl violatedIsl -> "Could not create flow" }, - errorDescription : { Isl violatedIsl -> + errorDescription: { Isl violatedIsl -> getPortViolationError("destination", violatedIsl.dstPort, violatedIsl.dstSwitch.dpId) } ] @@ -829,7 +837,7 @@ class FlowCrudSpec extends HealthCheckSpecification { @Tidy @Tags(LOW_PRIORITY) - def "System allows to set/update description/priority/max-latency for a flow"(){ + def "System allows to set/update description/priority/max-latency for a flow"() { given: "Two active neighboring switches" def switchPair = topologyHelper.getNeighboringSwitchPair() @@ -894,11 +902,11 @@ class FlowCrudSpec extends HealthCheckSpecification { def endpointName = "source" def swWithoutVxlan = swPair.src - def encapsTypesWithoutVxlan = srcProps.supportedTransitEncapsulation.collect {it.toString().toUpperCase()} + def encapsTypesWithoutVxlan = srcProps.supportedTransitEncapsulation.collect { it.toString().toUpperCase() } if (srcProps.supportedTransitEncapsulation.contains(FlowEncapsulationType.VXLAN.toString().toLowerCase())) { swWithoutVxlan = swPair.dst - encapsTypesWithoutVxlan = dstProps.supportedTransitEncapsulation.collect {it.toString().toUpperCase()} + encapsTypesWithoutVxlan = dstProps.supportedTransitEncapsulation.collect { it.toString().toUpperCase() } endpointName = "destination" } @@ -998,11 +1006,11 @@ class FlowCrudSpec extends HealthCheckSpecification { and: "Flow history shows actual info into stateBefore and stateAfter sections" def flowHistory = northbound.getFlowHistory(flow.flowId) - with(flowHistory.last().dumps.find { it.type == "stateBefore" }){ + with(flowHistory.last().dumps.find { it.type == "stateBefore" }) { it.sourcePort == flow.source.portNumber it.sourceVlan == flow.source.vlanId } - with(flowHistory.last().dumps.find { it.type == "stateAfter" }){ + with(flowHistory.last().dumps.find { it.type == "stateAfter" }) { it.sourcePort == updatedFlow.source.portNumber it.sourceVlan == updatedFlow.source.vlanId } @@ -1052,10 +1060,10 @@ class FlowCrudSpec extends HealthCheckSpecification { and: "Flow history shows actual info into stateBefore and stateAfter sections" def flowHistory2 = northbound.getFlowHistory(flow.flowId) - with(flowHistory2.last().dumps.find { it.type == "stateBefore" }){ + with(flowHistory2.last().dumps.find { it.type == "stateBefore" }) { it.destinationSwitch == dstSwitch.dpId.toString() } - with(flowHistory2.last().dumps.find { it.type == "stateAfter" }){ + with(flowHistory2.last().dumps.find { it.type == "stateAfter" }) { it.destinationSwitch == newDstSwitch.dpId.toString() } @@ -1206,9 +1214,9 @@ class FlowCrudSpec extends HealthCheckSpecification { then: "Update the flow: port number and vlanId on the src/dst endpoints" def updatedFlow = flow.jacksonCopy().tap { - it.source.portNumber = activeTraffGens.find { it.switchConnected.dpId == switchPair.src.dpId}.switchPort + it.source.portNumber = activeTraffGens.find { it.switchConnected.dpId == switchPair.src.dpId }.switchPort it.source.vlanId = updatedFlowDstEndpoint.source.vlanId - 1 - it.destination.portNumber = activeTraffGens.find { it.switchConnected.dpId == switchPair.dst.dpId}.switchPort + it.destination.portNumber = activeTraffGens.find { it.switchConnected.dpId == switchPair.dst.dpId }.switchPort it.destination.vlanId = updatedFlowDstEndpoint.destination.vlanId - 1 } flowHelperV2.updateFlow(flow.flowId, updatedFlow) @@ -1270,7 +1278,8 @@ class FlowCrudSpec extends HealthCheckSpecification { when: "Update the dst endpoint to make this flow as multi switch flow" def newPortNumber = topology.getAllowedPortsForSwitch(topology.activeSwitches.find { - it.dpId == swPair.dst.dpId } + it.dpId == swPair.dst.dpId + } ).first() flowHelperV2.updateFlow(flow.flowId, flow.tap { it.destination.switchId = swPair.dst.dpId @@ -1291,7 +1300,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } and: "Involved switches pass switch validation" - [swPair.src, swPair.dst].each {sw -> + [swPair.src, swPair.dst].each { sw -> with(northbound.validateSwitch(sw.dpId)) { validation -> validation.verifyRuleSectionsAreEmpty(["missing", "excess", "misconfigured"]) validation.verifyMeterSectionsAreEmpty(["missing", "excess", "misconfigured"]) @@ -1322,6 +1331,51 @@ class FlowCrudSpec extends HealthCheckSpecification { !error && flowHelperV2.deleteFlow(flow.flowId) } + @Tidy + def "Able to #method with empty VLAN stats and non-zero VLANs (#5063)"() { + given: "A flow with non empty vlans stats and with src and dst vlans set to '0'" + def switches = topologyHelper.getSwitchPairs().shuffled().first() + def flowRequest = flowHelperV2.randomFlow(switches, false).tap { + it.source.tap { it.vlanId = 0 } + it.destination.tap { it.vlanId = 0 } + it.statistics = new FlowStatistics([1, 2, 3] as Set) + } + def flow = flowHelperV2.addFlow(flowRequest) + def UNUSED_VLAN = 1909 + + when: "Try to #method update flow with empty VLAN stats and non-zero VLANs" + def updatedFlow = updateCall(flow.getFlowId(), flowRequest, UNUSED_VLAN) + + then: "Flow is really updated" + def actualFlow = northboundV2.getFlow(flow.getFlowId()) + actualFlow.getSource() == updatedFlow.getSource() + actualFlow.getDestination() == updatedFlow.getDestination() + actualFlow.getStatistics() == updatedFlow.getStatistics() + + cleanup: + Wrappers.silent { + flowHelperV2.deleteFlow(flow.getFlowId()) + } + + where: + method | updateCall + "partial" | { String flowId, FlowRequestV2 originalFlow, Integer newVlan -> + flowHelperV2.partialUpdate(flowId, new FlowPatchV2().tap { + it.source = new FlowPatchEndpoint().tap {it.vlanId = newVlan} + it.destination = new FlowPatchEndpoint().tap {it.vlanId = newVlan} + it.statistics = new FlowStatistics([] as Set) + }) + } + ""| { String flowId, FlowRequestV2 originalFlow, Integer newVlan -> + flowHelperV2.updateFlow(flowId, originalFlow.tap { + it.source = originalFlow.source.tap{it.vlanId = newVlan} + it.destination = originalFlow.destination.tap{it.vlanId = newVlan} + it.statistics = new FlowStatistics([] as Set) + }) + + } + } + @Shared def errorDescription = { String operation, FlowRequestV2 flow, String endpoint, FlowRequestV2 conflictingFlow, String conflictingEndpoint -> @@ -1485,12 +1539,12 @@ class FlowCrudSpec extends HealthCheckSpecification { flowToConflict.source.portNumber = dominantFlow.source.portNumber flowToConflict.source.vlanId = dominantFlow.source.vlanId }, - getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> "Could not $operation flow" }, - getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> errorDescription(operation, dominantFlow, "source", flowToConflict, "source") } ], @@ -1500,12 +1554,12 @@ class FlowCrudSpec extends HealthCheckSpecification { flowToConflict.destination.portNumber = dominantFlow.destination.portNumber flowToConflict.destination.vlanId = dominantFlow.destination.vlanId }, - getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> "Could not $operation flow" }, - getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> errorDescription(operation, dominantFlow, "destination", flowToConflict, "destination") } ], @@ -1516,12 +1570,12 @@ class FlowCrudSpec extends HealthCheckSpecification { flowToConflict.source.vlanId = 0 dominantFlow.source.vlanId = 0 }, - getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> "Could not $operation flow" }, - getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> errorDescription(operation, dominantFlow, "source", flowToConflict, "source") } ], @@ -1532,12 +1586,12 @@ class FlowCrudSpec extends HealthCheckSpecification { flowToConflict.destination.vlanId = 0 dominantFlow.destination.vlanId = 0 }, - getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> "Could not $operation flow" }, - getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> errorDescription(operation, dominantFlow, "destination", flowToConflict, "destination") } ] From 3f19b842dde6cf7e93a108b4268698e6c028b81a Mon Sep 17 00:00:00 2001 From: ichupin Date: Thu, 2 Mar 2023 14:11:32 +0100 Subject: [PATCH 17/45] add error field filling for join --- .../info/switches/v2/LogicalPortsValidationEntryV2.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java index 26b4502a5bf..c7ab56e4e2d 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; import lombok.Data; +import org.apache.commons.lang3.StringUtils; import java.io.Serializable; import java.util.ArrayList; @@ -59,6 +60,8 @@ static LogicalPortsValidationEntryV2 join(List en builder.proper(joinLists(nonNullEntries.stream().map(LogicalPortsValidationEntryV2::getProper))); builder.missing(joinLists(nonNullEntries.stream().map(LogicalPortsValidationEntryV2::getMissing))); builder.misconfigured(joinLists(nonNullEntries.stream().map(LogicalPortsValidationEntryV2::getMisconfigured))); + nonNullEntries.stream().filter(e -> StringUtils.isNoneBlank(e.error)).findFirst() + .map(LogicalPortsValidationEntryV2::getError).ifPresent(builder::error); return builder.build(); } From 85481638f9a38e17f4e97fe86bf052ecea533b8a Mon Sep 17 00:00:00 2001 From: ichupin Date: Thu, 2 Mar 2023 13:19:52 +0000 Subject: [PATCH 18/45] Fix for SwitchManagerTopology, set coordinator bolt to listen SpeakerWorkerBolt, now it should listen call back registration tasks and register this "subtasks" from SwitchValidationFsm. --- .../info/switches/v2/LogicalPortsValidationEntryV2.java | 1 + .../wfm/topology/switchmanager/SwitchManagerTopology.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java index c7ab56e4e2d..4d4701350ea 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java @@ -75,6 +75,7 @@ List split(int firstChunkSize, int chunkSize) { .excess(entry.getExcess()) .proper(entry.getProper()) .misconfigured(entry.getMisconfigured()) + .error(error) .build()); } return result; diff --git a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/SwitchManagerTopology.java b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/SwitchManagerTopology.java index e37d826c451..2b725cd1f4e 100644 --- a/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/SwitchManagerTopology.java +++ b/src-java/swmanager-topology/swmanager-storm-topology/src/main/java/org/openkilda/wfm/topology/switchmanager/SwitchManagerTopology.java @@ -72,7 +72,8 @@ public StormTopology createTopology() { declareSpout(builder, new CoordinatorSpout(), CoordinatorSpout.ID); declareBolt(builder, new CoordinatorBolt(), CoordinatorBolt.ID) .allGrouping(CoordinatorSpout.ID) - .fieldsGrouping(SwitchManagerHub.ID, CoordinatorBolt.INCOME_STREAM, FIELDS_KEY); + .fieldsGrouping(SwitchManagerHub.ID, CoordinatorBolt.INCOME_STREAM, FIELDS_KEY) + .fieldsGrouping(SpeakerWorkerBolt.ID, CoordinatorBolt.INCOME_STREAM, FIELDS_KEY); PersistenceManager persistenceManager = new PersistenceManager(configurationProvider); From 7865996da0816e2a3311e096dfc4c300f95499eb Mon Sep 17 00:00:00 2001 From: ichupin Date: Thu, 2 Mar 2023 14:29:19 +0100 Subject: [PATCH 19/45] Added unit test for LogicalPortsValidationEntryV2Test --- .../v2/LogicalPortsValidationEntryV2Test.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java b/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java index 0249df0ffd3..cb1fe5aba6a 100644 --- a/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java +++ b/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java @@ -42,6 +42,22 @@ public void splitAndUniteEmptyLogicalPortEntryTest() { assertEquals(entry, united); } + @Test + public void splitAndUniteEmptyLogicalPortEntryWithErrorTest() { + LogicalPortsValidationEntryV2 entry = LogicalPortsValidationEntryV2.builder() + .asExpected(false) + .error("Timeout for waiting response on DumpLogicalPortsRequest() Details: Error in SpeakerWorkerService") + .missing(new ArrayList<>()) + .misconfigured(new ArrayList<>()) + .excess(new ArrayList<>()) + .proper(new ArrayList<>()) + .build(); + List list = entry.split(4, 4); + assertEquals(1, list.size()); + LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); + assertEquals(entry, united); + } + @Test public void splitAndUniteNullLogicalPortEntryTest() { LogicalPortsValidationEntryV2 entry = LogicalPortsValidationEntryV2.builder() From 1ddf0a75e62a1543c87c1d00b2360c18e4aeee98 Mon Sep 17 00:00:00 2001 From: ichupin Date: Thu, 2 Mar 2023 15:21:38 +0100 Subject: [PATCH 20/45] Added unit test for LogicalPortsValidationEntryV2Test --- .../v2/LogicalPortsValidationEntryV2Test.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java b/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java index cb1fe5aba6a..09bad73c087 100644 --- a/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java +++ b/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java @@ -46,7 +46,8 @@ public void splitAndUniteEmptyLogicalPortEntryTest() { public void splitAndUniteEmptyLogicalPortEntryWithErrorTest() { LogicalPortsValidationEntryV2 entry = LogicalPortsValidationEntryV2.builder() .asExpected(false) - .error("Timeout for waiting response on DumpLogicalPortsRequest() Details: Error in SpeakerWorkerService") + .error("Timeout for waiting response on DumpLogicalPortsRequest()" + + " Details: Error in SpeakerWorkerService") .missing(new ArrayList<>()) .misconfigured(new ArrayList<>()) .excess(new ArrayList<>()) @@ -163,6 +164,22 @@ public void splitAndUniteManyEntriesLogicalPortEntryTest() { assertEquals(entry, united); } + @Test + public void splitAndUniteManyEntriesLogicalPortEntryWithErrorMessageTest() { + LogicalPortsValidationEntryV2 entry = LogicalPortsValidationEntryV2.builder() + .asExpected(true) + .error("Some error message") + .missing(buildLogicalPortsInfo(1, 500)) + .excess(buildLogicalPortsInfo(1000, 600)) + .proper(buildLogicalPortsInfo(2000, 700)) + .misconfigured(buildMisconfiguredLogicalPortsInfo(3000, 800)) + .build(); + List list = entry.split(100, 200); + assertEquals(14, list.size()); + LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); + assertEquals(entry, united); + } + @Test public void splitAndUniteManyEntriesByOneLogicalPortEntryTest() { LogicalPortsValidationEntryV2 entry = LogicalPortsValidationEntryV2.builder() From c0c37dba6afb2991266c25a3c76bc697c239a48a Mon Sep 17 00:00:00 2001 From: ichupin Date: Thu, 2 Mar 2023 14:32:03 +0000 Subject: [PATCH 21/45] checkstyle fixes --- .../info/switches/v2/LogicalPortsValidationEntryV2Test.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java b/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java index 09bad73c087..2baa84dad67 100644 --- a/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java +++ b/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java @@ -46,8 +46,8 @@ public void splitAndUniteEmptyLogicalPortEntryTest() { public void splitAndUniteEmptyLogicalPortEntryWithErrorTest() { LogicalPortsValidationEntryV2 entry = LogicalPortsValidationEntryV2.builder() .asExpected(false) - .error("Timeout for waiting response on DumpLogicalPortsRequest()" + - " Details: Error in SpeakerWorkerService") + .error("Timeout for waiting response on DumpLogicalPortsRequest()" + + " Details: Error in SpeakerWorkerService") .missing(new ArrayList<>()) .misconfigured(new ArrayList<>()) .excess(new ArrayList<>()) From af9537441a32efc7bdc411e989e3f83be4292161 Mon Sep 17 00:00:00 2001 From: ichupin Date: Mon, 6 Mar 2023 10:56:27 +0100 Subject: [PATCH 22/45] review fixes --- .../v2/LogicalPortsValidationEntryV2Test.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java b/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java index 2baa84dad67..86921016eaa 100644 --- a/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java +++ b/src-java/base-topology/base-messaging/src/test/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2Test.java @@ -37,7 +37,7 @@ public void splitAndUniteEmptyLogicalPortEntryTest() { .proper(new ArrayList<>()) .build(); List list = entry.split(4, 4); - assertEquals(1, list.size()); + assertEquals("Method split should return the list with the length of 1", 1, list.size()); LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); assertEquals(entry, united); } @@ -46,7 +46,7 @@ public void splitAndUniteEmptyLogicalPortEntryTest() { public void splitAndUniteEmptyLogicalPortEntryWithErrorTest() { LogicalPortsValidationEntryV2 entry = LogicalPortsValidationEntryV2.builder() .asExpected(false) - .error("Timeout for waiting response on DumpLogicalPortsRequest()" + .error("Timeout for waiting response on DumpLogicalPortsRequest()" + " Details: Error in SpeakerWorkerService") .missing(new ArrayList<>()) .misconfigured(new ArrayList<>()) @@ -54,7 +54,7 @@ public void splitAndUniteEmptyLogicalPortEntryWithErrorTest() { .proper(new ArrayList<>()) .build(); List list = entry.split(4, 4); - assertEquals(1, list.size()); + assertEquals("Method split should return the list with the length of 1", 1, list.size()); LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); assertEquals(entry, united); } @@ -69,7 +69,7 @@ public void splitAndUniteNullLogicalPortEntryTest() { .proper(null) .build(); List list = entry.split(4, 4); - assertEquals(1, list.size()); + assertEquals("Method split should return the list with the length of 1", 1, list.size()); LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); assertEquals(entry, united); } @@ -84,7 +84,7 @@ public void splitAndUniteOneLogicalPortEntryTest() { .misconfigured(buildMisconfiguredLogicalPortsInfo(4, 1)) .build(); List list = entry.split(4, 4); - assertEquals(1, list.size()); + assertEquals("Method split should return the list with the length of 1", 1, list.size()); LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); assertEquals(entry, united); } @@ -99,7 +99,7 @@ public void splitAndUniteFourLogicalPortEntryTest() { .misconfigured(buildMisconfiguredLogicalPortsInfo(4, 1)) .build(); List list = entry.split(1, 1); - assertEquals(4, list.size()); + assertEquals("Method split should return the list with the length of 4", 4, list.size()); LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); assertEquals(entry, united); } @@ -114,7 +114,7 @@ public void splitAndUniteNotDividedLogicalPortEntryTest() { .misconfigured(buildMisconfiguredLogicalPortsInfo(4, 4)) .build(); List list = entry.split(2, 3); - assertEquals(4, list.size()); + assertEquals("Method split should return the list with the length of 4", 4, list.size()); LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); assertEquals(entry, united); } @@ -129,7 +129,7 @@ public void splitAndUniteTwoEntryTest() { .misconfigured(new ArrayList<>()) .build(); List list = entry.split(2, 2); - assertEquals(3, list.size()); + assertEquals("Method split should return the list with the length of 3", 3, list.size()); LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); assertEquals(entry, united); } @@ -144,7 +144,7 @@ public void splitAndUniteHugeChunkLogicalPortEntryTest() { .misconfigured(buildMisconfiguredLogicalPortsInfo(4, 4)) .build(); List list = entry.split(100, 200); - assertEquals(1, list.size()); + assertEquals("Method split should return the list with the length of 1", 1, list.size()); LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); assertEquals(entry, united); } @@ -159,7 +159,7 @@ public void splitAndUniteManyEntriesLogicalPortEntryTest() { .misconfigured(buildMisconfiguredLogicalPortsInfo(3000, 800)) .build(); List list = entry.split(100, 200); - assertEquals(14, list.size()); + assertEquals("Method split should return the list with the length of 14", 14, list.size()); LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); assertEquals(entry, united); } @@ -175,7 +175,7 @@ public void splitAndUniteManyEntriesLogicalPortEntryWithErrorMessageTest() { .misconfigured(buildMisconfiguredLogicalPortsInfo(3000, 800)) .build(); List list = entry.split(100, 200); - assertEquals(14, list.size()); + assertEquals("Method split should return the list with the length of 14", 14, list.size()); LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); assertEquals(entry, united); } @@ -190,7 +190,7 @@ public void splitAndUniteManyEntriesByOneLogicalPortEntryTest() { .misconfigured(buildMisconfiguredLogicalPortsInfo(3000, 800)) .build(); List list = entry.split(1, 1); - assertEquals(2600, list.size()); + assertEquals("Method split should return the list with the length of 2600", 2600, list.size()); LogicalPortsValidationEntryV2 united = LogicalPortsValidationEntryV2.join(list); assertEquals(entry, united); } From 8f1871601a216d11b69be98c1781d39e07d76fcb Mon Sep 17 00:00:00 2001 From: ichupin Date: Mon, 6 Mar 2023 12:37:31 +0100 Subject: [PATCH 23/45] review fixes --- .../info/switches/v2/LogicalPortsValidationEntryV2.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java index 4d4701350ea..121792e116d 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java @@ -60,8 +60,12 @@ static LogicalPortsValidationEntryV2 join(List en builder.proper(joinLists(nonNullEntries.stream().map(LogicalPortsValidationEntryV2::getProper))); builder.missing(joinLists(nonNullEntries.stream().map(LogicalPortsValidationEntryV2::getMissing))); builder.misconfigured(joinLists(nonNullEntries.stream().map(LogicalPortsValidationEntryV2::getMisconfigured))); - nonNullEntries.stream().filter(e -> StringUtils.isNoneBlank(e.error)).findFirst() - .map(LogicalPortsValidationEntryV2::getError).ifPresent(builder::error); + if (nonNullEntries.stream().map(e -> e.error).distinct().count() > 1) { + builder.error(nonNullEntries.stream().map(e -> e.error) + .distinct().collect(Collectors.joining(","))); + } else { + nonNullEntries.stream().map(e -> e.error).filter(Objects::nonNull).findFirst().ifPresent(builder::error); + } return builder.build(); } From 58e4163713749c3d55b5f38ebd4665e64570d5da Mon Sep 17 00:00:00 2001 From: ichupin Date: Mon, 6 Mar 2023 12:49:37 +0100 Subject: [PATCH 24/45] checkstyle fixes --- .../info/switches/v2/LogicalPortsValidationEntryV2.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java index 121792e116d..63bba1f6100 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java @@ -23,7 +23,6 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; import lombok.Data; -import org.apache.commons.lang3.StringUtils; import java.io.Serializable; import java.util.ArrayList; From 132f0be419e38aedde5c9891f22505f74ffaf110 Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Wed, 22 Feb 2023 15:17:37 +0100 Subject: [PATCH 25/45] Add error messages when switch is not found to the link validation. Add unit tests. Add details about parameters to the feature description. Add input parameters validation --- .../path-validation/path-validation.md | 28 +- .../base-topology/base-messaging/build.gradle | 1 + .../info/network/PathValidationResult.java | 13 +- ...ionDto.java => PathValidationPayload.java} | 24 +- .../command/flow/PathValidateRequest.java | 13 +- .../openkilda/model/PathValidationData.java | 5 +- .../mappers/PathValidationDataMapper.java | 38 +- .../topology/nbworker/bolts/RouterBolt.java | 4 +- .../nbworker/services/PathsService.java | 5 +- .../nbworker/validators/PathValidator.java | 330 ++++++--- .../nbworker/services/PathsServiceTest.java | 348 --------- .../validators/PathValidatorTest.java | 683 ++++++++++++++++++ .../controller/v2/NetworkControllerV2.java | 29 +- .../northbound/service/NetworkService.java | 8 +- .../service/impl/NetworkServiceImpl.java | 10 +- 15 files changed, 982 insertions(+), 557 deletions(-) rename src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/{PathValidationDto.java => PathValidationPayload.java} (69%) create mode 100644 src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java diff --git a/docs/design/solutions/path-validation/path-validation.md b/docs/design/solutions/path-validation/path-validation.md index 8d8fff9d764..228461f3485 100644 --- a/docs/design/solutions/path-validation/path-validation.md +++ b/docs/design/solutions/path-validation/path-validation.md @@ -18,15 +18,24 @@ after validation in the future. REST URL: ```/v2/network/path/check```, method: ```GET``` -A user is required to provide a path represented by a list of nodes. Nodes must be ordered from start to end, +A user is required to provide a path represented by a list of nodes. Nodes must be ordered from start to end; each next element is the next hop. A user can add optional parameters: -- encapsulation_type: enum value "TRANSIT_VLAN" or "VXLAN". -- max_bandwidth: bandwidth required for this path. -- max_latency: the first tier latency value. -- max_latency_tier2: the second tier latency value. -- path_computation_strategy: "COST|LATENCY|MAX_LATENCY|COST_AND_AVAILABLE_BANDWIDTH". -- reuse_flow_resources: a flow ID. Verify the given path as if it is created instead of the existing flow, that is as if -the resources of some flow are released before validation. +- `encapsulation_type`: enum value "TRANSIT_VLAN" or "VXLAN". API returns an error for every switch in the list if it + doesn't support the given encapsulation type. +- `max_bandwidth`: bandwidth required for this path. API returns an error for each segment of the given path when the + available bandwidth on the segment is less than the given value. When used in combination with `reuse_flow_resources`, + available bandwidth as if the given flow doesn't consume any bandwidth. +- `max_latency`: the first tier latency value in milliseconds. API returns an error for each segment of the given path, + which max latency is greater than the given value. +- `max_latency_tier2`: the second tier latency value in milliseconds. API returns an error for each segment of the given + path, which max latency is greater than the given value. +- `path_computation_strategy`: an enum value PathComputationStrategy. API will return different set of errors depending + on the selected strategy. For example, when COST is selected API ignores available bandwidth and latency parameters. + If none is selected, all validations are executed. +- `reuse_flow_resources`: a flow ID. Verify the given path as if it is created instead of the existing flow, that is as + if the resources of some flow are released before validation. Returns an error if this flow doesn't exist. +- `diverse_with_flow`: a flow ID. Verify whether the given path intersects with the given flow. API returns an error for + each common segment. Returns an error if this flow doesn't exist. ### Request Body ```json @@ -36,7 +45,8 @@ the resources of some flow are released before validation. "max_latency": 0, "max_latency_tier2": 0, "path_computation_strategy": "COST|LATENCY|MAX_LATENCY|COST_AND_AVAILABLE_BANDWIDTH", - "reuse_flow_resources": 0, + "reuse_flow_resources": "flow_id", + "diverse_with_flow": "diverse_flow_id", "nodes": [ { "switch_id": "00:00:00:00:00:00:00:01", diff --git a/src-java/base-topology/base-messaging/build.gradle b/src-java/base-topology/base-messaging/build.gradle index 0488bb67a9b..1ccedc0f36a 100644 --- a/src-java/base-topology/base-messaging/build.gradle +++ b/src-java/base-topology/base-messaging/build.gradle @@ -15,6 +15,7 @@ dependencies { implementation 'com.google.guava:guava' implementation 'org.apache.commons:commons-lang3' implementation 'org.slf4j:slf4j-api' + implementation 'javax.validation:validation-api' testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.vintage:junit-vintage-engine' diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidationResult.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidationResult.java index adb80d520c8..0c073674d24 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidationResult.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidationResult.java @@ -1,4 +1,4 @@ -/* Copyright 2018 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,6 @@ import org.openkilda.messaging.info.InfoData; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.Builder; @@ -32,15 +30,6 @@ @EqualsAndHashCode(callSuper = false) @JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) public class PathValidationResult extends InfoData { - @JsonProperty("is_valid") Boolean isValid; - @JsonProperty("errors") List errors; - - @JsonCreator - public PathValidationResult(@JsonProperty("is_valid") Boolean isValid, - @JsonProperty("errors") List errors) { - this.isValid = isValid; - this.errors = errors; - } } diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationPayload.java similarity index 69% rename from src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java rename to src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationPayload.java index e879b2a7058..8a55eacf6a1 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationDto.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationPayload.java @@ -1,4 +1,4 @@ -/* Copyright 2019 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,18 +26,21 @@ import lombok.Value; import java.util.List; +import javax.validation.constraints.PositiveOrZero; @Value @Builder @JsonInclude(JsonInclude.Include.NON_NULL) -public class PathValidationDto { +public class PathValidationPayload { @JsonProperty("bandwidth") Long bandwidth; @JsonProperty("max_latency") + @PositiveOrZero(message = "max_latency cannot be negative") Long latencyMs; @JsonProperty("max_latency_tier2") + @PositiveOrZero(message = "max_latency_tier2 cannot be negative") Long latencyTier2ms; @JsonProperty("nodes") @@ -56,14 +59,15 @@ public class PathValidationDto { PathComputationStrategy pathComputationStrategy; @JsonCreator - public PathValidationDto(@JsonProperty("bandwidth") Long bandwidth, - @JsonProperty("latency_ms") Long latencyMs, - @JsonProperty("max_latency_tier2") Long latencyTier2ms, - @JsonProperty("nodes") List nodes, - @JsonProperty("diverse_with_flow") String diverseWithFlow, - @JsonProperty("reuse_flow_resources") String reuseFlowResources, - @JsonProperty("flow_encapsulation_type") FlowEncapsulationType flowEncapsulationType, - @JsonProperty("path_computation_strategy") PathComputationStrategy computationStrategy) { + public PathValidationPayload( + @JsonProperty("bandwidth") Long bandwidth, + @JsonProperty("latency_ms") Long latencyMs, + @JsonProperty("max_latency_tier2") Long latencyTier2ms, + @JsonProperty("nodes") List nodes, + @JsonProperty("diverse_with_flow") String diverseWithFlow, + @JsonProperty("reuse_flow_resources") String reuseFlowResources, + @JsonProperty("flow_encapsulation_type") FlowEncapsulationType flowEncapsulationType, + @JsonProperty("path_computation_strategy") PathComputationStrategy computationStrategy) { this.bandwidth = bandwidth; this.latencyMs = latencyMs; this.latencyTier2ms = latencyTier2ms; diff --git a/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java b/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java index 3979a0ad324..e63f228eef4 100644 --- a/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java +++ b/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java @@ -17,10 +17,8 @@ import org.openkilda.messaging.nbtopology.annotations.ReadRequest; import org.openkilda.messaging.nbtopology.request.BaseRequest; -import org.openkilda.messaging.payload.network.PathValidationDto; +import org.openkilda.messaging.payload.network.PathValidationPayload; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.EqualsAndHashCode; import lombok.Value; @@ -31,12 +29,5 @@ @ReadRequest @EqualsAndHashCode(callSuper = true) public class PathValidateRequest extends BaseRequest { - - @JsonProperty("path") - PathValidationDto pathValidationDto; - - @JsonCreator - public PathValidateRequest(@JsonProperty("path") PathValidationDto pathValidationDtoDto) { - this.pathValidationDto = pathValidationDtoDto; - } + PathValidationPayload pathValidationPayload; } diff --git a/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java b/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java index 594fb6403d3..b11717e7dca 100644 --- a/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java +++ b/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java @@ -18,6 +18,7 @@ import lombok.Builder; import lombok.Value; +import java.time.Duration; import java.util.List; @Value @@ -25,8 +26,8 @@ public class PathValidationData { Long bandwidth; - Long latencyMs; - Long latencyTier2ms; + Duration latency; + Duration latencyTier2; List pathSegments; String diverseWithFlow; String reuseFlowResources; diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java index 5578ae4e5df..aef43fb50aa 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java @@ -15,12 +15,13 @@ package org.openkilda.wfm.share.mappers; -import org.openkilda.messaging.payload.network.PathValidationDto; +import org.openkilda.messaging.payload.network.PathValidationPayload; import org.openkilda.model.PathValidationData; import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; +import java.time.Duration; import java.util.LinkedList; import java.util.List; @@ -31,32 +32,35 @@ public abstract class PathValidationDataMapper { /** * Converts NB PathValidationDto to messaging PathValidationData. - * @param pathValidationDto NB representation of a path validation data + * @param pathValidationPayload NB representation of a path validation data * @return the messaging representation of a path validation data */ - public PathValidationData toPathValidationData(PathValidationDto pathValidationDto) { + public PathValidationData toPathValidationData(PathValidationPayload pathValidationPayload) { List segments = new LinkedList<>(); - for (int i = 0; i < pathValidationDto.getNodes().size() - 1; i++) { + for (int i = 0; i < pathValidationPayload.getNodes().size() - 1; i++) { segments.add(PathValidationData.PathSegmentValidationData.builder() - .srcSwitchId(pathValidationDto.getNodes().get(i).getSwitchId()) - .srcPort(pathValidationDto.getNodes().get(i).getOutputPort()) - .destSwitchId(pathValidationDto.getNodes().get(i + 1).getSwitchId()) - .destPort(pathValidationDto.getNodes().get(i + 1).getInputPort()) + .srcSwitchId(pathValidationPayload.getNodes().get(i).getSwitchId()) + .srcPort(pathValidationPayload.getNodes().get(i).getOutputPort()) + .destSwitchId(pathValidationPayload.getNodes().get(i + 1).getSwitchId()) + .destPort(pathValidationPayload.getNodes().get(i + 1).getInputPort()) .build()); } return PathValidationData.builder() - .srcSwitchId(pathValidationDto.getNodes().get(0).getSwitchId()) - .destSwitchId(pathValidationDto.getNodes().get(pathValidationDto.getNodes().size() - 1).getSwitchId()) - .bandwidth(pathValidationDto.getBandwidth()) - .latencyMs(pathValidationDto.getLatencyMs()) - .latencyTier2ms(pathValidationDto.getLatencyTier2ms()) - .diverseWithFlow(pathValidationDto.getDiverseWithFlow()) - .reuseFlowResources(pathValidationDto.getReuseFlowResources()) + .srcSwitchId(pathValidationPayload.getNodes().get(0).getSwitchId()) + .destSwitchId( + pathValidationPayload.getNodes().get(pathValidationPayload.getNodes().size() - 1).getSwitchId()) + .bandwidth(pathValidationPayload.getBandwidth()) + .latency(pathValidationPayload.getLatencyMs() == null ? null : + Duration.ofMillis(pathValidationPayload.getLatencyMs())) + .latencyTier2(pathValidationPayload.getLatencyTier2ms() == null ? null : + Duration.ofMillis(pathValidationPayload.getLatencyTier2ms())) + .diverseWithFlow(pathValidationPayload.getDiverseWithFlow()) + .reuseFlowResources(pathValidationPayload.getReuseFlowResources()) .flowEncapsulationType(FlowEncapsulationTypeMapper.INSTANCE.toOpenKildaModel( - pathValidationDto.getFlowEncapsulationType())) - .pathComputationStrategy(pathValidationDto.getPathComputationStrategy()) + pathValidationPayload.getFlowEncapsulationType())) + .pathComputationStrategy(pathValidationPayload.getPathComputationStrategy()) .pathSegments(segments) .build(); } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java index d62f2d7ce2a..821bc643e06 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java @@ -89,9 +89,7 @@ private void processRequest(Tuple input, String key, BaseRequest request) { emitWithContext(StreamType.FEATURE_TOGGLES.toString(), input, new Values(request)); } else if (request instanceof KildaConfigurationBaseRequest) { emitWithContext(StreamType.KILDA_CONFIG.toString(), input, new Values(request)); - } else if (request instanceof GetPathsRequest) { - emitWithContext(StreamType.PATHS.toString(), input, new Values(request)); - } else if (request instanceof PathValidateRequest) { + } else if (request instanceof GetPathsRequest || request instanceof PathValidateRequest) { emitWithContext(StreamType.PATHS.toString(), input, new Values(request)); } else if (request instanceof HistoryRequest) { emitWithContext(StreamType.HISTORY.toString(), input, new Values(request)); diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java index 1b791b11941..e4da63c734e 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java @@ -142,10 +142,11 @@ public List getPaths( public List validatePath(PathValidateRequest request) { PathValidator pathValidator = new PathValidator(islRepository, flowRepository, - switchPropertiesRepository); + switchPropertiesRepository, + switchRepository); return Collections.singletonList(pathValidator.validatePath( - PathValidationDataMapper.INSTANCE.toPathValidationData(request.getPathValidationDto()) + PathValidationDataMapper.INSTANCE.toPathValidationData(request.getPathValidationPayload()) )); } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java index 12b56e02e87..bf34d39244f 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java @@ -24,46 +24,62 @@ import org.openkilda.model.FlowPath; import org.openkilda.model.Isl; import org.openkilda.model.IslStatus; +import org.openkilda.model.PathComputationStrategy; import org.openkilda.model.PathSegment; import org.openkilda.model.PathValidationData; +import org.openkilda.model.PathValidationData.PathSegmentValidationData; +import org.openkilda.model.Switch; +import org.openkilda.model.SwitchId; import org.openkilda.model.SwitchProperties; import org.openkilda.persistence.repositories.FlowRepository; import org.openkilda.persistence.repositories.IslRepository; import org.openkilda.persistence.repositories.SwitchPropertiesRepository; +import org.openkilda.persistence.repositories.SwitchRepository; +import com.google.common.collect.Sets; import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import java.time.Duration; import java.util.Collections; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collectors; - public class PathValidator { + public static final String LATENCY = "latency"; + public static final String LATENCY_TIER_2 = "latency tier 2"; private final IslRepository islRepository; private final FlowRepository flowRepository; private final SwitchPropertiesRepository switchPropertiesRepository; + private final SwitchRepository switchRepository; + public PathValidator(IslRepository islRepository, FlowRepository flowRepository, - SwitchPropertiesRepository switchPropertiesRepository) { + SwitchPropertiesRepository switchPropertiesRepository, + SwitchRepository switchRepository) { this.islRepository = islRepository; this.flowRepository = flowRepository; this.switchPropertiesRepository = switchPropertiesRepository; + this.switchRepository = switchRepository; } /** - * Validates whether it is possible to create a path with the given parameters. When there obstacles, this validator - * returns all errors found on each segment. For example, when there is a path 1-2-3-4 and there is no link between - * 1 and 2, no sufficient latency between 2 and 3, and not enough bandwidth between 3-4, then this validator returns - * all 3 errors. + * Validates whether it is possible to create a path with the given parameters. When there are obstacles, this + * validator returns all errors found on each segment. For example, when there is a path 1-2-3-4 and there is + * no link between 1 and 2, no sufficient latency between 2 and 3, and not enough bandwidth between 3-4, then + * this validator returns all 3 errors. + * * @param pathValidationData path parameters to validate. * @return a response object containing the validation result and errors if any */ @@ -71,7 +87,7 @@ public PathValidationResult validatePath(PathValidationData pathValidationData) Set result = pathValidationData.getPathSegments().stream() .map(segment -> executeValidations( new InputData(pathValidationData, segment), - new RepositoryData(islRepository, flowRepository, switchPropertiesRepository), + new RepositoryData(islRepository, flowRepository, switchPropertiesRepository, switchRepository), getValidations(pathValidationData))) .flatMap(Set::stream) .collect(Collectors.toSet()); @@ -96,91 +112,153 @@ private List>> getValidations( PathValidationData pathValidationData) { List>> validationFunctions = new LinkedList<>(); - validationFunctions.add(this::validateLink); + validationFunctions.add(this::validateForwardAndReverseLinks); - if (pathValidationData.getLatencyMs() != null - && pathValidationData.getLatencyMs() != 0 - && (pathValidationData.getPathComputationStrategy() == null - || pathValidationData.getPathComputationStrategy() == LATENCY - || pathValidationData.getPathComputationStrategy() == MAX_LATENCY)) { + if (isLatencyValidationRequired(pathValidationData)) { validationFunctions.add(this::validateLatency); } - if (pathValidationData.getLatencyTier2ms() != null - && pathValidationData.getLatencyTier2ms() != 0 - && (pathValidationData.getPathComputationStrategy() == null - || pathValidationData.getPathComputationStrategy() == LATENCY - || pathValidationData.getPathComputationStrategy() == MAX_LATENCY)) { + if (isLatencyTier2ValidationRequired(pathValidationData)) { validationFunctions.add(this::validateLatencyTier2); } - if (pathValidationData.getBandwidth() != null - && pathValidationData.getBandwidth() != 0 - && (pathValidationData.getPathComputationStrategy() == null - || pathValidationData.getPathComputationStrategy() == COST_AND_AVAILABLE_BANDWIDTH)) { + if (isBandwidthValidationRequired(pathValidationData)) { validationFunctions.add(this::validateBandwidth); } - if (pathValidationData.getDiverseWithFlow() != null && !pathValidationData.getDiverseWithFlow().isEmpty()) { + if (isDiverseWithFlowValidationRequired(pathValidationData)) { validationFunctions.add(this::validateDiverseWithFlow); } - if (pathValidationData.getFlowEncapsulationType() != null) { + if (isEncapsulationTypeValidationRequired(pathValidationData)) { validationFunctions.add(this::validateEncapsulationType); } return validationFunctions; } - private Set validateLink(InputData inputData, RepositoryData repositoryData) { - Optional forward = repositoryData.getIslRepository().findByEndpoints( + private boolean isEncapsulationTypeValidationRequired(PathValidationData pathValidationData) { + return pathValidationData.getFlowEncapsulationType() != null; + } + + private boolean isDiverseWithFlowValidationRequired(PathValidationData pathValidationData) { + return StringUtils.isNotBlank(pathValidationData.getDiverseWithFlow()); + } + + private boolean isBandwidthValidationRequired(PathValidationData pathValidationData) { + return pathValidationData.getBandwidth() != null + && pathValidationData.getBandwidth() != 0 + && (pathValidationData.getPathComputationStrategy() == null + || pathValidationData.getPathComputationStrategy() == COST_AND_AVAILABLE_BANDWIDTH); + } + + private boolean isLatencyTier2ValidationRequired(PathValidationData pathValidationData) { + return pathValidationData.getLatencyTier2() != null + && !pathValidationData.getLatencyTier2().isZero() + && (pathValidationData.getPathComputationStrategy() == null + || pathValidationData.getPathComputationStrategy() == PathComputationStrategy.LATENCY + || pathValidationData.getPathComputationStrategy() == MAX_LATENCY); + } + + private boolean isLatencyValidationRequired(PathValidationData pathValidationData) { + return pathValidationData.getLatency() != null + && !pathValidationData.getLatency().isZero() + && (pathValidationData.getPathComputationStrategy() == null + || pathValidationData.getPathComputationStrategy() == PathComputationStrategy.LATENCY + || pathValidationData.getPathComputationStrategy() == MAX_LATENCY); + } + + private Set validateForwardAndReverseLinks(InputData inputData, RepositoryData repositoryData) { + Map switchMap = repositoryData.getSwitchRepository().findByIds( + Sets.newHashSet(inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getDestSwitchId())); + Set errors = Sets.newHashSet(); + if (!switchMap.containsKey(inputData.getSegment().getSrcSwitchId())) { + errors.add(getSrcSwitchNotFoundError(inputData)); + } + if (!switchMap.containsKey(inputData.getSegment().getDestSwitchId())) { + errors.add(getDestSwitchNotFoundError(inputData)); + } + if (!errors.isEmpty()) { + return errors; + } + + errors.addAll(validateForwardLink(inputData, repositoryData)); + errors.addAll(validateReverseLink(inputData, repositoryData)); + + return errors; + } + + private Set validateForwardLink(InputData inputData, RepositoryData repositoryData) { + return validateLink( inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort(), inputData.getSegment().getDestSwitchId(), - inputData.getSegment().getDestPort()); - if (!forward.isPresent()) { - return Collections.singleton(getNoForwardIslError(inputData)); - } - - if (forward.get().getStatus() != IslStatus.ACTIVE) { - return Collections.singleton(getForwardIslNotActiveError(inputData)); - } + inputData.getSegment().getDestPort(), + repositoryData, inputData, + this::getNoForwardIslError, this::getForwardIslNotActiveError); + } - Optional reverse = repositoryData.getIslRepository().findByEndpoints( + private Set validateReverseLink(InputData inputData, RepositoryData repositoryData) { + return validateLink( inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort(), inputData.getSegment().getSrcSwitchId(), - inputData.getSegment().getSrcPort()); - if (!reverse.isPresent()) { - return Collections.singleton(getNoReverseIslError(inputData)); - } + inputData.getSegment().getSrcPort(), + repositoryData, inputData, + this::getNoReverseIslError, this::getReverseIslNotActiveError); + } - if (reverse.get().getStatus() != IslStatus.ACTIVE) { - return Collections.singleton(getReverseIslNotActiveError(inputData)); + private Set validateLink(SwitchId srcSwitchId, int srcPort, SwitchId destSwitchId, int destPort, + RepositoryData repositoryData, InputData inputData, + Function noLinkErrorProducer, + Function linkNotActiveErrorProducer) { + Optional isl = getIslByEndPoints(srcSwitchId, srcPort, destSwitchId, destPort, repositoryData); + Set errors = Sets.newHashSet(); + if (!isl.isPresent()) { + errors.add(noLinkErrorProducer.apply(inputData)); + } + if (isl.isPresent() && isl.get().getStatus() != IslStatus.ACTIVE) { + errors.add(linkNotActiveErrorProducer.apply(inputData)); } - return Collections.emptySet(); + return errors; + } + + private Optional getIslByEndPoints(SwitchId srcSwitchId, int srcPort, SwitchId destSwitchId, int destPort, + RepositoryData repositoryData) { + return repositoryData.getIslRepository().findByEndpoints(srcSwitchId, srcPort, destSwitchId, destPort); + } + + private Optional getForwardIslOfSegmentData(PathSegmentValidationData data, RepositoryData repositoryData) { + return repositoryData.getIslRepository().findByEndpoints( + data.getSrcSwitchId(), data.getSrcPort(), + data.getDestSwitchId(), data.getDestPort()); + } + + private Optional getReverseIslOfSegmentData(PathSegmentValidationData data, RepositoryData repositoryData) { + return repositoryData.getIslRepository().findByEndpoints( + data.getDestSwitchId(), data.getDestPort(), + data.getSrcSwitchId(), data.getSrcPort()); } private Set validateBandwidth(InputData inputData, RepositoryData repositoryData) { - Optional forward = repositoryData.getIslRepository().findByEndpoints( - inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort(), - inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort()); + Optional forward = getForwardIslOfSegmentData(inputData.getSegment(), repositoryData); + Optional reverse = getReverseIslOfSegmentData(inputData.getSegment(), repositoryData); + + Set errors = Sets.newHashSet(); if (!forward.isPresent()) { - return Collections.singleton(getNoForwardIslError(inputData)); + errors.add(getNoForwardIslError(inputData)); + } + if (!reverse.isPresent()) { + errors.add(getNoForwardIslError(inputData)); + } + if (!errors.isEmpty()) { + return errors; } - Set errors = new HashSet<>(); if (getForwardBandwidthWithReusableResources(forward.get(), inputData) < inputData.getPath().getBandwidth()) { errors.add(getForwardBandwidthErrorMessage(inputData, forward.get().getAvailableBandwidth())); } - - Optional reverse = repositoryData.getIslRepository().findByEndpoints( - inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort(), - inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort()); - if (!reverse.isPresent()) { - return Collections.singleton(getNoForwardIslError(inputData)); - } if (getReverseBandwidthWithReusableResources(reverse.get(), inputData) < inputData.getPath().getBandwidth()) { errors.add(getReverseBandwidthErrorMessage(inputData, reverse.get().getAvailableBandwidth())); } @@ -188,20 +266,28 @@ private Set validateBandwidth(InputData inputData, RepositoryData reposi return errors; } + private boolean isSameForwardPathSegment(PathSegmentValidationData segmentData, PathSegment pathSegment) { + return segmentData.getSrcSwitchId().equals(pathSegment.getSrcSwitchId()) + && segmentData.getDestSwitchId().equals(pathSegment.getDestSwitchId()) + && segmentData.getSrcPort().equals(pathSegment.getSrcPort()) + && segmentData.getDestPort().equals(pathSegment.getDestPort()); + } + + private boolean isSameReversePathSegment(PathSegmentValidationData segmentData, PathSegment pathSegment) { + return segmentData.getSrcSwitchId().equals(pathSegment.getDestSwitchId()) + && segmentData.getDestSwitchId().equals(pathSegment.getSrcSwitchId()) + && segmentData.getSrcPort().equals(pathSegment.getDestPort()) + && segmentData.getDestPort().equals(pathSegment.getSrcPort()); + } + private long getForwardBandwidthWithReusableResources(Isl isl, InputData inputData) { return getBandwidthWithReusableResources(inputData, isl, - pathSegment -> inputData.getSegment().getSrcSwitchId().equals(pathSegment.getSrcSwitchId()) - && inputData.getSegment().getDestSwitchId().equals(pathSegment.getDestSwitchId()) - && inputData.getSegment().getSrcPort().equals(pathSegment.getSrcPort()) - && inputData.getSegment().getDestPort().equals(pathSegment.getDestPort())); + pathSegment -> isSameForwardPathSegment(inputData.getSegment(), pathSegment)); } private long getReverseBandwidthWithReusableResources(Isl isl, InputData inputData) { return getBandwidthWithReusableResources(inputData, isl, - pathSegment -> inputData.getSegment().getSrcSwitchId().equals(pathSegment.getDestSwitchId()) - && inputData.getSegment().getDestSwitchId().equals(pathSegment.getSrcSwitchId()) - && inputData.getSegment().getSrcPort().equals(pathSegment.getDestPort()) - && inputData.getSegment().getDestPort().equals(pathSegment.getSrcPort())); + pathSegment -> isSameReversePathSegment(inputData.getSegment(), pathSegment)); } private long getBandwidthWithReusableResources(InputData inputData, Isl isl, @@ -225,32 +311,28 @@ private long getBandwidthWithReusableResources(InputData inputData, Isl isl, } private Set validateEncapsulationType(InputData inputData, RepositoryData repositoryData) { - Set errors = new HashSet<>(); - Optional srcSwitchProperties = switchPropertiesRepository.findBySwitchId( - inputData.getPath().getSrcSwitchId()); - if (!srcSwitchProperties.isPresent()) { - errors.add(getSrcSwitchNotFoundError(inputData)); - } + Set errors = Sets.newHashSet(); + Map switchPropertiesMap = switchPropertiesRepository.findBySwitchIds( + Sets.newHashSet(inputData.getPath().getSrcSwitchId(), inputData.getPath().getDestSwitchId())); - srcSwitchProperties.ifPresent(switchProperties -> { - if (!switchProperties.getSupportedTransitEncapsulation() + if (!switchPropertiesMap.containsKey(inputData.getPath().getSrcSwitchId())) { + errors.add(getSrcSwitchNotFoundError(inputData)); + } else { + if (!switchPropertiesMap.get(inputData.getPath().getSrcSwitchId()).getSupportedTransitEncapsulation() .contains(inputData.getPath().getFlowEncapsulationType())) { errors.add(getSrcSwitchDoesNotSupportEncapsulationTypeError(inputData)); } - }); - - Optional destSwitchProperties = switchPropertiesRepository.findBySwitchId( - inputData.getPath().getDestSwitchId()); - if (!destSwitchProperties.isPresent()) { - errors.add(getDestSwitchNotFoundError(inputData)); } - destSwitchProperties.ifPresent(switchProperties -> { - if (!switchProperties.getSupportedTransitEncapsulation() + if (!switchPropertiesMap.containsKey(inputData.getPath().getDestSwitchId())) { + errors.add(getDestSwitchNotFoundError(inputData)); + } else { + if (!switchPropertiesMap.get(inputData.getPath().getDestSwitchId()).getSupportedTransitEncapsulation() .contains(inputData.getPath().getFlowEncapsulationType())) { errors.add(getDestSwitchDoesNotSupportEncapsulationTypeError(inputData)); } - }); + } + return errors; } @@ -280,57 +362,66 @@ private Set validateDiverseWithFlow(InputData inputData, RepositoryData return Collections.emptySet(); } - private Set validateLatency(InputData inputData, RepositoryData repositoryData) { - Optional isl = repositoryData.getIslRepository().findByEndpoints( - inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort(), - inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort()); - if (!isl.isPresent()) { - return Collections.singleton(getNoForwardIslError(inputData)); - } - // TODO make sure that comparing numbers of the same order - if (isl.get().getLatency() > inputData.getPath().getLatencyMs()) { - return Collections.singleton(getLatencyErrorMessage(inputData, isl.get().getLatency())); - } + private Set validateLatencyTier2(InputData inputData, RepositoryData repositoryData) { + return validateLatency(inputData, repositoryData, inputData.getPath()::getLatencyTier2, LATENCY_TIER_2); + } - return Collections.emptySet(); + private Set validateLatency(InputData inputData, RepositoryData repositoryData) { + return validateLatency(inputData, repositoryData, inputData.getPath()::getLatency, LATENCY); } - private Set validateLatencyTier2(InputData inputData, RepositoryData repositoryData) { - Optional isl = repositoryData.getIslRepository().findByEndpoints( - inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort(), - inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort()); - if (!isl.isPresent()) { - return Collections.singleton(getNoForwardIslError(inputData)); + private Set validateLatency(InputData inputData, RepositoryData repositoryData, + Supplier inputLatency, String latencyType) { + Optional forward = getForwardIslOfSegmentData(inputData.getSegment(), repositoryData); + Optional reverse = getReverseIslOfSegmentData(inputData.getSegment(), repositoryData); + + Set errors = Sets.newHashSet(); + if (!forward.isPresent()) { + errors.add(getNoForwardIslError(inputData)); + } + if (!reverse.isPresent()) { + errors.add(getNoReverseIslError(inputData)); } - // TODO make sure that comparing numbers of the same orders - if (isl.get().getLatency() > inputData.getPath().getLatencyTier2ms()) { - return Collections.singleton(getLatencyTier2ErrorMessage(inputData, isl.get().getLatency())); + if (!errors.isEmpty()) { + return errors; } - return Collections.emptySet(); + Duration actualLatency = Duration.ofNanos(forward.get().getLatency()); + if (actualLatency.compareTo(inputLatency.get()) > 0) { + errors.add(getForwardLatencyErrorMessage(inputData, inputLatency.get(), latencyType, actualLatency)); + } + Duration actualReverseLatency = Duration.ofNanos(reverse.get().getLatency()); + if (actualReverseLatency.compareTo(inputLatency.get()) > 0) { + errors.add(getReverseLatencyErrorMessage(inputData, inputLatency.get(), latencyType, actualReverseLatency)); + } + return errors; } - private String getLatencyTier2ErrorMessage(InputData data, long actualLatency) { + private String getForwardLatencyErrorMessage(InputData data, Duration expectedLatency, String latencyType, + Duration actualLatency) { return String.format( - "Requested latency tier 2 is too low between source switch %s port %d and destination switch" - + " %s port %d. Requested %d, but the link supports %d", + "Requested %s is too low between end points: switch %s port %d and switch" + + " %s port %d. Requested %d ms, but the link supports %d ms", + latencyType, data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), - data.getPath().getLatencyTier2ms(), actualLatency); + expectedLatency.toMillis(), actualLatency.toMillis()); } - private String getLatencyErrorMessage(InputData data, long actualLatency) { + private String getReverseLatencyErrorMessage(InputData data, Duration expectedLatency, String latencyType, + Duration actualLatency) { return String.format( - "Requested latency is too low between source switch %s port %d and destination switch %s port %d." - + " Requested %d, but the link supports %d", - data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + "Requested %s is too low between end points: switch %s port %d and switch" + + " %s port %d. Requested %d ms, but the link supports %d ms", + latencyType, data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), - data.getPath().getLatencyMs(), actualLatency); + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + expectedLatency.toMillis(), actualLatency.toMillis()); } private String getForwardBandwidthErrorMessage(InputData data, long actualBandwidth) { return String.format( - "There is not enough bandwidth between the source switch %s port %d and destination switch %s port %d" + "There is not enough bandwidth between end points: switch %s port %d and switch %s port %d" + " (forward path). Requested bandwidth %d, but the link supports %d", data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), @@ -339,7 +430,7 @@ private String getForwardBandwidthErrorMessage(InputData data, long actualBandwi private String getReverseBandwidthErrorMessage(InputData data, long actualBandwidth) { return String.format( - "There is not enough bandwidth between the source switch %s port %d and destination switch %s port %d" + "There is not enough bandwidth between end points: switch %s port %d and switch %s port %d" + " (reverse path). Requested bandwidth %d, but the link supports %d", data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), @@ -348,28 +439,28 @@ private String getReverseBandwidthErrorMessage(InputData data, long actualBandwi private String getNoForwardIslError(InputData data) { return String.format( - "There is no ISL between source switch %s port %d and destination switch %s port %d", + "There is no ISL between end points: switch %s port %d and switch %s port %d", data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); } private String getNoReverseIslError(InputData data) { return String.format( - "There is no ISL between source switch %s port %d and destination switch %s port %d", + "There is no ISL between end points: switch %s port %d and switch %s port %d", data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort()); } private String getForwardIslNotActiveError(InputData data) { return String.format( - "The ISL is not in ACTIVE state between source switch %s port %d and destination switch %s port %d", + "The ISL is not in ACTIVE state between end points: switch %s port %d and switch %s port %d", data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); } private String getReverseIslNotActiveError(InputData data) { return String.format( - "The ISL is not in ACTIVE state between source switch %s port %d and destination switch %s port %d", + "The ISL is not in ACTIVE state between end points: switch %s port %d and switch %s port %d", data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort()); } @@ -395,12 +486,12 @@ private String getDestSwitchNotFoundError(InputData data) { } private String getSrcSwitchDoesNotSupportEncapsulationTypeError(InputData data) { - return String.format("The switch %s doesn't support encapsulation type %s", + return String.format("The switch %s doesn't support the encapsulation type %s", data.getSegment().getSrcSwitchId(), data.getPath().getFlowEncapsulationType()); } private String getDestSwitchDoesNotSupportEncapsulationTypeError(InputData data) { - return String.format("The switch %s doesn't support encapsulation type %s", + return String.format("The switch %s doesn't support the encapsulation type %s", data.getSegment().getDestSwitchId(), data.getPath().getFlowEncapsulationType()); } @@ -417,5 +508,6 @@ private static class RepositoryData { IslRepository islRepository; FlowRepository flowRepository; SwitchPropertiesRepository switchPropertiesRepository; + SwitchRepository switchRepository; } } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java index 5a2ebdb35cd..aa71cabbc7c 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java @@ -16,43 +16,30 @@ package org.openkilda.wfm.topology.nbworker.services; import static junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.openkilda.model.FlowEncapsulationType.TRANSIT_VLAN; import static org.openkilda.model.FlowEncapsulationType.VXLAN; import static org.openkilda.model.PathComputationStrategy.COST; -import static org.openkilda.model.PathComputationStrategy.COST_AND_AVAILABLE_BANDWIDTH; import static org.openkilda.model.PathComputationStrategy.LATENCY; import static org.openkilda.model.PathComputationStrategy.MAX_LATENCY; import org.openkilda.config.provider.PropertiesBasedConfigurationProvider; -import org.openkilda.messaging.command.flow.PathValidateRequest; -import org.openkilda.messaging.info.network.PathValidationResult; import org.openkilda.messaging.info.network.PathsInfoData; -import org.openkilda.messaging.payload.flow.PathNodePayload; -import org.openkilda.messaging.payload.network.PathValidationDto; -import org.openkilda.model.Flow; import org.openkilda.model.FlowEncapsulationType; -import org.openkilda.model.FlowPath; -import org.openkilda.model.FlowPathDirection; import org.openkilda.model.Isl; import org.openkilda.model.IslStatus; import org.openkilda.model.KildaConfiguration; import org.openkilda.model.PathComputationStrategy; -import org.openkilda.model.PathId; -import org.openkilda.model.PathSegment; import org.openkilda.model.Switch; import org.openkilda.model.SwitchId; import org.openkilda.model.SwitchProperties; import org.openkilda.model.SwitchStatus; -import org.openkilda.model.cookie.FlowSegmentCookie; import org.openkilda.pce.PathComputerConfig; import org.openkilda.pce.exception.RecoverableException; import org.openkilda.pce.exception.UnroutableFlowException; import org.openkilda.persistence.inmemory.InMemoryGraphBasedTest; -import org.openkilda.persistence.repositories.FlowRepository; import org.openkilda.persistence.repositories.IslRepository; import org.openkilda.persistence.repositories.KildaConfigurationRepository; import org.openkilda.persistence.repositories.RepositoryFactory; @@ -66,8 +53,6 @@ import org.junit.Test; import java.time.Duration; -import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -79,7 +64,6 @@ public class PathsServiceTest extends InMemoryGraphBasedTest { private static final SwitchId SWITCH_ID_1 = new SwitchId(1); private static final SwitchId SWITCH_ID_2 = new SwitchId(2); private static final SwitchId SWITCH_ID_3 = new SwitchId(3); - private static final SwitchId SWITCH_ID_4 = new SwitchId(4); public static final long BASE_LATENCY = 10000; public static final long MIN_LATENCY = BASE_LATENCY - SWITCH_COUNT; @@ -87,7 +71,6 @@ public class PathsServiceTest extends InMemoryGraphBasedTest { private static SwitchRepository switchRepository; private static SwitchPropertiesRepository switchPropertiesRepository; private static IslRepository islRepository; - private static FlowRepository flowRepository; private static PathsService pathsService; @@ -98,7 +81,6 @@ public static void setUpOnce() { switchRepository = repositoryFactory.createSwitchRepository(); switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); islRepository = repositoryFactory.createIslRepository(); - flowRepository = repositoryFactory.createFlowRepository(); kildaConfigurationRepository = repositoryFactory.createKildaConfigurationRepository(); PathComputerConfig pathComputerConfig = new PropertiesBasedConfigurationProvider() .getConfiguration(PathComputerConfig.class); @@ -309,285 +291,6 @@ public void findNPathsByDefaultEncapsulationAndCost() assertVxlanAndCostPathes(paths); } - @Test - public void whenValidPath_validatePathReturnsValidResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(0L) - .latencyTier2ms(0L) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertTrue(responses.get(0).getIsValid()); - } - - @Test - public void whenValidPathButNotEnoughBandwidth_validateReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(1000000000L) - .latencyMs(0L) - .latencyTier2ms(0L) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertEquals("There must be 2 errors: forward and reverse paths", - 2, responses.get(0).getErrors().size()); - Collections.sort(responses.get(0).getErrors()); - assertEquals(responses.get(0).getErrors().get(0), - "There is not enough bandwidth between the source switch 00:00:00:00:00:00:00:01 port 6 and " - + "destination switch 00:00:00:00:00:00:00:03 port 6 (forward path). " - + "Requested bandwidth 1000000000, but the link supports 1003"); - assertEquals(responses.get(0).getErrors().get(1), - "There is not enough bandwidth between the source switch 00:00:00:00:00:00:00:03 port 6 and " - + "destination switch 00:00:00:00:00:00:00:01 port 6 (reverse path). " - + "Requested bandwidth 1000000000, but the link supports 1003"); - } - - @Test - public void whenValidPathButTooLowLatency_andLatencyStrategy_validateReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(10L) - .latencyTier2ms(0L) - .pathComputationStrategy(LATENCY) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertTrue(responses.get(0).getErrors().get(0) - .contains("Requested latency is too low between source switch")); - } - - @Test - public void whenValidPathButTooLowLatency_andMaxLatencyStrategy_validateReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(10L) - .latencyTier2ms(0L) - .pathComputationStrategy(MAX_LATENCY) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertTrue(responses.get(0).getErrors().get(0) - .contains("Requested latency is too low between source switch")); - } - - @Test - public void whenValidPathButTooLowLatencyTier2_andLatencyStrategy_validateReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(10000000000L) - .latencyTier2ms(100L) - .pathComputationStrategy(LATENCY) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertTrue(responses.get(0).getErrors().get(0) - .contains("Requested latency tier 2 is too low")); - } - - @Test - public void whenSwitchDoesNotSupportEncapsulationType_validateReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - // TODO investigate: when uncommenting the following line, switch 3 supports VXLAN. Why? - //nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .flowEncapsulationType(org.openkilda.messaging.payload.flow.FlowEncapsulationType.VXLAN) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertTrue(responses.get(0).getErrors().get(0) - .contains("doesn't support encapsulation type VXLAN")); - } - - @Test - public void whenNoLinkBetweenSwitches_validatePathReturnsErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 1)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 0, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(0L) - .latencyTier2ms(0L) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertTrue(responses.get(0).getErrors().get(0) - .startsWith("There is no ISL between source switch")); - } - - @Test - public void whenDiverseWith_andExistsIntersection_validateReturnsError() { - Switch switch1 = Switch.builder().switchId(SWITCH_ID_1).build(); - Switch switch2 = Switch.builder().switchId(SWITCH_ID_3).build(); - createFlow("flow_1", switch1, 6, switch2, 6); - - assertTrue(flowRepository.findById("flow_1").isPresent()); - assertFalse(flowRepository.findById("flow_1").get().getData().getPaths().isEmpty()); - - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(0L) - .latencyTier2ms(0L) - .diverseWithFlow("flow_1") - .build()); - List responses = pathsService.validatePath(request); - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertEquals(1, responses.get(0).getErrors().size()); - assertTrue(responses.get(0).getErrors().get(0).startsWith("The following segment intersects with the flow")); - } - - @Test - public void whenMultipleProblemsOnPath_validatePathReturnsAllErrorResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_4, 7, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(1000000L) - .latencyMs(10L) - .latencyTier2ms(0L) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertFalse(responses.get(0).getIsValid()); - assertFalse(responses.get(0).getErrors().isEmpty()); - assertEquals("There must be 5 errors in total: 2 bandwidth (forward and reverse paths), " - + "2 links are not present, and 1 latency", 5, responses.get(0).getErrors().size()); - } - - @Test - public void whenNonLatencyPathComputationStrategy_ignoreLatencyAnd_validatePathReturnsSuccessResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(0L) - .latencyMs(10L) - .latencyTier2ms(0L) - .pathComputationStrategy(COST) - .build()); - List responses = pathsService.validatePath(request); - - assertFalse(responses.isEmpty()); - assertTrue(responses.get(0).getIsValid()); - } - - @Test - public void whenValidPathWithExistingFlowAndReuseResources_validatePathReturnsSuccessResponseTest() { - List nodes = new LinkedList<>(); - nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); - nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); - nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); - PathValidateRequest request = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(1000L) - .latencyMs(0L) - .latencyTier2ms(0L) - .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) - .build()); - List responsesBefore = pathsService.validatePath(request); - - assertFalse(responsesBefore.isEmpty()); - assertTrue("The path using default segments with bandwidth 1003 must be valid", - responsesBefore.get(0).getIsValid()); - - Optional islForward = islRepository.findByEndpoints(SWITCH_ID_3, 7, SWITCH_ID_2, 7); - assertTrue(islForward.isPresent()); - islForward.get().setAvailableBandwidth(100L); - Optional islReverse = islRepository.findByEndpoints(SWITCH_ID_2, 7, SWITCH_ID_3, 7); - assertTrue(islReverse.isPresent()); - islReverse.get().setAvailableBandwidth(100L); - - String flowToReuse = "flow_3_2"; - createFlow(flowToReuse, Switch.builder().switchId(SWITCH_ID_3).build(), 2000, - Switch.builder().switchId(SWITCH_ID_2).build(), 2000, - false, 900L, islForward.get()); - - List responsesAfter = pathsService.validatePath(request); - - assertFalse(responsesAfter.isEmpty()); - assertFalse("The path must not be valid because the flow %s consumes bandwidth", - responsesAfter.get(0).getIsValid()); - assertFalse(responsesAfter.get(0).getErrors().isEmpty()); - assertEquals("There must be 2 errors in total: not enough bandwidth on forward and reverse paths", - 2, responsesAfter.get(0).getErrors().size()); - assertTrue(responsesAfter.get(0).getErrors().get(0).contains("There is not enough bandwidth")); - assertTrue(responsesAfter.get(0).getErrors().get(1).contains("There is not enough bandwidth")); - - PathValidateRequest requestWithReuseResources = new PathValidateRequest(PathValidationDto.builder() - .nodes(nodes) - .bandwidth(1000L) - .latencyMs(0L) - .latencyTier2ms(0L) - .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) - .reuseFlowResources(flowToReuse) - .build()); - - List responseWithReuseResources = pathsService.validatePath(requestWithReuseResources); - - assertFalse(responseWithReuseResources.isEmpty()); - assertTrue("The path must be valid because, although the flow %s consumes bandwidth, the validator" - + " includes the consumed bandwidth to available bandwidth", - responseWithReuseResources.get(0).getIsValid()); - } - private void assertMaxLatencyPaths(List paths, Duration maxLatency, long expectedCount, FlowEncapsulationType encapsulationType) { assertEquals(expectedCount, paths.size()); @@ -673,55 +376,4 @@ private void createSwitchProperties(Switch sw, FlowEncapsulationType... encapsul .build()); } - private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort) { - createFlow(flowId, srcSwitch, srcPort, destSwitch, destPort, null, null, null); - } - - private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort, - Boolean ignoreBandwidth, Long bandwidth, Isl isl) { - Flow flow = Flow.builder() - .flowId(flowId) - .srcSwitch(srcSwitch) - .srcPort(srcPort) - .destSwitch(destSwitch) - .destPort(destPort) - .build(); - Optional.ofNullable(ignoreBandwidth).ifPresent(flow::setIgnoreBandwidth); - Optional.ofNullable(bandwidth).ifPresent(flow::setBandwidth); - FlowPath forwardPath = FlowPath.builder() - .pathId(new PathId("path_1")) - .srcSwitch(srcSwitch) - .destSwitch(destSwitch) - .cookie(new FlowSegmentCookie(FlowPathDirection.FORWARD, 1L)) - .bandwidth(flow.getBandwidth()) - .ignoreBandwidth(false) - .segments(Collections.singletonList(PathSegment.builder() - .pathId(new PathId("forward_segment")) - .srcSwitch(srcSwitch) - .srcPort(isl == null ? srcPort : isl.getSrcPort()) - .destSwitch(destSwitch) - .destPort(isl == null ? destPort : isl.getDestPort()) - .build())) - .build(); - - flow.setForwardPath(forwardPath); - FlowPath reversePath = FlowPath.builder() - .pathId(new PathId("path_2")) - .srcSwitch(destSwitch) - .destSwitch(srcSwitch) - .cookie(new FlowSegmentCookie(FlowPathDirection.REVERSE, 1L)) - .bandwidth(flow.getBandwidth()) - .ignoreBandwidth(false) - .segments(Collections.singletonList(PathSegment.builder() - .pathId(new PathId("reverse_segment")) - .srcSwitch(destSwitch) - .srcPort(isl == null ? destPort : isl.getDestPort()) - .destSwitch(srcSwitch) - .destPort(isl == null ? srcPort : isl.getSrcPort()) - .build())) - .build(); - flow.setReversePath(reversePath); - - flowRepository.add(flow); - } } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java new file mode 100644 index 00000000000..14d1434cb3f --- /dev/null +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java @@ -0,0 +1,683 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.wfm.topology.nbworker.validators; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; +import static org.openkilda.model.FlowEncapsulationType.TRANSIT_VLAN; +import static org.openkilda.model.PathComputationStrategy.COST; +import static org.openkilda.model.PathComputationStrategy.COST_AND_AVAILABLE_BANDWIDTH; +import static org.openkilda.model.PathComputationStrategy.LATENCY; +import static org.openkilda.model.PathComputationStrategy.MAX_LATENCY; + +import org.openkilda.config.provider.PropertiesBasedConfigurationProvider; +import org.openkilda.messaging.command.flow.PathValidateRequest; +import org.openkilda.messaging.info.network.PathValidationResult; +import org.openkilda.messaging.payload.flow.PathNodePayload; +import org.openkilda.messaging.payload.network.PathValidationPayload; +import org.openkilda.model.Flow; +import org.openkilda.model.FlowPath; +import org.openkilda.model.FlowPathDirection; +import org.openkilda.model.Isl; +import org.openkilda.model.IslStatus; +import org.openkilda.model.PathId; +import org.openkilda.model.PathSegment; +import org.openkilda.model.Switch; +import org.openkilda.model.SwitchId; +import org.openkilda.model.SwitchProperties; +import org.openkilda.model.SwitchStatus; +import org.openkilda.model.cookie.FlowSegmentCookie; +import org.openkilda.pce.PathComputerConfig; +import org.openkilda.persistence.inmemory.InMemoryGraphBasedTest; +import org.openkilda.persistence.repositories.FlowRepository; +import org.openkilda.persistence.repositories.IslRepository; +import org.openkilda.persistence.repositories.RepositoryFactory; +import org.openkilda.persistence.repositories.SwitchPropertiesRepository; +import org.openkilda.persistence.repositories.SwitchRepository; +import org.openkilda.wfm.topology.nbworker.services.PathsService; + +import com.google.common.collect.Sets; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class PathValidatorTest extends InMemoryGraphBasedTest { + private static final SwitchId SWITCH_ID_0 = new SwitchId(0); + private static final SwitchId SWITCH_ID_1 = new SwitchId(1); + private static final SwitchId SWITCH_ID_2 = new SwitchId(2); + private static final SwitchId SWITCH_ID_3 = new SwitchId(3); + private static SwitchRepository switchRepository; + private static SwitchPropertiesRepository switchPropertiesRepository; + private static IslRepository islRepository; + private static FlowRepository flowRepository; + private static PathsService pathsService; + + private boolean isSetupDone; + + @BeforeClass + public static void setUpOnce() { + RepositoryFactory repositoryFactory = persistenceManager.getRepositoryFactory(); + switchRepository = repositoryFactory.createSwitchRepository(); + switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); + islRepository = repositoryFactory.createIslRepository(); + flowRepository = repositoryFactory.createFlowRepository(); + PathComputerConfig pathComputerConfig = new PropertiesBasedConfigurationProvider() + .getConfiguration(PathComputerConfig.class); + pathsService = new PathsService(repositoryFactory, pathComputerConfig, islRepository, + repositoryFactory.createFlowRepository()); + } + + + @Before + public void createTestTopology() { + if (!isSetupDone) { + Switch switch0 = Switch.builder().switchId(SWITCH_ID_0).status(SwitchStatus.ACTIVE).build(); + Switch switchA = Switch.builder().switchId(SWITCH_ID_1).status(SwitchStatus.ACTIVE).build(); + Switch switchB = Switch.builder().switchId(SWITCH_ID_2).status(SwitchStatus.ACTIVE).build(); + Switch switchTransit = Switch.builder().switchId(SWITCH_ID_3).status(SwitchStatus.ACTIVE).build(); + + switchRepository.add(switch0); + switchRepository.add(switchA); + switchRepository.add(switchB); + switchRepository.add(switchTransit); + + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switch0) + .supportedTransitEncapsulation(Sets.newHashSet(TRANSIT_VLAN)) + .build()); + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switchA) + .supportedTransitEncapsulation(Sets.newHashSet(TRANSIT_VLAN)) + .build()); + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switchB) + .supportedTransitEncapsulation(Sets.newHashSet(TRANSIT_VLAN)) + .build()); + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switchTransit) + .supportedTransitEncapsulation(Sets.newHashSet(TRANSIT_VLAN)) + .build()); + + createOneWayIsl(switchA, 6, switchTransit, 6, 10, 2_000_000, 10_000, IslStatus.ACTIVE); + createOneWayIsl(switchTransit, 6, switchA, 6, 10, 2_000_000, 10_000, IslStatus.ACTIVE); + + createOneWayIsl(switchB, 7, switchTransit, 7, 15, 3_000_000, 20_000, IslStatus.ACTIVE); + createOneWayIsl(switchTransit, 7, switchB, 7, 15, 3_000_000, 20_000, IslStatus.ACTIVE); + + createOneWayIsl(switchA, 12, switch0, 12, 10, 2_000_000, 10_000, IslStatus.INACTIVE); + createOneWayIsl(switch0, 12, switchA, 12, 10, 2_000_000, 10_000, IslStatus.INACTIVE); + + isSetupDone = true; + } + } + + @Test + public void whenInactiveLink_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 12)); + nodes.add(new PathNodePayload(SWITCH_ID_0, 12, null)); + + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + Set expectedErrorMessages = Sets.newHashSet( + "The ISL is not in ACTIVE state between end points: switch 00:00:00:00:00:00:00:00 port 12 and switch" + + " 00:00:00:00:00:00:00:01 port 12", + "The ISL is not in ACTIVE state between end points: switch 00:00:00:00:00:00:00:01 port 12 and switch" + + " 00:00:00:00:00:00:00:00 port 12" + ); + + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenValidPath_validatePathReturnsValidResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertTrue(responses.get(0).getIsValid()); + } + + @Test + public void whenValidPathButNotEnoughBandwidth_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(1000000000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertEquals("There must be 2 errors: forward and reverse paths", + 2, responses.get(0).getErrors().size()); + Collections.sort(responses.get(0).getErrors()); + assertEquals(responses.get(0).getErrors().get(0), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:01 port 6 and " + + "switch 00:00:00:00:00:00:00:03 port 6 (forward path). Requested bandwidth 1000000000," + + " but the link supports 10000"); + assertEquals(responses.get(0).getErrors().get(1), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03 port 6 and " + + "switch 00:00:00:00:00:00:00:01 port 6 (reverse path). Requested bandwidth 1000000000," + + " but the link supports 10000"); + } + + @Test + public void whenNoSwitchOnPath_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(new SwitchId("01:01:01:01"), null, 6)); + nodes.add(new PathNodePayload(new SwitchId("01:01:01:02"), 6, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(1000000000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + Set expectedErrorMessages = Sets.newHashSet( + "The following switch has not been found: 00:00:00:00:01:01:01:01", + "The following switch has not been found: 00:00:00:00:01:01:01:02", + "There is no ISL between end points: switch 00:00:00:00:01:01:01:01 port 6" + + " and switch 00:00:00:00:01:01:01:02 port 6" + ); + + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenValidPathButTooLowLatency_andLatencyStrategy_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(1L) + .latencyTier2ms(0L) + .pathComputationStrategy(LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + + Set expectedErrorMessages = Sets.newHashSet(); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03 port" + + " 7 and switch 00:00:00:00:00:00:00:02 port 7. Requested 1 ms, but the link supports 3 ms"); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:02 port" + + " 7 and switch 00:00:00:00:00:00:00:03 port 7. Requested 1 ms, but the link supports 3 ms"); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:01 port" + + " 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03 port" + + " 6 and switch 00:00:00:00:00:00:00:01 port 6. Requested 1 ms, but the link supports 2 ms"); + + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenValidPathButTooLowLatency_andMaxLatencyStrategy_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(1L) + .latencyTier2ms(0L) + .pathComputationStrategy(MAX_LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + + Set expectedErrorMessages = Sets.newHashSet(); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03 port" + + " 7 and switch 00:00:00:00:00:00:00:02 port 7. Requested 1 ms, but the link supports 3 ms"); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:02 port" + + " 7 and switch 00:00:00:00:00:00:00:03 port 7. Requested 1 ms, but the link supports 3 ms"); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:01 port" + + " 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03 port" + + " 6 and switch 00:00:00:00:00:00:00:01 port 6. Requested 1 ms, but the link supports 2 ms"); + + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenValidPathButTooLowLatencyTier2_andLatencyStrategy_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10000000000L) + .latencyTier2ms(1L) + .pathComputationStrategy(LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + + Set expectedErrors = Sets.newHashSet(); + expectedErrors.add("Requested latency tier 2 is too low between end points: switch 00:00:00:00:00:00:00:03 port" + + " 7 and switch 00:00:00:00:00:00:00:02 port 7. Requested 1 ms, but the link supports 3 ms"); + expectedErrors.add("Requested latency tier 2 is too low between end points: switch 00:00:00:00:00:00:00:02 port" + + " 7 and switch 00:00:00:00:00:00:00:03 port 7. Requested 1 ms, but the link supports 3 ms"); + expectedErrors.add("Requested latency tier 2 is too low between end points: switch 00:00:00:00:00:00:00:01 port" + + " 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); + expectedErrors.add("Requested latency tier 2 is too low between end points: switch 00:00:00:00:00:00:00:03 port" + + " 6 and switch 00:00:00:00:00:00:00:01 port 6. Requested 1 ms, but the link supports 2 ms"); + + Set actualErrors = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrors, actualErrors); + } + + @Test + public void whenSwitchDoesNotSupportEncapsulationType_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .flowEncapsulationType(org.openkilda.messaging.payload.flow.FlowEncapsulationType.VXLAN) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertEquals(responses.get(0).getErrors().get(0), + "The switch 00:00:00:00:00:00:00:03 doesn't support the encapsulation type VXLAN"); + } + + @Test + public void whenNoLinkBetweenSwitches_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 1)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 0, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + Set expectedErrorMessages = Sets.newHashSet(); + expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:02 port 0 and switch" + + " 00:00:00:00:00:00:00:01 port 1"); + expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:01 port 1 and switch" + + " 00:00:00:00:00:00:00:02 port 0"); + + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + + } + + @Test + public void whenNoLinkBetweenSwitches_andValidateLatency_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 1)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 0, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(1L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + Set expectedErrorMessages = Sets.newHashSet(); + expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:02 port 0 and switch" + + " 00:00:00:00:00:00:00:01 port 1"); + expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:01 port 1 and switch" + + " 00:00:00:00:00:00:00:02 port 0"); + + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenDiverseWith_andNoLinkExists_validatePathReturnsError() { + Switch switch1 = Switch.builder().switchId(SWITCH_ID_1).build(); + Switch switch2 = Switch.builder().switchId(SWITCH_ID_3).build(); + createFlow("flow_1", switch1, 6, switch2, 6); + + assertTrue(flowRepository.findById("flow_1").isPresent()); + assertFalse(flowRepository.findById("flow_1").get().getData().getPaths().isEmpty()); + + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_0, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .diverseWithFlow("flow_1") + .build()); + List responses = pathsService.validatePath(request); + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + Set expectedErrorMessages = Sets.newHashSet(); + expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:03 port 7 and switch" + + " 00:00:00:00:00:00:00:00 port 7"); + expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:00 port 7 and switch" + + " 00:00:00:00:00:00:00:03 port 7"); + expectedErrorMessages.add( + "The following segment intersects with the flow flow_1: source switch 00:00:00:00:00:00:00:01" + + " port 6 and destination switch 00:00:00:00:00:00:00:03 port 6"); + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenDiverseWith_andExistsIntersection_validatePathReturnsError() { + Switch switch1 = Switch.builder().switchId(SWITCH_ID_1).build(); + Switch switch2 = Switch.builder().switchId(SWITCH_ID_3).build(); + createFlow("flow_1", switch1, 6, switch2, 6); + + assertTrue(flowRepository.findById("flow_1").isPresent()); + assertFalse(flowRepository.findById("flow_1").get().getData().getPaths().isEmpty()); + + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .diverseWithFlow("flow_1") + .build()); + List responses = pathsService.validatePath(request); + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertEquals(1, responses.get(0).getErrors().size()); + assertEquals(responses.get(0).getErrors().get(0), + "The following segment intersects with the flow flow_1: source switch 00:00:00:00:00:00:00:01" + + " port 6 and destination switch 00:00:00:00:00:00:00:03 port 6"); + } + + @Test + public void whenMultipleProblemsOnPath_validatePathReturnsAllErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(new SwitchId("FF"), 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(1000000L) + .latencyMs(1L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + + assertEquals("There must be 7 errors in total: 2 not enough bandwidth (forward and reverse paths), " + + "2 link is not present, 2 latency, and 1 switch is not found", + 7, responses.get(0).getErrors().size()); + Set expectedErrorMessages = Sets.newHashSet(); + expectedErrorMessages.add("The following switch has not been found: 00:00:00:00:00:00:00:ff"); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:01" + + " port 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); + expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03" + + " port 6 and switch 00:00:00:00:00:00:00:01 port 6. Requested 1 ms, but the link supports 2 ms"); + expectedErrorMessages.add("There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03" + + " port 6 and switch 00:00:00:00:00:00:00:01 port 6 (reverse path). Requested bandwidth 1000000," + + " but the link supports 10000"); + expectedErrorMessages.add("There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:01" + + " port 6 and switch 00:00:00:00:00:00:00:03 port 6 (forward path). Requested bandwidth 1000000," + + " but the link supports 10000"); + expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:03 port 7 and" + + " switch 00:00:00:00:00:00:00:ff port 7"); + expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:ff port 7 and" + + " switch 00:00:00:00:00:00:00:03 port 7"); + assertEquals(expectedErrorMessages, Sets.newHashSet(responses.get(0).getErrors())); + } + + @Test + public void whenValidPathAndDiverseFlowDoesNotExist_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST) + .diverseWithFlow("non_existing_flow_id") + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertEquals("Could not find the diverse flow with ID non_existing_flow_id", + responses.get(0).getErrors().get(0)); + } + + @Test + public void whenNonLatencyPathComputationStrategy_ignoreLatencyAnd_validatePathReturnsSuccessResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertTrue(responses.get(0).getIsValid()); + } + + @Test + public void whenValidPathWithExistingFlowAndReuseResources_validatePathReturnsSuccessResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(1000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) + .build()); + List responsesBefore = pathsService.validatePath(request); + + assertFalse(responsesBefore.isEmpty()); + assertTrue("The path using default segments with bandwidth 1003 must be valid", + responsesBefore.get(0).getIsValid()); + + Optional islForward = islRepository.findByEndpoints(SWITCH_ID_3, 7, SWITCH_ID_2, 7); + assertTrue(islForward.isPresent()); + islForward.get().setAvailableBandwidth(100L); + Optional islReverse = islRepository.findByEndpoints(SWITCH_ID_2, 7, SWITCH_ID_3, 7); + assertTrue(islReverse.isPresent()); + islReverse.get().setAvailableBandwidth(100L); + + String flowToReuse = "flow_3_2"; + createFlow(flowToReuse, Switch.builder().switchId(SWITCH_ID_3).build(), 2000, + Switch.builder().switchId(SWITCH_ID_2).build(), 2000, + false, 900L, islForward.get()); + + List responsesAfter = pathsService.validatePath(request); + + assertFalse(responsesAfter.isEmpty()); + assertFalse("The path must not be valid because the flow %s consumes bandwidth", + responsesAfter.get(0).getIsValid()); + assertFalse(responsesAfter.get(0).getErrors().isEmpty()); + assertEquals("There must be 2 errors in total: not enough bandwidth on forward and reverse paths", + 2, responsesAfter.get(0).getErrors().size()); + assertEquals(responsesAfter.get(0).getErrors().get(0), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:02 port 7 and" + + " switch 00:00:00:00:00:00:00:03 port 7 (reverse path). Requested bandwidth 1000, but the" + + " link supports 100"); + assertEquals(responsesAfter.get(0).getErrors().get(1), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03 port 7 and" + + " switch 00:00:00:00:00:00:00:02 port 7 (forward path). Requested bandwidth 1000, but the" + + " link supports 100"); + + PathValidateRequest requestWithReuseResources = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(1000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) + .reuseFlowResources(flowToReuse) + .build()); + + List responseWithReuseResources = pathsService.validatePath(requestWithReuseResources); + + assertFalse(responseWithReuseResources.isEmpty()); + assertTrue("The path must be valid because, although the flow %s consumes bandwidth, the validator" + + " includes the consumed bandwidth to available bandwidth", + responseWithReuseResources.get(0).getIsValid()); + } + + private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort) { + createFlow(flowId, srcSwitch, srcPort, destSwitch, destPort, null, null, null); + } + + private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort, + Boolean ignoreBandwidth, Long bandwidth, Isl isl) { + Flow flow = Flow.builder() + .flowId(flowId) + .srcSwitch(srcSwitch) + .srcPort(srcPort) + .destSwitch(destSwitch) + .destPort(destPort) + .build(); + Optional.ofNullable(ignoreBandwidth).ifPresent(flow::setIgnoreBandwidth); + Optional.ofNullable(bandwidth).ifPresent(flow::setBandwidth); + FlowPath forwardPath = FlowPath.builder() + .pathId(new PathId("path_1")) + .srcSwitch(srcSwitch) + .destSwitch(destSwitch) + .cookie(new FlowSegmentCookie(FlowPathDirection.FORWARD, 1L)) + .bandwidth(flow.getBandwidth()) + .ignoreBandwidth(false) + .segments(Collections.singletonList(PathSegment.builder() + .pathId(new PathId("forward_segment")) + .srcSwitch(srcSwitch) + .srcPort(isl == null ? srcPort : isl.getSrcPort()) + .destSwitch(destSwitch) + .destPort(isl == null ? destPort : isl.getDestPort()) + .build())) + .build(); + + flow.setForwardPath(forwardPath); + FlowPath reversePath = FlowPath.builder() + .pathId(new PathId("path_2")) + .srcSwitch(destSwitch) + .destSwitch(srcSwitch) + .cookie(new FlowSegmentCookie(FlowPathDirection.REVERSE, 1L)) + .bandwidth(flow.getBandwidth()) + .ignoreBandwidth(false) + .segments(Collections.singletonList(PathSegment.builder() + .pathId(new PathId("reverse_segment")) + .srcSwitch(destSwitch) + .srcPort(isl == null ? destPort : isl.getDestPort()) + .destSwitch(srcSwitch) + .destPort(isl == null ? srcPort : isl.getSrcPort()) + .build())) + .build(); + flow.setReversePath(reversePath); + + flowRepository.add(flow); + } + + private void createOneWayIsl(Switch srcSwitch, int srcPort, Switch dstSwitch, int dstPort, int cost, + long latency, int bandwidth, IslStatus islStatus) { + islRepository.add(Isl.builder() + .srcSwitch(srcSwitch) + .srcPort(srcPort) + .destSwitch(dstSwitch) + .destPort(dstPort) + .status(islStatus) + .actualStatus(islStatus) + .cost(cost) + .availableBandwidth(bandwidth) + .maxBandwidth(bandwidth) + .latency(latency) + .build()); + } +} diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java index 9aca291ea89..53e29ce5164 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java @@ -17,7 +17,7 @@ import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.error.MessageException; -import org.openkilda.messaging.payload.network.PathValidationDto; +import org.openkilda.messaging.payload.network.PathValidationPayload; import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; import org.openkilda.northbound.service.NetworkService; @@ -36,33 +36,32 @@ @RequestMapping("/v2/network") public class NetworkControllerV2 { + private final NetworkService networkService; + @Autowired - private NetworkService networkService; + public NetworkControllerV2(NetworkService networkService) { + this.networkService = networkService; + } /** * Validates that a given path complies with the chosen strategy and the network availability. - * It is required that the input contains path nodes. Other parameters are opti - * @param pathValidationDto a payload with a path and additional flow parameters provided by a user + * It is required that the input contains path nodes. Other parameters are optional. + * @param pathValidationPayload a payload with a path and additional flow parameters provided by a user * @return either a successful response or the list of errors */ @GetMapping(path = "/path/check") @ApiOperation(value = "Validates that a given path complies with the chosen strategy and the network availability") @ResponseStatus(HttpStatus.OK) public CompletableFuture validateCustomFlowPath( - @RequestBody PathValidationDto pathValidationDto) { - validateInput(pathValidationDto); - - return networkService.validateFlowPath(pathValidationDto); - } - - private void validateInput(PathValidationDto pathValidationDto) { - //TODO validate all fields + @RequestBody PathValidationPayload pathValidationPayload) { - if (pathValidationDto == null - || pathValidationDto.getNodes() == null - || pathValidationDto.getNodes().size() < 2) { + if (pathValidationPayload == null + || pathValidationPayload.getNodes() == null + || pathValidationPayload.getNodes().size() < 2) { throw new MessageException(ErrorType.DATA_INVALID, "Invalid Request Body", "Invalid 'nodes' value in the request body"); } + + return networkService.validateFlowPath(pathValidationPayload); } } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java index 09c8a9ebb2a..e2d41ed822a 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java @@ -15,7 +15,7 @@ package org.openkilda.northbound.service; -import org.openkilda.messaging.payload.network.PathValidationDto; +import org.openkilda.messaging.payload.network.PathValidationPayload; import org.openkilda.messaging.payload.network.PathsDto; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.PathComputationStrategy; @@ -40,10 +40,10 @@ CompletableFuture getPaths( /** * Validates that a flow with the given path can possibly be created. If it is not possible, - * it responds with the reasons, such as: not enough bandwidth, requested latency it too low, there is no + * it responds with the reasons, such as: not enough bandwidth, requested latency is too low, there is no * links between the selected switches, and so on. - * @param pathValidationDto a path together with validation parameters provided by a user + * @param pathValidationPayload a path together with validation parameters provided by a user * @return either a successful response or the list of errors */ - CompletableFuture validateFlowPath(PathValidationDto pathValidationDto); + CompletableFuture validateFlowPath(PathValidationPayload pathValidationPayload); } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java index 5652244ce92..3f5a5c5cbad 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java @@ -23,7 +23,7 @@ import org.openkilda.messaging.info.network.PathsInfoData; import org.openkilda.messaging.nbtopology.request.GetPathsRequest; import org.openkilda.messaging.payload.network.PathDto; -import org.openkilda.messaging.payload.network.PathValidationDto; +import org.openkilda.messaging.payload.network.PathValidationPayload; import org.openkilda.messaging.payload.network.PathsDto; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.PathComputationStrategy; @@ -99,14 +99,14 @@ public CompletableFuture getPaths( /** * Validates that a flow with the given path can possibly be created. If it is not possible, - * it responds with the reasons, such as: not enough bandwidth, requested latency it too low, there is no + * it responds with the reasons, such as: not enough bandwidth, requested latency is too low, there is no * links between the selected switches, and so on. - * @param pathValidationDto a path together with validation parameters provided by a user + * @param pathValidationPayload a path together with validation parameters provided by a user * @return either a successful response or a list of errors */ @Override - public CompletableFuture validateFlowPath(PathValidationDto pathValidationDto) { - PathValidateRequest request = new PathValidateRequest(pathValidationDto); + public CompletableFuture validateFlowPath(PathValidationPayload pathValidationPayload) { + PathValidateRequest request = new PathValidateRequest(pathValidationPayload); CommandMessage message = new CommandMessage(request, System.currentTimeMillis(), RequestCorrelationId.getId()); From dc16dd252c8e28fe285f66e2e72064c21bf69afa Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Tue, 7 Mar 2023 13:24:50 +0100 Subject: [PATCH 26/45] Remove redundant spaces and fix typos. --- docs/design/solutions/path-validation/path-validation.md | 2 +- .../openkilda/wfm/share/mappers/PathValidationDataMapper.java | 1 - .../openkilda/wfm/topology/nbworker/services/PathsService.java | 1 - .../wfm/topology/nbworker/services/PathsServiceTest.java | 2 -- 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/design/solutions/path-validation/path-validation.md b/docs/design/solutions/path-validation/path-validation.md index 228461f3485..53b40af1aa9 100644 --- a/docs/design/solutions/path-validation/path-validation.md +++ b/docs/design/solutions/path-validation/path-validation.md @@ -8,7 +8,7 @@ path computation strategy, and other. ## Implementation details The validation of a path is done for each segment and each validation type individually. This way, the validation -collects all errors on the path and returns them all in a single response. The response is concise amd formed +collects all errors on the path and returns them all in a single response. The response is concise and formed in human-readable format. There is no locking of resources for this path and, therefore, no guarantee that it will be possible to create this flow diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java index aef43fb50aa..43eb995c4e2 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java @@ -64,5 +64,4 @@ public PathValidationData toPathValidationData(PathValidationPayload pathValidat .pathSegments(segments) .build(); } - } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java index e4da63c734e..9ccfef9b084 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java @@ -149,5 +149,4 @@ public List validatePath(PathValidateRequest request) { PathValidationDataMapper.INSTANCE.toPathValidationData(request.getPathValidationPayload()) )); } - } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java index aa71cabbc7c..d1fbd312b4c 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java @@ -346,7 +346,6 @@ private void assertPathLength(List paths) { } } - private void createIsl(Switch srcSwitch, int srcPort, Switch dstSwitch, int dstPort, int cost, long latency, int bandwidth) { createOneWayIsl(srcSwitch, srcPort, dstSwitch, dstPort, cost, latency, bandwidth); @@ -375,5 +374,4 @@ private void createSwitchProperties(Switch sw, FlowEncapsulationType... encapsul .supportedTransitEncapsulation(Sets.newHashSet(encapsulation)) .build()); } - } From 2818012ce1ae52b9ea7542f22bdbc60fd61d1378 Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Thu, 2 Feb 2023 21:51:24 +0100 Subject: [PATCH 27/45] Fixed bug with empty vlan stats and set vlans Closes #5063 --- .../services/FlowOperationsService.java | 10 +- .../services/FlowOperationsServiceTest.java | 116 ++++++++++++++++-- 2 files changed, 113 insertions(+), 13 deletions(-) diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java index baed2cba6d7..c1cd1ed1cc9 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java @@ -572,8 +572,7 @@ private void validateFlow(FlowPatch flowPatch, Flow flow) throws InvalidFlowExce + "at the same time"); } - if ((flow.getVlanStatistics() != null && !flow.getVlanStatistics().isEmpty()) - || (flowPatch.getVlanStatistics() != null && !flowPatch.getVlanStatistics().isEmpty())) { + if (!isVlanStatisticsEmpty(flowPatch, flow)) { boolean zeroResultSrcVlan = isResultingVlanValueIsZero(flowPatch.getSource(), flow.getSrcVlan()); boolean zeroResultDstVlan = isResultingVlanValueIsZero(flowPatch.getDestination(), flow.getDestVlan()); @@ -733,6 +732,13 @@ private Collection getDiverseWithFlow(Flow flow) { .collect(Collectors.toSet()); } + private static boolean isVlanStatisticsEmpty(FlowPatch flowPatch, Flow flow) { + if (flowPatch.getVlanStatistics() != null) { + return flowPatch.getVlanStatistics().isEmpty(); + } + return flow.getVlanStatistics() == null || flow.getVlanStatistics().isEmpty(); + } + @Data @Builder static class UpdateFlowResult { diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsServiceTest.java index d8db615fc75..2eb4d40f34a 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsServiceTest.java @@ -80,8 +80,10 @@ public class FlowOperationsServiceTest extends InMemoryGraphBasedTest { public static final SwitchId SWITCH_ID_3 = new SwitchId(3); public static final SwitchId SWITCH_ID_4 = new SwitchId(4); public static final int VLAN_1 = 1; - public static final int PORT_1 = 1; - public static final int PORT_2 = 2; + public static final int PORT_1 = 2; + public static final int PORT_2 = 3; + public static final int VLAN_2 = 4; + public static final int VLAN_3 = 5; private static FlowOperationsService flowOperationsService; private static FlowRepository flowRepository; @@ -255,28 +257,111 @@ public void updateVlanStatisticsTest() throws FlowNotFoundException, InvalidFlow } @Test - public void updateVlanStatisticsToZeroDstVlanIsZeroTest() throws FlowNotFoundException, InvalidFlowException { - runUpdateVlanStatisticsToZero(VLAN_1, 0); + public void updateVlanStatisticsToZeroOldSrcAndDstVlanAreZeroTest() + throws FlowNotFoundException, InvalidFlowException { + runUpdateVlanStatisticsToZeroTest(0, 0, VLAN_1, VLAN_2); + } + + @Test + public void updateVlanStatisticsToZeroOldDstVlanAreNotZeroTest() + throws FlowNotFoundException, InvalidFlowException { + runUpdateVlanStatisticsToZeroTest(VLAN_3, 0, VLAN_1, VLAN_2); + } + + @Test + public void updateVlanStatisticsToZeroOldSrcVlanAreNotZeroTest() + throws FlowNotFoundException, InvalidFlowException { + runUpdateVlanStatisticsToZeroTest(0, VLAN_3, VLAN_1, VLAN_2); + } + + @Test + public void updateVlanStatisticsToZeroDstVlanIsZeroNewVlansNullTest() + throws FlowNotFoundException, InvalidFlowException { + runUpdateVlanStatisticsToZeroTest(VLAN_1, 0, null, null); } @Test - public void updateVlanStatisticsToZeroSrcVlanIsZeroTest() throws FlowNotFoundException, InvalidFlowException { - runUpdateVlanStatisticsToZero(0, VLAN_1); + public void updateVlanStatisticsToZeroSrcVlanIsZeroNewVlansNullTest() + throws FlowNotFoundException, InvalidFlowException { + runUpdateVlanStatisticsToZeroTest(0, VLAN_1, null, null); } @Test - public void updateVlanStatisticsToZeroSrcAndVlanAreZeroTest() throws FlowNotFoundException, InvalidFlowException { - runUpdateVlanStatisticsToZero(0, 0); + public void updateVlanStatisticsToZeroSrcAndDstVlanAreZeroNewVlansNullTest() + throws FlowNotFoundException, InvalidFlowException { + runUpdateVlanStatisticsToZeroTest(0, 0, null, null); + } + + @Test + public void updateVlanStatisticsToZeroDstVlanIsZeroNewVlansZerosTest() + throws FlowNotFoundException, InvalidFlowException { + runUpdateVlanStatisticsToZeroTest(VLAN_1, 0, 0, 0); + } + + @Test + public void updateVlanStatisticsToZeroSrcVlanIsZeroNewVlansZerosTest() + throws FlowNotFoundException, InvalidFlowException { + runUpdateVlanStatisticsToZeroTest(0, VLAN_1, 0, 0); + } + + @Test + public void updateVlanStatisticsToZeroSrcAndDstVlanAreZeroNewVlansZerosTest() + throws FlowNotFoundException, InvalidFlowException { + runUpdateVlanStatisticsToZeroTest(0, 0, 0, 0); + } + + private void runUpdateVlanStatisticsToZeroTest( + int oldSrcVlan, int oldDstVlan, Integer newSrcVlan, Integer newDstVLan) + throws FlowNotFoundException, InvalidFlowException { + Set originalVlanStatistics = Sets.newHashSet(1, 2, 3); + Flow flow = new TestFlowBuilder() + .flowId(FLOW_ID_1) + .srcSwitch(switchA) + .srcVlan(oldSrcVlan) + .destSwitch(switchB) + .destVlan(oldDstVlan) + .vlanStatistics(originalVlanStatistics) + .build(); + flowRepository.add(flow); + + FlowPatch receivedFlow = FlowPatch.builder() + .flowId(FLOW_ID_1) + .vlanStatistics(new HashSet<>()) + .source(buildPathEndpoint(newSrcVlan)) + .destination(buildPathEndpoint(newDstVLan)) + .build(); + + Flow updatedFlow = flowOperationsService.updateFlow(new FlowCarrierImpl(), receivedFlow); + assertTrue(updatedFlow.getVlanStatistics().isEmpty()); } @Test(expected = IllegalArgumentException.class) - public void unableToUpdateVlanStatisticsTest() throws FlowNotFoundException, InvalidFlowException { + public void unableToUpdateVlanStatisticsOldVlansSetNewVlansNullTest() + throws FlowNotFoundException, InvalidFlowException { + runUnableToUpdateVlanStatisticsTest(VLAN_1, VLAN_2, null, null); + } + + @Test(expected = IllegalArgumentException.class) + public void unableToUpdateVlanStatisticsOldVlansSetNewVlansSetTest() + throws FlowNotFoundException, InvalidFlowException { + runUnableToUpdateVlanStatisticsTest(VLAN_1, VLAN_2, VLAN_2, VLAN_3); + } + + @Test(expected = IllegalArgumentException.class) + public void unableToUpdateVlanStatisticsOldVlansZeroNewVlansSetTest() + throws FlowNotFoundException, InvalidFlowException { + runUnableToUpdateVlanStatisticsTest(0, 0, VLAN_1, VLAN_2); + } + + private void runUnableToUpdateVlanStatisticsTest( + int oldSrcVlan, int oldDstVlan, Integer newSrcVlan, Integer newDstVLan) + throws FlowNotFoundException, InvalidFlowException { Flow flow = new TestFlowBuilder() .flowId(FLOW_ID_1) .srcSwitch(switchA) - .srcVlan(VLAN_1) + .srcVlan(oldSrcVlan) .destSwitch(switchB) - .destVlan(VLAN_1) + .destVlan(oldDstVlan) .vlanStatistics(new HashSet<>()) .build(); flowRepository.add(flow); @@ -284,6 +369,8 @@ public void unableToUpdateVlanStatisticsTest() throws FlowNotFoundException, Inv FlowPatch receivedFlow = FlowPatch.builder() .flowId(FLOW_ID_1) .vlanStatistics(Sets.newHashSet(1, 2, 3)) + .source(PatchEndpoint.builder().vlanId(newSrcVlan).build()) + .destination(PatchEndpoint.builder().vlanId(newDstVLan).build()) .build(); flowOperationsService.updateFlow(new FlowCarrierImpl(), receivedFlow); @@ -916,6 +1003,13 @@ private void runUpdateVlanStatisticsToZero(int srcVLan, int dstVlan) assertTrue(updatedFlow.getVlanStatistics().isEmpty()); } + private static PatchEndpoint buildPathEndpoint(Integer vlan) { + if (vlan == null) { + return null; + } + return PatchEndpoint.builder().vlanId(vlan).build(); + } + private static class FlowCarrierImpl implements FlowOperationsCarrier { @Override public void emitPeriodicPingUpdate(String flowId, boolean enabled) { From 0db21d4e832b7569e5ac498c2958eca46740c484 Mon Sep 17 00:00:00 2001 From: ichupin Date: Wed, 8 Mar 2023 00:46:30 +0100 Subject: [PATCH 28/45] review fixes --- .../switches/v2/LogicalPortsValidationEntryV2.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java index 63bba1f6100..b10afd8fc19 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java @@ -59,11 +59,13 @@ static LogicalPortsValidationEntryV2 join(List en builder.proper(joinLists(nonNullEntries.stream().map(LogicalPortsValidationEntryV2::getProper))); builder.missing(joinLists(nonNullEntries.stream().map(LogicalPortsValidationEntryV2::getMissing))); builder.misconfigured(joinLists(nonNullEntries.stream().map(LogicalPortsValidationEntryV2::getMisconfigured))); - if (nonNullEntries.stream().map(e -> e.error).distinct().count() > 1) { - builder.error(nonNullEntries.stream().map(e -> e.error) - .distinct().collect(Collectors.joining(","))); - } else { - nonNullEntries.stream().map(e -> e.error).filter(Objects::nonNull).findFirst().ifPresent(builder::error); + List errorList = nonNullEntries.stream().map(e -> e.error).distinct().collect(Collectors.toList()); + if (errorList.size() > 1) { + builder.error(errorList.stream() + .map(e -> Objects.isNull(e) ? "There is an error while splitting and joining entities" : e) + .collect(Collectors.joining(","))); + }else { + errorList.stream().filter(Objects::nonNull).findFirst().ifPresent(builder::error); } return builder.build(); } From 6856d50ad6ce5ff045640575a1b641bf17e91966 Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Wed, 8 Mar 2023 11:01:33 +0100 Subject: [PATCH 29/45] Remove redundant constructor. --- .../network/PathValidationPayload.java | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationPayload.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationPayload.java index 8a55eacf6a1..237de4b2877 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationPayload.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationPayload.java @@ -19,7 +19,6 @@ import org.openkilda.messaging.payload.flow.PathNodePayload; import org.openkilda.model.PathComputationStrategy; -import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; @@ -57,24 +56,4 @@ public class PathValidationPayload { @JsonProperty("path_computation_strategy") PathComputationStrategy pathComputationStrategy; - - @JsonCreator - public PathValidationPayload( - @JsonProperty("bandwidth") Long bandwidth, - @JsonProperty("latency_ms") Long latencyMs, - @JsonProperty("max_latency_tier2") Long latencyTier2ms, - @JsonProperty("nodes") List nodes, - @JsonProperty("diverse_with_flow") String diverseWithFlow, - @JsonProperty("reuse_flow_resources") String reuseFlowResources, - @JsonProperty("flow_encapsulation_type") FlowEncapsulationType flowEncapsulationType, - @JsonProperty("path_computation_strategy") PathComputationStrategy computationStrategy) { - this.bandwidth = bandwidth; - this.latencyMs = latencyMs; - this.latencyTier2ms = latencyTier2ms; - this.nodes = nodes; - this.diverseWithFlow = diverseWithFlow; - this.reuseFlowResources = reuseFlowResources; - this.flowEncapsulationType = flowEncapsulationType; - this.pathComputationStrategy = computationStrategy; - } } From d6a7078c113205985ec8baed8c2a881022fa94d3 Mon Sep 17 00:00:00 2001 From: dmitrii-beliakov Date: Wed, 8 Mar 2023 12:23:38 +0100 Subject: [PATCH 30/45] Make subflow description field editable --- .../flowhs/mapper/YFlowRequestMapper.java | 2 +- .../flowhs/mapper/YFlowRequestMapperTest.java | 150 ++++++++++++++++++ .../service/yflow/YFlowUpdateServiceTest.java | 63 ++++---- .../FermaYFlowRepositoryTest.java | 33 ++++ .../spec/flows/yflows/YFlowUpdateSpec.groovy | 3 +- 5 files changed, 218 insertions(+), 33 deletions(-) create mode 100644 src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/mapper/YFlowRequestMapperTest.java diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/mapper/YFlowRequestMapper.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/mapper/YFlowRequestMapper.java index 5a86a532576..8da2e9f34b8 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/mapper/YFlowRequestMapper.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/mapper/YFlowRequestMapper.java @@ -92,7 +92,7 @@ public Collection toRequestedFlows(YFlowRequest request) { .destVlan(subFlow.getEndpoint().getOuterVlanId()) .destInnerVlan(subFlow.getEndpoint().getInnerVlanId()) .detectConnectedDevices(new DetectConnectedDevices()) //TODO: map it? - .description(request.getDescription()) + .description(subFlow.getDescription()) .flowEncapsulationType(request.getEncapsulationType()) .bandwidth(request.getMaximumBandwidth()) .ignoreBandwidth(request.isIgnoreBandwidth()) diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/mapper/YFlowRequestMapperTest.java b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/mapper/YFlowRequestMapperTest.java new file mode 100644 index 00000000000..a0e3a149b99 --- /dev/null +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/mapper/YFlowRequestMapperTest.java @@ -0,0 +1,150 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.wfm.topology.flowhs.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; + +import org.openkilda.messaging.command.yflow.SubFlowDto; +import org.openkilda.messaging.command.yflow.SubFlowSharedEndpointEncapsulation; +import org.openkilda.messaging.command.yflow.YFlowRequest; +import org.openkilda.model.FlowEncapsulationType; +import org.openkilda.model.FlowEndpoint; +import org.openkilda.model.FlowStatus; +import org.openkilda.model.PathComputationStrategy; +import org.openkilda.model.SwitchId; +import org.openkilda.wfm.topology.flowhs.model.RequestedFlow; + +import org.junit.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class YFlowRequestMapperTest { + public static final PathComputationStrategy REQUEST_COMPUTATION_STRATEGY = + PathComputationStrategy.COST_AND_AVAILABLE_BANDWIDTH; + private static final SwitchId SUB_FLOW_SWITCH_ID_1 = new SwitchId("00:01"); + private static final String SUB_FLOW_1_ID = "subflow1 id"; + private static final String SUB_FLOW_DESCRIPTION = "subflow description"; + private static final Instant SUB_FLOW_TIME_UPDATE = Instant.ofEpochMilli(100_000); + private static final int SUB_FLOW_SHARED_END_POINT_VLAN_ID = 12; + private static final int SUB_FLOW_SHARED_END_POINT_INNER_VLAN_ID = 13; + private static final SubFlowSharedEndpointEncapsulation SUB_FLOW_SHARED_ENDPOINT_ENCAPSULATION = + new SubFlowSharedEndpointEncapsulation(SUB_FLOW_SHARED_END_POINT_VLAN_ID, + SUB_FLOW_SHARED_END_POINT_INNER_VLAN_ID); + private static final int SUB_FLOW_PORT_NUMBER = 1; + private static final int SUB_FLOW_INNER_VLAN = 1000; + private static final int SUB_FLOW_OUTER_VLAN = 1001; + private static final FlowEndpoint SUB_FLOW_END_POINT = + new FlowEndpoint(SUB_FLOW_SWITCH_ID_1, SUB_FLOW_PORT_NUMBER, SUB_FLOW_OUTER_VLAN, SUB_FLOW_INNER_VLAN); + private static final FlowStatus SUB_FLOW_STATUS = FlowStatus.UP; + public static final YFlowRequest.Type REQUEST_TYPE = YFlowRequest.Type.CREATE; + public static final FlowEncapsulationType REQUEST_ENCAPSULATION_TYPE = FlowEncapsulationType.TRANSIT_VLAN; + public static final long REQUEST_MAXIMUM_BANDWIDTH = 100500L; + public static final boolean REQUEST_IGNORE_BANDWIDTH = true; + public static final boolean REQUEST_STRICT_BANDWIDTH = true; + public static final boolean REQUEST_PINNED = true; + public static final int REQUEST_PRIORITY = 2; + public static final long REQUEST_MAX_LATENCY = 42L; + public static final long REQUEST_MAX_LATENCY_TIER_2 = 84L; + public static final boolean REQUEST_PERIODIC_PINGS = true; + public static final boolean REQUEST_ALLOCATE_PROTECTED_PATH = true; + private static final SwitchId REQUEST_SWITCH_ID = new SwitchId("00:02"); + private static final int REQUEST_PORT_NUMBER = 50; + public static final FlowEndpoint REQUEST_SHARED_END_POINT = + new FlowEndpoint(REQUEST_SWITCH_ID, REQUEST_PORT_NUMBER); + public static final String REQUEST_Y_FLOW_ID = "request y-flow id"; + public static final String REQUEST_DESCRIPTION = "request description"; + public static final String REQUEST_DIVERSE_FLOW_ID = "request diverse flow id"; + + @Test + public void toRequestedFlow() { + List subFlowDtos = Collections.singletonList(createSubFlowDto()); + YFlowRequest request = createYflowRequest(subFlowDtos); + + RequestedFlow actualRequestedFlow = new ArrayList<>( + new YFlowRequestMapperImpl().toRequestedFlows(request)).get(0); + + // taken from the request + assertEquals(REQUEST_SWITCH_ID, actualRequestedFlow.getSrcSwitch()); + assertEquals(REQUEST_PORT_NUMBER, actualRequestedFlow.getSrcPort()); + assertEquals(REQUEST_ENCAPSULATION_TYPE, actualRequestedFlow.getFlowEncapsulationType()); + assertEquals(REQUEST_MAXIMUM_BANDWIDTH, actualRequestedFlow.getBandwidth()); + assertEquals(REQUEST_IGNORE_BANDWIDTH, actualRequestedFlow.isIgnoreBandwidth()); + assertEquals(REQUEST_STRICT_BANDWIDTH, actualRequestedFlow.isStrictBandwidth()); + assertEquals(REQUEST_PINNED, actualRequestedFlow.isPinned()); + assertEquals(Long.valueOf(REQUEST_PRIORITY), Long.valueOf(actualRequestedFlow.getPriority())); + assertEquals(Long.valueOf(REQUEST_MAX_LATENCY), actualRequestedFlow.getMaxLatency()); + assertEquals(Long.valueOf(REQUEST_MAX_LATENCY_TIER_2), actualRequestedFlow.getMaxLatencyTier2()); + assertEquals(REQUEST_PERIODIC_PINGS, actualRequestedFlow.isPeriodicPings()); + assertEquals(REQUEST_COMPUTATION_STRATEGY, actualRequestedFlow.getPathComputationStrategy()); + assertEquals(REQUEST_ALLOCATE_PROTECTED_PATH, actualRequestedFlow.isAllocateProtectedPath()); + assertNotEquals("A diverse flow id from the request is ignored", + REQUEST_DIVERSE_FLOW_ID, actualRequestedFlow.getDiverseFlowId()); + assertNull(actualRequestedFlow.getDiverseFlowId()); + + // taken from subflows + assertEquals(SUB_FLOW_1_ID, actualRequestedFlow.getFlowId()); + assertEquals(SUB_FLOW_SHARED_END_POINT_VLAN_ID, actualRequestedFlow.getSrcVlan()); + assertEquals(SUB_FLOW_SHARED_END_POINT_INNER_VLAN_ID, actualRequestedFlow.getSrcInnerVlan()); + assertEquals(SUB_FLOW_SWITCH_ID_1, actualRequestedFlow.getDestSwitch()); + assertEquals(SUB_FLOW_PORT_NUMBER, actualRequestedFlow.getDestPort()); + assertEquals(SUB_FLOW_OUTER_VLAN, actualRequestedFlow.getDestVlan()); + assertEquals(SUB_FLOW_INNER_VLAN, actualRequestedFlow.getDestInnerVlan()); + assertEquals(SUB_FLOW_DESCRIPTION, actualRequestedFlow.getDescription()); + assertNotEquals(REQUEST_DESCRIPTION, actualRequestedFlow.getDescription()); + assertEquals(SUB_FLOW_SHARED_END_POINT_VLAN_ID, actualRequestedFlow.getSrcVlan()); + assertEquals(SUB_FLOW_SHARED_END_POINT_INNER_VLAN_ID, actualRequestedFlow.getSrcInnerVlan()); + } + + private YFlowRequest createYflowRequest(List subFlowDtos) { + return YFlowRequest.builder() + .allocateProtectedPath(REQUEST_ALLOCATE_PROTECTED_PATH) + .diverseFlowId(REQUEST_DIVERSE_FLOW_ID) + .description(REQUEST_DESCRIPTION) + .encapsulationType(REQUEST_ENCAPSULATION_TYPE) + .ignoreBandwidth(REQUEST_IGNORE_BANDWIDTH) + .maximumBandwidth(REQUEST_MAXIMUM_BANDWIDTH) + .maxLatency(REQUEST_MAX_LATENCY) + .maxLatencyTier2(REQUEST_MAX_LATENCY_TIER_2) + .pathComputationStrategy(REQUEST_COMPUTATION_STRATEGY) + .periodicPings(REQUEST_PERIODIC_PINGS) + .pinned(REQUEST_PINNED) + .priority(REQUEST_PRIORITY) + .sharedEndpoint(REQUEST_SHARED_END_POINT) + .strictBandwidth(REQUEST_STRICT_BANDWIDTH) + .subFlows(subFlowDtos) + .type(REQUEST_TYPE) + .yFlowId(REQUEST_Y_FLOW_ID) + .build(); + } + + private SubFlowDto createSubFlowDto() { + + + return SubFlowDto.builder() + .endpoint(SUB_FLOW_END_POINT) + .description(SUB_FLOW_DESCRIPTION) + .flowId(SUB_FLOW_1_ID) + .timeUpdate(SUB_FLOW_TIME_UPDATE) + .sharedEndpoint(SUB_FLOW_SHARED_ENDPOINT_ENCAPSULATION) + .status(SUB_FLOW_STATUS) + .build(); + } +} diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowUpdateServiceTest.java b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowUpdateServiceTest.java index 4bcc477167d..7560070bd36 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowUpdateServiceTest.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowUpdateServiceTest.java @@ -74,6 +74,9 @@ @RunWith(MockitoJUnitRunner.class) public class YFlowUpdateServiceTest extends AbstractYFlowTest { private static final int METER_ALLOCATION_RETRIES_LIMIT = 3; + private static final String SUB_FLOW_ID_1 = "test_sub_flow_id_1"; + private static final String SUB_FLOW_ID_2 = "test_sub_flow_id_2"; + private static final String Y_FLOW_ID = "test_successful_y_flow"; @Mock private FlowGenericCarrier flowCreateHubCarrier; @@ -106,8 +109,8 @@ public void shouldUpdateFlowWithTransitSwitches() request.setMaximumBandwidth(2000L); request.getSubFlows().get(0).setEndpoint(newFirstEndpoint); request.getSubFlows().get(1).setEndpoint(newSecondEndpoint); - preparePathComputationForUpdate("test_flow_1", buildNewFirstSubFlowPathPair()); - preparePathComputationForUpdate("test_flow_2", buildNewSecondSubFlowPathPair()); + preparePathComputationForUpdate(SUB_FLOW_ID_1, buildNewFirstSubFlowPathPair()); + preparePathComputationForUpdate(SUB_FLOW_ID_2, buildNewSecondSubFlowPathPair()); prepareYPointComputation(SWITCH_SHARED, SWITCH_NEW_FIRST_EP, SWITCH_NEW_SECOND_EP, SWITCH_TRANSIT); // when @@ -135,9 +138,9 @@ public void shouldUpdateFlowWithProtectedPath() request.getSubFlows().get(0).setEndpoint(newFirstEndpoint); request.getSubFlows().get(1).setEndpoint(newSecondEndpoint); - preparePathComputationForUpdate("test_flow_1", + preparePathComputationForUpdate(SUB_FLOW_ID_1, buildNewFirstSubFlowPathPair(), buildNewFirstSubFlowProtectedPathPair()); - preparePathComputationForUpdate("test_flow_2", + preparePathComputationForUpdate(SUB_FLOW_ID_2, buildNewSecondSubFlowPathPair(), buildNewSecondSubFlowProtectedPathPair()); prepareYPointComputation(SWITCH_SHARED, SWITCH_NEW_FIRST_EP, SWITCH_NEW_SECOND_EP, SWITCH_TRANSIT, SWITCH_TRANSIT); @@ -169,9 +172,9 @@ public void shouldFailIfNoPathAvailableForFirstSubFlow() request.getSubFlows().get(0).setEndpoint(newFirstEndpoint); request.getSubFlows().get(1).setEndpoint(newSecondEndpoint); - when(pathComputer.getPath(buildFlowIdArgumentMatch("test_flow_1"), any(), anyBoolean())) + when(pathComputer.getPath(buildFlowIdArgumentMatch(SUB_FLOW_ID_1), any(), anyBoolean())) .thenThrow(new UnroutableFlowException(injectedErrorMessage)); - preparePathComputationForUpdate("test_flow_2", buildNewSecondSubFlowPathPair(), buildSecondSubFlowPathPair()); + preparePathComputationForUpdate(SUB_FLOW_ID_2, buildNewSecondSubFlowPathPair(), buildSecondSubFlowPathPair()); prepareYPointComputation(SWITCH_SHARED, SWITCH_NEW_FIRST_EP, SWITCH_NEW_SECOND_EP, SWITCH_TRANSIT); // when @@ -199,8 +202,8 @@ public void shouldFailIfNoPathAvailableForSecondSubFlow() request.getSubFlows().get(0).setEndpoint(newFirstEndpoint); request.getSubFlows().get(1).setEndpoint(newSecondEndpoint); - preparePathComputationForUpdate("test_flow_1", buildNewFirstSubFlowPathPair(), buildFirstSubFlowPathPair()); - when(pathComputer.getPath(buildFlowIdArgumentMatch("test_flow_2"), any(), anyBoolean())) + preparePathComputationForUpdate(SUB_FLOW_ID_1, buildNewFirstSubFlowPathPair(), buildFirstSubFlowPathPair()); + when(pathComputer.getPath(buildFlowIdArgumentMatch(SUB_FLOW_ID_2), any(), anyBoolean())) .thenThrow(new UnroutableFlowException(injectedErrorMessage)); prepareYPointComputation(SWITCH_SHARED, SWITCH_NEW_FIRST_EP, SWITCH_NEW_SECOND_EP, SWITCH_TRANSIT); @@ -229,18 +232,18 @@ public void shouldFailIfNoResourcesAvailable() request.getSubFlows().get(0).setEndpoint(newFirstEndpoint); request.getSubFlows().get(1).setEndpoint(newSecondEndpoint); - preparePathComputationForUpdate("test_flow_1", buildNewFirstSubFlowPathPair(), buildFirstSubFlowPathPair()); - preparePathComputationForUpdate("test_flow_2", buildNewSecondSubFlowPathPair(), buildSecondSubFlowPathPair()); + preparePathComputationForUpdate(SUB_FLOW_ID_1, buildNewFirstSubFlowPathPair(), buildFirstSubFlowPathPair()); + preparePathComputationForUpdate(SUB_FLOW_ID_2, buildNewSecondSubFlowPathPair(), buildSecondSubFlowPathPair()); prepareYPointComputation(SWITCH_SHARED, SWITCH_NEW_FIRST_EP, SWITCH_NEW_SECOND_EP, SWITCH_TRANSIT); doThrow(new ResourceAllocationException(injectedErrorMessage)) - .when(flowResourcesManager).allocateMeter(eq("test_successful_yflow"), eq(SWITCH_TRANSIT)); + .when(flowResourcesManager).allocateMeter(eq(Y_FLOW_ID), eq(SWITCH_TRANSIT)); // when processUpdateRequestAndSpeakerCommands(request); verifyYFlowStatus(request.getYFlowId(), FlowStatus.UP); verify(flowResourcesManager, times(METER_ALLOCATION_RETRIES_LIMIT + 2)) // +1 from YFlowCreateFsm - .allocateMeter(eq("test_successful_yflow"), eq(SWITCH_TRANSIT)); + .allocateMeter(eq(Y_FLOW_ID), eq(SWITCH_TRANSIT)); YFlow flow = getYFlow(request.getYFlowId()); assertEquals(1000L, flow.getMaximumBandwidth()); @@ -261,8 +264,8 @@ public void shouldFailOnUnsuccessfulMeterInstallation() request.getSubFlows().get(0).setEndpoint(newFirstEndpoint); request.getSubFlows().get(1).setEndpoint(newSecondEndpoint); - preparePathComputationForUpdate("test_flow_1", buildNewFirstSubFlowPathPair(), buildFirstSubFlowPathPair()); - preparePathComputationForUpdate("test_flow_2", buildNewSecondSubFlowPathPair(), buildSecondSubFlowPathPair()); + preparePathComputationForUpdate(SUB_FLOW_ID_1, buildNewFirstSubFlowPathPair(), buildFirstSubFlowPathPair()); + preparePathComputationForUpdate(SUB_FLOW_ID_2, buildNewSecondSubFlowPathPair(), buildSecondSubFlowPathPair()); prepareYPointComputation(SWITCH_SHARED, SWITCH_NEW_FIRST_EP, SWITCH_NEW_SECOND_EP, SWITCH_TRANSIT); YFlowUpdateService service = makeYFlowUpdateService(0); @@ -271,7 +274,7 @@ public void shouldFailOnUnsuccessfulMeterInstallation() service.handleRequest(request.getYFlowId(), new CommandContext(), request); verifyYFlowStatus(request.getYFlowId(), FlowStatus.IN_PROGRESS, FlowStatus.IN_PROGRESS, FlowStatus.UP); // and - handleSpeakerCommandsAndFailInstall(service, request.getYFlowId(), "test_successful_yflow"); + handleSpeakerCommandsAndFailInstall(service, request.getYFlowId(), Y_FLOW_ID); // then verifyYFlowStatus(request.getYFlowId(), FlowStatus.UP); @@ -287,15 +290,15 @@ public void shouldFailOnUnsuccessfulMeterInstallation() @Test public void shouldFailOnTimeoutDuringMeterInstallation() - throws UnroutableFlowException, RecoverableException, DuplicateKeyException, UnknownKeyException { + throws UnroutableFlowException, RecoverableException, DuplicateKeyException { // given YFlowRequest request = createYFlow(); request.setMaximumBandwidth(2000L); request.getSubFlows().get(0).setEndpoint(newFirstEndpoint); request.getSubFlows().get(1).setEndpoint(newSecondEndpoint); - preparePathComputationForUpdate("test_flow_1", buildNewFirstSubFlowPathPair(), buildFirstSubFlowPathPair()); - preparePathComputationForUpdate("test_flow_2", buildNewSecondSubFlowPathPair(), buildSecondSubFlowPathPair()); + preparePathComputationForUpdate(SUB_FLOW_ID_1, buildNewFirstSubFlowPathPair(), buildFirstSubFlowPathPair()); + preparePathComputationForUpdate(SUB_FLOW_ID_2, buildNewSecondSubFlowPathPair(), buildSecondSubFlowPathPair()); prepareYPointComputation(SWITCH_SHARED, SWITCH_NEW_FIRST_EP, SWITCH_NEW_SECOND_EP, SWITCH_TRANSIT); YFlowUpdateService service = makeYFlowUpdateService(0); @@ -325,24 +328,24 @@ public void shouldPatchFlowWithTransitSwitches() createYFlow(); List subFlowPartialUpdateDtos = new ArrayList<>(); subFlowPartialUpdateDtos.add(SubFlowPartialUpdateDto.builder() - .flowId("test_flow_1") + .flowId(SUB_FLOW_ID_1) .endpoint(FlowPartialUpdateEndpoint.builder() .switchId(SWITCH_NEW_FIRST_EP).portNumber(2).vlanId(103).build()) .build()); subFlowPartialUpdateDtos.add(SubFlowPartialUpdateDto.builder() - .flowId("test_flow_2") + .flowId(SUB_FLOW_ID_2) .endpoint(FlowPartialUpdateEndpoint.builder() .switchId(SWITCH_NEW_SECOND_EP).portNumber(3).vlanId(104).build()) .build()); - YFlowPartialUpdateRequest request = YFlowPartialUpdateRequest.builder() - .yFlowId("test_successful_yflow") + final YFlowPartialUpdateRequest request = YFlowPartialUpdateRequest.builder() + .yFlowId(Y_FLOW_ID) .maximumBandwidth(2000L) .subFlows(subFlowPartialUpdateDtos) .build(); - preparePathComputationForUpdate("test_flow_1", buildNewFirstSubFlowPathPair()); - preparePathComputationForUpdate("test_flow_2", buildNewSecondSubFlowPathPair()); + preparePathComputationForUpdate(SUB_FLOW_ID_1, buildNewFirstSubFlowPathPair()); + preparePathComputationForUpdate(SUB_FLOW_ID_2, buildNewSecondSubFlowPathPair()); prepareYPointComputation(SWITCH_SHARED, SWITCH_NEW_FIRST_EP, SWITCH_NEW_SECOND_EP, SWITCH_TRANSIT); // when @@ -361,10 +364,10 @@ public void shouldPatchFlowWithTransitSwitches() } private YFlowRequest createYFlow() throws UnroutableFlowException, RecoverableException, DuplicateKeyException { - YFlowRequest request = buildYFlowRequest("test_successful_yflow", "test_flow_1", "test_flow_2") + final YFlowRequest request = buildYFlowRequest(Y_FLOW_ID, SUB_FLOW_ID_1, SUB_FLOW_ID_2) .build(); - preparePathComputationForCreate("test_flow_1", buildFirstSubFlowPathPair()); - preparePathComputationForCreate("test_flow_2", buildSecondSubFlowPathPair()); + preparePathComputationForCreate(SUB_FLOW_ID_1, buildFirstSubFlowPathPair()); + preparePathComputationForCreate(SUB_FLOW_ID_2, buildSecondSubFlowPathPair()); prepareYPointComputation(SWITCH_SHARED, SWITCH_FIRST_EP, SWITCH_SECOND_EP, SWITCH_TRANSIT); processCreateRequestAndSpeakerCommands(request); @@ -377,12 +380,12 @@ private YFlowRequest createYFlow() throws UnroutableFlowException, RecoverableEx private YFlowRequest createYFlowWithProtectedPath() throws UnroutableFlowException, RecoverableException, DuplicateKeyException { - YFlowRequest request = buildYFlowRequest("test_successful_yflow", "test_flow_1", "test_flow_2") + final YFlowRequest request = buildYFlowRequest(Y_FLOW_ID, SUB_FLOW_ID_1, SUB_FLOW_ID_2) .allocateProtectedPath(true) .build(); - preparePathComputationForCreate("test_flow_1", + preparePathComputationForCreate(SUB_FLOW_ID_1, buildFirstSubFlowPathPair(), buildFirstSubFlowProtectedPathPair()); - preparePathComputationForCreate("test_flow_2", + preparePathComputationForCreate(SUB_FLOW_ID_2, buildSecondSubFlowPathPair(), buildSecondSubFlowProtectedPathPair()); prepareYPointComputation(SWITCH_SHARED, SWITCH_FIRST_EP, SWITCH_SECOND_EP, SWITCH_TRANSIT, SWITCH_TRANSIT); prepareYPointComputation(SWITCH_SHARED, SWITCH_FIRST_EP, SWITCH_SECOND_EP, SWITCH_ALT_TRANSIT, diff --git a/src-java/kilda-persistence-tinkerpop/src/test/java/org/openkilda/persistence/ferma/repositories/FermaYFlowRepositoryTest.java b/src-java/kilda-persistence-tinkerpop/src/test/java/org/openkilda/persistence/ferma/repositories/FermaYFlowRepositoryTest.java index 6ef43ab1ad6..99945dc57bb 100644 --- a/src-java/kilda-persistence-tinkerpop/src/test/java/org/openkilda/persistence/ferma/repositories/FermaYFlowRepositoryTest.java +++ b/src-java/kilda-persistence-tinkerpop/src/test/java/org/openkilda/persistence/ferma/repositories/FermaYFlowRepositoryTest.java @@ -15,8 +15,10 @@ package org.openkilda.persistence.ferma.repositories; +import static com.google.common.collect.Sets.newHashSet; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import org.openkilda.model.Flow; import org.openkilda.model.FlowEncapsulationType; @@ -42,12 +44,17 @@ import org.junit.Test; import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; public class FermaYFlowRepositoryTest extends InMemoryGraphBasedTest { static final String Y_FLOW_ID_1 = "y_flow_1"; static final String FLOW_ID_1 = "test_flow_1"; static final String FLOW_ID_2 = "test_flow_2"; static final String FLOW_ID_3 = "test_flow_3"; + public static final String SUB_FLOW_1_DESCRIPTION_UPDATED = "SubFlow1 description updated"; + public static final String SUB_FLOW_2_DESCRIPTION_UPDATED = "SubFlow2 description updated"; + public static final String Y_FLOW_DESCRIPTION = "Y-flow description"; FlowRepository flowRepository; YFlowRepository yFlowRepository; @@ -74,16 +81,42 @@ public void shouldCreateFlow() { createYFlow(Y_FLOW_ID_1, FLOW_ID_1, FLOW_ID_2); createTestFlow(FLOW_ID_3, switch1, PORT_3, VLAN_2, switch2, PORT_2, VLAN_1); + assertTrue(yFlowRepository.findYFlowId(FLOW_ID_1).isPresent()); assertEquals(Y_FLOW_ID_1, yFlowRepository.findYFlowId(FLOW_ID_1).get()); + assertTrue(yFlowRepository.findYFlowId(FLOW_ID_2).isPresent()); assertEquals(Y_FLOW_ID_1, yFlowRepository.findYFlowId(FLOW_ID_2).get()); assertFalse(yFlowRepository.findYFlowId(FLOW_ID_3).isPresent()); } + @Test + public void editYSubFlowDescriptionViaActualFlowDescription() { + YFlow flow = createYFlow(Y_FLOW_ID_1, FLOW_ID_1, FLOW_ID_2); + + assertTrue(flowRepository.findById(FLOW_ID_1).isPresent()); + Flow subflow1 = flowRepository.findById(FLOW_ID_1).get(); + subflow1.setDescription(SUB_FLOW_1_DESCRIPTION_UPDATED); + + assertTrue(flowRepository.findById(FLOW_ID_2).isPresent()); + Flow subflow2 = flowRepository.findById(FLOW_ID_2).get(); + subflow2.setDescription(SUB_FLOW_2_DESCRIPTION_UPDATED); + + assertTrue(yFlowRepository.findById(Y_FLOW_ID_1).isPresent()); + YFlow updatedYfFlow = yFlowRepository.findById(Y_FLOW_ID_1).get(); + Set actualEditedDescriptions = updatedYfFlow.getSubFlows().stream() + .map(YSubFlow::getFlow) + .map(Flow::getDescription) + .collect(Collectors.toSet()); + + Set expectedDescriptions = newHashSet(SUB_FLOW_1_DESCRIPTION_UPDATED, SUB_FLOW_2_DESCRIPTION_UPDATED); + assertEquals(expectedDescriptions, actualEditedDescriptions); + } + private YFlow createYFlow(String yFlowId, String flowId1, String flowId2) { YFlow yFlow = YFlow.builder() .yFlowId(yFlowId) .encapsulationType(FlowEncapsulationType.TRANSIT_VLAN) .sharedEndpoint(new SharedEndpoint(SWITCH_ID_1, PORT_1)) + .description(Y_FLOW_DESCRIPTION) .build(); Flow flow1 = createTestFlow(flowId1, switch1, PORT_1, VLAN_1, switch2, PORT_3, VLAN_3); Flow flow2 = createTestFlow(flowId2, switch1, PORT_1, VLAN_2, switch3, PORT_4, VLAN_3); diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/yflows/YFlowUpdateSpec.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/yflows/YFlowUpdateSpec.groovy index ab5f2d69206..63317b86363 100644 --- a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/yflows/YFlowUpdateSpec.groovy +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/yflows/YFlowUpdateSpec.groovy @@ -130,8 +130,7 @@ class YFlowUpdateSpec extends HealthCheckSpecification { //update involved switches after update involvedSwitches.addAll(pathHelper.getInvolvedYSwitches(yFlow.YFlowId)) involvedSwitches.unique { it.dpId } - def ignores = ["subFlows.timeUpdate", "subFlows.status", "timeUpdate", "status", - "subFlows.description" /* https://github.com/telstra/open-kilda/issues/4984 */] + def ignores = ["subFlows.timeUpdate", "subFlows.status", "timeUpdate", "status"] then: "Requested updates are reflected in the response and in 'get' API" expect updateResponse, sameBeanAs(yFlow, ignores) From c58fe462f5b6f2cde5bf82163059c4559ff0b8b8 Mon Sep 17 00:00:00 2001 From: ichupin Date: Wed, 8 Mar 2023 12:46:16 +0100 Subject: [PATCH 31/45] checkstyle fixes --- .../info/switches/v2/LogicalPortsValidationEntryV2.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java index b10afd8fc19..c2546fb0801 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/switches/v2/LogicalPortsValidationEntryV2.java @@ -64,7 +64,7 @@ static LogicalPortsValidationEntryV2 join(List en builder.error(errorList.stream() .map(e -> Objects.isNull(e) ? "There is an error while splitting and joining entities" : e) .collect(Collectors.joining(","))); - }else { + } else { errorList.stream().filter(Objects::nonNull).findFirst().ifPresent(builder::error); } return builder.build(); From 7b192606b323a9dd68acc85707940e1eedb49942 Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Wed, 8 Mar 2023 13:31:52 +0100 Subject: [PATCH 32/45] Remove redundant constructor. Remove repositories from parameters and create them using the repo factory. --- .../openkilda/wfm/topology/nbworker/bolts/PathsBolt.java | 3 +-- .../wfm/topology/nbworker/services/PathsService.java | 7 +++---- .../wfm/topology/nbworker/services/PathsServiceTest.java | 3 +-- .../topology/nbworker/validators/PathValidatorTest.java | 3 +-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java index c544ea9f751..49bbacc4ade 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java @@ -47,8 +47,7 @@ public PathsBolt(PersistenceManager persistenceManager, PathComputerConfig pathC public void init() { super.init(); - pathService = new PathsService(repositoryFactory, pathComputerConfig, repositoryFactory.createIslRepository(), - repositoryFactory.createFlowRepository()); + pathService = new PathsService(repositoryFactory, pathComputerConfig); } @Override diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java index 9ccfef9b084..edb7ae52723 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java @@ -61,13 +61,12 @@ public class PathsService { private final IslRepository islRepository; private final FlowRepository flowRepository; - public PathsService(RepositoryFactory repositoryFactory, PathComputerConfig pathComputerConfig, - IslRepository islRepository, FlowRepository flowRepository) { + public PathsService(RepositoryFactory repositoryFactory, PathComputerConfig pathComputerConfig) { switchRepository = repositoryFactory.createSwitchRepository(); switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); kildaConfigurationRepository = repositoryFactory.createKildaConfigurationRepository(); - this.islRepository = islRepository; - this.flowRepository = flowRepository; + this.islRepository = repositoryFactory.createIslRepository(); + this.flowRepository = repositoryFactory.createFlowRepository(); PathComputerFactory pathComputerFactory = new PathComputerFactory( pathComputerConfig, new AvailableNetworkFactory(pathComputerConfig, repositoryFactory)); pathComputer = pathComputerFactory.getPathComputer(); diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java index d1fbd312b4c..71ac0b080f6 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java @@ -84,8 +84,7 @@ public static void setUpOnce() { kildaConfigurationRepository = repositoryFactory.createKildaConfigurationRepository(); PathComputerConfig pathComputerConfig = new PropertiesBasedConfigurationProvider() .getConfiguration(PathComputerConfig.class); - pathsService = new PathsService(repositoryFactory, pathComputerConfig, islRepository, - repositoryFactory.createFlowRepository()); + pathsService = new PathsService(repositoryFactory, pathComputerConfig); } @Before diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java index 14d1434cb3f..f318a2a7678 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java @@ -83,8 +83,7 @@ public static void setUpOnce() { flowRepository = repositoryFactory.createFlowRepository(); PathComputerConfig pathComputerConfig = new PropertiesBasedConfigurationProvider() .getConfiguration(PathComputerConfig.class); - pathsService = new PathsService(repositoryFactory, pathComputerConfig, islRepository, - repositoryFactory.createFlowRepository()); + pathsService = new PathsService(repositoryFactory, pathComputerConfig); } From 8ce0409a62092f57a7e50fc74368b9a2d1cb64ce Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Fri, 10 Mar 2023 12:51:23 +0100 Subject: [PATCH 33/45] Change the request method to POST. Make latency calculation for the whole path. Remove inner class in PathValidator; introduce caching for ISL and Switch repositories. --- .../nbworker/validators/PathValidator.java | 231 +++++++++++------- .../validators/PathValidatorTest.java | 136 +++++------ .../controller/v2/NetworkControllerV2.java | 4 +- 3 files changed, 214 insertions(+), 157 deletions(-) diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java index bf34d39244f..22027eec96b 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java @@ -16,7 +16,6 @@ package org.openkilda.wfm.topology.nbworker.validators; import static org.openkilda.model.PathComputationStrategy.COST_AND_AVAILABLE_BANDWIDTH; -import static org.openkilda.model.PathComputationStrategy.LATENCY; import static org.openkilda.model.PathComputationStrategy.MAX_LATENCY; import org.openkilda.messaging.info.network.PathValidationResult; @@ -33,22 +32,24 @@ import org.openkilda.model.SwitchProperties; import org.openkilda.persistence.repositories.FlowRepository; import org.openkilda.persistence.repositories.IslRepository; +import org.openkilda.persistence.repositories.IslRepository.IslEndpoints; import org.openkilda.persistence.repositories.SwitchPropertiesRepository; import org.openkilda.persistence.repositories.SwitchRepository; import com.google.common.collect.Sets; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import org.apache.commons.lang3.StringUtils; import java.time.Duration; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -56,13 +57,14 @@ public class PathValidator { - public static final String LATENCY = "latency"; - public static final String LATENCY_TIER_2 = "latency tier 2"; + private static final String LATENCY = "latency"; + private static final String LATENCY_TIER_2 = "latency tier 2"; private final IslRepository islRepository; private final FlowRepository flowRepository; private final SwitchPropertiesRepository switchPropertiesRepository; - private final SwitchRepository switchRepository; + private Map> islCache; + private Map> switchCache; public PathValidator(IslRepository islRepository, FlowRepository flowRepository, @@ -84,35 +86,63 @@ public PathValidator(IslRepository islRepository, * @return a response object containing the validation result and errors if any */ public PathValidationResult validatePath(PathValidationData pathValidationData) { + initCache(); Set result = pathValidationData.getPathSegments().stream() .map(segment -> executeValidations( - new InputData(pathValidationData, segment), - new RepositoryData(islRepository, flowRepository, switchPropertiesRepository, switchRepository), - getValidations(pathValidationData))) + InputData.of(pathValidationData, segment), + getPerSegmentValidations(pathValidationData))) .flatMap(Set::stream) .collect(Collectors.toSet()); + result.addAll(executeValidations(InputData.of(pathValidationData), + getPerPathValidations(pathValidationData))); + return PathValidationResult.builder() .isValid(result.isEmpty()) .errors(new LinkedList<>(result)) .build(); } + private void initCache() { + islCache = new HashMap<>(); + switchCache = new HashMap<>(); + } + + private Optional findIslByEndpoints(SwitchId srcSwitchId, int srcPort, SwitchId destSwitchId, int destPort) { + IslEndpoints endpoints = new IslEndpoints(srcSwitchId.toString(), srcPort, destSwitchId.toString(), destPort); + if (!islCache.containsKey(endpoints)) { + islCache.put(endpoints, + islRepository.findByEndpoints(srcSwitchId, srcPort, destSwitchId, destPort)); + } + return islCache.get(endpoints); + } + + private Map findSwitchesByIds(Set switchIds) { + Map result = new HashMap<>(); + switchIds.forEach(s -> { + if (!switchCache.containsKey(s)) { + switchCache.put(s, switchRepository.findById(s)); + } + if (switchCache.get(s).isPresent()) { + result.put(s, switchCache.get(s).get()); + } + }); + + return result; + } + private Set executeValidations(InputData inputData, - RepositoryData repositoryData, - List>> validations) { + List>> validations) { return validations.stream() - .map(f -> f.apply(inputData, repositoryData)) + .map(f -> f.apply(inputData)) .flatMap(Set::stream) .collect(Collectors.toSet()); } - private List>> getValidations( + private List>> getPerPathValidations( PathValidationData pathValidationData) { - List>> validationFunctions = new LinkedList<>(); - - validationFunctions.add(this::validateForwardAndReverseLinks); + List>> validationFunctions = new LinkedList<>(); if (isLatencyValidationRequired(pathValidationData)) { validationFunctions.add(this::validateLatency); @@ -122,6 +152,15 @@ private List>> getValidations( validationFunctions.add(this::validateLatencyTier2); } + return validationFunctions; + } + + private List>> getPerSegmentValidations( + PathValidationData pathValidationData) { + List>> validationFunctions = new LinkedList<>(); + + validationFunctions.add(this::validateForwardAndReverseLinks); + if (isBandwidthValidationRequired(pathValidationData)) { validationFunctions.add(this::validateBandwidth); } @@ -168,8 +207,8 @@ private boolean isLatencyValidationRequired(PathValidationData pathValidationDat || pathValidationData.getPathComputationStrategy() == MAX_LATENCY); } - private Set validateForwardAndReverseLinks(InputData inputData, RepositoryData repositoryData) { - Map switchMap = repositoryData.getSwitchRepository().findByIds( + private Set validateForwardAndReverseLinks(InputData inputData) { + Map switchMap = findSwitchesByIds( Sets.newHashSet(inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getDestSwitchId())); Set errors = Sets.newHashSet(); if (!switchMap.containsKey(inputData.getSegment().getSrcSwitchId())) { @@ -182,37 +221,37 @@ private Set validateForwardAndReverseLinks(InputData inputData, Reposito return errors; } - errors.addAll(validateForwardLink(inputData, repositoryData)); - errors.addAll(validateReverseLink(inputData, repositoryData)); + errors.addAll(validateForwardLink(inputData)); + errors.addAll(validateReverseLink(inputData)); return errors; } - private Set validateForwardLink(InputData inputData, RepositoryData repositoryData) { + private Set validateForwardLink(InputData inputData) { return validateLink( inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort(), inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort(), - repositoryData, inputData, + inputData, this::getNoForwardIslError, this::getForwardIslNotActiveError); } - private Set validateReverseLink(InputData inputData, RepositoryData repositoryData) { + private Set validateReverseLink(InputData inputData) { return validateLink( inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort(), inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort(), - repositoryData, inputData, + inputData, this::getNoReverseIslError, this::getReverseIslNotActiveError); } private Set validateLink(SwitchId srcSwitchId, int srcPort, SwitchId destSwitchId, int destPort, - RepositoryData repositoryData, InputData inputData, + InputData inputData, Function noLinkErrorProducer, Function linkNotActiveErrorProducer) { - Optional isl = getIslByEndPoints(srcSwitchId, srcPort, destSwitchId, destPort, repositoryData); + Optional isl = getIslByEndPoints(srcSwitchId, srcPort, destSwitchId, destPort); Set errors = Sets.newHashSet(); if (!isl.isPresent()) { errors.add(noLinkErrorProducer.apply(inputData)); @@ -224,33 +263,32 @@ private Set validateLink(SwitchId srcSwitchId, int srcPort, SwitchId des return errors; } - private Optional getIslByEndPoints(SwitchId srcSwitchId, int srcPort, SwitchId destSwitchId, int destPort, - RepositoryData repositoryData) { - return repositoryData.getIslRepository().findByEndpoints(srcSwitchId, srcPort, destSwitchId, destPort); + private Optional getIslByEndPoints(SwitchId srcSwitchId, int srcPort, SwitchId destSwitchId, int destPort) { + return findIslByEndpoints(srcSwitchId, srcPort, destSwitchId, destPort); } - private Optional getForwardIslOfSegmentData(PathSegmentValidationData data, RepositoryData repositoryData) { - return repositoryData.getIslRepository().findByEndpoints( + private Optional getForwardIslOfSegmentData(PathSegmentValidationData data) { + return findIslByEndpoints( data.getSrcSwitchId(), data.getSrcPort(), data.getDestSwitchId(), data.getDestPort()); } - private Optional getReverseIslOfSegmentData(PathSegmentValidationData data, RepositoryData repositoryData) { - return repositoryData.getIslRepository().findByEndpoints( + private Optional getReverseIslOfSegmentData(PathSegmentValidationData data) { + return findIslByEndpoints( data.getDestSwitchId(), data.getDestPort(), data.getSrcSwitchId(), data.getSrcPort()); } - private Set validateBandwidth(InputData inputData, RepositoryData repositoryData) { - Optional forward = getForwardIslOfSegmentData(inputData.getSegment(), repositoryData); - Optional reverse = getReverseIslOfSegmentData(inputData.getSegment(), repositoryData); + private Set validateBandwidth(InputData inputData) { + Optional forward = getForwardIslOfSegmentData(inputData.getSegment()); + Optional reverse = getReverseIslOfSegmentData(inputData.getSegment()); Set errors = Sets.newHashSet(); if (!forward.isPresent()) { errors.add(getNoForwardIslError(inputData)); } if (!reverse.isPresent()) { - errors.add(getNoForwardIslError(inputData)); + errors.add(getNoReverseIslError(inputData)); } if (!errors.isEmpty()) { return errors; @@ -310,7 +348,7 @@ private long getBandwidthWithReusableResources(InputData inputData, Isl isl, return isl.getAvailableBandwidth(); } - private Set validateEncapsulationType(InputData inputData, RepositoryData repositoryData) { + private Set validateEncapsulationType(InputData inputData) { Set errors = Sets.newHashSet(); Map switchPropertiesMap = switchPropertiesRepository.findBySwitchIds( Sets.newHashSet(inputData.getPath().getSrcSwitchId(), inputData.getPath().getDestSwitchId())); @@ -336,8 +374,8 @@ private Set validateEncapsulationType(InputData inputData, RepositoryDat return errors; } - private Set validateDiverseWithFlow(InputData inputData, RepositoryData repositoryData) { - if (!repositoryData.getIslRepository().findByEndpoints(inputData.getSegment().getSrcSwitchId(), + private Set validateDiverseWithFlow(InputData inputData) { + if (!findIslByEndpoints(inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getSrcPort(), inputData.getSegment().getDestSwitchId(), inputData.getSegment().getDestPort()).isPresent()) { @@ -362,67 +400,93 @@ private Set validateDiverseWithFlow(InputData inputData, RepositoryData return Collections.emptySet(); } - private Set validateLatencyTier2(InputData inputData, RepositoryData repositoryData) { - return validateLatency(inputData, repositoryData, inputData.getPath()::getLatencyTier2, LATENCY_TIER_2); + private Set validateLatencyTier2(InputData inputData) { + return validateLatency(inputData, inputData.getPath()::getLatencyTier2, LATENCY_TIER_2); } - private Set validateLatency(InputData inputData, RepositoryData repositoryData) { - return validateLatency(inputData, repositoryData, inputData.getPath()::getLatency, LATENCY); + private Set validateLatency(InputData inputData) { + return validateLatency(inputData, inputData.getPath()::getLatency, LATENCY); } - private Set validateLatency(InputData inputData, RepositoryData repositoryData, - Supplier inputLatency, String latencyType) { - Optional forward = getForwardIslOfSegmentData(inputData.getSegment(), repositoryData); - Optional reverse = getReverseIslOfSegmentData(inputData.getSegment(), repositoryData); + private Set validateLatency(InputData inputData, Supplier inputLatency, String latencyType) { + Optional actualForwardDuration = getForwardPathLatency(inputData); + Optional actualReverseDuration = getReversePathLatency(inputData); Set errors = Sets.newHashSet(); - if (!forward.isPresent()) { - errors.add(getNoForwardIslError(inputData)); + if (!actualForwardDuration.isPresent()) { + errors.add(getNoLinkOnPath()); } - if (!reverse.isPresent()) { - errors.add(getNoReverseIslError(inputData)); + if (!actualReverseDuration.isPresent()) { + errors.add(getNoLinkOnPath()); } if (!errors.isEmpty()) { return errors; } - Duration actualLatency = Duration.ofNanos(forward.get().getLatency()); + Duration actualLatency = actualForwardDuration.get(); if (actualLatency.compareTo(inputLatency.get()) > 0) { errors.add(getForwardLatencyErrorMessage(inputData, inputLatency.get(), latencyType, actualLatency)); } - Duration actualReverseLatency = Duration.ofNanos(reverse.get().getLatency()); + Duration actualReverseLatency = actualReverseDuration.get(); if (actualReverseLatency.compareTo(inputLatency.get()) > 0) { errors.add(getReverseLatencyErrorMessage(inputData, inputLatency.get(), latencyType, actualReverseLatency)); } return errors; } + private Optional getPathLatency(InputData inputData, + Function> getIslfunction) { + try { + return Optional.of(Duration.ofNanos(inputData.getPath().getPathSegments().stream() + .filter(s -> s.getDestPort() != null + && s.getDestSwitchId() != null + && s.getSrcSwitchId() != null + && s.getSrcPort() != null) + .map(getIslfunction) + .map(isl -> isl.orElseThrow(() -> new IllegalArgumentException( + "Cannot calculate latency because there is no link on this path segment"))) + .map(Isl::getLatency) + .mapToLong(Long::longValue) + .sum())); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + } + + private Optional getForwardPathLatency(InputData inputData) { + return getPathLatency(inputData, s -> + getIslByEndPoints(s.getSrcSwitchId(), s.getSrcPort(), s.getDestSwitchId(), s.getDestPort())); + } + + private Optional getReversePathLatency(InputData inputData) { + return getPathLatency(inputData, s -> + getIslByEndPoints(s.getDestSwitchId(), s.getDestPort(), s.getSrcSwitchId(), s.getSrcPort())); + } + private String getForwardLatencyErrorMessage(InputData data, Duration expectedLatency, String latencyType, Duration actualLatency) { return String.format( - "Requested %s is too low between end points: switch %s port %d and switch" - + " %s port %d. Requested %d ms, but the link supports %d ms", + "Requested %s is too low on the path between: switch %s and switch %s. " + + "Requested %d ms, but the sum on the path is %d ms.", latencyType, - data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), - data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), + data.getPath().getSrcSwitchId(), data.getPath().getDestSwitchId(), expectedLatency.toMillis(), actualLatency.toMillis()); } private String getReverseLatencyErrorMessage(InputData data, Duration expectedLatency, String latencyType, Duration actualLatency) { return String.format( - "Requested %s is too low between end points: switch %s port %d and switch" - + " %s port %d. Requested %d ms, but the link supports %d ms", + "Requested %s is too low on the path between: switch %s and switch %s. " + + "Requested %d ms, but the sum on the path is %d ms.", latencyType, - data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), - data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getPath().getDestSwitchId(), data.getPath().getSrcSwitchId(), expectedLatency.toMillis(), actualLatency.toMillis()); } private String getForwardBandwidthErrorMessage(InputData data, long actualBandwidth) { return String.format( "There is not enough bandwidth between end points: switch %s port %d and switch %s port %d" - + " (forward path). Requested bandwidth %d, but the link supports %d", + + " (forward path). Requested bandwidth %d, but the link supports %d.", data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), data.getPath().getBandwidth(), actualBandwidth); @@ -431,83 +495,86 @@ private String getForwardBandwidthErrorMessage(InputData data, long actualBandwi private String getReverseBandwidthErrorMessage(InputData data, long actualBandwidth) { return String.format( "There is not enough bandwidth between end points: switch %s port %d and switch %s port %d" - + " (reverse path). Requested bandwidth %d, but the link supports %d", + + " (reverse path). Requested bandwidth %d, but the link supports %d.", data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getPath().getBandwidth(), actualBandwidth); } + private String getNoLinkOnPath() { + return "Path latency cannot be calculated because there is no link at least at one path segment."; + } + private String getNoForwardIslError(InputData data) { return String.format( - "There is no ISL between end points: switch %s port %d and switch %s port %d", + "There is no ISL between end points: switch %s port %d and switch %s port %d.", data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); } private String getNoReverseIslError(InputData data) { return String.format( - "There is no ISL between end points: switch %s port %d and switch %s port %d", + "There is no ISL between end points: switch %s port %d and switch %s port %d.", data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort()); } private String getForwardIslNotActiveError(InputData data) { return String.format( - "The ISL is not in ACTIVE state between end points: switch %s port %d and switch %s port %d", + "The ISL is not in ACTIVE state between end points: switch %s port %d and switch %s port %d.", data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); } private String getReverseIslNotActiveError(InputData data) { return String.format( - "The ISL is not in ACTIVE state between end points: switch %s port %d and switch %s port %d", + "The ISL is not in ACTIVE state between end points: switch %s port %d and switch %s port %d.", data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort()); } private String getNoDiverseFlowFoundError(InputData data) { - return String.format("Could not find the diverse flow with ID %s", data.getPath().getDiverseWithFlow()); + return String.format("Could not find the diverse flow with ID %s.", data.getPath().getDiverseWithFlow()); } private String getNotDiverseSegmentError(InputData data) { return String.format("The following segment intersects with the flow %s: source switch %s port %d and " - + "destination switch %s port %d", + + "destination switch %s port %d.", data.getPath().getDiverseWithFlow(), data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); } private String getSrcSwitchNotFoundError(InputData data) { - return String.format("The following switch has not been found: %s", data.getSegment().getSrcSwitchId()); + return String.format("The following switch has not been found: %s.", data.getSegment().getSrcSwitchId()); } private String getDestSwitchNotFoundError(InputData data) { - return String.format("The following switch has not been found: %s", data.getSegment().getDestSwitchId()); + return String.format("The following switch has not been found: %s.", data.getSegment().getDestSwitchId()); } private String getSrcSwitchDoesNotSupportEncapsulationTypeError(InputData data) { - return String.format("The switch %s doesn't support the encapsulation type %s", + return String.format("The switch %s doesn't support the encapsulation type %s.", data.getSegment().getSrcSwitchId(), data.getPath().getFlowEncapsulationType()); } private String getDestSwitchDoesNotSupportEncapsulationTypeError(InputData data) { - return String.format("The switch %s doesn't support the encapsulation type %s", + return String.format("The switch %s doesn't support the encapsulation type %s.", data.getSegment().getDestSwitchId(), data.getPath().getFlowEncapsulationType()); } @Getter - @AllArgsConstructor + @AllArgsConstructor(access = AccessLevel.PRIVATE) private static class InputData { PathValidationData path; PathValidationData.PathSegmentValidationData segment; - } - @Getter - @AllArgsConstructor - private static class RepositoryData { - IslRepository islRepository; - FlowRepository flowRepository; - SwitchPropertiesRepository switchPropertiesRepository; - SwitchRepository switchRepository; + public static InputData of(PathValidationData pathValidationData) { + return new InputData(pathValidationData, null); + } + + public static InputData of(PathValidationData path, PathSegmentValidationData segment) { + return new InputData(path, segment); + } } } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java index f318a2a7678..c5975fdfffd 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java @@ -149,9 +149,9 @@ public void whenInactiveLink_validatePathReturnsErrorResponseTest() { Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); Set expectedErrorMessages = Sets.newHashSet( "The ISL is not in ACTIVE state between end points: switch 00:00:00:00:00:00:00:00 port 12 and switch" - + " 00:00:00:00:00:00:00:01 port 12", + + " 00:00:00:00:00:00:00:01 port 12.", "The ISL is not in ACTIVE state between end points: switch 00:00:00:00:00:00:00:01 port 12 and switch" - + " 00:00:00:00:00:00:00:00 port 12" + + " 00:00:00:00:00:00:00:00 port 12." ); assertEquals(expectedErrorMessages, actualErrorMessages); @@ -196,11 +196,11 @@ public void whenValidPathButNotEnoughBandwidth_validatePathReturnsErrorResponseT assertEquals(responses.get(0).getErrors().get(0), "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:01 port 6 and " + "switch 00:00:00:00:00:00:00:03 port 6 (forward path). Requested bandwidth 1000000000," - + " but the link supports 10000"); + + " but the link supports 10000."); assertEquals(responses.get(0).getErrors().get(1), "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03 port 6 and " + "switch 00:00:00:00:00:00:00:01 port 6 (reverse path). Requested bandwidth 1000000000," - + " but the link supports 10000"); + + " but the link supports 10000."); } @Test @@ -221,10 +221,12 @@ public void whenNoSwitchOnPath_validatePathReturnsErrorResponseTest() { Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); Set expectedErrorMessages = Sets.newHashSet( - "The following switch has not been found: 00:00:00:00:01:01:01:01", - "The following switch has not been found: 00:00:00:00:01:01:01:02", + "The following switch has not been found: 00:00:00:00:01:01:01:01.", + "The following switch has not been found: 00:00:00:00:01:01:01:02.", "There is no ISL between end points: switch 00:00:00:00:01:01:01:01 port 6" - + " and switch 00:00:00:00:01:01:01:02 port 6" + + " and switch 00:00:00:00:01:01:01:02 port 6.", + "There is no ISL between end points: switch 00:00:00:00:01:01:01:02 port 6" + + " and switch 00:00:00:00:01:01:01:01 port 6." ); assertEquals(expectedErrorMessages, actualErrorMessages); @@ -249,15 +251,11 @@ public void whenValidPathButTooLowLatency_andLatencyStrategy_validatePathReturns assertFalse(responses.get(0).getIsValid()); assertFalse(responses.get(0).getErrors().isEmpty()); - Set expectedErrorMessages = Sets.newHashSet(); - expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03 port" - + " 7 and switch 00:00:00:00:00:00:00:02 port 7. Requested 1 ms, but the link supports 3 ms"); - expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:02 port" - + " 7 and switch 00:00:00:00:00:00:00:03 port 7. Requested 1 ms, but the link supports 3 ms"); - expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:01 port" - + " 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); - expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03 port" - + " 6 and switch 00:00:00:00:00:00:00:01 port 6. Requested 1 ms, but the link supports 2 ms"); + Set expectedErrorMessages = Sets.newHashSet( + "Requested latency is too low on the path between: switch 00:00:00:00:00:00:00:01 and" + + " switch 00:00:00:00:00:00:00:02. Requested 1 ms, but the sum on the path is 5 ms.", + "Requested latency is too low on the path between: switch 00:00:00:00:00:00:00:02 and" + + " switch 00:00:00:00:00:00:00:01. Requested 1 ms, but the sum on the path is 5 ms."); Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); assertEquals(expectedErrorMessages, actualErrorMessages); @@ -282,15 +280,11 @@ public void whenValidPathButTooLowLatency_andMaxLatencyStrategy_validatePathRetu assertFalse(responses.get(0).getIsValid()); assertFalse(responses.get(0).getErrors().isEmpty()); - Set expectedErrorMessages = Sets.newHashSet(); - expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03 port" - + " 7 and switch 00:00:00:00:00:00:00:02 port 7. Requested 1 ms, but the link supports 3 ms"); - expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:02 port" - + " 7 and switch 00:00:00:00:00:00:00:03 port 7. Requested 1 ms, but the link supports 3 ms"); - expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:01 port" - + " 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); - expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03 port" - + " 6 and switch 00:00:00:00:00:00:00:01 port 6. Requested 1 ms, but the link supports 2 ms"); + Set expectedErrorMessages = Sets.newHashSet( + "Requested latency is too low on the path between: switch 00:00:00:00:00:00:00:01 and" + + " switch 00:00:00:00:00:00:00:02. Requested 1 ms, but the sum on the path is 5 ms.", + "Requested latency is too low on the path between: switch 00:00:00:00:00:00:00:02 and" + + " switch 00:00:00:00:00:00:00:01. Requested 1 ms, but the sum on the path is 5 ms."); Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); assertEquals(expectedErrorMessages, actualErrorMessages); @@ -315,18 +309,14 @@ public void whenValidPathButTooLowLatencyTier2_andLatencyStrategy_validatePathRe assertFalse(responses.get(0).getIsValid()); assertFalse(responses.get(0).getErrors().isEmpty()); - Set expectedErrors = Sets.newHashSet(); - expectedErrors.add("Requested latency tier 2 is too low between end points: switch 00:00:00:00:00:00:00:03 port" - + " 7 and switch 00:00:00:00:00:00:00:02 port 7. Requested 1 ms, but the link supports 3 ms"); - expectedErrors.add("Requested latency tier 2 is too low between end points: switch 00:00:00:00:00:00:00:02 port" - + " 7 and switch 00:00:00:00:00:00:00:03 port 7. Requested 1 ms, but the link supports 3 ms"); - expectedErrors.add("Requested latency tier 2 is too low between end points: switch 00:00:00:00:00:00:00:01 port" - + " 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); - expectedErrors.add("Requested latency tier 2 is too low between end points: switch 00:00:00:00:00:00:00:03 port" - + " 6 and switch 00:00:00:00:00:00:00:01 port 6. Requested 1 ms, but the link supports 2 ms"); + Set expectedErrorMessages = Sets.newHashSet( + "Requested latency tier 2 is too low on the path between: switch 00:00:00:00:00:00:00:01 and" + + " switch 00:00:00:00:00:00:00:02. Requested 1 ms, but the sum on the path is 5 ms.", + "Requested latency tier 2 is too low on the path between: switch 00:00:00:00:00:00:00:02 and" + + " switch 00:00:00:00:00:00:00:01. Requested 1 ms, but the sum on the path is 5 ms."); Set actualErrors = Sets.newHashSet(responses.get(0).getErrors()); - assertEquals(expectedErrors, actualErrors); + assertEquals(expectedErrorMessages, actualErrors); } @Test @@ -344,8 +334,11 @@ public void whenSwitchDoesNotSupportEncapsulationType_validatePathReturnsErrorRe assertFalse(responses.isEmpty()); assertFalse(responses.get(0).getIsValid()); assertFalse(responses.get(0).getErrors().isEmpty()); - assertEquals(responses.get(0).getErrors().get(0), - "The switch 00:00:00:00:00:00:00:03 doesn't support the encapsulation type VXLAN"); + Set expectedErrors = Sets.newHashSet( + "The switch 00:00:00:00:00:00:00:01 doesn't support the encapsulation type VXLAN.", + "The switch 00:00:00:00:00:00:00:03 doesn't support the encapsulation type VXLAN."); + Set actualErrors = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrors, actualErrors); } @Test @@ -364,11 +357,11 @@ public void whenNoLinkBetweenSwitches_validatePathReturnsErrorResponseTest() { assertFalse(responses.isEmpty()); assertFalse(responses.get(0).getIsValid()); assertFalse(responses.get(0).getErrors().isEmpty()); - Set expectedErrorMessages = Sets.newHashSet(); - expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:02 port 0 and switch" - + " 00:00:00:00:00:00:00:01 port 1"); - expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:01 port 1 and switch" - + " 00:00:00:00:00:00:00:02 port 0"); + Set expectedErrorMessages = Sets.newHashSet( + "There is no ISL between end points: switch 00:00:00:00:00:00:00:02 port 0 and switch" + + " 00:00:00:00:00:00:00:01 port 1.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:01 port 1 and switch" + + " 00:00:00:00:00:00:00:02 port 0."); Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); assertEquals(expectedErrorMessages, actualErrorMessages); @@ -391,11 +384,12 @@ public void whenNoLinkBetweenSwitches_andValidateLatency_validatePathReturnsErro assertFalse(responses.isEmpty()); assertFalse(responses.get(0).getIsValid()); assertFalse(responses.get(0).getErrors().isEmpty()); - Set expectedErrorMessages = Sets.newHashSet(); - expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:02 port 0 and switch" - + " 00:00:00:00:00:00:00:01 port 1"); - expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:01 port 1 and switch" - + " 00:00:00:00:00:00:00:02 port 0"); + Set expectedErrorMessages = Sets.newHashSet( + "There is no ISL between end points: switch 00:00:00:00:00:00:00:02 port 0 and switch" + + " 00:00:00:00:00:00:00:01 port 1.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:01 port 1 and switch" + + " 00:00:00:00:00:00:00:02 port 0.", + "Path latency cannot be calculated because there is no link at least at one path segment."); Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); assertEquals(expectedErrorMessages, actualErrorMessages); @@ -424,14 +418,13 @@ public void whenDiverseWith_andNoLinkExists_validatePathReturnsError() { List responses = pathsService.validatePath(request); assertFalse(responses.isEmpty()); assertFalse(responses.get(0).getIsValid()); - Set expectedErrorMessages = Sets.newHashSet(); - expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:03 port 7 and switch" - + " 00:00:00:00:00:00:00:00 port 7"); - expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:00 port 7 and switch" - + " 00:00:00:00:00:00:00:03 port 7"); - expectedErrorMessages.add( + Set expectedErrorMessages = Sets.newHashSet( + "There is no ISL between end points: switch 00:00:00:00:00:00:00:03 port 7 and switch" + + " 00:00:00:00:00:00:00:00 port 7.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:00 port 7 and switch" + + " 00:00:00:00:00:00:00:03 port 7.", "The following segment intersects with the flow flow_1: source switch 00:00:00:00:00:00:00:01" - + " port 6 and destination switch 00:00:00:00:00:00:00:03 port 6"); + + " port 6 and destination switch 00:00:00:00:00:00:00:03 port 6."); Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); assertEquals(expectedErrorMessages, actualErrorMessages); } @@ -462,7 +455,7 @@ public void whenDiverseWith_andExistsIntersection_validatePathReturnsError() { assertEquals(1, responses.get(0).getErrors().size()); assertEquals(responses.get(0).getErrors().get(0), "The following segment intersects with the flow flow_1: source switch 00:00:00:00:00:00:00:01" - + " port 6 and destination switch 00:00:00:00:00:00:00:03 port 6"); + + " port 6 and destination switch 00:00:00:00:00:00:00:03 port 6."); } @Test @@ -485,23 +478,20 @@ public void whenMultipleProblemsOnPath_validatePathReturnsAllErrorResponseTest() assertEquals("There must be 7 errors in total: 2 not enough bandwidth (forward and reverse paths), " + "2 link is not present, 2 latency, and 1 switch is not found", - 7, responses.get(0).getErrors().size()); - Set expectedErrorMessages = Sets.newHashSet(); - expectedErrorMessages.add("The following switch has not been found: 00:00:00:00:00:00:00:ff"); - expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:01" - + " port 6 and switch 00:00:00:00:00:00:00:03 port 6. Requested 1 ms, but the link supports 2 ms"); - expectedErrorMessages.add("Requested latency is too low between end points: switch 00:00:00:00:00:00:00:03" - + " port 6 and switch 00:00:00:00:00:00:00:01 port 6. Requested 1 ms, but the link supports 2 ms"); - expectedErrorMessages.add("There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03" + 6, responses.get(0).getErrors().size()); + Set expectedErrorMessages = Sets.newHashSet( + "The following switch has not been found: 00:00:00:00:00:00:00:ff.", + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03" + " port 6 and switch 00:00:00:00:00:00:00:01 port 6 (reverse path). Requested bandwidth 1000000," - + " but the link supports 10000"); - expectedErrorMessages.add("There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:01" + + " but the link supports 10000.", + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:01" + " port 6 and switch 00:00:00:00:00:00:00:03 port 6 (forward path). Requested bandwidth 1000000," - + " but the link supports 10000"); - expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:03 port 7 and" - + " switch 00:00:00:00:00:00:00:ff port 7"); - expectedErrorMessages.add("There is no ISL between end points: switch 00:00:00:00:00:00:00:ff port 7 and" - + " switch 00:00:00:00:00:00:00:03 port 7"); + + " but the link supports 10000.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:03 port 7 and" + + " switch 00:00:00:00:00:00:00:ff port 7.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:ff port 7 and" + + " switch 00:00:00:00:00:00:00:03 port 7.", + "Path latency cannot be calculated because there is no link at least at one path segment."); assertEquals(expectedErrorMessages, Sets.newHashSet(responses.get(0).getErrors())); } @@ -524,7 +514,7 @@ public void whenValidPathAndDiverseFlowDoesNotExist_validatePathReturnsErrorResp assertFalse(responses.isEmpty()); assertFalse(responses.get(0).getIsValid()); assertFalse(responses.get(0).getErrors().isEmpty()); - assertEquals("Could not find the diverse flow with ID non_existing_flow_id", + assertEquals("Could not find the diverse flow with ID non_existing_flow_id.", responses.get(0).getErrors().get(0)); } @@ -589,11 +579,11 @@ public void whenValidPathWithExistingFlowAndReuseResources_validatePathReturnsSu assertEquals(responsesAfter.get(0).getErrors().get(0), "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:02 port 7 and" + " switch 00:00:00:00:00:00:00:03 port 7 (reverse path). Requested bandwidth 1000, but the" - + " link supports 100"); + + " link supports 100."); assertEquals(responsesAfter.get(0).getErrors().get(1), "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03 port 7 and" + " switch 00:00:00:00:00:00:00:02 port 7 (forward path). Requested bandwidth 1000, but the" - + " link supports 100"); + + " link supports 100."); PathValidateRequest requestWithReuseResources = new PathValidateRequest(PathValidationPayload.builder() .nodes(nodes) diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java index 53e29ce5164..e672b9c1672 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java @@ -24,7 +24,7 @@ import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; @@ -49,7 +49,7 @@ public NetworkControllerV2(NetworkService networkService) { * @param pathValidationPayload a payload with a path and additional flow parameters provided by a user * @return either a successful response or the list of errors */ - @GetMapping(path = "/path/check") + @PostMapping(path = "/path/check") @ApiOperation(value = "Validates that a given path complies with the chosen strategy and the network availability") @ResponseStatus(HttpStatus.OK) public CompletableFuture validateCustomFlowPath( From acc952357f82910b03c00103e4c84a78423ac61e Mon Sep 17 00:00:00 2001 From: dbeliakov Date: Mon, 13 Mar 2023 14:38:52 +0100 Subject: [PATCH 34/45] Change caching map to LazyMap impl. Remove computation strategy null from the conditions if the validation is needed. Remove computation strategy from bandwidth validation condition at all. --- .../nbworker/validators/PathValidator.java | 51 +++++++------------ 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java index 22027eec96b..b27cf5e7254 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java @@ -15,9 +15,6 @@ package org.openkilda.wfm.topology.nbworker.validators; -import static org.openkilda.model.PathComputationStrategy.COST_AND_AVAILABLE_BANDWIDTH; -import static org.openkilda.model.PathComputationStrategy.MAX_LATENCY; - import org.openkilda.messaging.info.network.PathValidationResult; import org.openkilda.model.Flow; import org.openkilda.model.FlowPath; @@ -40,11 +37,12 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; +import org.apache.commons.collections4.map.LazyMap; import org.apache.commons.lang3.StringUtils; +import org.apache.storm.shade.com.google.common.collect.Maps; import java.time.Duration; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -86,7 +84,6 @@ public PathValidator(IslRepository islRepository, * @return a response object containing the validation result and errors if any */ public PathValidationResult validatePath(PathValidationData pathValidationData) { - initCache(); Set result = pathValidationData.getPathSegments().stream() .map(segment -> executeValidations( InputData.of(pathValidationData, segment), @@ -103,32 +100,25 @@ public PathValidationResult validatePath(PathValidationData pathValidationData) .build(); } - private void initCache() { - islCache = new HashMap<>(); - switchCache = new HashMap<>(); - } - private Optional findIslByEndpoints(SwitchId srcSwitchId, int srcPort, SwitchId destSwitchId, int destPort) { - IslEndpoints endpoints = new IslEndpoints(srcSwitchId.toString(), srcPort, destSwitchId.toString(), destPort); - if (!islCache.containsKey(endpoints)) { - islCache.put(endpoints, - islRepository.findByEndpoints(srcSwitchId, srcPort, destSwitchId, destPort)); + if (islCache == null) { + islCache = LazyMap.lazyMap(Maps.newHashMap(), endpoints -> islRepository.findByEndpoints( + new SwitchId(endpoints.getSrcSwitch()), endpoints.getSrcPort(), + new SwitchId(endpoints.getDestSwitch()), endpoints.getDestPort())); } - return islCache.get(endpoints); + return islCache.get(new IslEndpoints(srcSwitchId.toString(), srcPort, destSwitchId.toString(), destPort)); } private Map findSwitchesByIds(Set switchIds) { - Map result = new HashMap<>(); - switchIds.forEach(s -> { - if (!switchCache.containsKey(s)) { - switchCache.put(s, switchRepository.findById(s)); - } - if (switchCache.get(s).isPresent()) { - result.put(s, switchCache.get(s).get()); - } - }); + if (switchCache == null) { + switchCache = LazyMap.lazyMap(Maps.newHashMap(), switchRepository::findById); + } - return result; + return switchIds.stream() + .map(switchCache::get) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toMap(Switch::getSwitchId, Function.identity())); } private Set executeValidations(InputData inputData, @@ -186,17 +176,14 @@ private boolean isDiverseWithFlowValidationRequired(PathValidationData pathValid private boolean isBandwidthValidationRequired(PathValidationData pathValidationData) { return pathValidationData.getBandwidth() != null - && pathValidationData.getBandwidth() != 0 - && (pathValidationData.getPathComputationStrategy() == null - || pathValidationData.getPathComputationStrategy() == COST_AND_AVAILABLE_BANDWIDTH); + && pathValidationData.getBandwidth() != 0; } private boolean isLatencyTier2ValidationRequired(PathValidationData pathValidationData) { return pathValidationData.getLatencyTier2() != null && !pathValidationData.getLatencyTier2().isZero() - && (pathValidationData.getPathComputationStrategy() == null - || pathValidationData.getPathComputationStrategy() == PathComputationStrategy.LATENCY - || pathValidationData.getPathComputationStrategy() == MAX_LATENCY); + && (pathValidationData.getPathComputationStrategy() == PathComputationStrategy.LATENCY + || pathValidationData.getPathComputationStrategy() == PathComputationStrategy.MAX_LATENCY); } private boolean isLatencyValidationRequired(PathValidationData pathValidationData) { @@ -204,7 +191,7 @@ private boolean isLatencyValidationRequired(PathValidationData pathValidationDat && !pathValidationData.getLatency().isZero() && (pathValidationData.getPathComputationStrategy() == null || pathValidationData.getPathComputationStrategy() == PathComputationStrategy.LATENCY - || pathValidationData.getPathComputationStrategy() == MAX_LATENCY); + || pathValidationData.getPathComputationStrategy() == PathComputationStrategy.MAX_LATENCY); } private Set validateForwardAndReverseLinks(InputData inputData) { From ca0ca7eba7f043891ae69881e602339433a3fe2e Mon Sep 17 00:00:00 2001 From: pkazlenka Date: Wed, 8 Mar 2023 10:30:13 +0100 Subject: [PATCH 35/45] #4527: Path validation tests * Added new tests for path check/validation feature * New NorthboundServiceV2 call to query path check endpoint * Added PathHelper methods to convert path representation, etc. --- .../functionaltests/helpers/PathHelper.groovy | 124 +++++++++++++++++- .../spec/network/PathCheckSpec.groovy | 116 ++++++++++++++++ .../northbound/NorthboundServiceV2.java | 5 + .../northbound/NorthboundServiceV2Impl.java | 10 ++ 4 files changed, 249 insertions(+), 6 deletions(-) create mode 100644 src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/network/PathCheckSpec.groovy diff --git a/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/PathHelper.groovy b/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/PathHelper.groovy index 009c6156bee..774bbae7965 100644 --- a/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/PathHelper.groovy +++ b/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/PathHelper.groovy @@ -1,11 +1,13 @@ package org.openkilda.functionaltests.helpers -import static org.springframework.beans.factory.config.ConfigurableBeanFactory.SCOPE_PROTOTYPE - +import groovy.util.logging.Slf4j import org.openkilda.messaging.info.event.PathNode import org.openkilda.messaging.payload.flow.FlowPathPayload import org.openkilda.messaging.payload.flow.FlowPathPayload.FlowProtectedPath import org.openkilda.messaging.payload.flow.PathNodePayload +import org.openkilda.messaging.payload.network.PathValidationPayload +import org.openkilda.model.FlowEncapsulationType +import org.openkilda.model.PathComputationStrategy import org.openkilda.northbound.dto.v2.flows.FlowPathV2.PathNodeV2 import org.openkilda.northbound.dto.v2.yflows.YFlowPaths import org.openkilda.testing.model.topology.TopologyDefinition @@ -15,8 +17,6 @@ import org.openkilda.testing.service.database.Database import org.openkilda.testing.service.northbound.NorthboundService import org.openkilda.testing.service.northbound.NorthboundServiceV2 import org.openkilda.testing.tools.IslUtils - -import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.annotation.Scope @@ -25,6 +25,13 @@ import org.springframework.stereotype.Component import java.util.AbstractMap.SimpleEntry import java.util.stream.Collectors +import static org.openkilda.model.FlowEncapsulationType.TRANSIT_VLAN +import static org.openkilda.model.FlowEncapsulationType.VXLAN +import static org.openkilda.model.PathComputationStrategy.COST +import static org.openkilda.model.PathComputationStrategy.COST_AND_AVAILABLE_BANDWIDTH +import static org.openkilda.model.PathComputationStrategy.LATENCY +import static org.springframework.beans.factory.config.ConfigurableBeanFactory.SCOPE_PROTOTYPE + /** * Holds utility methods for working with flow paths. */ @@ -36,9 +43,11 @@ class PathHelper { @Autowired TopologyDefinition topology - @Autowired @Qualifier("islandNb") + @Autowired + @Qualifier("islandNb") NorthboundService northbound - @Autowired @Qualifier("islandNbV2") + @Autowired + @Qualifier("islandNbV2") NorthboundServiceV2 northboundV2 @Autowired IslUtils islUtils @@ -101,6 +110,14 @@ class PathHelper { return islToAvoid } + /** + * Method to call in test cleanup if test required to manipulate path ISL's cost + */ + void 'remove ISL properties artifacts after manipulating paths weights'() { + northbound.deleteLinkProps(northbound.getLinkProps(topology.isls)) + } + + /** * Get list of ISLs that are involved in given path. * Note: will only return forward-way isls. You'll have to reverse them yourself if required. @@ -211,6 +228,17 @@ class PathHelper { return pathNodes } + static List convertToPathNodePayload(List path) { + def result = [new PathNodePayload(path[0].getSwitchId(), null, path[0].getPortNo())] + for (int i = 1; i < path.size() - 1; i += 2) { + result.add(new PathNodePayload(path.get(i).getSwitchId(), + path.get(i).getPortNo(), + path.get(i + 1).getPortNo())) + } + result.add(new PathNodePayload(path[-1].getSwitchId(), path[-1].getPortNo(), null)) + return result + } + /** * Converts path nodes (in the form of List) to a List representation */ @@ -269,4 +297,88 @@ class PathHelper { int getCost(List path) { return getInvolvedIsls(path).sum { database.getIslCost(it) } as int } + + List 'get path check errors'(List path, + Long bandwidth, + Long latencyMs, + Long latencyTier2ms, + String diverseWithFlow, + String reuseFlowResources, + FlowEncapsulationType flowEncapsulationType = TRANSIT_VLAN, + PathComputationStrategy pathComputationStrategy = COST) { + return northboundV2.checkPath(PathValidationPayload.builder() + .nodes(convertToPathNodePayload(path)) + .bandwidth(bandwidth) + .latencyMs(latencyMs) + .latencyTier2ms(latencyTier2ms) + .diverseWithFlow(diverseWithFlow) + .reuseFlowResources(reuseFlowResources) + .flowEncapsulationType(convertEncapsulationType(flowEncapsulationType)) + .pathComputationStrategy(pathComputationStrategy) + .build()) + .getErrors() + } + + List 'get path check errors'(List path, + Long bandwidth, + FlowEncapsulationType flowEncapsulationType = TRANSIT_VLAN) { + return 'get path check errors'(path, + bandwidth, + null, + null, + null, + null, + flowEncapsulationType, + COST_AND_AVAILABLE_BANDWIDTH) + } + + List 'get path check errors'(List path) { + return 'get path check errors'(path, + null, + null, + null, + null, + null, + null, + COST_AND_AVAILABLE_BANDWIDTH) + } + + List 'get path check errors'(List path, + String flowId, + Long maxLatency, + Long maxLatencyTier2 = null) { + return 'get path check errors'(path, + null, + maxLatency, + maxLatencyTier2, + null, + flowId, + null, + LATENCY) + } + + List 'get path check errors'(List path, + String flowId) { + return 'get path check errors'(path, + null, + null, + null, + flowId, + null, + null, + null) + } + + private static org.openkilda.messaging.payload.flow.FlowEncapsulationType convertEncapsulationType(FlowEncapsulationType origin) { + // Let's laugh on this naive implementation after the third encapsulation type is introduced, not before. + if (origin == VXLAN) { + return org.openkilda.messaging.payload.flow.FlowEncapsulationType.VXLAN + } else { + return org.openkilda.messaging.payload.flow.FlowEncapsulationType.TRANSIT_VLAN + } + } + + private static "pick random most probably free port"() { + return new Random().nextInt(1000) + 2000; //2000..2999 + } } diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/network/PathCheckSpec.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/network/PathCheckSpec.groovy new file mode 100644 index 00000000000..33d7b1536ce --- /dev/null +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/network/PathCheckSpec.groovy @@ -0,0 +1,116 @@ +package org.openkilda.functionaltests.spec.network + +import org.openkilda.functionaltests.HealthCheckSpecification +import org.openkilda.functionaltests.extension.failfast.Tidy +import org.openkilda.functionaltests.extension.tags.Tags +import org.openkilda.functionaltests.helpers.Wrappers +import spock.lang.See + +import static groovyx.gpars.GParsPool.withPool +import static org.openkilda.functionaltests.extension.tags.Tag.LOW_PRIORITY +import static org.openkilda.functionaltests.extension.tags.Tag.SMOKE +import static org.openkilda.model.FlowEncapsulationType.TRANSIT_VLAN +import static org.openkilda.model.FlowEncapsulationType.VXLAN +import static org.openkilda.model.PathComputationStrategy.COST + +@See("https://github.com/telstra/open-kilda/tree/develop/docs/design/solutions/path-validation/path-validation.md") +class PathCheckSpec extends HealthCheckSpecification{ + + @Tidy + @Tags(SMOKE) + def "No path validation errors for valid path without limitations"() { + given: "Path for non-neighbouring switches" + def path = topologyHelper.getNotNeighboringSwitchPair().getPaths().sort {it.size()}.first() + + when: "Check the path without limitations" + def validationErrors = pathHelper.'get path check errors'(path) + + then: "Path check result doesn't have errors" + validationErrors.isEmpty() + } + + @Tidy + @Tags(SMOKE) + def "Path check errors returned for each segment and each type of problem"() { + given: "Path of at least three switches" + def switchPair = topologyHelper.getAllNotNeighboringSwitchPairs().shuffled().first() + def path = switchPair.getPaths() + .sort {it.size()} + .first() + def srcSwitch = switchPair.getSrc() + + and: "Source switch supports only Transit VLAN encapsulation" + def backupSwitchProperties = switchHelper.getCachedSwProps(srcSwitch.getDpId()) + switchHelper.updateSwitchProperties(srcSwitch, backupSwitchProperties.jacksonCopy().tap { + it.supportedTransitEncapsulation = [TRANSIT_VLAN.toString()] + }) + + when: "Check the path where one switch doesn't support flow encapsulation type and all links don't have enough BW" + def validationErrors = pathHelper.'get path check errors'(path, Long.MAX_VALUE, VXLAN) + + then: "Path check result has multiple lack of bandwidth errors and at least one encapsulation support one" + !validationErrors.findAll{it == "The switch ${srcSwitch.getDpId()} doesn\'t support the encapsulation type VXLAN."}.isEmpty() + validationErrors.findAll {it.contains("not enough bandwidth")}.size() == + pathHelper.getInvolvedIsls(path).size() * 2 + + cleanup: + switchHelper.updateSwitchProperties(srcSwitch, backupSwitchProperties) + } + + @Tidy + @Tags(LOW_PRIORITY) + def "Latency check errors are returned for the whole existing flow"() { + given: "Path of at least three switches" + def switchPair = topologyHelper.getNotNeighboringSwitchPair() + def path = switchPair.getPaths() + .sort {it.size()} + .first() + + and: "Flow with cost computation strategy on that path" + withPool { + switchPair.paths.findAll { it != path }.eachParallel { pathHelper.makePathMorePreferable(path, it) } + } + def flow = flowHelperV2.addFlow( + flowHelperV2.randomFlow(switchPair, false).tap {it.pathComputationStrategy = COST}) + + when: "Check the path (equal to the flow) if the computation strategy would be LATENCY and max_latency would be too low" + def checkErrors = pathHelper."get path check errors"(path, flow.getFlowId(), 1, 2) + + then: "Path check result returns latency validation errors (1 per tier1 and tier 2, per forward and revers paths)" + checkErrors.findAll {it.contains("Requested latency is too low")}.size() == 2 + checkErrors.findAll {it.contains("Requested latency tier 2 is too low")}.size() == 2 + + cleanup: + pathHelper."remove ISL properties artifacts after manipulating paths weights"() + Wrappers.silent{flowHelperV2.deleteFlow(flow.getFlowId())} + } + + @Tidy + @Tags(LOW_PRIORITY) + def "Path intersection check errors are returned for each segment of existing flow"() { + given: "Switch pair with two paths having minimal amount of intersecting segment" + def switchPair = topologyHelper.getAllNeighboringSwitchPairs().sort {it.paths.size()}.first() + def (flowPath, intersectingPath) = switchPair.paths.subsequences().findAll{it.size() == 2 //all combinations of path pairs + && [1,2].contains(it[0].intersect(it[1]).size())} //where path pair has one or two common segments + .first() + + and: "Flow on the chosen path" + withPool { + switchPair.paths.findAll { it != flowPath }.eachParallel { pathHelper.makePathMorePreferable(flowPath, it) } + } + def flow = flowHelperV2.addFlow( + flowHelperV2.randomFlow(switchPair, false)) + + when: "Check if the potential path has intersections with existing one" + def checkErrors = pathHelper."get path check errors"(intersectingPath, flow.getFlowId()) + + then: "Path check reports expected amount of intersecting segments" + def expectedIntersectionCheckErrors = pathHelper.convertToPathNodePayload(flowPath).intersect(pathHelper.convertToPathNodePayload(intersectingPath)).size() + checkErrors.findAll {it.contains("The following segment intersects with the flow ${flow.getFlowId()}")}.size() + == expectedIntersectionCheckErrors + + cleanup: + pathHelper."remove ISL properties artifacts after manipulating paths weights"() + Wrappers.silent{flowHelperV2.deleteFlow(flow.getFlowId())} + } +} diff --git a/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/northbound/NorthboundServiceV2.java b/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/northbound/NorthboundServiceV2.java index d3f70591120..c66e0c202f5 100644 --- a/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/northbound/NorthboundServiceV2.java +++ b/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/northbound/NorthboundServiceV2.java @@ -16,6 +16,7 @@ package org.openkilda.testing.service.northbound; import org.openkilda.messaging.payload.flow.FlowIdStatusPayload; +import org.openkilda.messaging.payload.network.PathValidationPayload; import org.openkilda.model.SwitchId; import org.openkilda.northbound.dto.v2.flows.FlowHistoryStatusesResponse; import org.openkilda.northbound.dto.v2.flows.FlowLoopPayload; @@ -27,6 +28,7 @@ import org.openkilda.northbound.dto.v2.flows.FlowRequestV2; import org.openkilda.northbound.dto.v2.flows.FlowRerouteResponseV2; import org.openkilda.northbound.dto.v2.flows.FlowResponseV2; +import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; import org.openkilda.northbound.dto.v2.links.BfdProperties; import org.openkilda.northbound.dto.v2.links.BfdPropertiesPayload; import org.openkilda.northbound.dto.v2.switches.LagPortRequest; @@ -171,4 +173,7 @@ BfdPropertiesPayload setLinkBfd(SwitchId srcSwId, Integer srcPort, SwitchId dstS SwitchValidationV2ExtendedResult validateSwitch(SwitchId switchId); SwitchValidationV2ExtendedResult validateSwitch(SwitchId switchId, String include, String exclude); + + //network + PathValidateResponse checkPath(PathValidationPayload pathValidationPayload); } diff --git a/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/northbound/NorthboundServiceV2Impl.java b/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/northbound/NorthboundServiceV2Impl.java index a04c8102935..b3fc71603a7 100644 --- a/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/northbound/NorthboundServiceV2Impl.java +++ b/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/northbound/NorthboundServiceV2Impl.java @@ -17,6 +17,7 @@ import org.openkilda.messaging.Utils; import org.openkilda.messaging.payload.flow.FlowIdStatusPayload; +import org.openkilda.messaging.payload.network.PathValidationPayload; import org.openkilda.model.SwitchId; import org.openkilda.northbound.dto.v2.flows.FlowHistoryStatusesResponse; import org.openkilda.northbound.dto.v2.flows.FlowLoopPayload; @@ -28,6 +29,7 @@ import org.openkilda.northbound.dto.v2.flows.FlowRequestV2; import org.openkilda.northbound.dto.v2.flows.FlowRerouteResponseV2; import org.openkilda.northbound.dto.v2.flows.FlowResponseV2; +import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; import org.openkilda.northbound.dto.v2.links.BfdProperties; import org.openkilda.northbound.dto.v2.links.BfdPropertiesPayload; import org.openkilda.northbound.dto.v2.switches.LagPortRequest; @@ -511,4 +513,12 @@ public SwitchValidationV2ExtendedResult validateSwitch(SwitchId switchId, String new HttpEntity(buildHeadersWithCorrelationId()), SwitchValidationResultV2.class, switchId).getBody()); return new SwitchValidationV2ExtendedResult(switchId, result); } + + @Override + public PathValidateResponse checkPath(PathValidationPayload pathValidationPayload) { + return restTemplate.exchange("/api/v2/network/path/check", + HttpMethod.POST, + new HttpEntity<>(pathValidationPayload, buildHeadersWithCorrelationId()), + PathValidateResponse.class).getBody(); + } } From b9a608f105f1dd08e59641ea68720ec9ad685f4c Mon Sep 17 00:00:00 2001 From: pkazlenka Date: Wed, 15 Mar 2023 10:53:48 +0100 Subject: [PATCH 36/45] #4574: Switch validation when GPRC is down * Added tests on bugfix * Added new enum ContainerName * Small refactoring to move from constants to new enum --- .../helpers/DockerHelper.groovy | 26 +++++++++-- .../helpers/WfmManipulator.groovy | 17 +++---- .../helpers/model/ContainerName.groovy | 18 ++++++++ .../spec/grpc/GrpcBaseSpecification.groovy | 4 +- .../switches/SwitchValidationSpecV2.groovy | 46 +++++++++++++++++++ .../org/openkilda/testing/ConstantsGrpc.java | 1 - .../testing/service/database/Database.java | 4 ++ .../service/database/DatabaseSupportImpl.java | 11 +++++ 8 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/model/ContainerName.groovy diff --git a/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/DockerHelper.groovy b/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/DockerHelper.groovy index 9430143fb92..7aa99edc38b 100644 --- a/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/DockerHelper.groovy +++ b/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/DockerHelper.groovy @@ -3,7 +3,9 @@ package org.openkilda.functionaltests.helpers import com.spotify.docker.client.DefaultDockerClient import com.spotify.docker.client.DockerClient import com.spotify.docker.client.DockerClient.ListContainersParam +import com.spotify.docker.client.messages.Container import groovy.util.logging.Slf4j +import org.openkilda.functionaltests.helpers.model.ContainerName @Slf4j class DockerHelper { @@ -24,10 +26,8 @@ class DockerHelper { log.debug("Network name: $networkName") } - String getContainerIp(String containerName) { - dockerClient.listContainers(ListContainersParam.allContainers()).find { - it.names().contains("/" + containerName) - }.networkSettings().networks()[networkName].ipAddress() + String getContainerIp(ContainerName containerName) { + "get container by name"(containerName).networkSettings().networks()[networkName].ipAddress() } void restartContainer(String containerId) { @@ -38,6 +38,24 @@ class DockerHelper { dockerClient.waitContainer(containerId) } + String getContainerId(ContainerName containerName) { + return "get container by name"(containerName).id() + } + + void pauseContainer(String containerId) { + dockerClient.pauseContainer(containerId) + } + + void resumeContainer(String containerId) { + dockerClient.unpauseContainer(containerId) + } + + Container "get container by name" (ContainerName containerName) { + return dockerClient.listContainers(ListContainersParam.allContainers()).find { + it.names().contains(containerName.toString()) + } + } + private String getNetworkName() { dockerClient.listNetworks()*.name().find { it.contains('_default') && it.contains('kilda') } } diff --git a/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/WfmManipulator.groovy b/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/WfmManipulator.groovy index 0fa39e2db6a..ddb1201c0a2 100644 --- a/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/WfmManipulator.groovy +++ b/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/WfmManipulator.groovy @@ -1,8 +1,8 @@ package org.openkilda.functionaltests.helpers +import static org.openkilda.functionaltests.helpers.model.ContainerName.WFM + import com.spotify.docker.client.DockerClient -import com.spotify.docker.client.DockerClient.ListContainersParam -import com.spotify.docker.client.messages.Container import com.spotify.docker.client.messages.ContainerConfig import groovy.util.logging.Slf4j @@ -10,7 +10,6 @@ import java.util.concurrent.TimeUnit @Slf4j class WfmManipulator { - private static final String WFM_CONTAINER_NAME = "/wfm" /* Storm topologies require some time to fully roll after booting. * TODO(rtretiak): find a more reliable way to wait for the H-hour * Not respecting this wait may lead to subsequent tests instability @@ -19,14 +18,12 @@ class WfmManipulator { DockerClient dockerClient DockerHelper dockerHelper - Container wfmContainer + String wfmContainerId WfmManipulator(String dockerHost) { dockerHelper = new DockerHelper(dockerHost) dockerClient = dockerHelper.dockerClient - wfmContainer = dockerClient.listContainers(ListContainersParam.allContainers()).find { - it.names().contains(WFM_CONTAINER_NAME) - } + wfmContainerId = dockerHelper."get container by name"(WFM).id() } /** @@ -35,9 +32,9 @@ class WfmManipulator { */ def restartWfm(boolean wait = true) { log.warn "Restarting wfm" - dockerHelper.restartContainer(wfmContainer.id()) + dockerHelper.restartContainer(wfmContainerId) if (wait) { - dockerHelper.waitContainer(wfmContainer.id()) + dockerHelper.waitContainer(wfmContainerId) TimeUnit.SECONDS.sleep(WFM_WARMUP_SECONDS) } } @@ -57,7 +54,7 @@ class WfmManipulator { def container def image try { - image = dockerClient.commitContainer(wfmContainer.id(), "kilda/testing", null, + image = dockerClient.commitContainer(wfmContainerId, "kilda/testing", null, ContainerConfig.builder() .cmd(['/bin/bash', '-c', "PATH=\${PATH}:/opt/storm/bin;make -C /app $action-$topologyName".toString()]) diff --git a/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/model/ContainerName.groovy b/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/model/ContainerName.groovy new file mode 100644 index 00000000000..97da2d1a621 --- /dev/null +++ b/src-java/testing/functional-tests/src/main/groovy/org/openkilda/functionaltests/helpers/model/ContainerName.groovy @@ -0,0 +1,18 @@ +package org.openkilda.functionaltests.helpers.model + +enum ContainerName { + GRPC("grpc-speaker"), + GRPC_STUB("grpc-stub"), + WFM("wfm") + + private final String id; + + ContainerName(String id) { + this.id = id + } + + @Override + String toString() { + return "/${this.id}" + } +} \ No newline at end of file diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/grpc/GrpcBaseSpecification.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/grpc/GrpcBaseSpecification.groovy index 6e85121a163..22e53a73553 100644 --- a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/grpc/GrpcBaseSpecification.groovy +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/grpc/GrpcBaseSpecification.groovy @@ -2,8 +2,8 @@ package org.openkilda.functionaltests.spec.grpc import static org.openkilda.functionaltests.extension.tags.Tag.SMOKE_SWITCHES import static org.openkilda.functionaltests.extension.tags.Tag.TOPOLOGY_DEPENDENT +import static org.openkilda.functionaltests.helpers.model.ContainerName.GRPC_STUB import static org.openkilda.testing.Constants.NON_EXISTENT_SWITCH_ID -import static org.openkilda.testing.ConstantsGrpc.GRPC_STUB_CONTAINER_NAME import org.openkilda.functionaltests.HealthCheckBaseSpecification import org.openkilda.functionaltests.extension.tags.Tags @@ -45,7 +45,7 @@ class GrpcBaseSpecification extends HealthCheckBaseSpecification { if (profile == "virtual") { /* Create fake switch for running test using grpc-stub NOTE: The grpc-stub service covers positive test cases only */ - def grpcStubIp = new DockerHelper(dockerHost).getContainerIp(GRPC_STUB_CONTAINER_NAME) + def grpcStubIp = new DockerHelper(dockerHost).getContainerIp(GRPC_STUB) new SwitchDto(NON_EXISTENT_SWITCH_ID, grpcStubIp,37040, "host", "desc", SwitchChangeType.ACTIVATED, false, "of_version", "manufacturer", "hardware", "software", "serial_number", "pop", new SwitchLocationDto(48.860611, 2.337633, "street", "city", "country")) as List diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/SwitchValidationSpecV2.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/SwitchValidationSpecV2.groovy index 7e984a5fc86..fa90482e50e 100644 --- a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/SwitchValidationSpecV2.groovy +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/switches/SwitchValidationSpecV2.groovy @@ -1,10 +1,17 @@ package org.openkilda.functionaltests.spec.switches +import org.openkilda.functionaltests.helpers.DockerHelper +import org.openkilda.functionaltests.helpers.model.ContainerName +import spock.lang.Shared +import spock.lang.Unroll + import static groovyx.gpars.GParsPool.withPool import static org.junit.jupiter.api.Assumptions.assumeTrue +import static org.openkilda.functionaltests.extension.tags.Tag.LOW_PRIORITY import static org.openkilda.functionaltests.extension.tags.Tag.SMOKE import static org.openkilda.functionaltests.extension.tags.Tag.SMOKE_SWITCHES import static org.openkilda.functionaltests.extension.tags.Tag.TOPOLOGY_DEPENDENT +import static org.openkilda.functionaltests.extension.tags.Tag.VIRTUAL import static org.openkilda.functionaltests.helpers.SwitchHelper.isDefaultMeter import static org.openkilda.model.MeterId.MAX_SYSTEM_RULE_METER_ID import static org.openkilda.model.MeterId.MIN_FLOW_METER_ID @@ -65,6 +72,9 @@ class SwitchValidationSpecV2 extends HealthCheckSpecification { @Autowired @Qualifier("kafkaProducerProperties") Properties producerProps + @Value('${docker.host}') + @Shared + String dockerHost @Tidy def "Able to validate and sync a terminating switch with proper rules and meters"() { @@ -1008,6 +1018,42 @@ misconfigured" sectionsToVerifyPresence << [["meters"], ["meters", "groups"], ["meters", "groups", "rules"]] } + @Unroll + @Tidy + @Tags([VIRTUAL, LOW_PRIORITY]) + def "Able to validate switch using #apiVersion API when GRPC is down"() { + given: "Random switch without LAG feature enabled" + def aSwitch = topology.getSwitches().find { + !database.getSwitch(it.getDpId()).getFeatures().contains(SwitchFeature.LAG) + } + + and: "GRPC container is down" + def dockerHelper = new DockerHelper(dockerHost) + def grpcContainerId = dockerHelper.getContainerId(ContainerName.GRPC) + dockerHelper.pauseContainer(grpcContainerId) + + and: "Switch has a new feature artificially added directly to DB" + def originalFeatures = database.getSwitch(aSwitch.getDpId()).getFeatures() as Set + def newFeatures = originalFeatures + SwitchFeature.LAG + database.setSwitchFeatures(aSwitch.getDpId(), newFeatures) + + when: "Validate switch" + def validationResult = iface.validateSwitch(aSwitch.getDpId()) + + then: "Validation is successful" + validationResult.getLogicalPorts().getError() == + "Timeout for waiting response on DumpLogicalPortsRequest() Details: Error in SpeakerWorkerService" + + cleanup: + dockerHelper.resumeContainer(grpcContainerId) + database.setSwitchFeatures(aSwitch.getDpId(), originalFeatures) + + where: + apiVersion | iface + "V2" | northboundV2 + "V1" | northbound + } + List getCreatedMeterIds(SwitchId switchId) { return northbound.getAllMeters(switchId).meterEntries.findAll { it.meterId > MAX_SYSTEM_RULE_METER_ID }*.meterId } diff --git a/src-java/testing/test-library/src/main/java/org/openkilda/testing/ConstantsGrpc.java b/src-java/testing/test-library/src/main/java/org/openkilda/testing/ConstantsGrpc.java index cf45b7496f8..d75b421d20c 100644 --- a/src-java/testing/test-library/src/main/java/org/openkilda/testing/ConstantsGrpc.java +++ b/src-java/testing/test-library/src/main/java/org/openkilda/testing/ConstantsGrpc.java @@ -22,7 +22,6 @@ public final class ConstantsGrpc { public static final Integer REMOTE_LOG_PORT = 10514; public static final OnOffState DEFAULT_LOG_MESSAGES_STATE = OnOffState.OFF; public static final OnOffState DEFAULT_LOG_OF_MESSAGES_STATE = OnOffState.OFF; - public static final String GRPC_STUB_CONTAINER_NAME = "grpc-stub"; private ConstantsGrpc() { throw new UnsupportedOperationException(); diff --git a/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/database/Database.java b/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/database/Database.java index d31c6862337..9e1e2702392 100644 --- a/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/database/Database.java +++ b/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/database/Database.java @@ -20,6 +20,7 @@ import org.openkilda.model.FlowMirrorPoints; import org.openkilda.model.PathId; import org.openkilda.model.Switch; +import org.openkilda.model.SwitchFeature; import org.openkilda.model.SwitchId; import org.openkilda.model.SwitchStatus; import org.openkilda.model.TransitVlan; @@ -30,6 +31,7 @@ import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.Set; public interface Database { @@ -63,6 +65,8 @@ public interface Database { void setSwitchStatus(SwitchId switchId, SwitchStatus swStatus); + void setSwitchFeatures(SwitchId switchId, Set switchFeatures); + List getPaths(SwitchId src, SwitchId dst); void removeConnectedDevices(SwitchId sw); diff --git a/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/database/DatabaseSupportImpl.java b/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/database/DatabaseSupportImpl.java index 61d0b8534c8..81d9fff93ee 100644 --- a/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/database/DatabaseSupportImpl.java +++ b/src-java/testing/test-library/src/main/java/org/openkilda/testing/service/database/DatabaseSupportImpl.java @@ -28,6 +28,7 @@ import org.openkilda.model.MeterId; import org.openkilda.model.PathId; import org.openkilda.model.Switch; +import org.openkilda.model.SwitchFeature; import org.openkilda.model.SwitchId; import org.openkilda.model.SwitchStatus; import org.openkilda.model.TransitVlan; @@ -66,6 +67,7 @@ import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; @Component @@ -229,6 +231,15 @@ public Switch getSwitch(SwitchId switchId) { .orElseThrow(() -> new IllegalStateException(format("Switch %s not found", switchId)))); } + @Override + public void setSwitchFeatures(SwitchId switchId, Set switchFeatures) { + transactionManager.doInTransaction(() -> { + Switch sw = switchRepository.findById(switchId) + .orElseThrow(() -> new IllegalStateException(format("Switch %s not found", switchId))); + sw.setFeatures(switchFeatures); + }); + } + @Override public void setSwitchStatus(SwitchId switchId, SwitchStatus swStatus) { transactionManager.doInTransaction(() -> { From bace9cfd0ce7725cb8b90c6dceea49d781c3f6f6 Mon Sep 17 00:00:00 2001 From: Fedir Kantur Date: Wed, 15 Mar 2023 15:57:17 +0000 Subject: [PATCH 37/45] Add overlapping stats support for one switch flow --- .../share/service/IntersectionComputer.java | 116 ++++++++++-------- .../service/IntersectionComputerTest.java | 93 ++++++++++---- .../flowhs/validation/FlowValidatorTest.java | 30 ++--- 3 files changed, 153 insertions(+), 86 deletions(-) diff --git a/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java b/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java index 1c38abc67d5..fb33124e716 100644 --- a/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java +++ b/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java @@ -1,4 +1,4 @@ -/* Copyright 2019 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,22 +29,23 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Computes intersection counters for flow paths in flow group. */ public class IntersectionComputer { - private Set targetPathEdges; - private Set targetPathSwitches; + private final Set targetPathEdges = new HashSet<>(); + private final Set targetPathSwitches = new HashSet<>(); - private Map> otherEdges; + private final Map> otherEdges = new HashMap<>(); + private final Map> otherSwitches = new HashMap<>(); /** * Construct intersection counter for current flow group and target flow and flow path. @@ -56,25 +57,46 @@ public class IntersectionComputer { */ public IntersectionComputer(String targetFlowId, PathId targetForwardPathId, PathId targetReversePathId, Collection paths) { - List targetPath = paths.stream() - .flatMap(path -> path.getSegments().stream()) - .filter(e -> e.getPathId().equals(targetForwardPathId) - || e.getPathId().equals(targetReversePathId)) - .map(Edge::fromPathSegment) - .collect(Collectors.toList()); + paths.forEach(path -> { + if (path.getPathId().equals(targetForwardPathId) || path.getPathId().equals(targetReversePathId)) { + handleTargetPath(path); + } + if (!path.getFlow().getFlowId().equals(targetFlowId)) { + handleAnotherPath(path); + } + }); + } - targetPathEdges = new HashSet<>(targetPath); - targetPathSwitches = targetPath.stream() - .flatMap(e -> Stream.of(e.getSrcSwitch(), e.getDestSwitch())) - .collect(Collectors.toSet()); + private void handleTargetPath(FlowPath path) { + if (path.isOneSwitchFlow()) { + targetPathSwitches.add(path.getSrcSwitchId()); + } else { + path.getSegments().forEach(segment -> { + targetPathEdges.add(Edge.fromPathSegment(segment)); + targetPathSwitches.add(segment.getSrcSwitchId()); + targetPathSwitches.add(segment.getDestSwitchId()); + }); + } + } - otherEdges = paths.stream() - .filter(e -> !e.getFlow().getFlowId().equals(targetFlowId)) - .flatMap(path -> path.getSegments().stream()) - .collect(Collectors.groupingBy( - PathSegment::getPathId, - Collectors.mapping(Edge::fromPathSegment, Collectors.toSet()) - )); + private void handleAnotherPath(FlowPath path) { + Set switches = new HashSet<>(); + Set edges = new HashSet<>(); + if (path.isOneSwitchFlow()) { + switches.add(path.getSrcSwitchId()); + } else { + path.getSegments().forEach(segment -> { + switches.add(segment.getSrcSwitchId()); + switches.add(segment.getDestSwitchId()); + edges.add(Edge.fromPathSegment(segment)); + }); + } + if (!switches.isEmpty()) { + otherSwitches.put(path.getPathId(), switches); + } + if (!edges.isEmpty()) { + otherEdges.put(path.getPathId(), edges); + } } /** @@ -84,7 +106,9 @@ public IntersectionComputer(String targetFlowId, PathId targetForwardPathId, Pat */ public OverlappingSegmentsStats getOverlappingStats() { return computeIntersectionCounters( - otherEdges.values().stream().flatMap(Collection::stream).collect(Collectors.toSet())); + otherEdges.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()), + otherSwitches.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()) + ); } /** @@ -95,28 +119,9 @@ public OverlappingSegmentsStats getOverlappingStats() { public OverlappingSegmentsStats getOverlappingStats(PathId forwardPathId, PathId reversePathId) { Set edges = new HashSet<>(otherEdges.getOrDefault(forwardPathId, Collections.emptySet())); edges.addAll(otherEdges.getOrDefault(reversePathId, Collections.emptySet())); - return computeIntersectionCounters(edges); - } - - OverlappingSegmentsStats computeIntersectionCounters(Set otherEdges) { - Set switches = new HashSet<>(); - Set edges = new HashSet<>(); - for (Edge edge : otherEdges) { - switches.add(edge.getSrcSwitch()); - switches.add(edge.getDestSwitch()); - edges.add(edge); - } - - int edgesOverlap = Sets.intersection(edges, targetPathEdges).size(); - int switchesOverlap = Sets.intersection(switches, targetPathSwitches).size(); - return new OverlappingSegmentsStats(edgesOverlap, - switchesOverlap, - percent(edgesOverlap, targetPathEdges.size()), - percent(switchesOverlap, targetPathSwitches.size())); - } - - private int percent(int n, int from) { - return (int) ((n * 100.0f) / from); + Set switches = new HashSet<>(otherSwitches.getOrDefault(forwardPathId, Collections.emptySet())); + switches.addAll(otherSwitches.getOrDefault(reversePathId, Collections.emptySet())); + return computeIntersectionCounters(edges, switches); } /** @@ -201,6 +206,19 @@ public static List calculatePathIntersectionFromDest(Collection edges, Set switches) { + int edgesOverlap = Sets.intersection(edges, targetPathEdges).size(); + int switchesOverlap = Sets.intersection(switches, targetPathSwitches).size(); + return new OverlappingSegmentsStats(edgesOverlap, + switchesOverlap, + percent(edgesOverlap, targetPathEdges.size()), + percent(switchesOverlap, targetPathSwitches.size())); + } + + private int percent(int n, int from) { + return (int) ((n * 100.0f) / from); + } + private static List getLongestIntersectionOfSegments(List> pathSegments) { List result = new ArrayList<>(); // Iterate over the first path's segments and check other paths' segments. @@ -230,10 +248,10 @@ private static List getLongestIntersectionOfSegments(List 0) { diff --git a/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java b/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java index 42a6ddc3277..b7cf9bd00d9 100644 --- a/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java +++ b/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java @@ -1,4 +1,4 @@ -/* Copyright 2019 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,7 +72,7 @@ public void setup() { } @Test - public void noGroupIntersections() { + public void noGroupIntersectionsTest() { List paths = getFlowPaths(); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); @@ -82,7 +82,7 @@ public void noGroupIntersections() { } @Test - public void noGroupIntersectionsInOneFlow() { + public void noGroupIntersectionsInOneFlowTest() { List paths = getFlowPaths(); paths.addAll(getFlowPaths(NEW_PATH_ID, NEW_PATH_ID_REVERSE, flow)); @@ -93,7 +93,7 @@ public void noGroupIntersectionsInOneFlow() { } @Test - public void shouldNoIntersections() { + public void noIntersectionsTest() { List paths = getFlowPaths(); FlowPath path = FlowPath.builder() @@ -122,7 +122,7 @@ public void shouldNoIntersections() { } @Test - public void shouldNotIntersectPathInSameFlow() { + public void doesntIntersectPathInSameFlowTest() { List paths = getFlowPaths(); FlowPath newPath = FlowPath.builder() @@ -142,7 +142,7 @@ public void shouldNotIntersectPathInSameFlow() { } @Test - public void shouldNotFailIfNoSegments() { + public void doesntFailIfNoSegmentsTest() { IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, Collections.emptyList()); OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); @@ -151,7 +151,7 @@ public void shouldNotFailIfNoSegments() { } @Test - public void shouldNotFailIfNoIntersectionSegments() { + public void doesntFailIfNoIntersectionSegmentsTest() { List paths = getFlowPaths(); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); @@ -161,7 +161,7 @@ public void shouldNotFailIfNoIntersectionSegments() { } @Test - public void switchIntersectionByPathId() { + public void switchIntersectionByPathIdTest() { List paths = getFlowPaths(); FlowPath newPath = FlowPath.builder() @@ -181,7 +181,7 @@ public void switchIntersectionByPathId() { } @Test - public void partialIntersection() { + public void partialIntersectionTest() { List paths = getFlowPaths(); FlowPath newPath = FlowPath.builder() @@ -201,7 +201,7 @@ public void partialIntersection() { } @Test - public void fullIntersection() { + public void fullIntersectionTest() { List paths = getFlowPaths(); paths.addAll(getFlowPaths(NEW_PATH_ID, NEW_PATH_ID_REVERSE, flow2)); @@ -212,29 +212,29 @@ public void fullIntersection() { } @Test - public void isProtectedPathOverlapsPositive() { + public void isProtectedPathOverlapsPositiveTest() { List paths = getFlowPaths(PATH_ID, PATH_ID_REVERSE, flow); List primarySegments = getFlowPathSegments(paths); - List protectedSegmets = Collections.singletonList( + List protectedSegments = Collections.singletonList( buildPathSegment(PATH_ID, SWITCH_ID_A, SWITCH_ID_B, 1, 1)); - assertTrue(IntersectionComputer.isProtectedPathOverlaps(primarySegments, protectedSegmets)); + assertTrue(IntersectionComputer.isProtectedPathOverlaps(primarySegments, protectedSegments)); } @Test - public void isProtectedPathOverlapsNegative() { + public void isProtectedPathOverlapsNegativeTest() { List paths = getFlowPaths(PATH_ID, PATH_ID_REVERSE, flow); List primarySegments = getFlowPathSegments(paths); - List protectedSegmets = Collections.singletonList( + List protectedSegments = Collections.singletonList( buildPathSegment(PATH_ID, SWITCH_ID_A, SWITCH_ID_C, 3, 3)); - assertFalse(IntersectionComputer.isProtectedPathOverlaps(primarySegments, protectedSegmets)); + assertFalse(IntersectionComputer.isProtectedPathOverlaps(primarySegments, protectedSegments)); } @Test - public void shouldCalculateSharedPathWithSingleSegment() { + public void calculateSharedPathWithSingleSegmentTest() { FlowPath firstPath = FlowPath.builder() .pathId(PATH_ID) .srcSwitch(makeSwitch(SWITCH_ID_A)) @@ -263,7 +263,7 @@ public void shouldCalculateSharedPathWithSingleSegment() { } @Test - public void shouldCalculateSharedPathWithNoSegments() { + public void calculateSharedPathWithNoSegmentsTest() { FlowPath firstPath = FlowPath.builder() .pathId(PATH_ID) .srcSwitch(makeSwitch(SWITCH_ID_A)) @@ -289,7 +289,7 @@ public void shouldCalculateSharedPathWithNoSegments() { } @Test - public void shouldCalculateSharedPathAsFullPath() { + public void calculateSharedPathAsFullPathTest() { FlowPath firstPath = FlowPath.builder() .pathId(PATH_ID) .srcSwitch(makeSwitch(SWITCH_ID_A)) @@ -320,7 +320,7 @@ public void shouldCalculateSharedPathAsFullPath() { } @Test(expected = IllegalArgumentException.class) - public void failCalculateSharedPathForOnePath() { + public void failCalculateSharedPathForOnePathTest() { FlowPath firstPath = FlowPath.builder() .pathId(PATH_ID) .srcSwitch(makeSwitch(SWITCH_ID_A)) @@ -329,12 +329,12 @@ public void failCalculateSharedPathForOnePath() { buildPathSegment(NEW_PATH_ID, SWITCH_ID_A, SWITCH_ID_B, 1, 1), buildPathSegment(NEW_PATH_ID, SWITCH_ID_B, SWITCH_ID_C, 2, 2))) .build(); - IntersectionComputer.calculatePathIntersectionFromSource(asList(firstPath)); + IntersectionComputer.calculatePathIntersectionFromSource(Collections.singletonList(firstPath)); fail(); } @Test(expected = IllegalArgumentException.class) - public void failCalculateSharedPathForDifferentSources() { + public void failCalculateSharedPathForDifferentSourcesTest() { FlowPath firstPath = FlowPath.builder() .pathId(PATH_ID) .srcSwitch(makeSwitch(SWITCH_ID_A)) @@ -356,6 +356,55 @@ public void failCalculateSharedPathForDifferentSources() { fail(); } + @Test + public void oneSwitchIntersectionTest() { + List paths = getFlowPaths(); + FlowPath newPath = FlowPath.builder() + .pathId(NEW_PATH_ID) + .srcSwitch(makeSwitch(SWITCH_ID_A)) + .destSwitch(makeSwitch(SWITCH_ID_A)) + .build(); + flow2.addPaths(newPath); + paths.add(newPath); + + IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); + OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + + assertEquals(new OverlappingSegmentsStats(0, 1, 0, 33), stats); + } + + @Test + public void twoOneSwitchesIntersectionTest() { + Flow firstFlow = new TestFlowBuilder(FLOW_ID) + .srcSwitch(Switch.builder().switchId(SWITCH_ID_A).build()) + .destSwitch(Switch.builder().switchId(SWITCH_ID_A).build()) + .build(); + Flow secondFlow = new TestFlowBuilder(FLOW_ID2) + .srcSwitch(Switch.builder().switchId(SWITCH_ID_A).build()) + .destSwitch(Switch.builder().switchId(SWITCH_ID_A).build()) + .build(); + + FlowPath firstPath = FlowPath.builder() + .pathId(PATH_ID) + .srcSwitch(makeSwitch(SWITCH_ID_A)) + .destSwitch(makeSwitch(SWITCH_ID_A)) + .build(); + FlowPath secondPath = FlowPath.builder() + .pathId(NEW_PATH_ID) + .srcSwitch(makeSwitch(SWITCH_ID_A)) + .destSwitch(makeSwitch(SWITCH_ID_A)) + .build(); + + firstFlow.addPaths(firstPath); + secondFlow.addPaths(secondPath); + + IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, + asList(firstPath, secondPath)); + OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + + assertEquals(new OverlappingSegmentsStats(0, 1, 0, 100), stats); + } + private List getFlowPathSegments(List paths) { return paths.stream() .flatMap(e -> e.getSegments().stream()) diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java index 7f95504db3e..a2dcb913fa2 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java @@ -321,21 +321,6 @@ public void doesntFailOnAddingFlowToDiverseGroupWithExistingOneSwitchFlowTest() flowValidator.checkDiverseFlow(flow); } - private RequestedFlow getTestRequestWithMaxLatencyAndMaxLatencyTier2(Long maxLatency, Long maxLatencyTier2) { - return RequestedFlow.builder() - .flowId(FLOW_1) - .maxLatency(maxLatency) - .maxLatencyTier2(maxLatencyTier2) - .srcSwitch(SWITCH_ID_1) - .srcPort(10) - .srcVlan(11) - .destSwitch(SWITCH_ID_2) - .destPort(12) - .destVlan(13) - .detectConnectedDevices(new DetectConnectedDevices()) - .build(); - } - @Test(expected = InvalidFlowException.class) public void failIfMaxLatencyTier2HigherThanMaxLatency() throws InvalidFlowException { RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2((long) 1000, (long) 500); @@ -360,6 +345,21 @@ public void doesntFailIfMaxLatencyTier2andMaxLatencyAreEqual() throws InvalidFlo flowValidator.checkMaxLatencyTier(flow); } + private RequestedFlow getTestRequestWithMaxLatencyAndMaxLatencyTier2(Long maxLatency, Long maxLatencyTier2) { + return RequestedFlow.builder() + .flowId(FLOW_1) + .maxLatency(maxLatency) + .maxLatencyTier2(maxLatencyTier2) + .srcSwitch(SWITCH_ID_1) + .srcPort(10) + .srcVlan(11) + .destSwitch(SWITCH_ID_2) + .destPort(12) + .destVlan(13) + .detectConnectedDevices(new DetectConnectedDevices()) + .build(); + } + private RequestedFlow buildFlow(int srcVlan, int dstVlan, Set statVlans) { return RequestedFlow.builder() .flowId(FLOW_1) From b938cdd6c25d8ed36ad3ac62ec497f89f65ff1ad Mon Sep 17 00:00:00 2001 From: pkazlenka Date: Tue, 28 Feb 2023 14:07:30 +0100 Subject: [PATCH 38/45] #5067: Added vlan in range check for flow statistics tests * Added tests for 'create' and 'partially update' methods with non-allowed statistics vlan ids --- .../spec/flows/FlowCrudSpec.groovy | 171 +++++++++++------- 1 file changed, 105 insertions(+), 66 deletions(-) diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy index 03300eab6e4..43dd7b0c12b 100644 --- a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy @@ -1,9 +1,9 @@ package org.openkilda.functionaltests.spec.flows - import org.openkilda.functionaltests.exception.ExpectedHttpClientErrorException import org.openkilda.functionaltests.helpers.Wrappers -import org.openkilda.model.PathComputationStrategy +import org.openkilda.northbound.dto.v2.flows.FlowPatchV2 +import org.openkilda.northbound.dto.v2.flows.FlowStatistics import static groovyx.gpars.GParsPool.withPool import static org.junit.jupiter.api.Assumptions.assumeTrue @@ -68,7 +68,10 @@ class FlowCrudSpec extends HealthCheckSpecification { final static Integer IMPOSSIBLY_LOW_LATENCY = 1 final static Long IMPOSSIBLY_HIGH_BANDWIDTH = Long.MAX_VALUE - @Autowired @Shared + final static FlowStatistics FLOW_STATISTICS_CAUSING_ERROR = + new FlowStatistics([[4095, 0].shuffled().first(), 2001] as Set) + @Autowired + @Shared Provider traffExamProvider @Shared @@ -125,7 +128,9 @@ class FlowCrudSpec extends HealthCheckSpecification { } and: "Flow writes stats" - if(trafficApplicable) { statsHelper.verifyFlowWritesStats(flow.flowId, beforeTraffic, true) } + if (trafficApplicable) { + statsHelper.verifyFlowWritesStats(flow.flowId, beforeTraffic, true) + } when: "Remove the flow" flowHelperV2.deleteFlow(flow.flowId) @@ -179,7 +184,7 @@ class FlowCrudSpec extends HealthCheckSpecification { where: data << [ [ - description : "same switch-port but vlans on src and dst are swapped", + description : "same switch-port but vlans on src and dst are swapped", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch).tap { @@ -195,7 +200,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "same switch-port but vlans on src and dst are different", + description : "same switch-port but vlans on src and dst are different", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch) @@ -209,7 +214,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "vlan-port of new src = vlan-port of existing dst (+ different src)", + description : "vlan-port of new src = vlan-port of existing dst (+ different src)", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch) @@ -228,7 +233,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "vlan-port of new dst = vlan-port of existing src (but different switches)", + description : "vlan-port of new dst = vlan-port of existing src (but different switches)", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch).tap { @@ -244,7 +249,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "vlan of new dst = vlan of existing src and port of new dst = port of " + + description : "vlan of new dst = vlan of existing src and port of new dst = port of " + "existing dst", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches @@ -257,7 +262,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "default and tagged flows on the same port on dst switch", + description : "default and tagged flows on the same port on dst switch", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch) @@ -269,7 +274,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "default and tagged flows on the same port on src switch", + description : "default and tagged flows on the same port on src switch", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch) @@ -281,7 +286,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "tagged and default flows on the same port on dst switch", + description : "tagged and default flows on the same port on dst switch", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch).tap { @@ -294,7 +299,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "tagged and default flows on the same port on src switch", + description : "tagged and default flows on the same port on src switch", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch).tap { @@ -307,7 +312,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "default and tagged flows on the same ports on src and dst switches", + description : "default and tagged flows on the same ports on src and dst switches", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch) @@ -321,7 +326,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } ], [ - description : "tagged and default flows on the same ports on src and dst switches", + description : "tagged and default flows on the same ports on src and dst switches", getNotConflictingFlows: { def (Switch srcSwitch, Switch dstSwitch) = getTopology().activeSwitches def flow1 = getFlowHelperV2().randomFlow(srcSwitch, dstSwitch).tap { @@ -573,7 +578,7 @@ class FlowCrudSpec extends HealthCheckSpecification { validation.verifyRuleSectionsAreEmpty(["excess", "missing"]) def swProps = switchHelper.getCachedSwProps(it.dpId) def amountOfMultiTableRules = swProps.multiTable ? 1 : 0 - def amountOfServer42Rules = (swProps.server42FlowRtt && it.dpId in [srcSwitch.dpId,dstSwitch.dpId]) ? 1 : 0 + def amountOfServer42Rules = (swProps.server42FlowRtt && it.dpId in [srcSwitch.dpId, dstSwitch.dpId]) ? 1 : 0 if (swProps.multiTable && swProps.server42FlowRtt) { if ((flow.destination.getSwitchId() == it.dpId && flow.destination.vlanId) || (flow.source.getSwitchId() == it.dpId && flow.source.vlanId)) amountOfServer42Rules += 1 @@ -602,12 +607,13 @@ class FlowCrudSpec extends HealthCheckSpecification { !actualException && flowHelperV2.deleteFlow(flow.flowId) where: - problem | update | expectedException - "invalid encapsulation type"| - {FlowRequestV2 flowToSpoil -> - flowToSpoil.setEncapsulationType("fake") - return flowToSpoil} | - new ExpectedHttpClientErrorException(HttpStatus.BAD_REQUEST, ~/Can not parse arguments of the create flow request/) + problem | update | expectedException + "invalid encapsulation type" | + { FlowRequestV2 flowToSpoil -> + flowToSpoil.setEncapsulationType("fake") + return flowToSpoil + } | + new ExpectedHttpClientErrorException(HttpStatus.BAD_REQUEST, ~/Can not parse arguments of the create flow request/) "unavailable latency" | {FlowRequestV2 flowToSpoil -> flowToSpoil.setMaxLatency(IMPOSSIBLY_LOW_LATENCY) @@ -615,33 +621,65 @@ class FlowCrudSpec extends HealthCheckSpecification { return flowToSpoil}| new ExpectedHttpClientErrorException(HttpStatus.NOT_FOUND, ~/Latency limit: Requested path must have latency ${ - IMPOSSIBLY_LOW_LATENCY}ms or lower/) - + IMPOSSIBLY_LOW_LATENCY}ms or lower/) | + "invalid statistics vlan number" | + { FlowRequestV2 flowToSpoil -> + flowToSpoil.setStatistics(FLOW_STATISTICS_CAUSING_ERROR) + def source = flowToSpoil.getSource() + source.setVlanId(0) + flowToSpoil.setSource(source) + return flowToSpoil + } | + new ExpectedHttpClientErrorException(HttpStatus.BAD_REQUEST, + ~/To collect vlan statistics, the vlan IDs must be from 1 up to 4094/) } @Tidy + @Tags([LOW_PRIORITY]) def "Unable to update to a flow with unavailable bandwidth"() { given: "A flow" def (Switch srcSwitch, Switch dstSwitch) = topology.activeSwitches - def flow = flowHelperV2.randomFlow(srcSwitch, dstSwitch) - flowHelperV2.addFlow(flow) + def flow = flowHelperV2.addFlow(flowHelperV2.randomFlow(srcSwitch, dstSwitch)) def expectedException = new ExpectedHttpClientErrorException(HttpStatus.NOT_FOUND, ~/Not enough bandwidth or no path found. Switch ${srcSwitch.dpId.toString() } doesn't have links with enough bandwidth, Failed to find path with requested bandwidth=${IMPOSSIBLY_HIGH_BANDWIDTH}/) when: "Try to update the flow " - def flowInfo = northboundV2.getFlow(flow.flowId) - flowInfo = flowInfo.tap { it.maximumBandwidth = IMPOSSIBLY_HIGH_BANDWIDTH } - northboundV2.updateFlow(flowInfo.flowId, - flowHelperV2.toRequest(flowInfo)) + northboundV2.updateFlow(flow.getFlowId(), + flowHelperV2.toRequest(flow.tap { it.maximumBandwidth = IMPOSSIBLY_HIGH_BANDWIDTH })) then: "Flow is not updated" def actualException = thrown(HttpClientErrorException) expectedException.equals(actualException) cleanup: "Remove the flow" - Wrappers.silent {flowHelperV2.deleteFlow(flow.flowId)} + Wrappers.silent { flowHelperV2.deleteFlow(flow.flowId) } + } + + @Tidy + @Tags([LOW_PRIORITY]) + def "Unable to partially update to a flow with statistics vlan set to 0 or above 4094"() { + given: "A flow" + def (Switch srcSwitch, Switch dstSwitch) = topology.activeSwitches + def flowRequest = flowHelperV2.randomFlow(srcSwitch, dstSwitch) + flowRequest.destination.vlanId = 0 + def flow = flowHelperV2.addFlow(flowRequest) + def expectedException = new ExpectedHttpClientErrorException(HttpStatus.BAD_REQUEST, + ~/To collect vlan statistics, the vlan IDs must be from 1 up to 4094/) + + + when: "Try to partially update the flow" + def partialUpdateRequest = + northboundV2.partialUpdate(flow.getFlowId(), + new FlowPatchV2().tap {it.statistics = FLOW_STATISTICS_CAUSING_ERROR}) + + then: "Flow is not updated" + def actualException = thrown(HttpClientErrorException) + expectedException.equals(actualException) + + cleanup: "Remove the flow" + Wrappers.silent { flowHelperV2.deleteFlow(flow.flowId) } } @Tidy @@ -668,22 +706,22 @@ class FlowCrudSpec extends HealthCheckSpecification { where: data << [ [ - switchType: "source", - port : "srcPort", - errorMessage : { Isl violatedIsl -> + switchType : "source", + port : "srcPort", + errorMessage : { Isl violatedIsl -> "Could not create flow" }, - errorDescription : { Isl violatedIsl -> + errorDescription: { Isl violatedIsl -> getPortViolationError("source", violatedIsl.srcPort, violatedIsl.srcSwitch.dpId) } ], [ - switchType: "destination", - port : "dstPort", - errorMessage : { Isl violatedIsl -> + switchType : "destination", + port : "dstPort", + errorMessage : { Isl violatedIsl -> "Could not create flow" }, - errorDescription : { Isl violatedIsl -> + errorDescription: { Isl violatedIsl -> getPortViolationError("destination", violatedIsl.dstPort, violatedIsl.dstSwitch.dpId) } ] @@ -830,7 +868,7 @@ class FlowCrudSpec extends HealthCheckSpecification { @Tidy @Tags(LOW_PRIORITY) - def "System allows to set/update description/priority/max-latency for a flow"(){ + def "System allows to set/update description/priority/max-latency for a flow"() { given: "Two active neighboring switches" def switchPair = topologyHelper.getNeighboringSwitchPair() @@ -895,11 +933,11 @@ class FlowCrudSpec extends HealthCheckSpecification { def endpointName = "source" def swWithoutVxlan = swPair.src - def encapsTypesWithoutVxlan = srcProps.supportedTransitEncapsulation.collect {it.toString().toUpperCase()} + def encapsTypesWithoutVxlan = srcProps.supportedTransitEncapsulation.collect { it.toString().toUpperCase() } if (srcProps.supportedTransitEncapsulation.contains(FlowEncapsulationType.VXLAN.toString().toLowerCase())) { swWithoutVxlan = swPair.dst - encapsTypesWithoutVxlan = dstProps.supportedTransitEncapsulation.collect {it.toString().toUpperCase()} + encapsTypesWithoutVxlan = dstProps.supportedTransitEncapsulation.collect { it.toString().toUpperCase() } endpointName = "destination" } @@ -999,11 +1037,11 @@ class FlowCrudSpec extends HealthCheckSpecification { and: "Flow history shows actual info into stateBefore and stateAfter sections" def flowHistory = northbound.getFlowHistory(flow.flowId) - with(flowHistory.last().dumps.find { it.type == "stateBefore" }){ + with(flowHistory.last().dumps.find { it.type == "stateBefore" }) { it.sourcePort == flow.source.portNumber it.sourceVlan == flow.source.vlanId } - with(flowHistory.last().dumps.find { it.type == "stateAfter" }){ + with(flowHistory.last().dumps.find { it.type == "stateAfter" }) { it.sourcePort == updatedFlow.source.portNumber it.sourceVlan == updatedFlow.source.vlanId } @@ -1053,10 +1091,10 @@ class FlowCrudSpec extends HealthCheckSpecification { and: "Flow history shows actual info into stateBefore and stateAfter sections" def flowHistory2 = northbound.getFlowHistory(flow.flowId) - with(flowHistory2.last().dumps.find { it.type == "stateBefore" }){ + with(flowHistory2.last().dumps.find { it.type == "stateBefore" }) { it.destinationSwitch == dstSwitch.dpId.toString() } - with(flowHistory2.last().dumps.find { it.type == "stateAfter" }){ + with(flowHistory2.last().dumps.find { it.type == "stateAfter" }) { it.destinationSwitch == newDstSwitch.dpId.toString() } @@ -1207,9 +1245,9 @@ class FlowCrudSpec extends HealthCheckSpecification { then: "Update the flow: port number and vlanId on the src/dst endpoints" def updatedFlow = flow.jacksonCopy().tap { - it.source.portNumber = activeTraffGens.find { it.switchConnected.dpId == switchPair.src.dpId}.switchPort + it.source.portNumber = activeTraffGens.find { it.switchConnected.dpId == switchPair.src.dpId }.switchPort it.source.vlanId = updatedFlowDstEndpoint.source.vlanId - 1 - it.destination.portNumber = activeTraffGens.find { it.switchConnected.dpId == switchPair.dst.dpId}.switchPort + it.destination.portNumber = activeTraffGens.find { it.switchConnected.dpId == switchPair.dst.dpId }.switchPort it.destination.vlanId = updatedFlowDstEndpoint.destination.vlanId - 1 } flowHelperV2.updateFlow(flow.flowId, updatedFlow) @@ -1271,7 +1309,8 @@ class FlowCrudSpec extends HealthCheckSpecification { when: "Update the dst endpoint to make this flow as multi switch flow" def newPortNumber = topology.getAllowedPortsForSwitch(topology.activeSwitches.find { - it.dpId == swPair.dst.dpId } + it.dpId == swPair.dst.dpId + } ).first() flowHelperV2.updateFlow(flow.flowId, flow.tap { it.destination.switchId = swPair.dst.dpId @@ -1292,7 +1331,7 @@ class FlowCrudSpec extends HealthCheckSpecification { } and: "Involved switches pass switch validation" - [swPair.src, swPair.dst].each {sw -> + [swPair.src, swPair.dst].each { sw -> with(northbound.validateSwitch(sw.dpId)) { validation -> validation.verifyRuleSectionsAreEmpty(["missing", "excess", "misconfigured"]) validation.verifyMeterSectionsAreEmpty(["missing", "excess", "misconfigured"]) @@ -1508,12 +1547,12 @@ class FlowCrudSpec extends HealthCheckSpecification { flowToConflict.source.portNumber = dominantFlow.source.portNumber flowToConflict.source.vlanId = dominantFlow.source.vlanId }, - getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> "Could not $operation flow" }, - getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> errorDescription(operation, dominantFlow, "source", flowToConflict, "source") } ], @@ -1523,12 +1562,12 @@ class FlowCrudSpec extends HealthCheckSpecification { flowToConflict.destination.portNumber = dominantFlow.destination.portNumber flowToConflict.destination.vlanId = dominantFlow.destination.vlanId }, - getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> "Could not $operation flow" }, - getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> errorDescription(operation, dominantFlow, "destination", flowToConflict, "destination") } ], @@ -1539,12 +1578,12 @@ class FlowCrudSpec extends HealthCheckSpecification { flowToConflict.source.vlanId = 0 dominantFlow.source.vlanId = 0 }, - getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> "Could not $operation flow" }, - getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> errorDescription(operation, dominantFlow, "source", flowToConflict, "source") } ], @@ -1555,12 +1594,12 @@ class FlowCrudSpec extends HealthCheckSpecification { flowToConflict.destination.vlanId = 0 dominantFlow.destination.vlanId = 0 }, - getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorMessage : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> "Could not $operation flow" }, - getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, - String operation = "create" -> + getErrorDescription : { FlowRequestV2 dominantFlow, FlowRequestV2 flowToConflict, + String operation = "create" -> errorDescription(operation, dominantFlow, "destination", flowToConflict, "destination") } ] From 50078ebdaf92cd8a026dd2ca0bd277ddf7fb8b58 Mon Sep 17 00:00:00 2001 From: pkazlenka Date: Thu, 2 Mar 2023 09:19:57 +0100 Subject: [PATCH 39/45] #5067: Added vlan in range check for flow statistics tests * Fixed import error --- .../openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy index 43dd7b0c12b..a12f2117265 100644 --- a/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy +++ b/src-java/testing/functional-tests/src/test/groovy/org/openkilda/functionaltests/spec/flows/FlowCrudSpec.groovy @@ -2,6 +2,7 @@ package org.openkilda.functionaltests.spec.flows import org.openkilda.functionaltests.exception.ExpectedHttpClientErrorException import org.openkilda.functionaltests.helpers.Wrappers +import org.openkilda.model.PathComputationStrategy import org.openkilda.northbound.dto.v2.flows.FlowPatchV2 import org.openkilda.northbound.dto.v2.flows.FlowStatistics @@ -621,7 +622,7 @@ class FlowCrudSpec extends HealthCheckSpecification { return flowToSpoil}| new ExpectedHttpClientErrorException(HttpStatus.NOT_FOUND, ~/Latency limit: Requested path must have latency ${ - IMPOSSIBLY_LOW_LATENCY}ms or lower/) | + IMPOSSIBLY_LOW_LATENCY}ms or lower/) "invalid statistics vlan number" | { FlowRequestV2 flowToSpoil -> flowToSpoil.setStatistics(FLOW_STATISTICS_CAUSING_ERROR) From c34b59ce21f746f33e68642f3db5e1c5208ce01f Mon Sep 17 00:00:00 2001 From: dmitrii-beliakov Date: Fri, 17 Mar 2023 10:47:28 +0100 Subject: [PATCH 40/45] Add V2 API /network/path/check to validate whether an arbitrary path complies with certain requirements (issue #4527) --- .../path-validation/path-validation.md | 97 +++ .../base-topology/base-messaging/build.gradle | 1 + .../info/network/PathValidationResult.java | 35 + .../messaging/payload/network/PathDto.java | 2 + .../network/PathValidationPayload.java | 59 ++ .../command/flow/PathValidateRequest.java | 33 + .../openkilda/model/PathValidationData.java | 49 ++ .../java/org/openkilda/pce/PathComputer.java | 13 +- .../org/openkilda/pce/finder/PathFinder.java | 17 +- .../mappers/FlowEncapsulationTypeMapper.java | 31 + .../mappers/PathValidationDataMapper.java | 67 ++ .../topology/nbworker/bolts/PathsBolt.java | 3 + .../topology/nbworker/bolts/RouterBolt.java | 3 +- .../nbworker/services/PathsService.java | 41 +- .../nbworker/validators/PathValidator.java | 600 ++++++++++++++ .../nbworker/services/PathsServiceTest.java | 1 - .../validators/PathValidatorTest.java | 729 ++++++++++++++++++ .../dto/v2/flows/PathValidateResponse.java | 33 + .../controller/v1/NetworkController.java | 6 +- .../controller/v2/NetworkControllerV2.java | 67 ++ .../northbound/converter/PathMapper.java | 4 + .../northbound/service/NetworkService.java | 13 +- .../service/impl/NetworkServiceImpl.java | 22 + 23 files changed, 1904 insertions(+), 22 deletions(-) create mode 100644 docs/design/solutions/path-validation/path-validation.md create mode 100644 src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidationResult.java create mode 100644 src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationPayload.java create mode 100644 src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java create mode 100644 src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java create mode 100644 src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/FlowEncapsulationTypeMapper.java create mode 100644 src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java create mode 100644 src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java create mode 100644 src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java create mode 100644 src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java create mode 100644 src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java diff --git a/docs/design/solutions/path-validation/path-validation.md b/docs/design/solutions/path-validation/path-validation.md new file mode 100644 index 00000000000..53b40af1aa9 --- /dev/null +++ b/docs/design/solutions/path-validation/path-validation.md @@ -0,0 +1,97 @@ +# Path Validation + +## Motivation +The main use case is to allow users to verify whether some arbitrary flow path is possible to create with the given +constraints without actually creating this flow. A path here is a sequence of nodes, that is a sequence of switches +with in and out ports. Constraints are various parameters such as max latency, bandwidth, encapsulation type, +path computation strategy, and other. + +## Implementation details +The validation of a path is done for each segment and each validation type individually. This way, the validation +collects all errors on the path and returns them all in a single response. The response is concise and formed +in human-readable format. + +There is no locking of resources for this path and, therefore, no guarantee that it will be possible to create this flow +after validation in the future. + +## Northbound API + +REST URL: ```/v2/network/path/check```, method: ```GET``` + +A user is required to provide a path represented by a list of nodes. Nodes must be ordered from start to end; +each next element is the next hop. A user can add optional parameters: +- `encapsulation_type`: enum value "TRANSIT_VLAN" or "VXLAN". API returns an error for every switch in the list if it + doesn't support the given encapsulation type. +- `max_bandwidth`: bandwidth required for this path. API returns an error for each segment of the given path when the + available bandwidth on the segment is less than the given value. When used in combination with `reuse_flow_resources`, + available bandwidth as if the given flow doesn't consume any bandwidth. +- `max_latency`: the first tier latency value in milliseconds. API returns an error for each segment of the given path, + which max latency is greater than the given value. +- `max_latency_tier2`: the second tier latency value in milliseconds. API returns an error for each segment of the given + path, which max latency is greater than the given value. +- `path_computation_strategy`: an enum value PathComputationStrategy. API will return different set of errors depending + on the selected strategy. For example, when COST is selected API ignores available bandwidth and latency parameters. + If none is selected, all validations are executed. +- `reuse_flow_resources`: a flow ID. Verify the given path as if it is created instead of the existing flow, that is as + if the resources of some flow are released before validation. Returns an error if this flow doesn't exist. +- `diverse_with_flow`: a flow ID. Verify whether the given path intersects with the given flow. API returns an error for + each common segment. Returns an error if this flow doesn't exist. + +### Request Body +```json +{ + "encapsulation_type": "TRANSIT_VLAN|VXLAN", + "max_bandwidth": 0, + "max_latency": 0, + "max_latency_tier2": 0, + "path_computation_strategy": "COST|LATENCY|MAX_LATENCY|COST_AND_AVAILABLE_BANDWIDTH", + "reuse_flow_resources": "flow_id", + "diverse_with_flow": "diverse_flow_id", + "nodes": [ + { + "switch_id": "00:00:00:00:00:00:00:01", + "output_port": 0 + }, + { + "switch_id": "00:00:00:00:00:00:00:02", + "input_port": 0, + "output_port": 1 + }, + { + "switch_id": "00:00:00:00:00:00:00:03", + "input_port": 0 + } + ] +} +``` + +### Responses + +Code: 200 + +Content: +- is_valid: True if the path is valid and conform to the strategy, false otherwise +- errors: List of strings with error descriptions if any +- correlation_id: Correlation ID from correlation_id Header +```json +{ + "correlation_id": "string", + "is_valid": "bool", + "errors": [ + "error0", + "errorN" + ] +} +``` + +Codes: 4XX,5XX +```json +{ + "correlation_id": "string", + "error-description": "string", + "error-message": "string", + "error-type": "string", + "timestamp": 0 +} +``` + diff --git a/src-java/base-topology/base-messaging/build.gradle b/src-java/base-topology/base-messaging/build.gradle index 0488bb67a9b..1ccedc0f36a 100644 --- a/src-java/base-topology/base-messaging/build.gradle +++ b/src-java/base-topology/base-messaging/build.gradle @@ -15,6 +15,7 @@ dependencies { implementation 'com.google.guava:guava' implementation 'org.apache.commons:commons-lang3' implementation 'org.slf4j:slf4j-api' + implementation 'javax.validation:validation-api' testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.vintage:junit-vintage-engine' diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidationResult.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidationResult.java new file mode 100644 index 00000000000..0c073674d24 --- /dev/null +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/info/network/PathValidationResult.java @@ -0,0 +1,35 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.messaging.info.network; + +import org.openkilda.messaging.info.InfoData; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Value; + +import java.util.List; + +@Value +@Builder +@EqualsAndHashCode(callSuper = false) +@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) +public class PathValidationResult extends InfoData { + Boolean isValid; + List errors; +} diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathDto.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathDto.java index d6084efa10b..d40c9878e43 100644 --- a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathDto.java +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathDto.java @@ -20,12 +20,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; import lombok.Value; import java.time.Duration; import java.util.List; @Value +@Builder @JsonInclude(JsonInclude.Include.NON_NULL) public class PathDto { @JsonProperty("bandwidth") diff --git a/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationPayload.java b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationPayload.java new file mode 100644 index 00000000000..237de4b2877 --- /dev/null +++ b/src-java/base-topology/base-messaging/src/main/java/org/openkilda/messaging/payload/network/PathValidationPayload.java @@ -0,0 +1,59 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.messaging.payload.network; + +import org.openkilda.messaging.payload.flow.FlowEncapsulationType; +import org.openkilda.messaging.payload.flow.PathNodePayload; +import org.openkilda.model.PathComputationStrategy; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; +import javax.validation.constraints.PositiveOrZero; + +@Value +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PathValidationPayload { + @JsonProperty("bandwidth") + Long bandwidth; + + @JsonProperty("max_latency") + @PositiveOrZero(message = "max_latency cannot be negative") + Long latencyMs; + + @JsonProperty("max_latency_tier2") + @PositiveOrZero(message = "max_latency_tier2 cannot be negative") + Long latencyTier2ms; + + @JsonProperty("nodes") + List nodes; + + @JsonProperty("diverse_with_flow") + String diverseWithFlow; + + @JsonProperty("reuse_flow_resources") + String reuseFlowResources; + + @JsonProperty("flow_encapsulation_type") + FlowEncapsulationType flowEncapsulationType; + + @JsonProperty("path_computation_strategy") + PathComputationStrategy pathComputationStrategy; +} diff --git a/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java b/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java new file mode 100644 index 00000000000..e63f228eef4 --- /dev/null +++ b/src-java/flowhs-topology/flowhs-messaging/src/main/java/org/openkilda/messaging/command/flow/PathValidateRequest.java @@ -0,0 +1,33 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.messaging.command.flow; + +import org.openkilda.messaging.nbtopology.annotations.ReadRequest; +import org.openkilda.messaging.nbtopology.request.BaseRequest; +import org.openkilda.messaging.payload.network.PathValidationPayload; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +/** + * Request to validate that the given path is possible to create with the given constraints and resources availability. + */ +@Value +@ReadRequest +@EqualsAndHashCode(callSuper = true) +public class PathValidateRequest extends BaseRequest { + PathValidationPayload pathValidationPayload; +} diff --git a/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java b/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java new file mode 100644 index 00000000000..b11717e7dca --- /dev/null +++ b/src-java/kilda-model/src/main/java/org/openkilda/model/PathValidationData.java @@ -0,0 +1,49 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.model; + +import lombok.Builder; +import lombok.Value; + +import java.time.Duration; +import java.util.List; + +@Value +@Builder +public class PathValidationData { + + Long bandwidth; + Duration latency; + Duration latencyTier2; + List pathSegments; + String diverseWithFlow; + String reuseFlowResources; + FlowEncapsulationType flowEncapsulationType; + SwitchId srcSwitchId; + Integer srcPort; + SwitchId destSwitchId; + Integer destPort; + PathComputationStrategy pathComputationStrategy; + + @Value + @Builder + public static class PathSegmentValidationData { + SwitchId srcSwitchId; + Integer srcPort; + SwitchId destSwitchId; + Integer destPort; + } +} diff --git a/src-java/kilda-pce/src/main/java/org/openkilda/pce/PathComputer.java b/src-java/kilda-pce/src/main/java/org/openkilda/pce/PathComputer.java index 373cbbac34a..0e3080fdecd 100644 --- a/src-java/kilda-pce/src/main/java/org/openkilda/pce/PathComputer.java +++ b/src-java/kilda-pce/src/main/java/org/openkilda/pce/PathComputer.java @@ -30,12 +30,12 @@ import java.util.List; /** - * Represents computation operations on flow path. + * Represents computation operations on flow paths. */ public interface PathComputer { /** - * Gets path between source and destination switches for specified flow. The path is built over available ISLs + * Gets a path between source and destination switches for a specified flow. The path is built over available ISLs * only. * * @param flow the {@link Flow} instance @@ -46,7 +46,7 @@ default GetPathsResult getPath(Flow flow) throws UnroutableFlowException, Recove } /** - * Gets path between source and destination switch for specified flow. + * Gets a path between source and destination switches for a specified flow. * * @param flow the {@link Flow} instance. * @param reusePathsResources allow already allocated path resources (bandwidth) @@ -57,14 +57,17 @@ GetPathsResult getPath(Flow flow, Collection reusePathsResources, boolea throws UnroutableFlowException, RecoverableException; /** - * Gets N best paths. + * Gets the best N paths. N is a number, not greater than the count param, of all paths that can be found. * * @param srcSwitch source switchId * @param dstSwitch destination switchId + * @param count calculates no more than this number of paths * @param flowEncapsulationType target encapsulation type + * @param pathComputationStrategy depending on this strategy, different weight functions are used + * to determine the best path * @param maxLatency max latency * @param maxLatencyTier2 max latency tier2 - * @return an list of N (or less) best paths ordered from best to worst. + * @return a list of the best N paths ordered from best to worst. */ List getNPaths(SwitchId srcSwitch, SwitchId dstSwitch, int count, FlowEncapsulationType flowEncapsulationType, PathComputationStrategy pathComputationStrategy, diff --git a/src-java/kilda-pce/src/main/java/org/openkilda/pce/finder/PathFinder.java b/src-java/kilda-pce/src/main/java/org/openkilda/pce/finder/PathFinder.java index 8e818adb64f..188087c8564 100644 --- a/src-java/kilda-pce/src/main/java/org/openkilda/pce/finder/PathFinder.java +++ b/src-java/kilda-pce/src/main/java/org/openkilda/pce/finder/PathFinder.java @@ -43,7 +43,7 @@ FindPathResult findPathWithMinWeight(AvailableNetwork network, * * @return a pair of ordered lists that represents the path from start to end, or an empty list if no path found. * Returns backUpPathComputationWayUsed = true if found path has latency greater than maxLatency. - * Returns empty path if found path has latency greater than latencyLimit. + * Returns an empty path if the found path has latency greater than latencyLimit. */ FindPathResult findPathWithMinWeightAndLatencyLimits(AvailableNetwork network, SwitchId startSwitchId, SwitchId endSwitchId, @@ -64,18 +64,25 @@ FindPathResult findPathWithWeightCloseToMaxWeight(AvailableNetwork network, throws UnroutableFlowException; /** - * Find N (or less) best paths. + * Find the best N paths. + * N is a number, not greater than count, of all paths that can be found. * - * @return an list of N (or less) best paths. + * @param startSwitchId source switchId + * @param endSwitchId destination switchId + * @param network available network + * @param count find no more than this number of paths + * @param weightFunction use this weight function for the path computation + * @return a list of the best N paths. */ List findNPathsBetweenSwitches( AvailableNetwork network, SwitchId startSwitchId, SwitchId endSwitchId, int count, WeightFunction weightFunction) throws UnroutableFlowException; /** - * Find N (or less) best paths wih max weight restrictions. + * Find the best N paths with max weight restrictions. + * N is a number, not greater than count, of all paths that can be found. * - * @return an list of N (or less) best paths. + * @return a list of the best N paths. */ List findNPathsBetweenSwitches( AvailableNetwork network, SwitchId startSwitchId, SwitchId endSwitchId, int count, diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/FlowEncapsulationTypeMapper.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/FlowEncapsulationTypeMapper.java new file mode 100644 index 00000000000..8f5d28839a1 --- /dev/null +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/FlowEncapsulationTypeMapper.java @@ -0,0 +1,31 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.wfm.share.mappers; + +import org.openkilda.model.FlowEncapsulationType; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface FlowEncapsulationTypeMapper { + + FlowEncapsulationTypeMapper INSTANCE = Mappers.getMapper(FlowEncapsulationTypeMapper.class); + + FlowEncapsulationType toOpenKildaModel( + org.openkilda.messaging.payload.flow.FlowEncapsulationType flowEncapsulationType); + +} diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java new file mode 100644 index 00000000000..43eb995c4e2 --- /dev/null +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/share/mappers/PathValidationDataMapper.java @@ -0,0 +1,67 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.wfm.share.mappers; + +import org.openkilda.messaging.payload.network.PathValidationPayload; +import org.openkilda.model.PathValidationData; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.time.Duration; +import java.util.LinkedList; +import java.util.List; + +@Mapper +public abstract class PathValidationDataMapper { + + public static PathValidationDataMapper INSTANCE = Mappers.getMapper(PathValidationDataMapper.class); + + /** + * Converts NB PathValidationDto to messaging PathValidationData. + * @param pathValidationPayload NB representation of a path validation data + * @return the messaging representation of a path validation data + */ + public PathValidationData toPathValidationData(PathValidationPayload pathValidationPayload) { + List segments = new LinkedList<>(); + + for (int i = 0; i < pathValidationPayload.getNodes().size() - 1; i++) { + segments.add(PathValidationData.PathSegmentValidationData.builder() + .srcSwitchId(pathValidationPayload.getNodes().get(i).getSwitchId()) + .srcPort(pathValidationPayload.getNodes().get(i).getOutputPort()) + .destSwitchId(pathValidationPayload.getNodes().get(i + 1).getSwitchId()) + .destPort(pathValidationPayload.getNodes().get(i + 1).getInputPort()) + .build()); + } + + return PathValidationData.builder() + .srcSwitchId(pathValidationPayload.getNodes().get(0).getSwitchId()) + .destSwitchId( + pathValidationPayload.getNodes().get(pathValidationPayload.getNodes().size() - 1).getSwitchId()) + .bandwidth(pathValidationPayload.getBandwidth()) + .latency(pathValidationPayload.getLatencyMs() == null ? null : + Duration.ofMillis(pathValidationPayload.getLatencyMs())) + .latencyTier2(pathValidationPayload.getLatencyTier2ms() == null ? null : + Duration.ofMillis(pathValidationPayload.getLatencyTier2ms())) + .diverseWithFlow(pathValidationPayload.getDiverseWithFlow()) + .reuseFlowResources(pathValidationPayload.getReuseFlowResources()) + .flowEncapsulationType(FlowEncapsulationTypeMapper.INSTANCE.toOpenKildaModel( + pathValidationPayload.getFlowEncapsulationType())) + .pathComputationStrategy(pathValidationPayload.getPathComputationStrategy()) + .pathSegments(segments) + .build(); + } +} diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java index f87cfa2f05b..49bbacc4ade 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/PathsBolt.java @@ -15,6 +15,7 @@ package org.openkilda.wfm.topology.nbworker.bolts; +import org.openkilda.messaging.command.flow.PathValidateRequest; import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.error.MessageException; import org.openkilda.messaging.info.InfoData; @@ -55,6 +56,8 @@ List processRequest(Tuple tuple, BaseRequest request) { List result = null; if (request instanceof GetPathsRequest) { result = getPaths((GetPathsRequest) request); + } else if (request instanceof PathValidateRequest) { + result = pathService.validatePath((PathValidateRequest) request); } else { unhandledInput(tuple); } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java index ecf83aecf44..821bc643e06 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/bolts/RouterBolt.java @@ -20,6 +20,7 @@ import org.openkilda.messaging.Message; import org.openkilda.messaging.command.CommandData; import org.openkilda.messaging.command.CommandMessage; +import org.openkilda.messaging.command.flow.PathValidateRequest; import org.openkilda.messaging.error.ErrorMessage; import org.openkilda.messaging.info.InfoMessage; import org.openkilda.messaging.nbtopology.request.BaseRequest; @@ -88,7 +89,7 @@ private void processRequest(Tuple input, String key, BaseRequest request) { emitWithContext(StreamType.FEATURE_TOGGLES.toString(), input, new Values(request)); } else if (request instanceof KildaConfigurationBaseRequest) { emitWithContext(StreamType.KILDA_CONFIG.toString(), input, new Values(request)); - } else if (request instanceof GetPathsRequest) { + } else if (request instanceof GetPathsRequest || request instanceof PathValidateRequest) { emitWithContext(StreamType.PATHS.toString(), input, new Values(request)); } else if (request instanceof HistoryRequest) { emitWithContext(StreamType.HISTORY.toString(), input, new Values(request)); diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java index 2c122bb1f10..a3e143d7f95 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/PathsService.java @@ -15,6 +15,8 @@ package org.openkilda.wfm.topology.nbworker.services; +import org.openkilda.messaging.command.flow.PathValidateRequest; +import org.openkilda.messaging.info.network.PathValidationResult; import org.openkilda.messaging.info.network.PathsInfoData; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.KildaConfiguration; @@ -28,6 +30,8 @@ import org.openkilda.pce.PathComputerFactory; import org.openkilda.pce.exception.RecoverableException; import org.openkilda.pce.exception.UnroutableFlowException; +import org.openkilda.persistence.repositories.FlowRepository; +import org.openkilda.persistence.repositories.IslRepository; import org.openkilda.persistence.repositories.KildaConfigurationRepository; import org.openkilda.persistence.repositories.RepositoryFactory; import org.openkilda.persistence.repositories.SwitchPropertiesRepository; @@ -35,10 +39,13 @@ import org.openkilda.wfm.error.SwitchNotFoundException; import org.openkilda.wfm.error.SwitchPropertiesNotFoundException; import org.openkilda.wfm.share.mappers.PathMapper; +import org.openkilda.wfm.share.mappers.PathValidationDataMapper; +import org.openkilda.wfm.topology.nbworker.validators.PathValidator; import lombok.extern.slf4j.Slf4j; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -47,15 +54,19 @@ @Slf4j public class PathsService { private final int defaultMaxPathCount; - private PathComputer pathComputer; - private SwitchRepository switchRepository; - private SwitchPropertiesRepository switchPropertiesRepository; - private KildaConfigurationRepository kildaConfigurationRepository; + private final PathComputer pathComputer; + private final SwitchRepository switchRepository; + private final SwitchPropertiesRepository switchPropertiesRepository; + private final KildaConfigurationRepository kildaConfigurationRepository; + private final IslRepository islRepository; + private final FlowRepository flowRepository; public PathsService(RepositoryFactory repositoryFactory, PathComputerConfig pathComputerConfig) { switchRepository = repositoryFactory.createSwitchRepository(); switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); kildaConfigurationRepository = repositoryFactory.createKildaConfigurationRepository(); + this.islRepository = repositoryFactory.createIslRepository(); + this.flowRepository = repositoryFactory.createFlowRepository(); PathComputerFactory pathComputerFactory = new PathComputerFactory( pathComputerConfig, new AvailableNetworkFactory(pathComputerConfig, repositoryFactory)); pathComputer = pathComputerFactory.getPathComputer(); @@ -87,7 +98,7 @@ public List getPaths( SwitchProperties srcProperties = switchPropertiesRepository.findBySwitchId(srcSwitchId).orElseThrow( () -> new SwitchPropertiesNotFoundException(srcSwitchId)); if (!srcProperties.getSupportedTransitEncapsulation().contains(flowEncapsulationType)) { - throw new IllegalArgumentException(String.format("Switch %s doesn't support %s encapslation type. Choose " + throw new IllegalArgumentException(String.format("Switch %s doesn't support %s encapsulation type. Choose " + "one of the supported encapsulation types %s or update switch properties and add needed " + "encapsulation type.", srcSwitchId, flowEncapsulationType, srcProperties.getSupportedTransitEncapsulation())); @@ -96,7 +107,7 @@ public List getPaths( SwitchProperties dstProperties = switchPropertiesRepository.findBySwitchId(dstSwitchId).orElseThrow( () -> new SwitchPropertiesNotFoundException(dstSwitchId)); if (!dstProperties.getSupportedTransitEncapsulation().contains(flowEncapsulationType)) { - throw new IllegalArgumentException(String.format("Switch %s doesn't support %s encapslation type. Choose " + throw new IllegalArgumentException(String.format("Switch %s doesn't support %s encapsulation type. Choose " + "one of the supported encapsulation types %s or update switch properties and add needed " + "encapsulation type.", dstSwitchId, requestEncapsulationType, dstProperties.getSupportedTransitEncapsulation())); @@ -120,4 +131,22 @@ public List getPaths( .map(path -> PathsInfoData.builder().path(path).build()) .collect(Collectors.toList()); } + + /** + * This method validates a path and collects errors if any. Validations depend on the information in the request. + * For example, if the request doesn't contain latency, the path will not be validated using max latency strategy. + * @param request request containing the path and parameters to validate + * @return a response with the success or the list of errors + */ + public List validatePath(PathValidateRequest request) { + PathValidator pathValidator = new PathValidator(islRepository, + flowRepository, + switchPropertiesRepository, + switchRepository, + kildaConfigurationRepository.getOrDefault()); + + return Collections.singletonList(pathValidator.validatePath( + PathValidationDataMapper.INSTANCE.toPathValidationData(request.getPathValidationPayload()) + )); + } } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java new file mode 100644 index 00000000000..d35b2868e96 --- /dev/null +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/validators/PathValidator.java @@ -0,0 +1,600 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.wfm.topology.nbworker.validators; + +import org.openkilda.messaging.info.network.PathValidationResult; +import org.openkilda.model.Flow; +import org.openkilda.model.FlowEncapsulationType; +import org.openkilda.model.FlowPath; +import org.openkilda.model.Isl; +import org.openkilda.model.IslStatus; +import org.openkilda.model.KildaConfiguration; +import org.openkilda.model.PathComputationStrategy; +import org.openkilda.model.PathSegment; +import org.openkilda.model.PathValidationData; +import org.openkilda.model.PathValidationData.PathSegmentValidationData; +import org.openkilda.model.Switch; +import org.openkilda.model.SwitchId; +import org.openkilda.model.SwitchProperties; +import org.openkilda.persistence.repositories.FlowRepository; +import org.openkilda.persistence.repositories.IslRepository; +import org.openkilda.persistence.repositories.IslRepository.IslEndpoints; +import org.openkilda.persistence.repositories.SwitchPropertiesRepository; +import org.openkilda.persistence.repositories.SwitchRepository; + +import com.google.common.collect.Sets; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.commons.collections4.map.LazyMap; +import org.apache.commons.lang3.StringUtils; +import org.apache.storm.shade.com.google.common.collect.Maps; + +import java.time.Duration; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class PathValidator { + + private static final String LATENCY = "latency"; + private static final String LATENCY_TIER_2 = "latency tier 2"; + private final IslRepository islRepository; + private final FlowRepository flowRepository; + private final SwitchPropertiesRepository switchPropertiesRepository; + private final SwitchRepository switchRepository; + private final KildaConfiguration kildaConfiguration; + private Map> islCache; + private Map> switchCache; + + public PathValidator(IslRepository islRepository, + FlowRepository flowRepository, + SwitchPropertiesRepository switchPropertiesRepository, + SwitchRepository switchRepository, KildaConfiguration kildaConfiguration) { + this.islRepository = islRepository; + this.flowRepository = flowRepository; + this.switchPropertiesRepository = switchPropertiesRepository; + this.switchRepository = switchRepository; + this.kildaConfiguration = kildaConfiguration; + } + + /** + * Validates whether it is possible to create a path with the given parameters. When there are obstacles, this + * validator returns all errors found on each segment. For example, when there is a path 1-2-3-4 and there is + * no link between 1 and 2, no sufficient latency between 2 and 3, and not enough bandwidth between 3-4, then + * this validator returns all 3 errors. + * + * @param pathValidationData path parameters to validate. + * @return a response object containing the validation result and errors if any + */ + public PathValidationResult validatePath(PathValidationData pathValidationData) { + Set result = pathValidationData.getPathSegments().stream() + .map(segment -> executeValidations( + InputData.of(pathValidationData, segment), + getPerSegmentValidations(pathValidationData))) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + + result.addAll(executeValidations(InputData.of(pathValidationData), + getPerPathValidations(pathValidationData))); + + return PathValidationResult.builder() + .isValid(result.isEmpty()) + .errors(new LinkedList<>(result)) + .build(); + } + + private Optional findIslByEndpoints(SwitchId srcSwitchId, int srcPort, SwitchId destSwitchId, int destPort) { + if (islCache == null) { + islCache = LazyMap.lazyMap(Maps.newHashMap(), endpoints -> islRepository.findByEndpoints( + new SwitchId(endpoints.getSrcSwitch()), endpoints.getSrcPort(), + new SwitchId(endpoints.getDestSwitch()), endpoints.getDestPort())); + } + return islCache.get(new IslEndpoints(srcSwitchId.toString(), srcPort, destSwitchId.toString(), destPort)); + } + + private Map findSwitchesByIds(Set switchIds) { + if (switchCache == null) { + switchCache = LazyMap.lazyMap(Maps.newHashMap(), switchRepository::findById); + } + + return switchIds.stream() + .map(switchCache::get) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toMap(Switch::getSwitchId, Function.identity())); + } + + private Set executeValidations(InputData inputData, + List>> validations) { + + return validations.stream() + .map(f -> f.apply(inputData)) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + } + + private List>> getPerPathValidations( + PathValidationData pathValidationData) { + List>> validationFunctions = new LinkedList<>(); + + if (isLatencyValidationRequired(pathValidationData)) { + validationFunctions.add(this::validateLatency); + } + + if (isLatencyTier2ValidationRequired(pathValidationData)) { + validationFunctions.add(this::validateLatencyTier2); + } + + return validationFunctions; + } + + private List>> getPerSegmentValidations( + PathValidationData pathValidationData) { + List>> validationFunctions = new LinkedList<>(); + + validationFunctions.add(this::validateForwardAndReverseLinks); + + if (isBandwidthValidationRequired(pathValidationData)) { + validationFunctions.add(this::validateBandwidth); + } + + if (isDiverseWithFlowValidationRequired(pathValidationData)) { + validationFunctions.add(this::validateDiverseWithFlow); + } + + if (isEncapsulationTypeValidationRequired(pathValidationData)) { + validationFunctions.add(this::validateEncapsulationType); + } + + return validationFunctions; + } + + private boolean isEncapsulationTypeValidationRequired(PathValidationData pathValidationData) { + return getOrDefaultFlowEncapsulationType(pathValidationData) != null; + } + + private boolean isDiverseWithFlowValidationRequired(PathValidationData pathValidationData) { + return StringUtils.isNotBlank(pathValidationData.getDiverseWithFlow()); + } + + private boolean isBandwidthValidationRequired(PathValidationData pathValidationData) { + return pathValidationData.getBandwidth() != null + && pathValidationData.getBandwidth() != 0; + } + + private boolean isLatencyTier2ValidationRequired(PathValidationData pathValidationData) { + return pathValidationData.getLatencyTier2() != null + && !pathValidationData.getLatencyTier2().isZero() + && (getOrDefaultPathComputationStrategy(pathValidationData) == PathComputationStrategy.LATENCY + || getOrDefaultPathComputationStrategy(pathValidationData) == PathComputationStrategy.MAX_LATENCY); + } + + private boolean isLatencyValidationRequired(PathValidationData pathValidationData) { + return pathValidationData.getLatency() != null + && !pathValidationData.getLatency().isZero() + && (getOrDefaultPathComputationStrategy(pathValidationData) == PathComputationStrategy.LATENCY + || getOrDefaultPathComputationStrategy(pathValidationData) == PathComputationStrategy.MAX_LATENCY); + } + + private Set validateForwardAndReverseLinks(InputData inputData) { + Map switchMap = findSwitchesByIds( + Sets.newHashSet(inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getDestSwitchId())); + Set errors = Sets.newHashSet(); + if (!switchMap.containsKey(inputData.getSegment().getSrcSwitchId())) { + errors.add(getSrcSwitchNotFoundError(inputData)); + } + if (!switchMap.containsKey(inputData.getSegment().getDestSwitchId())) { + errors.add(getDestSwitchNotFoundError(inputData)); + } + if (!errors.isEmpty()) { + return errors; + } + + errors.addAll(validateForwardLink(inputData)); + errors.addAll(validateReverseLink(inputData)); + + return errors; + } + + private Set validateForwardLink(InputData inputData) { + return validateLink( + inputData.getSegment().getSrcSwitchId(), + inputData.getSegment().getSrcPort(), + inputData.getSegment().getDestSwitchId(), + inputData.getSegment().getDestPort(), + inputData, + this::getNoForwardIslError, this::getForwardIslNotActiveError); + } + + private Set validateReverseLink(InputData inputData) { + return validateLink( + inputData.getSegment().getDestSwitchId(), + inputData.getSegment().getDestPort(), + inputData.getSegment().getSrcSwitchId(), + inputData.getSegment().getSrcPort(), + inputData, + this::getNoReverseIslError, this::getReverseIslNotActiveError); + } + + private Set validateLink(SwitchId srcSwitchId, int srcPort, SwitchId destSwitchId, int destPort, + InputData inputData, + Function noLinkErrorProducer, + Function linkNotActiveErrorProducer) { + Optional isl = getIslByEndPoints(srcSwitchId, srcPort, destSwitchId, destPort); + Set errors = Sets.newHashSet(); + if (!isl.isPresent()) { + errors.add(noLinkErrorProducer.apply(inputData)); + } + if (isl.isPresent() && isl.get().getStatus() != IslStatus.ACTIVE) { + errors.add(linkNotActiveErrorProducer.apply(inputData)); + } + + return errors; + } + + private Optional getIslByEndPoints(SwitchId srcSwitchId, int srcPort, SwitchId destSwitchId, int destPort) { + return findIslByEndpoints(srcSwitchId, srcPort, destSwitchId, destPort); + } + + private Optional getForwardIslOfSegmentData(PathSegmentValidationData data) { + return findIslByEndpoints( + data.getSrcSwitchId(), data.getSrcPort(), + data.getDestSwitchId(), data.getDestPort()); + } + + private Optional getReverseIslOfSegmentData(PathSegmentValidationData data) { + return findIslByEndpoints( + data.getDestSwitchId(), data.getDestPort(), + data.getSrcSwitchId(), data.getSrcPort()); + } + + private Set validateBandwidth(InputData inputData) { + Optional forward = getForwardIslOfSegmentData(inputData.getSegment()); + Optional reverse = getReverseIslOfSegmentData(inputData.getSegment()); + + Set errors = Sets.newHashSet(); + if (!forward.isPresent()) { + errors.add(getNoForwardIslError(inputData)); + } + if (!reverse.isPresent()) { + errors.add(getNoReverseIslError(inputData)); + } + if (!errors.isEmpty()) { + return errors; + } + + if (getForwardBandwidthWithReusableResources(forward.get(), inputData) < inputData.getPath().getBandwidth()) { + errors.add(getForwardBandwidthErrorMessage(inputData, forward.get().getAvailableBandwidth())); + } + if (getReverseBandwidthWithReusableResources(reverse.get(), inputData) < inputData.getPath().getBandwidth()) { + errors.add(getReverseBandwidthErrorMessage(inputData, reverse.get().getAvailableBandwidth())); + } + + return errors; + } + + private boolean isSameForwardPathSegment(PathSegmentValidationData segmentData, PathSegment pathSegment) { + return segmentData.getSrcSwitchId().equals(pathSegment.getSrcSwitchId()) + && segmentData.getDestSwitchId().equals(pathSegment.getDestSwitchId()) + && segmentData.getSrcPort().equals(pathSegment.getSrcPort()) + && segmentData.getDestPort().equals(pathSegment.getDestPort()); + } + + private boolean isSameReversePathSegment(PathSegmentValidationData segmentData, PathSegment pathSegment) { + return segmentData.getSrcSwitchId().equals(pathSegment.getDestSwitchId()) + && segmentData.getDestSwitchId().equals(pathSegment.getSrcSwitchId()) + && segmentData.getSrcPort().equals(pathSegment.getDestPort()) + && segmentData.getDestPort().equals(pathSegment.getSrcPort()); + } + + private long getForwardBandwidthWithReusableResources(Isl isl, InputData inputData) { + return getBandwidthWithReusableResources(inputData, isl, + pathSegment -> isSameForwardPathSegment(inputData.getSegment(), pathSegment)); + } + + private long getReverseBandwidthWithReusableResources(Isl isl, InputData inputData) { + return getBandwidthWithReusableResources(inputData, isl, + pathSegment -> isSameReversePathSegment(inputData.getSegment(), pathSegment)); + } + + private long getBandwidthWithReusableResources(InputData inputData, Isl isl, + Predicate pathSegmentPredicate) { + if (inputData.getPath().getReuseFlowResources() != null + && !inputData.getPath().getReuseFlowResources().isEmpty()) { + + Optional flow = flowRepository.findById(inputData.getPath().getReuseFlowResources()); + + Optional segment = flow.flatMap(value -> value.getPaths().stream() + .map(FlowPath::getSegments) + .flatMap(List::stream) + .filter(pathSegmentPredicate) + .findAny()); + + return segment.map(s -> isl.getAvailableBandwidth() + s.getBandwidth()) + .orElseGet(isl::getAvailableBandwidth); + } + + return isl.getAvailableBandwidth(); + } + + private Set validateEncapsulationType(InputData inputData) { + Set errors = Sets.newHashSet(); + Map switchPropertiesMap = switchPropertiesRepository.findBySwitchIds( + Sets.newHashSet(inputData.getSegment().getSrcSwitchId(), inputData.getSegment().getDestSwitchId())); + + if (!switchPropertiesMap.containsKey(inputData.getSegment().getSrcSwitchId())) { + errors.add(getSrcSwitchPropertiesNotFoundError(inputData)); + } else { + if (!switchPropertiesMap.get(inputData.getSegment().getSrcSwitchId()).getSupportedTransitEncapsulation() + .contains(getOrDefaultFlowEncapsulationType(inputData))) { + errors.add(getSrcSwitchDoesNotSupportEncapsulationTypeError(inputData)); + } + } + + if (!switchPropertiesMap.containsKey(inputData.getSegment().getDestSwitchId())) { + errors.add(getDestSwitchPropertiesNotFoundError(inputData)); + } else { + if (!switchPropertiesMap.get(inputData.getSegment().getDestSwitchId()).getSupportedTransitEncapsulation() + .contains(getOrDefaultFlowEncapsulationType(inputData))) { + errors.add(getDestSwitchDoesNotSupportEncapsulationTypeError((inputData))); + } + } + + return errors; + } + + private Set validateDiverseWithFlow(InputData inputData) { + if (!findIslByEndpoints(inputData.getSegment().getSrcSwitchId(), + inputData.getSegment().getSrcPort(), + inputData.getSegment().getDestSwitchId(), + inputData.getSegment().getDestPort()).isPresent()) { + return Collections.singleton(getNoForwardIslError(inputData)); + } + + Optional diverseFlow = flowRepository.findById(inputData.getPath().getDiverseWithFlow()); + if (!diverseFlow.isPresent()) { + return Collections.singleton(getNoDiverseFlowFoundError(inputData)); + } + + if (diverseFlow.get().getData().getPaths().stream() + .map(FlowPath::getSegments) + .flatMap(List::stream) + .anyMatch(pathSegment -> inputData.getSegment().getSrcSwitchId().equals(pathSegment.getSrcSwitchId()) + && inputData.getSegment().getDestSwitchId().equals(pathSegment.getDestSwitchId()) + && inputData.getSegment().getSrcPort().equals(pathSegment.getSrcPort()) + && inputData.getSegment().getDestPort().equals(pathSegment.getDestPort()))) { + return Collections.singleton(getNotDiverseSegmentError(inputData)); + } + + return Collections.emptySet(); + } + + private Set validateLatencyTier2(InputData inputData) { + return validateLatency(inputData, inputData.getPath()::getLatencyTier2, LATENCY_TIER_2); + } + + private Set validateLatency(InputData inputData) { + return validateLatency(inputData, inputData.getPath()::getLatency, LATENCY); + } + + private Set validateLatency(InputData inputData, Supplier inputLatency, String latencyType) { + Optional actualForwardDuration = getForwardPathLatency(inputData); + Optional actualReverseDuration = getReversePathLatency(inputData); + + Set errors = Sets.newHashSet(); + if (!actualForwardDuration.isPresent()) { + errors.add(getNoLinkOnPath()); + } + if (!actualReverseDuration.isPresent()) { + errors.add(getNoLinkOnPath()); + } + if (!errors.isEmpty()) { + return errors; + } + + Duration actualLatency = actualForwardDuration.get(); + if (actualLatency.compareTo(inputLatency.get()) > 0) { + errors.add(getForwardLatencyErrorMessage(inputData, inputLatency.get(), latencyType, actualLatency)); + } + Duration actualReverseLatency = actualReverseDuration.get(); + if (actualReverseLatency.compareTo(inputLatency.get()) > 0) { + errors.add(getReverseLatencyErrorMessage(inputData, inputLatency.get(), latencyType, actualReverseLatency)); + } + return errors; + } + + private Optional getPathLatency(InputData inputData, + Function> getIslfunction) { + try { + return Optional.of(Duration.ofNanos(inputData.getPath().getPathSegments().stream() + .filter(s -> s.getDestPort() != null + && s.getDestSwitchId() != null + && s.getSrcSwitchId() != null + && s.getSrcPort() != null) + .map(getIslfunction) + .map(isl -> isl.orElseThrow(() -> new IllegalArgumentException( + "Cannot calculate latency because there is no link on this path segment"))) + .map(Isl::getLatency) + .mapToLong(Long::longValue) + .sum())); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + } + + private Optional getForwardPathLatency(InputData inputData) { + return getPathLatency(inputData, s -> + getIslByEndPoints(s.getSrcSwitchId(), s.getSrcPort(), s.getDestSwitchId(), s.getDestPort())); + } + + private Optional getReversePathLatency(InputData inputData) { + return getPathLatency(inputData, s -> + getIslByEndPoints(s.getDestSwitchId(), s.getDestPort(), s.getSrcSwitchId(), s.getSrcPort())); + } + + private String getForwardLatencyErrorMessage(InputData data, Duration expectedLatency, String latencyType, + Duration actualLatency) { + return String.format( + "Requested %s is too low on the path between: switch %s and switch %s. " + + "Requested %d ms, but the sum on the path is %d ms.", + latencyType, + data.getPath().getSrcSwitchId(), data.getPath().getDestSwitchId(), + expectedLatency.toMillis(), actualLatency.toMillis()); + } + + private String getReverseLatencyErrorMessage(InputData data, Duration expectedLatency, String latencyType, + Duration actualLatency) { + return String.format( + "Requested %s is too low on the path between: switch %s and switch %s. " + + "Requested %d ms, but the sum on the path is %d ms.", + latencyType, + data.getPath().getDestSwitchId(), data.getPath().getSrcSwitchId(), + expectedLatency.toMillis(), actualLatency.toMillis()); + } + + private String getForwardBandwidthErrorMessage(InputData data, long actualBandwidth) { + return String.format( + "There is not enough bandwidth between end points: switch %s port %d and switch %s port %d" + + " (forward path). Requested bandwidth %d, but the link supports %d.", + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), + data.getPath().getBandwidth(), actualBandwidth); + } + + private String getReverseBandwidthErrorMessage(InputData data, long actualBandwidth) { + return String.format( + "There is not enough bandwidth between end points: switch %s port %d and switch %s port %d" + + " (reverse path). Requested bandwidth %d, but the link supports %d.", + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getPath().getBandwidth(), actualBandwidth); + } + + private String getNoLinkOnPath() { + return "Path latency cannot be calculated because there is no link at least at one path segment."; + } + + private String getNoForwardIslError(InputData data) { + return String.format( + "There is no ISL between end points: switch %s port %d and switch %s port %d.", + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); + } + + private String getNoReverseIslError(InputData data) { + return String.format( + "There is no ISL between end points: switch %s port %d and switch %s port %d.", + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort()); + } + + private String getForwardIslNotActiveError(InputData data) { + return String.format( + "The ISL is not in ACTIVE state between end points: switch %s port %d and switch %s port %d.", + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); + } + + private String getReverseIslNotActiveError(InputData data) { + return String.format( + "The ISL is not in ACTIVE state between end points: switch %s port %d and switch %s port %d.", + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort(), + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort()); + } + + private String getNoDiverseFlowFoundError(InputData data) { + return String.format("Could not find the diverse flow with ID %s.", data.getPath().getDiverseWithFlow()); + } + + private String getNotDiverseSegmentError(InputData data) { + return String.format("The following segment intersects with the flow %s: source switch %s port %d and " + + "destination switch %s port %d.", + data.getPath().getDiverseWithFlow(), + data.getSegment().getSrcSwitchId(), data.getSegment().getSrcPort(), + data.getSegment().getDestSwitchId(), data.getSegment().getDestPort()); + } + + private String getSrcSwitchNotFoundError(InputData data) { + return String.format("The following switch has not been found: %s.", data.getSegment().getSrcSwitchId()); + } + + private String getDestSwitchNotFoundError(InputData data) { + return String.format("The following switch has not been found: %s.", data.getSegment().getDestSwitchId()); + } + + private String getSrcSwitchPropertiesNotFoundError(InputData data) { + return String.format("The following switch properties have not been found: %s.", + data.getSegment().getSrcSwitchId()); + } + + private String getDestSwitchPropertiesNotFoundError(InputData data) { + return String.format("The following switch properties have not been found: %s.", + data.getSegment().getDestSwitchId()); + } + + private String getSrcSwitchDoesNotSupportEncapsulationTypeError(InputData data) { + return String.format("The switch %s doesn't support the encapsulation type %s.", + data.getSegment().getSrcSwitchId(), getOrDefaultFlowEncapsulationType(data)); + } + + private String getDestSwitchDoesNotSupportEncapsulationTypeError(InputData data) { + return String.format("The switch %s doesn't support the encapsulation type %s.", + data.getSegment().getDestSwitchId(), getOrDefaultFlowEncapsulationType(data)); + } + + private PathComputationStrategy getOrDefaultPathComputationStrategy(PathValidationData pathValidationData) { + return pathValidationData.getPathComputationStrategy() != null + ? pathValidationData.getPathComputationStrategy() : + kildaConfiguration.getPathComputationStrategy(); + } + + private FlowEncapsulationType getOrDefaultFlowEncapsulationType(PathValidationData pathValidationData) { + return getOrDefaultFlowEncapsulationType(pathValidationData.getFlowEncapsulationType()); + } + + private FlowEncapsulationType getOrDefaultFlowEncapsulationType(InputData inputData) { + return getOrDefaultFlowEncapsulationType(inputData.getPath().getFlowEncapsulationType()); + } + + private FlowEncapsulationType getOrDefaultFlowEncapsulationType(FlowEncapsulationType requestedType) { + return requestedType != null + ? requestedType + : kildaConfiguration.getFlowEncapsulationType(); + } + + @Getter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + private static class InputData { + PathValidationData path; + PathValidationData.PathSegmentValidationData segment; + + public static InputData of(PathValidationData pathValidationData) { + return new InputData(pathValidationData, null); + } + + public static InputData of(PathValidationData path, PathSegmentValidationData segment) { + return new InputData(path, segment); + } + } +} diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java index 3f2fe5de6a0..71ac0b080f6 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/services/PathsServiceTest.java @@ -345,7 +345,6 @@ private void assertPathLength(List paths) { } } - private void createIsl(Switch srcSwitch, int srcPort, Switch dstSwitch, int dstPort, int cost, long latency, int bandwidth) { createOneWayIsl(srcSwitch, srcPort, dstSwitch, dstPort, cost, latency, bandwidth); diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java new file mode 100644 index 00000000000..8ec1f8e39e8 --- /dev/null +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/test/java/org/openkilda/wfm/topology/nbworker/validators/PathValidatorTest.java @@ -0,0 +1,729 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.wfm.topology.nbworker.validators; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertTrue; +import static org.openkilda.model.FlowEncapsulationType.TRANSIT_VLAN; +import static org.openkilda.model.FlowEncapsulationType.VXLAN; +import static org.openkilda.model.PathComputationStrategy.COST; +import static org.openkilda.model.PathComputationStrategy.COST_AND_AVAILABLE_BANDWIDTH; +import static org.openkilda.model.PathComputationStrategy.LATENCY; +import static org.openkilda.model.PathComputationStrategy.MAX_LATENCY; + +import org.openkilda.config.provider.PropertiesBasedConfigurationProvider; +import org.openkilda.messaging.command.flow.PathValidateRequest; +import org.openkilda.messaging.info.network.PathValidationResult; +import org.openkilda.messaging.payload.flow.PathNodePayload; +import org.openkilda.messaging.payload.network.PathValidationPayload; +import org.openkilda.model.Flow; +import org.openkilda.model.FlowPath; +import org.openkilda.model.FlowPathDirection; +import org.openkilda.model.Isl; +import org.openkilda.model.IslStatus; +import org.openkilda.model.KildaConfiguration; +import org.openkilda.model.PathId; +import org.openkilda.model.PathSegment; +import org.openkilda.model.Switch; +import org.openkilda.model.SwitchId; +import org.openkilda.model.SwitchProperties; +import org.openkilda.model.SwitchStatus; +import org.openkilda.model.cookie.FlowSegmentCookie; +import org.openkilda.pce.PathComputerConfig; +import org.openkilda.persistence.inmemory.InMemoryGraphBasedTest; +import org.openkilda.persistence.repositories.FlowRepository; +import org.openkilda.persistence.repositories.IslRepository; +import org.openkilda.persistence.repositories.RepositoryFactory; +import org.openkilda.persistence.repositories.SwitchPropertiesRepository; +import org.openkilda.persistence.repositories.SwitchRepository; +import org.openkilda.wfm.topology.nbworker.services.PathsService; + +import com.google.common.collect.Sets; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class PathValidatorTest extends InMemoryGraphBasedTest { + private static final SwitchId SWITCH_ID_0 = new SwitchId(0); + private static final SwitchId SWITCH_ID_1 = new SwitchId(1); + private static final SwitchId SWITCH_ID_2 = new SwitchId(2); + private static final SwitchId SWITCH_ID_3 = new SwitchId(3); + private static final SwitchId SWITCH_ID_4 = new SwitchId(4); + private static SwitchRepository switchRepository; + private static SwitchPropertiesRepository switchPropertiesRepository; + private static IslRepository islRepository; + private static FlowRepository flowRepository; + private static PathsService pathsService; + + private boolean isSetupDone; + + @BeforeClass + public static void setUpOnce() { + RepositoryFactory repositoryFactory = persistenceManager.getRepositoryFactory(); + switchRepository = repositoryFactory.createSwitchRepository(); + switchPropertiesRepository = repositoryFactory.createSwitchPropertiesRepository(); + islRepository = repositoryFactory.createIslRepository(); + flowRepository = repositoryFactory.createFlowRepository(); + PathComputerConfig pathComputerConfig = new PropertiesBasedConfigurationProvider() + .getConfiguration(PathComputerConfig.class); + pathsService = new PathsService(repositoryFactory, pathComputerConfig); + KildaConfiguration kildaConfiguration = repositoryFactory.createKildaConfigurationRepository().getOrDefault(); + kildaConfiguration.setFlowEncapsulationType(TRANSIT_VLAN); + } + + @Before + public void createTestTopology() { + if (!isSetupDone) { + Switch switch0 = Switch.builder().switchId(SWITCH_ID_0).status(SwitchStatus.ACTIVE).build(); + Switch switchA = Switch.builder().switchId(SWITCH_ID_1).status(SwitchStatus.ACTIVE).build(); + Switch switchB = Switch.builder().switchId(SWITCH_ID_2).status(SwitchStatus.ACTIVE).build(); + Switch switchC = Switch.builder().switchId(SWITCH_ID_4).status(SwitchStatus.ACTIVE).build(); + Switch switchTransit = Switch.builder().switchId(SWITCH_ID_3).status(SwitchStatus.ACTIVE).build(); + + switchRepository.add(switch0); + switchRepository.add(switchA); + switchRepository.add(switchB); + switchRepository.add(switchC); + switchRepository.add(switchTransit); + + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switch0) + .supportedTransitEncapsulation(Sets.newHashSet(TRANSIT_VLAN)) + .build()); + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switchA) + .supportedTransitEncapsulation(Sets.newHashSet(TRANSIT_VLAN)) + .build()); + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switchB) + .supportedTransitEncapsulation(Sets.newHashSet(TRANSIT_VLAN)) + .build()); + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switchTransit) + .supportedTransitEncapsulation(Sets.newHashSet(TRANSIT_VLAN)) + .build()); + switchPropertiesRepository.add(SwitchProperties.builder() + .switchObj(switchC) + .supportedTransitEncapsulation(Sets.newHashSet(VXLAN)) + .build()); + + createOneWayIsl(switchA, 6, switchTransit, 6, 10, 2_000_000, 10_000, IslStatus.ACTIVE); + createOneWayIsl(switchTransit, 6, switchA, 6, 10, 2_000_000, 10_000, IslStatus.ACTIVE); + + createOneWayIsl(switchB, 7, switchTransit, 7, 15, 3_000_000, 20_000, IslStatus.ACTIVE); + createOneWayIsl(switchTransit, 7, switchB, 7, 15, 3_000_000, 20_000, IslStatus.ACTIVE); + + createOneWayIsl(switchA, 12, switch0, 12, 10, 2_000_000, 10_000, IslStatus.INACTIVE); + createOneWayIsl(switch0, 12, switchA, 12, 10, 2_000_000, 10_000, IslStatus.INACTIVE); + + isSetupDone = true; + } + } + + @Test + public void whenInactiveLink_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 12)); + nodes.add(new PathNodePayload(SWITCH_ID_0, 12, null)); + + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + Set expectedErrorMessages = Sets.newHashSet( + "The ISL is not in ACTIVE state between end points: switch 00:00:00:00:00:00:00:00 port 12 and switch" + + " 00:00:00:00:00:00:00:01 port 12.", + "The ISL is not in ACTIVE state between end points: switch 00:00:00:00:00:00:00:01 port 12 and switch" + + " 00:00:00:00:00:00:00:00 port 12." + ); + + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenValidPath_validatePathReturnsValidResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertTrue(responses.get(0).getIsValid()); + } + + @Test + public void whenValidPathButNotEnoughBandwidth_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(1000000000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertEquals("There must be 2 errors: forward and reverse paths", + 2, responses.get(0).getErrors().size()); + Collections.sort(responses.get(0).getErrors()); + assertEquals(responses.get(0).getErrors().get(0), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:01 port 6 and " + + "switch 00:00:00:00:00:00:00:03 port 6 (forward path). Requested bandwidth 1000000000," + + " but the link supports 10000."); + assertEquals(responses.get(0).getErrors().get(1), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03 port 6 and " + + "switch 00:00:00:00:00:00:00:01 port 6 (reverse path). Requested bandwidth 1000000000," + + " but the link supports 10000."); + } + + @Test + public void whenNoSwitchOnPath_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(new SwitchId("01:01:01:01"), null, 6)); + nodes.add(new PathNodePayload(new SwitchId("01:01:01:02"), 6, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(1000000000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + Set expectedErrorMessages = Sets.newHashSet( + "The following switch has not been found: 00:00:00:00:01:01:01:01.", + "The following switch properties have not been found: 00:00:00:00:01:01:01:01.", + "The following switch has not been found: 00:00:00:00:01:01:01:02.", + "The following switch properties have not been found: 00:00:00:00:01:01:01:02.", + "There is no ISL between end points: switch 00:00:00:00:01:01:01:01 port 6" + + " and switch 00:00:00:00:01:01:01:02 port 6.", + "There is no ISL between end points: switch 00:00:00:00:01:01:01:02 port 6" + + " and switch 00:00:00:00:01:01:01:01 port 6." + ); + + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenValidPathButTooLowLatency_andLatencyStrategy_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(1L) + .latencyTier2ms(0L) + .pathComputationStrategy(LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + + Set expectedErrorMessages = Sets.newHashSet( + "Requested latency is too low on the path between: switch 00:00:00:00:00:00:00:01 and" + + " switch 00:00:00:00:00:00:00:02. Requested 1 ms, but the sum on the path is 5 ms.", + "Requested latency is too low on the path between: switch 00:00:00:00:00:00:00:02 and" + + " switch 00:00:00:00:00:00:00:01. Requested 1 ms, but the sum on the path is 5 ms."); + + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenValidPathButTooLowLatency_andMaxLatencyStrategy_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(1L) + .latencyTier2ms(0L) + .pathComputationStrategy(MAX_LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + + Set expectedErrorMessages = Sets.newHashSet( + "Requested latency is too low on the path between: switch 00:00:00:00:00:00:00:01 and" + + " switch 00:00:00:00:00:00:00:02. Requested 1 ms, but the sum on the path is 5 ms.", + "Requested latency is too low on the path between: switch 00:00:00:00:00:00:00:02 and" + + " switch 00:00:00:00:00:00:00:01. Requested 1 ms, but the sum on the path is 5 ms."); + + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenValidPathButTooLowLatencyTier2_andLatencyStrategy_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10000000000L) + .latencyTier2ms(1L) + .pathComputationStrategy(LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + + Set expectedErrorMessages = Sets.newHashSet( + "Requested latency tier 2 is too low on the path between: switch 00:00:00:00:00:00:00:01 and" + + " switch 00:00:00:00:00:00:00:02. Requested 1 ms, but the sum on the path is 5 ms.", + "Requested latency tier 2 is too low on the path between: switch 00:00:00:00:00:00:00:02 and" + + " switch 00:00:00:00:00:00:00:01. Requested 1 ms, but the sum on the path is 5 ms."); + + Set actualErrors = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrors); + } + + @Test + public void whenSwitchDoesNotSupportEncapsulationType_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .flowEncapsulationType(org.openkilda.messaging.payload.flow.FlowEncapsulationType.VXLAN) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + Set expectedErrors = Sets.newHashSet( + "The switch 00:00:00:00:00:00:00:01 doesn't support the encapsulation type VXLAN.", + "The switch 00:00:00:00:00:00:00:03 doesn't support the encapsulation type VXLAN."); + Set actualErrors = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrors, actualErrors); + } + + @Test + public void whenNoEncapsulationTypeInRequest_useDefaultEncapsulationType_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_4, 6, 7)); + + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + Set expectedErrors = Sets.newHashSet( + "The switch 00:00:00:00:00:00:00:04 doesn't support the encapsulation type TRANSIT_VLAN.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:01 port 6 and switch " + + "00:00:00:00:00:00:00:04 port 6.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:04 port 6 and switch " + + "00:00:00:00:00:00:00:01 port 6."); + Set actualErrors = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrors, actualErrors); + } + + @Test + public void whenNoLinkBetweenSwitches_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 1)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 0, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + Set expectedErrorMessages = Sets.newHashSet( + "There is no ISL between end points: switch 00:00:00:00:00:00:00:02 port 0 and switch" + + " 00:00:00:00:00:00:00:01 port 1.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:01 port 1 and switch" + + " 00:00:00:00:00:00:00:02 port 0."); + + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + + } + + @Test + public void whenNoLinkBetweenSwitches_andValidateLatency_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 1)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 0, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(1L) + .latencyTier2ms(0L) + .pathComputationStrategy(LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + Set expectedErrorMessages = Sets.newHashSet( + "There is no ISL between end points: switch 00:00:00:00:00:00:00:02 port 0 and switch" + + " 00:00:00:00:00:00:00:01 port 1.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:01 port 1 and switch" + + " 00:00:00:00:00:00:00:02 port 0.", + "Path latency cannot be calculated because there is no link at least at one path segment."); + + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenDiverseWith_andNoLinkExists_validatePathReturnsError() { + Switch switch1 = Switch.builder().switchId(SWITCH_ID_1).build(); + Switch switch2 = Switch.builder().switchId(SWITCH_ID_3).build(); + createFlow("flow_1", switch1, 6, switch2, 6); + + assertTrue(flowRepository.findById("flow_1").isPresent()); + assertFalse(flowRepository.findById("flow_1").get().getData().getPaths().isEmpty()); + + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_0, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .diverseWithFlow("flow_1") + .build()); + List responses = pathsService.validatePath(request); + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + Set expectedErrorMessages = Sets.newHashSet( + "There is no ISL between end points: switch 00:00:00:00:00:00:00:03 port 7 and switch" + + " 00:00:00:00:00:00:00:00 port 7.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:00 port 7 and switch" + + " 00:00:00:00:00:00:00:03 port 7.", + "The following segment intersects with the flow flow_1: source switch 00:00:00:00:00:00:00:01" + + " port 6 and destination switch 00:00:00:00:00:00:00:03 port 6."); + Set actualErrorMessages = Sets.newHashSet(responses.get(0).getErrors()); + assertEquals(expectedErrorMessages, actualErrorMessages); + } + + @Test + public void whenDiverseWith_andExistsIntersection_validatePathReturnsError() { + Switch switch1 = Switch.builder().switchId(SWITCH_ID_1).build(); + Switch switch2 = Switch.builder().switchId(SWITCH_ID_3).build(); + createFlow("flow_1", switch1, 6, switch2, 6); + + assertTrue(flowRepository.findById("flow_1").isPresent()); + assertFalse(flowRepository.findById("flow_1").get().getData().getPaths().isEmpty()); + + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .diverseWithFlow("flow_1") + .build()); + List responses = pathsService.validatePath(request); + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertEquals(1, responses.get(0).getErrors().size()); + assertEquals(responses.get(0).getErrors().get(0), + "The following segment intersects with the flow flow_1: source switch 00:00:00:00:00:00:00:01" + + " port 6 and destination switch 00:00:00:00:00:00:00:03 port 6."); + } + + @Test + public void whenMultipleProblemsOnPath_validatePathReturnsAllErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(new SwitchId("FF"), 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(1000000L) + .latencyMs(1L) + .latencyTier2ms(0L) + .pathComputationStrategy(LATENCY) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + + assertEquals("There must be 7 errors in total: 2 not enough bandwidth (forward and reverse paths), " + + "2 link is not present, 2 latency, and 1 switch is not found", + 7, responses.get(0).getErrors().size()); + Set expectedErrorMessages = Sets.newHashSet( + "The following switch has not been found: 00:00:00:00:00:00:00:ff.", + "The following switch properties have not been found: 00:00:00:00:00:00:00:ff.", + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03" + + " port 6 and switch 00:00:00:00:00:00:00:01 port 6 (reverse path). Requested bandwidth 1000000," + + " but the link supports 10000.", + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:01" + + " port 6 and switch 00:00:00:00:00:00:00:03 port 6 (forward path). Requested bandwidth 1000000," + + " but the link supports 10000.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:03 port 7 and" + + " switch 00:00:00:00:00:00:00:ff port 7.", + "There is no ISL between end points: switch 00:00:00:00:00:00:00:ff port 7 and" + + " switch 00:00:00:00:00:00:00:03 port 7.", + "Path latency cannot be calculated because there is no link at least at one path segment."); + assertEquals(expectedErrorMessages, Sets.newHashSet(responses.get(0).getErrors())); + } + + @Test + public void whenValidPathAndDiverseFlowDoesNotExist_validatePathReturnsErrorResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST) + .diverseWithFlow("non_existing_flow_id") + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertFalse(responses.get(0).getIsValid()); + assertFalse(responses.get(0).getErrors().isEmpty()); + assertEquals("Could not find the diverse flow with ID non_existing_flow_id.", + responses.get(0).getErrors().get(0)); + } + + @Test + public void whenNonLatencyPathComputationStrategy_ignoreLatencyAnd_validatePathReturnsSuccessResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(10L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertTrue(responses.get(0).getIsValid()); + } + + @Test + public void whenNoPathComputationStrategyInRequest_ignoreLatencyAnd_validatePathReturnsSuccessResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(0L) + .latencyMs(0L) + .latencyTier2ms(0L) + .build()); + List responses = pathsService.validatePath(request); + + assertFalse(responses.isEmpty()); + assertTrue(responses.get(0).getIsValid()); + } + + @Test + public void whenValidPathWithExistingFlowAndReuseResources_validatePathReturnsSuccessResponseTest() { + List nodes = new LinkedList<>(); + nodes.add(new PathNodePayload(SWITCH_ID_1, null, 6)); + nodes.add(new PathNodePayload(SWITCH_ID_3, 6, 7)); + nodes.add(new PathNodePayload(SWITCH_ID_2, 7, null)); + PathValidateRequest request = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(1000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) + .build()); + List responsesBefore = pathsService.validatePath(request); + + assertFalse(responsesBefore.isEmpty()); + assertTrue("The path using default segments with bandwidth 1003 must be valid", + responsesBefore.get(0).getIsValid()); + + Optional islForward = islRepository.findByEndpoints(SWITCH_ID_3, 7, SWITCH_ID_2, 7); + assertTrue(islForward.isPresent()); + islForward.get().setAvailableBandwidth(100L); + Optional islReverse = islRepository.findByEndpoints(SWITCH_ID_2, 7, SWITCH_ID_3, 7); + assertTrue(islReverse.isPresent()); + islReverse.get().setAvailableBandwidth(100L); + + String flowToReuse = "flow_3_2"; + createFlow(flowToReuse, Switch.builder().switchId(SWITCH_ID_3).build(), 2000, + Switch.builder().switchId(SWITCH_ID_2).build(), 2000, + false, 900L, islForward.get()); + + List responsesAfter = pathsService.validatePath(request); + + assertFalse(responsesAfter.isEmpty()); + assertFalse("The path must not be valid because the flow %s consumes bandwidth", + responsesAfter.get(0).getIsValid()); + assertFalse(responsesAfter.get(0).getErrors().isEmpty()); + assertEquals("There must be 2 errors in total: not enough bandwidth on forward and reverse paths", + 2, responsesAfter.get(0).getErrors().size()); + assertEquals(responsesAfter.get(0).getErrors().get(0), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:02 port 7 and" + + " switch 00:00:00:00:00:00:00:03 port 7 (reverse path). Requested bandwidth 1000, but the" + + " link supports 100."); + assertEquals(responsesAfter.get(0).getErrors().get(1), + "There is not enough bandwidth between end points: switch 00:00:00:00:00:00:00:03 port 7 and" + + " switch 00:00:00:00:00:00:00:02 port 7 (forward path). Requested bandwidth 1000, but the" + + " link supports 100."); + + PathValidateRequest requestWithReuseResources = new PathValidateRequest(PathValidationPayload.builder() + .nodes(nodes) + .bandwidth(1000L) + .latencyMs(0L) + .latencyTier2ms(0L) + .pathComputationStrategy(COST_AND_AVAILABLE_BANDWIDTH) + .reuseFlowResources(flowToReuse) + .build()); + + List responseWithReuseResources = pathsService.validatePath(requestWithReuseResources); + + assertFalse(responseWithReuseResources.isEmpty()); + assertTrue("The path must be valid because, although the flow %s consumes bandwidth, the validator" + + " includes the consumed bandwidth to available bandwidth", + responseWithReuseResources.get(0).getIsValid()); + } + + private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort) { + createFlow(flowId, srcSwitch, srcPort, destSwitch, destPort, null, null, null); + } + + private void createFlow(String flowId, Switch srcSwitch, int srcPort, Switch destSwitch, int destPort, + Boolean ignoreBandwidth, Long bandwidth, Isl isl) { + Flow flow = Flow.builder() + .flowId(flowId) + .srcSwitch(srcSwitch) + .srcPort(srcPort) + .destSwitch(destSwitch) + .destPort(destPort) + .build(); + Optional.ofNullable(ignoreBandwidth).ifPresent(flow::setIgnoreBandwidth); + Optional.ofNullable(bandwidth).ifPresent(flow::setBandwidth); + FlowPath forwardPath = FlowPath.builder() + .pathId(new PathId("path_1")) + .srcSwitch(srcSwitch) + .destSwitch(destSwitch) + .cookie(new FlowSegmentCookie(FlowPathDirection.FORWARD, 1L)) + .bandwidth(flow.getBandwidth()) + .ignoreBandwidth(false) + .segments(Collections.singletonList(PathSegment.builder() + .pathId(new PathId("forward_segment")) + .srcSwitch(srcSwitch) + .srcPort(isl == null ? srcPort : isl.getSrcPort()) + .destSwitch(destSwitch) + .destPort(isl == null ? destPort : isl.getDestPort()) + .build())) + .build(); + + flow.setForwardPath(forwardPath); + FlowPath reversePath = FlowPath.builder() + .pathId(new PathId("path_2")) + .srcSwitch(destSwitch) + .destSwitch(srcSwitch) + .cookie(new FlowSegmentCookie(FlowPathDirection.REVERSE, 1L)) + .bandwidth(flow.getBandwidth()) + .ignoreBandwidth(false) + .segments(Collections.singletonList(PathSegment.builder() + .pathId(new PathId("reverse_segment")) + .srcSwitch(destSwitch) + .srcPort(isl == null ? destPort : isl.getDestPort()) + .destSwitch(srcSwitch) + .destPort(isl == null ? srcPort : isl.getSrcPort()) + .build())) + .build(); + flow.setReversePath(reversePath); + + flowRepository.add(flow); + } + + private void createOneWayIsl(Switch srcSwitch, int srcPort, Switch dstSwitch, int dstPort, int cost, + long latency, int bandwidth, IslStatus islStatus) { + islRepository.add(Isl.builder() + .srcSwitch(srcSwitch) + .srcPort(srcPort) + .destSwitch(dstSwitch) + .destPort(dstPort) + .status(islStatus) + .actualStatus(islStatus) + .cost(cost) + .availableBandwidth(bandwidth) + .maxBandwidth(bandwidth) + .latency(latency) + .build()); + } +} diff --git a/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java new file mode 100644 index 00000000000..4bdf1ba8223 --- /dev/null +++ b/src-java/northbound-service/northbound-api/src/main/java/org/openkilda/northbound/dto/v2/flows/PathValidateResponse.java @@ -0,0 +1,33 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.northbound.dto.v2.flows; + +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +@AllArgsConstructor +@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class) +public class PathValidateResponse { + Boolean isValid; + List errors; +} diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/NetworkController.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/NetworkController.java index f70bb105245..f4af192c029 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/NetworkController.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v1/NetworkController.java @@ -59,10 +59,10 @@ public class NetworkController extends BaseController { public CompletableFuture getPaths( @RequestParam("src_switch") SwitchId srcSwitchId, @RequestParam("dst_switch") SwitchId dstSwitchId, @ApiParam(value = "Valid values are: TRANSIT_VLAN, VXLAN. If encapsulation type is not specified, default " - + "value from Kilda Configuration will be used") + + "value from OpenKilda Configuration will be used") @RequestParam(value = "encapsulation_type", required = false) FlowEncapsulationType encapsulationType, @ApiParam(value = "Valid values are: COST, LATENCY, MAX_LATENCY, COST_AND_AVAILABLE_BANDWIDTH. If path " - + "computation strategy is not specified, default value from Kilda Configuration will be used") + + "computation strategy is not specified, default value from OpenKilda Configuration will be used") @RequestParam(value = "path_computation_strategy", required = false) PathComputationStrategy pathComputationStrategy, @ApiParam(value = "Maximum latency of flow path in milliseconds. Required for MAX_LATENCY strategy. " @@ -74,7 +74,7 @@ public CompletableFuture getPaths( + "Other strategies will ignore this parameter.") @RequestParam(value = "max_latency_tier2", required = false) Long maxLatencyTier2Ms, @ApiParam(value = "Maximum count of paths which will be calculated. " - + "If maximum path count is not specified, default value from Kilda Configuration will be used") + + "If maximum path count is not specified, default value from OpenKilda Configuration will be used") @RequestParam(value = "max_path_count", required = false) Integer maxPathCount) { Duration maxLatency = maxLatencyMs != null ? Duration.ofMillis(maxLatencyMs) : null; diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java new file mode 100644 index 00000000000..e672b9c1672 --- /dev/null +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/controller/v2/NetworkControllerV2.java @@ -0,0 +1,67 @@ +/* Copyright 2023 Telstra Open Source + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openkilda.northbound.controller.v2; + +import org.openkilda.messaging.error.ErrorType; +import org.openkilda.messaging.error.MessageException; +import org.openkilda.messaging.payload.network.PathValidationPayload; +import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; +import org.openkilda.northbound.service.NetworkService; + +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.concurrent.CompletableFuture; + +@RestController +@RequestMapping("/v2/network") +public class NetworkControllerV2 { + + private final NetworkService networkService; + + @Autowired + public NetworkControllerV2(NetworkService networkService) { + this.networkService = networkService; + } + + /** + * Validates that a given path complies with the chosen strategy and the network availability. + * It is required that the input contains path nodes. Other parameters are optional. + * @param pathValidationPayload a payload with a path and additional flow parameters provided by a user + * @return either a successful response or the list of errors + */ + @PostMapping(path = "/path/check") + @ApiOperation(value = "Validates that a given path complies with the chosen strategy and the network availability") + @ResponseStatus(HttpStatus.OK) + public CompletableFuture validateCustomFlowPath( + @RequestBody PathValidationPayload pathValidationPayload) { + + if (pathValidationPayload == null + || pathValidationPayload.getNodes() == null + || pathValidationPayload.getNodes().size() < 2) { + throw new MessageException(ErrorType.DATA_INVALID, "Invalid Request Body", + "Invalid 'nodes' value in the request body"); + } + + return networkService.validateFlowPath(pathValidationPayload); + } +} diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java index 54051c47cb4..ee45e4f1df8 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/converter/PathMapper.java @@ -16,10 +16,12 @@ package org.openkilda.northbound.converter; import org.openkilda.messaging.info.network.Path; +import org.openkilda.messaging.info.network.PathValidationResult; import org.openkilda.messaging.model.FlowPathDto; import org.openkilda.messaging.payload.flow.GroupFlowPathPayload; import org.openkilda.messaging.payload.flow.GroupFlowPathPayload.FlowProtectedPathsPayload; import org.openkilda.messaging.payload.network.PathDto; +import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; import org.mapstruct.Mapper; @@ -33,4 +35,6 @@ default PathDto mapToPath(Path data) { GroupFlowPathPayload mapGroupFlowPathPayload(FlowPathDto data); FlowProtectedPathsPayload mapFlowProtectedPathPayload(FlowPathDto.FlowProtectedPathDto data); + + PathValidateResponse toPathValidateResponse(PathValidationResult data); } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java index ccda5b49503..e2d41ed822a 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/NetworkService.java @@ -15,10 +15,12 @@ package org.openkilda.northbound.service; +import org.openkilda.messaging.payload.network.PathValidationPayload; import org.openkilda.messaging.payload.network.PathsDto; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.PathComputationStrategy; import org.openkilda.model.SwitchId; +import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; import java.time.Duration; import java.util.concurrent.CompletableFuture; @@ -29,10 +31,19 @@ public interface NetworkService { /** - * Gets pathes between two switches. + * Gets paths between two switches. */ CompletableFuture getPaths( SwitchId srcSwitch, SwitchId dstSwitch, FlowEncapsulationType encapsulationType, PathComputationStrategy pathComputationStrategy, Duration maxLatencyMs, Duration maxLatencyTier2, Integer maxPathCount); + + /** + * Validates that a flow with the given path can possibly be created. If it is not possible, + * it responds with the reasons, such as: not enough bandwidth, requested latency is too low, there is no + * links between the selected switches, and so on. + * @param pathValidationPayload a path together with validation parameters provided by a user + * @return either a successful response or the list of errors + */ + CompletableFuture validateFlowPath(PathValidationPayload pathValidationPayload); } diff --git a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java index bff33bb193c..3f5a5c5cbad 100644 --- a/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java +++ b/src-java/northbound-service/northbound/src/main/java/org/openkilda/northbound/service/impl/NetworkServiceImpl.java @@ -16,16 +16,20 @@ package org.openkilda.northbound.service.impl; import org.openkilda.messaging.command.CommandMessage; +import org.openkilda.messaging.command.flow.PathValidateRequest; import org.openkilda.messaging.error.ErrorType; import org.openkilda.messaging.error.MessageException; +import org.openkilda.messaging.info.network.PathValidationResult; import org.openkilda.messaging.info.network.PathsInfoData; import org.openkilda.messaging.nbtopology.request.GetPathsRequest; import org.openkilda.messaging.payload.network.PathDto; +import org.openkilda.messaging.payload.network.PathValidationPayload; import org.openkilda.messaging.payload.network.PathsDto; import org.openkilda.model.FlowEncapsulationType; import org.openkilda.model.PathComputationStrategy; import org.openkilda.model.SwitchId; import org.openkilda.northbound.converter.PathMapper; +import org.openkilda.northbound.dto.v2.flows.PathValidateResponse; import org.openkilda.northbound.messaging.MessagingChannel; import org.openkilda.northbound.service.NetworkService; import org.openkilda.northbound.utils.RequestCorrelationId; @@ -92,4 +96,22 @@ public CompletableFuture getPaths( return new PathsDto(pathsDtoList); }); } + + /** + * Validates that a flow with the given path can possibly be created. If it is not possible, + * it responds with the reasons, such as: not enough bandwidth, requested latency is too low, there is no + * links between the selected switches, and so on. + * @param pathValidationPayload a path together with validation parameters provided by a user + * @return either a successful response or a list of errors + */ + @Override + public CompletableFuture validateFlowPath(PathValidationPayload pathValidationPayload) { + PathValidateRequest request = new PathValidateRequest(pathValidationPayload); + + CommandMessage message = new CommandMessage(request, System.currentTimeMillis(), + RequestCorrelationId.getId()); + return messagingChannel.sendAndGet(nbworkerTopic, message) + .thenApply(PathValidationResult.class::cast) + .thenApply(pathMapper::toPathValidateResponse); + } } From d9a176a797595597e1e624955f8035e3647bf6ae Mon Sep 17 00:00:00 2001 From: Fedir Kantur Date: Fri, 17 Mar 2023 10:49:28 +0000 Subject: [PATCH 41/45] Update logic of overlapping stats calculation for not target flows --- .../share/service/IntersectionComputer.java | 95 +++++++++---------- .../service/IntersectionComputerTest.java | 79 ++++++++------- .../service/yflow/YFlowReadService.java | 8 +- .../services/FlowOperationsService.java | 8 +- 4 files changed, 98 insertions(+), 92 deletions(-) diff --git a/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java b/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java index fb33124e716..5ba9b6c3b0e 100644 --- a/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java +++ b/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java @@ -67,44 +67,12 @@ public IntersectionComputer(String targetFlowId, PathId targetForwardPathId, Pat }); } - private void handleTargetPath(FlowPath path) { - if (path.isOneSwitchFlow()) { - targetPathSwitches.add(path.getSrcSwitchId()); - } else { - path.getSegments().forEach(segment -> { - targetPathEdges.add(Edge.fromPathSegment(segment)); - targetPathSwitches.add(segment.getSrcSwitchId()); - targetPathSwitches.add(segment.getDestSwitchId()); - }); - } - } - - private void handleAnotherPath(FlowPath path) { - Set switches = new HashSet<>(); - Set edges = new HashSet<>(); - if (path.isOneSwitchFlow()) { - switches.add(path.getSrcSwitchId()); - } else { - path.getSegments().forEach(segment -> { - switches.add(segment.getSrcSwitchId()); - switches.add(segment.getDestSwitchId()); - edges.add(Edge.fromPathSegment(segment)); - }); - } - if (!switches.isEmpty()) { - otherSwitches.put(path.getPathId(), switches); - } - if (!edges.isEmpty()) { - otherEdges.put(path.getPathId(), edges); - } - } - /** * Returns {@link OverlappingSegmentsStats} between target path id and other flow paths in the group. * * @return {@link OverlappingSegmentsStats} instance. */ - public OverlappingSegmentsStats getOverlappingStats() { + public OverlappingSegmentsStats getTargetFlowOverlappingStats() { return computeIntersectionCounters( otherEdges.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()), otherSwitches.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()) @@ -116,28 +84,12 @@ public OverlappingSegmentsStats getOverlappingStats() { * * @return {@link OverlappingSegmentsStats} instance. */ - public OverlappingSegmentsStats getOverlappingStats(PathId forwardPathId, PathId reversePathId) { + public OverlappingSegmentsStats getAnotherFlowOverlappingStats(PathId forwardPathId, PathId reversePathId) { Set edges = new HashSet<>(otherEdges.getOrDefault(forwardPathId, Collections.emptySet())); edges.addAll(otherEdges.getOrDefault(reversePathId, Collections.emptySet())); Set switches = new HashSet<>(otherSwitches.getOrDefault(forwardPathId, Collections.emptySet())); switches.addAll(otherSwitches.getOrDefault(reversePathId, Collections.emptySet())); - return computeIntersectionCounters(edges, switches); - } - - /** - * Calculates is overlapping between primary and protected flow segments exists. - * - * @param primaryFlowSegments primary flow segments. - * @param protectedFlowSegments protected flow segments. - * @return is overlapping flag. - */ - public static boolean isProtectedPathOverlaps( - List primaryFlowSegments, List protectedFlowSegments) { - Set primaryEdges = primaryFlowSegments.stream().map(Edge::fromPathSegment).collect(Collectors.toSet()); - Set protectedEdges = protectedFlowSegments.stream().map(Edge::fromPathSegment) - .collect(Collectors.toSet()); - - return !Sets.intersection(primaryEdges, protectedEdges).isEmpty(); + return computeAnotherFlowIntersectionCounters(edges, switches); } /** @@ -206,6 +158,38 @@ public static List calculatePathIntersectionFromDest(Collection { + targetPathEdges.add(Edge.fromPathSegment(segment)); + targetPathSwitches.add(segment.getSrcSwitchId()); + targetPathSwitches.add(segment.getDestSwitchId()); + }); + } + } + + private void handleAnotherPath(FlowPath path) { + Set switches = new HashSet<>(); + Set edges = new HashSet<>(); + if (path.isOneSwitchFlow()) { + switches.add(path.getSrcSwitchId()); + } else { + path.getSegments().forEach(segment -> { + switches.add(segment.getSrcSwitchId()); + switches.add(segment.getDestSwitchId()); + edges.add(Edge.fromPathSegment(segment)); + }); + } + if (!switches.isEmpty()) { + otherSwitches.put(path.getPathId(), switches); + } + if (!edges.isEmpty()) { + otherEdges.put(path.getPathId(), edges); + } + } + private OverlappingSegmentsStats computeIntersectionCounters(Set edges, Set switches) { int edgesOverlap = Sets.intersection(edges, targetPathEdges).size(); int switchesOverlap = Sets.intersection(switches, targetPathSwitches).size(); @@ -215,6 +199,15 @@ private OverlappingSegmentsStats computeIntersectionCounters(Set edges, Se percent(switchesOverlap, targetPathSwitches.size())); } + private OverlappingSegmentsStats computeAnotherFlowIntersectionCounters(Set edges, Set switches) { + int edgesOverlap = Sets.intersection(edges, targetPathEdges).size(); + int switchesOverlap = Sets.intersection(switches, targetPathSwitches).size(); + return new OverlappingSegmentsStats(edgesOverlap, + switchesOverlap, + percent(edgesOverlap, edges.size()), + percent(switchesOverlap, switches.size())); + } + private int percent(int n, int from) { return (int) ((n * 100.0f) / from); } diff --git a/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java b/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java index b7cf9bd00d9..51f0c615247 100644 --- a/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java +++ b/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java @@ -17,8 +17,6 @@ import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.openkilda.messaging.payload.flow.OverlappingSegmentsStats; @@ -76,7 +74,7 @@ public void noGroupIntersectionsTest() { List paths = getFlowPaths(); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getOverlappingStats(); + OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); assertEquals(ZERO_STATS, stats); } @@ -87,7 +85,7 @@ public void noGroupIntersectionsInOneFlowTest() { paths.addAll(getFlowPaths(NEW_PATH_ID, NEW_PATH_ID_REVERSE, flow)); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getOverlappingStats(); + OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); assertEquals(ZERO_STATS, stats); } @@ -116,7 +114,7 @@ public void noIntersectionsTest() { paths.addAll(Lists.newArrayList(path, revPath)); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); assertEquals(ZERO_STATS, stats); } @@ -136,7 +134,7 @@ public void doesntIntersectPathInSameFlowTest() { paths.add(newPath); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); assertEquals(ZERO_STATS, stats); } @@ -145,7 +143,7 @@ public void doesntIntersectPathInSameFlowTest() { public void doesntFailIfNoSegmentsTest() { IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, Collections.emptyList()); - OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); assertEquals(ZERO_STATS, stats); } @@ -155,7 +153,7 @@ public void doesntFailIfNoIntersectionSegmentsTest() { List paths = getFlowPaths(); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); assertEquals(ZERO_STATS, stats); } @@ -175,7 +173,7 @@ public void switchIntersectionByPathIdTest() { paths.add(newPath); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); assertEquals(new OverlappingSegmentsStats(0, 1, 0, 33), stats); } @@ -195,42 +193,40 @@ public void partialIntersectionTest() { paths.add(newPath); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); assertEquals(new OverlappingSegmentsStats(1, 2, 50, 66), stats); } @Test - public void fullIntersectionTest() { + public void anotherPathPartialIntersectionTest() { List paths = getFlowPaths(); - paths.addAll(getFlowPaths(NEW_PATH_ID, NEW_PATH_ID_REVERSE, flow2)); - - IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); - - assertEquals(new OverlappingSegmentsStats(2, 3, 100, 100), stats); - } - @Test - public void isProtectedPathOverlapsPositiveTest() { - List paths = getFlowPaths(PATH_ID, PATH_ID_REVERSE, flow); + FlowPath newPath = FlowPath.builder() + .pathId(NEW_PATH_ID) + .srcSwitch(makeSwitch(SWITCH_ID_A)) + .destSwitch(makeSwitch(SWITCH_ID_B)) + .segments(Lists.newArrayList( + buildPathSegment(NEW_PATH_ID, SWITCH_ID_A, SWITCH_ID_B, 1, 1))) + .build(); + flow2.addPaths(newPath); + paths.add(newPath); - List primarySegments = getFlowPathSegments(paths); - List protectedSegments = Collections.singletonList( - buildPathSegment(PATH_ID, SWITCH_ID_A, SWITCH_ID_B, 1, 1)); + IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); + OverlappingSegmentsStats stats = computer.getAnotherFlowOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); - assertTrue(IntersectionComputer.isProtectedPathOverlaps(primarySegments, protectedSegments)); + assertEquals(new OverlappingSegmentsStats(1, 2, 100, 100), stats); } @Test - public void isProtectedPathOverlapsNegativeTest() { - List paths = getFlowPaths(PATH_ID, PATH_ID_REVERSE, flow); + public void fullIntersectionTest() { + List paths = getFlowPaths(); + paths.addAll(getFlowPaths(NEW_PATH_ID, NEW_PATH_ID_REVERSE, flow2)); - List primarySegments = getFlowPathSegments(paths); - List protectedSegments = Collections.singletonList( - buildPathSegment(PATH_ID, SWITCH_ID_A, SWITCH_ID_C, 3, 3)); + IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); + OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); - assertFalse(IntersectionComputer.isProtectedPathOverlaps(primarySegments, protectedSegments)); + assertEquals(new OverlappingSegmentsStats(2, 3, 100, 100), stats); } @Test @@ -368,11 +364,28 @@ public void oneSwitchIntersectionTest() { paths.add(newPath); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); assertEquals(new OverlappingSegmentsStats(0, 1, 0, 33), stats); } + @Test + public void oneSwitchAsAnotherFlowIntersectionTest() { + List paths = getFlowPaths(); + FlowPath newPath = FlowPath.builder() + .pathId(NEW_PATH_ID) + .srcSwitch(makeSwitch(SWITCH_ID_A)) + .destSwitch(makeSwitch(SWITCH_ID_A)) + .build(); + flow2.addPaths(newPath); + paths.add(newPath); + + IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); + OverlappingSegmentsStats stats = computer.getAnotherFlowOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + + assertEquals(new OverlappingSegmentsStats(0, 1, 0, 100), stats); + } + @Test public void twoOneSwitchesIntersectionTest() { Flow firstFlow = new TestFlowBuilder(FLOW_ID) @@ -400,7 +413,7 @@ public void twoOneSwitchesIntersectionTest() { IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, asList(firstPath, secondPath)); - OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); assertEquals(new OverlappingSegmentsStats(0, 1, 0, 100), stats); } diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java index 9f4679a0591..70a4f6eab9c 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java @@ -147,7 +147,7 @@ public YFlowPathsResponse getYFlowPaths(@NonNull String yFlowId) throws FlowNotF IntersectionComputer primaryIntersectionComputer = new IntersectionComputer( flow.getFlowId(), flow.getForwardPathId(), flow.getReversePathId(), flowPathsInDiverseGroup); - pathDtoBuilder.segmentsStats(primaryIntersectionComputer.getOverlappingStats()); + pathDtoBuilder.segmentsStats(primaryIntersectionComputer.getTargetFlowOverlappingStats()); Collection flowsInDiverseGroup = flowRepository.findByDiverseGroupId(diverseGroupId).stream() .filter(f -> !flow.getFlowId().equals(f.getFlowId())) @@ -162,7 +162,7 @@ public YFlowPathsResponse getYFlowPaths(@NonNull String yFlowId) throws FlowNotF IntersectionComputer protectedIntersectionComputer = new IntersectionComputer( flow.getFlowId(), flow.getProtectedForwardPathId(), flow.getProtectedReversePathId(), flowPathsInDiverseGroup); - protectedDtoBuilder.segmentsStats(protectedIntersectionComputer.getOverlappingStats()); + protectedDtoBuilder.segmentsStats(protectedIntersectionComputer.getTargetFlowOverlappingStats()); flowsInDiverseGroup.stream() .map(diverseFlow -> @@ -243,11 +243,11 @@ private FlowPathDto buildGroupPathFlowDto(Flow flow, boolean primaryPathCorrespo FlowPathDto.FlowPathDtoBuilder builder = buildFlowPathDto(flow) .primaryPathCorrespondStat(primaryPathCorrespondStat) .segmentsStats( - intersectionComputer.getOverlappingStats(flow.getForwardPathId(), flow.getReversePathId())); + intersectionComputer.getAnotherFlowOverlappingStats(flow.getForwardPathId(), flow.getReversePathId())); if (flow.isAllocateProtectedPath()) { FlowProtectedPathDto.FlowProtectedPathDtoBuilder protectedPathBuilder = buildFlowProtectedPathDto(flow) .segmentsStats( - intersectionComputer.getOverlappingStats( + intersectionComputer.getAnotherFlowOverlappingStats( flow.getProtectedForwardPathId(), flow.getProtectedReversePathId())); builder.protectedPath(protectedPathBuilder.build()); } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java index baed2cba6d7..607674852b3 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java @@ -277,7 +277,7 @@ public List getFlowPath(String flowId) throws FlowNotFoundException // target flow primary path FlowPathDtoBuilder targetFlowDtoBuilder = this.toFlowPathDtoBuilder(flow) - .segmentsStats(primaryIntersectionComputer.getOverlappingStats()); + .segmentsStats(primaryIntersectionComputer.getTargetFlowOverlappingStats()); // other flows in the group List payloads = flowsInGroup.stream() @@ -295,7 +295,7 @@ public List getFlowPath(String flowId) throws FlowNotFoundException .forwardPath(buildPathFromFlow(flow, flow.getProtectedForwardPath())) .reversePath(buildPathFromFlow(flow, flow.getProtectedReversePath())) .segmentsStats( - protectedIntersectionComputer.getOverlappingStats()) + protectedIntersectionComputer.getTargetFlowOverlappingStats()) .build()); // other flows in the group @@ -317,13 +317,13 @@ private FlowPathDto mapGroupPathFlowDto(Flow flow, boolean primaryPathCorrespond FlowPathDtoBuilder builder = this.toFlowPathDtoBuilder(flow) .primaryPathCorrespondStat(primaryPathCorrespondStat) .segmentsStats( - intersectionComputer.getOverlappingStats(flow.getForwardPathId(), flow.getReversePathId())); + intersectionComputer.getAnotherFlowOverlappingStats(flow.getForwardPathId(), flow.getReversePathId())); if (flow.isAllocateProtectedPath()) { builder.protectedPath(FlowProtectedPathDto.builder() .forwardPath(buildPathFromFlow(flow, flow.getProtectedForwardPath())) .reversePath(buildPathFromFlow(flow, flow.getProtectedReversePath())) .segmentsStats( - intersectionComputer.getOverlappingStats( + intersectionComputer.getAnotherFlowOverlappingStats( flow.getProtectedForwardPathId(), flow.getProtectedReversePathId())) .build()); } From b0cc5c1d6aea3bf72ce9044567bb9bf3afa38a67 Mon Sep 17 00:00:00 2001 From: Fedir Kantur Date: Fri, 17 Mar 2023 10:59:31 +0000 Subject: [PATCH 42/45] Fix checkstyle issues --- .../wfm/topology/flowhs/service/yflow/YFlowReadService.java | 6 ++++-- .../topology/nbworker/services/FlowOperationsService.java | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java index 70a4f6eab9c..142773217ac 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java @@ -162,7 +162,8 @@ public YFlowPathsResponse getYFlowPaths(@NonNull String yFlowId) throws FlowNotF IntersectionComputer protectedIntersectionComputer = new IntersectionComputer( flow.getFlowId(), flow.getProtectedForwardPathId(), flow.getProtectedReversePathId(), flowPathsInDiverseGroup); - protectedDtoBuilder.segmentsStats(protectedIntersectionComputer.getTargetFlowOverlappingStats()); + protectedDtoBuilder.segmentsStats( + protectedIntersectionComputer.getTargetFlowOverlappingStats()); flowsInDiverseGroup.stream() .map(diverseFlow -> @@ -243,7 +244,8 @@ private FlowPathDto buildGroupPathFlowDto(Flow flow, boolean primaryPathCorrespo FlowPathDto.FlowPathDtoBuilder builder = buildFlowPathDto(flow) .primaryPathCorrespondStat(primaryPathCorrespondStat) .segmentsStats( - intersectionComputer.getAnotherFlowOverlappingStats(flow.getForwardPathId(), flow.getReversePathId())); + intersectionComputer.getAnotherFlowOverlappingStats(flow.getForwardPathId(), + flow.getReversePathId())); if (flow.isAllocateProtectedPath()) { FlowProtectedPathDto.FlowProtectedPathDtoBuilder protectedPathBuilder = buildFlowProtectedPathDto(flow) .segmentsStats( diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java index 607674852b3..e8e5765a18b 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java @@ -317,7 +317,8 @@ private FlowPathDto mapGroupPathFlowDto(Flow flow, boolean primaryPathCorrespond FlowPathDtoBuilder builder = this.toFlowPathDtoBuilder(flow) .primaryPathCorrespondStat(primaryPathCorrespondStat) .segmentsStats( - intersectionComputer.getAnotherFlowOverlappingStats(flow.getForwardPathId(), flow.getReversePathId())); + intersectionComputer.getAnotherFlowOverlappingStats(flow.getForwardPathId(), + flow.getReversePathId())); if (flow.isAllocateProtectedPath()) { builder.protectedPath(FlowProtectedPathDto.builder() .forwardPath(buildPathFromFlow(flow, flow.getProtectedForwardPath())) From d7e1a67f36b8166b6a064af858b4d04586d09639 Mon Sep 17 00:00:00 2001 From: Fedir Kantur Date: Mon, 20 Mar 2023 12:34:06 +0000 Subject: [PATCH 43/45] Revert logic of overlapping stats calculation for not target flows. --- .../share/service/IntersectionComputer.java | 22 ++++------- .../service/IntersectionComputerTest.java | 37 ++++++++----------- .../update/actions/ValidateFlowAction.java | 8 +--- .../service/yflow/YFlowReadService.java | 10 ++--- .../flowhs/validation/FlowValidatorTest.java | 8 ++-- .../services/FlowOperationsService.java | 10 ++--- 6 files changed, 38 insertions(+), 57 deletions(-) diff --git a/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java b/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java index 5ba9b6c3b0e..5e958c961d7 100644 --- a/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java +++ b/src-java/base-topology/base-storm-topology/src/main/java/org/openkilda/wfm/share/service/IntersectionComputer.java @@ -72,7 +72,7 @@ public IntersectionComputer(String targetFlowId, PathId targetForwardPathId, Pat * * @return {@link OverlappingSegmentsStats} instance. */ - public OverlappingSegmentsStats getTargetFlowOverlappingStats() { + public OverlappingSegmentsStats getOverlappingStats() { return computeIntersectionCounters( otherEdges.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()), otherSwitches.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()) @@ -84,12 +84,12 @@ public OverlappingSegmentsStats getTargetFlowOverlappingStats() { * * @return {@link OverlappingSegmentsStats} instance. */ - public OverlappingSegmentsStats getAnotherFlowOverlappingStats(PathId forwardPathId, PathId reversePathId) { + public OverlappingSegmentsStats getOverlappingStats(PathId forwardPathId, PathId reversePathId) { Set edges = new HashSet<>(otherEdges.getOrDefault(forwardPathId, Collections.emptySet())); edges.addAll(otherEdges.getOrDefault(reversePathId, Collections.emptySet())); Set switches = new HashSet<>(otherSwitches.getOrDefault(forwardPathId, Collections.emptySet())); switches.addAll(otherSwitches.getOrDefault(reversePathId, Collections.emptySet())); - return computeAnotherFlowIntersectionCounters(edges, switches); + return computeIntersectionCounters(edges, switches); } /** @@ -199,17 +199,11 @@ private OverlappingSegmentsStats computeIntersectionCounters(Set edges, Se percent(switchesOverlap, targetPathSwitches.size())); } - private OverlappingSegmentsStats computeAnotherFlowIntersectionCounters(Set edges, Set switches) { - int edgesOverlap = Sets.intersection(edges, targetPathEdges).size(); - int switchesOverlap = Sets.intersection(switches, targetPathSwitches).size(); - return new OverlappingSegmentsStats(edgesOverlap, - switchesOverlap, - percent(edgesOverlap, edges.size()), - percent(switchesOverlap, switches.size())); - } - - private int percent(int n, int from) { - return (int) ((n * 100.0f) / from); + private int percent(int overlap, int total) { + if (total != 0) { + return (int) ((overlap * 100.0d) / total); + } + return 0; } private static List getLongestIntersectionOfSegments(List> pathSegments) { diff --git a/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java b/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java index 51f0c615247..033c7dc7f5d 100644 --- a/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java +++ b/src-java/base-topology/base-storm-topology/src/test/java/org/openkilda/wfm/share/service/IntersectionComputerTest.java @@ -34,7 +34,6 @@ import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; public class IntersectionComputerTest { private static final SwitchId SWITCH_ID_A = new SwitchId("00:00:00:00:00:00:00:0A"); @@ -74,7 +73,7 @@ public void noGroupIntersectionsTest() { List paths = getFlowPaths(); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); + OverlappingSegmentsStats stats = computer.getOverlappingStats(); assertEquals(ZERO_STATS, stats); } @@ -85,7 +84,7 @@ public void noGroupIntersectionsInOneFlowTest() { paths.addAll(getFlowPaths(NEW_PATH_ID, NEW_PATH_ID_REVERSE, flow)); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); + OverlappingSegmentsStats stats = computer.getOverlappingStats(); assertEquals(ZERO_STATS, stats); } @@ -114,7 +113,7 @@ public void noIntersectionsTest() { paths.addAll(Lists.newArrayList(path, revPath)); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); + OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); assertEquals(ZERO_STATS, stats); } @@ -134,7 +133,7 @@ public void doesntIntersectPathInSameFlowTest() { paths.add(newPath); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); + OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); assertEquals(ZERO_STATS, stats); } @@ -143,7 +142,7 @@ public void doesntIntersectPathInSameFlowTest() { public void doesntFailIfNoSegmentsTest() { IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, Collections.emptyList()); - OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); + OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); assertEquals(ZERO_STATS, stats); } @@ -153,7 +152,7 @@ public void doesntFailIfNoIntersectionSegmentsTest() { List paths = getFlowPaths(); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); + OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); assertEquals(ZERO_STATS, stats); } @@ -173,7 +172,7 @@ public void switchIntersectionByPathIdTest() { paths.add(newPath); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); + OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); assertEquals(new OverlappingSegmentsStats(0, 1, 0, 33), stats); } @@ -193,7 +192,7 @@ public void partialIntersectionTest() { paths.add(newPath); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); + OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); assertEquals(new OverlappingSegmentsStats(1, 2, 50, 66), stats); } @@ -213,9 +212,9 @@ public void anotherPathPartialIntersectionTest() { paths.add(newPath); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getAnotherFlowOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); - assertEquals(new OverlappingSegmentsStats(1, 2, 100, 100), stats); + assertEquals(new OverlappingSegmentsStats(1, 2, 50, 66), stats); } @Test @@ -224,7 +223,7 @@ public void fullIntersectionTest() { paths.addAll(getFlowPaths(NEW_PATH_ID, NEW_PATH_ID_REVERSE, flow2)); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); + OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); assertEquals(new OverlappingSegmentsStats(2, 3, 100, 100), stats); } @@ -364,7 +363,7 @@ public void oneSwitchIntersectionTest() { paths.add(newPath); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); + OverlappingSegmentsStats stats = computer.getOverlappingStats(); assertEquals(new OverlappingSegmentsStats(0, 1, 0, 33), stats); } @@ -381,9 +380,9 @@ public void oneSwitchAsAnotherFlowIntersectionTest() { paths.add(newPath); IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, paths); - OverlappingSegmentsStats stats = computer.getAnotherFlowOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); + OverlappingSegmentsStats stats = computer.getOverlappingStats(NEW_PATH_ID, NEW_PATH_ID_REVERSE); - assertEquals(new OverlappingSegmentsStats(0, 1, 0, 100), stats); + assertEquals(new OverlappingSegmentsStats(0, 1, 0, 33), stats); } @Test @@ -413,17 +412,11 @@ public void twoOneSwitchesIntersectionTest() { IntersectionComputer computer = new IntersectionComputer(FLOW_ID, PATH_ID, PATH_ID_REVERSE, asList(firstPath, secondPath)); - OverlappingSegmentsStats stats = computer.getTargetFlowOverlappingStats(); + OverlappingSegmentsStats stats = computer.getOverlappingStats(); assertEquals(new OverlappingSegmentsStats(0, 1, 0, 100), stats); } - private List getFlowPathSegments(List paths) { - return paths.stream() - .flatMap(e -> e.getSegments().stream()) - .collect(Collectors.toList()); - } - private List getFlowPaths() { return getFlowPaths(PATH_ID, PATH_ID_REVERSE, flow); } diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/update/actions/ValidateFlowAction.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/update/actions/ValidateFlowAction.java index 21096102b9e..80096a84507 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/update/actions/ValidateFlowAction.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/fsm/update/actions/ValidateFlowAction.java @@ -1,4 +1,4 @@ -/* Copyright 2021 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -96,12 +96,6 @@ protected Optional performWithResponse(State from, State to, Event even + "Therefore, remove the flow mirror points before changing the endpoint switch."); } - if (diverseFlowId != null - && targetFlow.getSrcSwitch().equals(targetFlow.getDestSwitch())) { - throw new FlowProcessingException(ErrorType.DATA_INVALID, - "Couldn't add one-switch flow into diverse group"); - } - transactionManager.doInTransaction(() -> { Flow foundFlow = getFlow(flowId); if (foundFlow.getStatus() == FlowStatus.IN_PROGRESS && stateMachine.getBulkUpdateFlowIds().isEmpty()) { diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java index 142773217ac..2783143e848 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/main/java/org/openkilda/wfm/topology/flowhs/service/yflow/YFlowReadService.java @@ -1,4 +1,4 @@ -/* Copyright 2021 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -147,7 +147,7 @@ public YFlowPathsResponse getYFlowPaths(@NonNull String yFlowId) throws FlowNotF IntersectionComputer primaryIntersectionComputer = new IntersectionComputer( flow.getFlowId(), flow.getForwardPathId(), flow.getReversePathId(), flowPathsInDiverseGroup); - pathDtoBuilder.segmentsStats(primaryIntersectionComputer.getTargetFlowOverlappingStats()); + pathDtoBuilder.segmentsStats(primaryIntersectionComputer.getOverlappingStats()); Collection flowsInDiverseGroup = flowRepository.findByDiverseGroupId(diverseGroupId).stream() .filter(f -> !flow.getFlowId().equals(f.getFlowId())) @@ -163,7 +163,7 @@ public YFlowPathsResponse getYFlowPaths(@NonNull String yFlowId) throws FlowNotF flow.getFlowId(), flow.getProtectedForwardPathId(), flow.getProtectedReversePathId(), flowPathsInDiverseGroup); protectedDtoBuilder.segmentsStats( - protectedIntersectionComputer.getTargetFlowOverlappingStats()); + protectedIntersectionComputer.getOverlappingStats()); flowsInDiverseGroup.stream() .map(diverseFlow -> @@ -244,12 +244,12 @@ private FlowPathDto buildGroupPathFlowDto(Flow flow, boolean primaryPathCorrespo FlowPathDto.FlowPathDtoBuilder builder = buildFlowPathDto(flow) .primaryPathCorrespondStat(primaryPathCorrespondStat) .segmentsStats( - intersectionComputer.getAnotherFlowOverlappingStats(flow.getForwardPathId(), + intersectionComputer.getOverlappingStats(flow.getForwardPathId(), flow.getReversePathId())); if (flow.isAllocateProtectedPath()) { FlowProtectedPathDto.FlowProtectedPathDtoBuilder protectedPathBuilder = buildFlowProtectedPathDto(flow) .segmentsStats( - intersectionComputer.getAnotherFlowOverlappingStats( + intersectionComputer.getOverlappingStats( flow.getProtectedForwardPathId(), flow.getProtectedReversePathId())); builder.protectedPath(protectedPathBuilder.build()); } diff --git a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java index a2dcb913fa2..062656686e8 100644 --- a/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java +++ b/src-java/flowhs-topology/flowhs-storm-topology/src/test/java/org/openkilda/wfm/topology/flowhs/validation/FlowValidatorTest.java @@ -271,13 +271,13 @@ public void checkForEncapsulationTypeRequirementCorrectTypeTest() throws Invalid @Test(expected = InvalidFlowException.class) public void failIfMaxLatencyTier2HigherThanMaxLatencyTest() throws InvalidFlowException { - RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2((long) 1000, (long) 500); + RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2(1000L, 500L); flowValidator.checkMaxLatencyTier(flow); } @Test(expected = InvalidFlowException.class) public void failIfMaxLatencyTier2butMaxLatencyIsNullTest() throws InvalidFlowException { - RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2(null, (long) 500); + RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2(null, 500L); flowValidator.checkMaxLatencyTier(flow); } @@ -323,13 +323,13 @@ public void doesntFailOnAddingFlowToDiverseGroupWithExistingOneSwitchFlowTest() @Test(expected = InvalidFlowException.class) public void failIfMaxLatencyTier2HigherThanMaxLatency() throws InvalidFlowException { - RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2((long) 1000, (long) 500); + RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2(1000L, 500L); flowValidator.checkMaxLatencyTier(flow); } @Test(expected = InvalidFlowException.class) public void failIfMaxLatencyTier2butMaxLatencyIsNull() throws InvalidFlowException { - RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2(null, (long) 500); + RequestedFlow flow = getTestRequestWithMaxLatencyAndMaxLatencyTier2(null, 500L); flowValidator.checkMaxLatencyTier(flow); } diff --git a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java index e8e5765a18b..48da5c3a9ce 100644 --- a/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java +++ b/src-java/nbworker-topology/nbworker-storm-topology/src/main/java/org/openkilda/wfm/topology/nbworker/services/FlowOperationsService.java @@ -1,4 +1,4 @@ -/* Copyright 2021 Telstra Open Source +/* Copyright 2023 Telstra Open Source * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -277,7 +277,7 @@ public List getFlowPath(String flowId) throws FlowNotFoundException // target flow primary path FlowPathDtoBuilder targetFlowDtoBuilder = this.toFlowPathDtoBuilder(flow) - .segmentsStats(primaryIntersectionComputer.getTargetFlowOverlappingStats()); + .segmentsStats(primaryIntersectionComputer.getOverlappingStats()); // other flows in the group List payloads = flowsInGroup.stream() @@ -295,7 +295,7 @@ public List getFlowPath(String flowId) throws FlowNotFoundException .forwardPath(buildPathFromFlow(flow, flow.getProtectedForwardPath())) .reversePath(buildPathFromFlow(flow, flow.getProtectedReversePath())) .segmentsStats( - protectedIntersectionComputer.getTargetFlowOverlappingStats()) + protectedIntersectionComputer.getOverlappingStats()) .build()); // other flows in the group @@ -317,14 +317,14 @@ private FlowPathDto mapGroupPathFlowDto(Flow flow, boolean primaryPathCorrespond FlowPathDtoBuilder builder = this.toFlowPathDtoBuilder(flow) .primaryPathCorrespondStat(primaryPathCorrespondStat) .segmentsStats( - intersectionComputer.getAnotherFlowOverlappingStats(flow.getForwardPathId(), + intersectionComputer.getOverlappingStats(flow.getForwardPathId(), flow.getReversePathId())); if (flow.isAllocateProtectedPath()) { builder.protectedPath(FlowProtectedPathDto.builder() .forwardPath(buildPathFromFlow(flow, flow.getProtectedForwardPath())) .reversePath(buildPathFromFlow(flow, flow.getProtectedReversePath())) .segmentsStats( - intersectionComputer.getAnotherFlowOverlappingStats( + intersectionComputer.getOverlappingStats( flow.getProtectedForwardPathId(), flow.getProtectedReversePathId())) .build()); } From a274fd324cf4f6faf7ab122a7118984562010215 Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Mon, 12 Dec 2022 22:13:15 +0400 Subject: [PATCH 44/45] Added High Level Design doc --- README.md | 2 ++ docs/design/kilda-high-level-design/design.md | 3 +++ .../kilda-high-level-design/kilda_design.png | Bin 0 -> 147219 bytes 3 files changed, 5 insertions(+) create mode 100644 docs/design/kilda-high-level-design/design.md create mode 100644 docs/design/kilda-high-level-design/kilda_design.png diff --git a/README.md b/README.md index a3b654eca64..564f5666c79 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ control millions of flows, and provide sub-second network telemetry. OpenKilda p - REST API and GUI to configure and manage OpenKilda capabilities; - and more! +You can find OpenKIlda high level design [here](docs/design/kilda-high-level-design/design.md). + ## Important notes ### Deprecation diff --git a/docs/design/kilda-high-level-design/design.md b/docs/design/kilda-high-level-design/design.md new file mode 100644 index 00000000000..fdc159353bc --- /dev/null +++ b/docs/design/kilda-high-level-design/design.md @@ -0,0 +1,3 @@ +## High Level Design + +![High Level Design](kilda_design.png "High level design") diff --git a/docs/design/kilda-high-level-design/kilda_design.png b/docs/design/kilda-high-level-design/kilda_design.png new file mode 100644 index 0000000000000000000000000000000000000000..24d26edcf54adc99c195856c169b476dff8464f6 GIT binary patch literal 147219 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

`R9o=n!6ZthB>o^K5}|j0FK<^l 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 literal 0 HcmV?d00001 From ecc787fb44712f65d7adc465263c8952b7fb4e4b Mon Sep 17 00:00:00 2001 From: Sergey Nikitin Date: Wed, 22 Mar 2023 13:20:52 +0100 Subject: [PATCH 45/45] Updated CHANGELOG.md (release-1.130.0) --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8523c82a23..c90ec88e8e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## v1.130.0 (23/03/2023) + +### Features: +- [#5070](https://github.com/telstra/open-kilda/pull/5070) Path validation (Issue: [#4527](https://github.com/telstra/open-kilda/issues/4527)) [**docs**] +- [#5078](https://github.com/telstra/open-kilda/pull/5078) Ability to add single switch flow into diverse group Closes #2072 (Issues: [#2072](https://github.com/telstra/open-kilda/issues/2072) [#2072](https://github.com/telstra/open-kilda/issues/2072)) + +### Bug Fixes: +- [#5064](https://github.com/telstra/open-kilda/pull/5064) Fixed bug with empty vlan stats and set vlans (Issue: [#5063](https://github.com/telstra/open-kilda/issues/5063)) [**storm-topologies**] +- [#5036](https://github.com/telstra/open-kilda/pull/5036) #4984: fix sub flow descriptions (Issue: [#4984](https://github.com/telstra/open-kilda/issues/4984)) +- [#5079](https://github.com/telstra/open-kilda/pull/5079) fix flow_id in path from flow update request ignored (Issue: [#5075](https://github.com/telstra/open-kilda/issues/5075)) +- [#5110](https://github.com/telstra/open-kilda/pull/5110) Bugfix/4574 switch validation fail for grpc timeout (Issue: [#4574](https://github.com/telstra/open-kilda/issues/4574)) + +### Improvements: +- [#5124](https://github.com/telstra/open-kilda/pull/5124) #4574: Switch validation when GPRC is down tests (Issue: [#4574](https://github.com/telstra/open-kilda/issues/4574)) [**tests**] +- [#5128](https://github.com/telstra/open-kilda/pull/5128) 5013 max bandwidth from topologyyaml is not applying (Issue: [#5013](https://github.com/telstra/open-kilda/issues/5013)) +- [#5104](https://github.com/telstra/open-kilda/pull/5104) #5063: Can't patch flow with empty vlan stats and non zeros src/dst v… (Issues: [#5063](https://github.com/telstra/open-kilda/issues/5063) [#5063](https://github.com/telstra/open-kilda/issues/5063)) [**tests**] +- [#5107](https://github.com/telstra/open-kilda/pull/5107) #5067: Added vlan in range check for flow statistics tests (Issue: [#5067](https://github.com/telstra/open-kilda/issues/5067)) [**tests**] +- [#5012](https://github.com/telstra/open-kilda/pull/5012) Added Kilda High Level Design doc [**docs**] +- [#5115](https://github.com/telstra/open-kilda/pull/5115) #4527: Path validation tests (Issues: [#4527](https://github.com/telstra/open-kilda/issues/4527) [#4527](https://github.com/telstra/open-kilda/issues/4527)) [**tests**] + +For the complete list of changes, check out [the commit log](https://github.com/telstra/open-kilda/compare/v1.129.1...v1.130.0). + +### Affected Components: +nbworker + +--- + ## v1.129.1 (16/03/2023) ### Bug Fixes: